
from docplex.mp.constants import CplexScope

from docplex.mp.basic import IndexableObject, _AbstractBendersAnnotated, _AbstractValuable
from docplex.mp.operand import LinearOperand
from docplex.mp.utils import is_number, is_quad_expr

from docplex.mp.sttck import StaticTypeChecker


class Var(IndexableObject, LinearOperand, _AbstractBendersAnnotated, _AbstractValuable):
    """Var()

    This class models decision variables.
    Decision variables are instantiated by :class:`docplex.mp.model.Model` methods such as :func:`docplex.mp.model.Model.var`.

    """

    __slots__ = ('_vartype', '_lb', '_ub')

    def __init__(self, model, vartype, name,
                 lb=None, ub=None,
                 _safe_lb=False, _safe_ub=False):
        IndexableObject.__init__(self, model, name)
        self._vartype = vartype

        if _safe_lb:
            #assert lb is not None
            self._lb = lb
        else:
            self._lb = vartype._compute_lb(lb, model)
        if _safe_ub:
            #assert ub is not None
            self._ub = ub
        else:
            self._ub = vartype._compute_ub(ub, model)

    @property
    def cplex_scope(self):
        return CplexScope.VAR_SCOPE

    # noinspection PyUnusedLocal
    def copy(self, new_model, var_mapping):
        return var_mapping[self]

    relaxed_copy = copy

    # linear operand api

    def as_variable(self):
        return self

    def iter_terms(self):
        yield self, 1

    def clone(self):
        return self

    def negate(self):
        return self.lfactory._new_monomial_expr(self, -1, safe=True)

    iter_sorted_terms = iter_terms

    def number_of_variables(self):
        return 1
    def number_of_terms(self):
        return 1

    def unchecked_get_coef(self, dvar):
        return 1 if dvar is self else 0

    def contains_var(self, dvar):
        return self is dvar

    def accepts_value(self, candidate_value, tolerance=1e-6):
        # INTERNAL
        return self.vartype.accept_domain_value(candidate_value, lb=self._lb, ub=self._ub, tolerance=tolerance)

    def check_name(self, new_name):
        self.check_lp_name(qualifier='variable', new_name=new_name, accept_empty=False, accept_none=False)

    def __hash__(self):
        return self._index

    def set_name(self, new_name):
        # INTERNAL
        self.check_name(new_name)
        self.model.set_var_name(self, new_name)

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new_name):
        self.set_name(new_name)

    @property
    def lb(self):
        """ This property is used to get or set the lower bound of the variable.

        Possible values for the lower bound depend on the variable type. Binary variables
        accept only 0 or 1 as bounds. An integer variable will convert the lower bound value to the
        ceiling integer value of the argument.
        """
        return self._lb

    @lb.setter
    def lb(self, new_lb):
        self.set_lb(new_lb)

    def set_lb(self, lb):
        if lb != self._lb:
            self._model.set_var_lb(self, lb)
            return self._lb

    def _internal_set_lb(self, lb):
        # Internal, used only by the model
        self._lb = lb

    def _internal_set_ub(self, ub):
        # INTERNAL
        self._ub = ub

    @property
    def ub(self):
        """ This property is used to get or set the upper bound of the variable.

        Possible values for the upper bound depend on the variable type. Binary variables
        accept only 0 or 1 as bounds. An integer variable will convert the upper bound value to the
        floor integer value of the argument.

        To reset the upper bound to its default infinity value, use :func:`docplex.mp.model.Model.infinity`.
        """
        return self._ub

    @ub.setter
    def ub(self, new_ub):
        self.set_ub(new_ub)

    def set_ub(self, ub):
        if ub != self._ub:
            self._model.set_var_ub(self, ub)
            return self._ub

    def has_free_lb(self):
        return self.lb <= - self._model.infinity

    def has_free_ub(self):
        return self.ub >= self._model.infinity

    def is_free(self):
        return self.has_free_lb() and self.has_free_ub()

    def _reset_bounds(self):
        vartype = self._vartype
        vtype_lb, vtype_ub = vartype.default_lb, vartype.default_ub
        self.set_lb(vtype_lb)
        self.set_ub(vtype_ub)

    @property
    def vartype(self):
        """ This property returns the variable type, an instance of :class:`VarType`.

        """
        return self._vartype

    def set_vartype(self, new_vartype):
        # INTERNAL
        self._model._set_var_type(self, new_vartype)

    def _set_vartype_internal(self, new_vartype):
        # INTERNAL
        self._vartype = new_vartype

    def _has_type(self, vartype_code):
        # internal
        return self.cplex_typecode == vartype_code

    def is_binary(self):
        """ Checks if the variable is binary.

        Returns:
            Boolean: True if the variable is of type Binary.
        """
        return self._has_type('B')

    def is_integer(self):
        """ Checks if the variable is integer.

        Returns:
            Boolean: True if the variable is of type Integer.
        """
        return self._has_type('I')

    def is_continuous(self):
        """ Checks if the variable is continuous.

        Returns:
            Boolean: True if the variable is of type Continuous.
        """
        return self._has_type('C')

    def is_discrete(self):
        """  Checks if the variable is discrete.

        Returns:
            Boolean: True if the variable is of  type Binary or Integer.
        """
        return self._vartype.is_discrete()

    @property
    def float_precision(self):
        return 0 if self.is_discrete() else self._model.float_precision

    def get_value(self):
        # for compatibility only: use solution_value instead
        print("* get_value() is deprecated, use property solution_value instead")  # pragma: no cover
        return self.solution_value  # pragma: no cover

    @property
    def solution_value(self):
        """ This property returns the solution value of the variable.

        Raises:
            DOCplexException
                if the model has not been solved succesfully.

        """
        self.model._check_has_solution()
        return self._raw_solution_value()

    @property
    def sv(self):
        """ Same as `solution_value` but shorter"""
        return super().sv

    def _raw_solution_value(self, s=None):
        sol = s or self.model._get_solution()
        return sol._get_var_value(self)

    def get_key(self):
        """ Returns the key used to create the variable, or None.

        When the variable is part of a list or dictionary of variables created from a sequence of keys,
        returns the key associated with the variable.

        Example:
            xs = m.continuous_var_dict(keys=['a', 'b', 'c'])
            xa = xs['a']
            assert xa.get_key() == 'a'

        :return:
            a Python object, possibly None.
        """
        container = self.container
        return container.get_var_key(self) if container else None

    def __mul__(self, e):
        return self.times(e)

    @classmethod
    def is_zero_op(cls, other):
        try:
            return other.is_zero()
        except AttributeError:
            return False

    def times(self, e):
        if is_number(e):
            return self.lfactory._new_monomial_expr(dvar=self, coeff=e, safe=False)

        elif self.is_zero_op(e):
            return self.lfactory.new_zero_expr()
        elif isinstance(e, LinearOperand):
            return self._model._qfactory.new_var_product(self, e)
        else:
            return self.to_linear_expr().multiply(e)

    def __rmul__(self, e):
        return self.times(e)

    def __add__(self, e):
        return self.plus(e)

    @staticmethod
    def _extract_calling_ct_xhs():
        _searched_patterns = [("lhs", 0), ("left_expr", 0), ("rhs", 1), ("right_expr", 1)]
        import inspect
        # need to get 2 steps higher to find caller to add/sub
        frame = inspect.stack()[2]
        code_context = frame.code_context # if PY3 else frame[4]

        def find_in_line(line_):
            for xhs_s, xhs_p in _searched_patterns:
                if xhs_s in line_:
                    return line_.find(xhs_s), xhs_p

            return -1, -1

        if code_context:
            line = code_context[0]
            if line:
                spos, lr = find_in_line(line)

                if spos > 1:
                    assert lr >= 0
                    # strip whitespace before code...
                    ct_varname = line[:spos - 1].lstrip()
                    # evaluate ct in caller locals dict
                    subframe = frame.frame # if PY3 else frame[0]
                    ct_object = subframe.f_locals.get(ct_varname)
                    # returns a constraint (or None if that fails), plus 0-1 (0 for lhs, 1 for rhs)
                    return ct_object, lr
        return None, -1

    # def _perform_arithmetic_to_self(self, self_arithmetic_method, e):
    #     # INTERNAL
    #     res = self_arithmetic_method(e)
    #     ct, xhs_pos = self._extract_calling_ct_xhs()
    #     if ct is not None:
    #         self.get_linear_factory().set_linear_constraint_expr_from_pos(ct, xhs_pos, res)
    #     return res

    def add(self, e):
        res = self.plus(e)
        ct, xhs_pos = self._extract_calling_ct_xhs()
        if ct is not None:
            self.lfactory.set_linear_constraint_expr_from_pos(ct, xhs_pos, res)
        return res

    def subtract(self, e):
        res = self.minus(e)
        ct, xhs_pos = self._extract_calling_ct_xhs()
        if ct is not None:
            self.lfactory.set_linear_constraint_expr_from_pos(ct, xhs_pos, res)
        return res

    def plus(self, e):
        if isinstance(e, Var):
            expr = self._make_linear_expr()
            expr._add_term(e)
            return expr

        elif is_number(e):
            return self._make_linear_expr(constant=e, safe=False)
        elif is_quad_expr(e):
            return e.plus(self)
        else:
            return self.to_linear_expr().add(e)

    def to_linear_expr(self):
        # INTERNAL
        return self._make_linear_expr()

    def _make_linear_expr(self, constant=0, safe=True):
        return self.lfactory.linear_expr(self, constant, safe=safe, transient=True)

    def __radd__(self, e):
        return self.plus(e)

    def __sub__(self, e):
        return self.minus(e)

    def minus(self, e):
        if isinstance(e, LinearOperand):
            return self.to_linear_expr().subtract(e)

        elif is_number(e):
            # v -k -> expression(v,-1) -k
            return self._make_linear_expr(constant=-e, safe=False)

        elif is_quad_expr(e):
            return e.rminus(self)
        else:
            return self.to_linear_expr().subtract(e)

    def __rsub__(self, e):

        expr = self.lfactory._to_linear_operand(e, force_clone=True)  # makes a clone.
        return expr.subtract(self)

    def divide(self, e):
        return self.to_linear_expr().divide(e)

    def __div__(self, e):
        return self.divide(e)

    def __truediv__(self, e):
        # for py3
        # INTERNAL
        return self.divide(e)  # pragma: no cover

    def __rtruediv__(self, e):
        # for py3
        self.fatal("Variable {0!s} cannot be used as denominator of {1!s}", self, e)  # pragma: no cover

    def __rdiv__(self, e):
        self.fatal("Variable {0!s} cannot be used as denominator of {1!s}", self, e)

    def __pos__(self):
        # the "+e" unary plus is syntactic sugar
        return self

    def __neg__(self):
        # the "-e" unary minus returns a linear expression
        return self.negate()

    def __pow__(self, power):
        # INTERNAL
        if 0 == power:
            return 1
        elif 1 == power:
            return self
        elif 2 == power:
            return self.square()
        else:
            self.model.unsupported_power_error(self, power)

    def __rshift__(self, other):
        """ Redefines the right-shift operator to create indicators.

        This operator allows to create indicators with the `>>` operator.
        It expects a linear constraint as second argument.

        :param other: a linear constraint used to create the indicator

        :return:
            an instance of IndicatorConstraint, that is not added to the model.
            Use `Model.add()` to add it to the model.

        Note:
            The variable must be binary, otherwise an exception is raised.

        Example:

            >>> m.add(b >> (x >=3)

            creates an indicator which links the satisfaction of the constraint (x >= 3)
            to the value of binary variable b.
        """
        return self._model.indicator_constraint(self, other)

    def square(self):
        return self._model._qfactory.new_var_square(self)

    def __int__(self):
        """ Converts a decision variable to a integer number.

        This is only possible for discrete variables,
        and when the model has been solved successfully.
        If the model has been solved, returns the variable's solution value.

        Returns:
            int: The variable's solution value.

        Raises:
            DOCplexException
                if the model has not been solved successfully.
            DOCplexException
                if the variable is not discrete.
        """

        if self.is_continuous():
            self.fatal("Cannot convert continuous variable value to int: {0!s}", self)
        return int(self.solution_value)

    def __float__(self):
        """ Converts a decision variable to a floating-point number.

        This is only possible when the model has been solved successfully,
        otherwise an exception is raised.
        If the model has been solved, it returns the variable's solution value.

        Returns:
            float: The variable's solution value.
        Raises:
            DOCplexException
                if the model has not been solved successfully.
        """
        return float(self.solution_value)

    def to_bool(self, precision=1e-6):
        """ Converts a variable value to True or False.

        Assuming the variable is discrete (integer or binary), returns True if the variable value is
        set a non-zero integer, taking into account precision.
        For binary variables, returns TRue if the variable value equals 1, taking into account precision.

        Raises:
            DOCplexException
                if the model has not been solved successfully.
            DOCplexException
                if the variable is not discrete

        Returns:
            Boolean: True if the variable value is nonzero, else False.
        """
        if not self.is_discrete():
            self.fatal("boolean conversion only for discrete variables, type is {0!s}", self.vartype)
        value = self.solution_value  # this property checks for a solution.
        return abs(value) >= precision

    def __str__(self):
        """
        Returns:
            string: A string representation of the variable.

        """
        return self.to_string()

    def to_string(self, use_space=False):
        return self.lp_name

    @property
    def lp_name(self):
        return self._name or "x%s" % self.index1

    @property
    def lpt_name(self):
        # 'c1', 'b2', 'i3' ... first letter of cplex code + index
        cpx_typecode = self.cplex_typecode.lower()
        radix = 'x' if cpx_typecode == 'c' else cpx_typecode[0]
        return "%s%d" % (radix, self.index1)

    @property
    def cplex_typecode(self):
        return self._vartype.cplex_typecode

    def _must_print_lb(self):
        return self.cplex_typecode not in 'SN' and self.lb == self._vartype.default_lb

    def __repr__(self):
        self_vartype, self_lb, self_ub = self._vartype, self.lb, self.ub
        # print lb for semi-xx
        if self._must_print_lb():
            repr_lb = ''
        else:
            repr_lb = ',lb={0:g}'.format(self_lb)
        if self_vartype.default_ub == self_ub:
            repr_ub = ''
        else:
            repr_ub = ',ub={0:g}'.format(self_ub)
        if self.has_name():
            repr_name = ",name='{0}'".format(self.name)
        else:
            repr_name = ''
        cpxc = self.cplex_typecode
        return "docplex.mp.Var(type={0}{1}{2}{3})". \
            format(cpxc, repr_name, repr_lb, repr_ub)

    @property
    def reduced_cost(self):
        """ Returns the reduced cost of the variable.

       This method will raise an exception if the model has not been solved as a LP.

        Note:
            For a large number of variables (> 100), using the `Model.reduced_costs()` method can be much faster.

        Returns:
            The reduced cost of the variable (a float value).

        See Also:

            :func:`docplex.mp.model.Model.reduced_costs`
        """
        return self._model._reduced_cost1(self)

    @property
    def basis_status(self):
        """ This property returns the basis status of the variable, if any.
        The variable must be continuous, otherwise an exception is raised.

        Returns:
            An enumerated value from the enumerated type `docplex.constants.BasisStatus`.

        Note:
            for the model to hold basis information, the model must have been solved as a LP problem.
            In some cases, a model which failed to solve may still have a basis available. Use
            `Model.has_basis()` to check whether the model has basis information or not.

        See Also:
            :func:`docplex.mp.model.Model.has_basis`
            :class:`docplex.mp.constants.BasisStatus`

        """
        if not self.is_continuous():
            self.fatal("Basis status is for continuous variables, {0!s} has type {1!s}", self, self.vartype.short_name)
        return self._model._var_basis_status1(self)

    @property
    def benders_annotation(self):
        """
        This property is used to get or set the Benders annotation of a variable.
        The value of the annotation must be a positive integer

        """
        return self.get_benders_annotation()

    @benders_annotation.setter
    def benders_annotation(self, new_anno):
        self.set_benders_annotation(new_anno)

    def iter_constraints(self):
        """ Returns an iterator traversing all constraints in which the variable is used.

        :return:
            An iterator.
        """
        for ct in self._model.iter_constraints():
            for ctv in ct.iter_variables():
                if ctv is self:
                    yield ct
                    break

    def equals(self, other):
        """
        This method is used to test equality to an expression.
        Because of the overloading of operator `==` through the redefinition of
        the `__eq__` method, you cannot use `==` to test for equality.
        In order to test that two decision variables ar ethe same, use th` Python `is` operator;
        use the `equals` method to test whether a given expression is equivalent to a variable:
        for example, calling `equals` with a linear expression which consists of this variable only,
        with a coefficient of 1, returns True.

        Args:
            other: an expression or a variable.

        :return:
            A boolean value, True if the passed variable is this very variable, or
            if the passed expression is equivalent to the variable, else False.

        """
        # noinspection PyPep8
        return self is other or \
               (isinstance(other, LinearOperand) and
                other.get_constant() == 0 and
                other.number_of_terms() == 1 and
                other.unchecked_get_coef(self) == 1)

    def as_logical_operand(self):
        # INTERNAL
        return self if self.is_binary() else None

    def _check_binary_variable_for_logical_op(self, op_name):
        if not self.is_binary():
            self.fatal("Logical {0} is available only for binary variables, {1} has type {2}",
                       op_name, self, self.vartype.short_name)

    def logical_and(self, other):
        self._check_binary_variable_for_logical_op(op_name="and")
        StaticTypeChecker.typecheck_logical_op(self, other, caller="Var.logical_and")
        return self.lfactory.new_logical_and_expr([self, other])

    def logical_or(self, other):
        self._check_binary_variable_for_logical_op(op_name="or")
        StaticTypeChecker.typecheck_logical_op(self, other, caller="Var.logical_or")
        return self.lfactory.new_logical_or_expr([self, other])

    def logical_not(self):
        self._check_binary_variable_for_logical_op(op_name="not")
        return self.lfactory.new_logical_not_expr(self)

    def __and__(self, other):
        return self.logical_and(other)

    def __or__(self, other):
        return self.logical_or(other)

    # no unary not in magic methods...


def is_var(x):
    return isinstance(x, Var)
