# -*- coding: utf-8 -*-
"""Transformation to fix and enforce disjunct True/False status."""

import logging
from math import fabs

from pyomo.common.config import ConfigBlock, ConfigValue, NonNegativeFloat
from pyomo.core.base import Transformation, TransformationFactory
from pyomo.core.base.block import Block
from pyomo.core.expr.numvalue import value
from pyomo.gdp import GDP_Error
from pyomo.gdp.disjunct import Disjunct, Disjunction

logger = logging.getLogger('pyomo.gdp.fix_disjuncts')


@TransformationFactory.register(
    'gdp.fix_disjuncts',
    doc="Fix disjuncts to their current Boolean values.")
class GDP_Disjunct_Fixer(Transformation):
    """Fix disjuncts to their current Boolean values.

    This reclassifies all disjuncts in the passed model instance as ctype Block and deactivates the
    constraints and disjunctions within inactive disjuncts.

    """

    def __init__(self, **kwargs):
        # TODO This uses an integer tolerance. At some point, these should be
        # standardized.
        super(GDP_Disjunct_Fixer, self).__init__(**kwargs)

    CONFIG = ConfigBlock("gdp.fix_disjuncts")
    CONFIG.declare('integer_tolerance', ConfigValue(
        default=1E-6,
        domain=NonNegativeFloat,
        description="tolerance on binary variable 0, 1 values"
    ))

    def _apply_to(self, model, **kwds):
        """Fix all disjuncts in the given model and reclassify them to Blocks."""
        config = self.config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)

        self._transformContainer(model)

        # Reclassify all disjuncts
        for disjunct_object in model.component_objects(Disjunct, descend_into=(Block, Disjunct)):
            disjunct_object.parent_block().reclassify_component_type(disjunct_object, Block)

    def _transformContainer(self, obj):
        """Find all disjuncts in the container and transform them."""
        for disjunct in obj.component_data_objects(ctype=Disjunct, active=True, descend_into=True):
            _binary = disjunct.binary_indicator_var
            if fabs(value(_binary) - 1) <= self.config.integer_tolerance:
                disjunct.indicator_var.fix(True)
                self._transformContainer(disjunct)
            elif fabs(value(_binary)) <= self.config.integer_tolerance:
                disjunct.deactivate()
            else:
                raise ValueError(
                    'Non-binary indicator variable value %s for disjunct %s'
                    % (disjunct.name, value(_binary)))

        for disjunction in obj.component_data_objects(ctype=Disjunction, active=True, descend_into=True):
            self._transformDisjunctionData(disjunction)

    def _transformDisjunctionData(self, disjunction):
        # the sum of all the indicator variable values of disjuncts in the
        # disjunction
        logical_sum = sum(value(disj.binary_indicator_var)
                          for disj in disjunction.disjuncts)

        # Check that the disjunctions are not being fixed to an infeasible
        # realization.
        if disjunction.xor and not logical_sum == 1:
            # for XOR disjunctions, the sum of all disjunct values should be 1
            raise GDP_Error(
                "Disjunction %s violated. "
                "Expected 1 disjunct to be active, but %s were active."
                % (disjunction.name, logical_sum))
        elif not logical_sum >= 1:
            # for non-XOR disjunctions, the sum of all disjunct values should
            # be at least 1
            raise GDP_Error(
                "Disjunction %s violated. "
                "Expected at least 1 disjunct to be active, "
                "but none were active.")
        else:
            # disjunction is in feasible realization. Deactivate it.
            disjunction.deactivate()
