# pylint: disable-msg=W0142
# copyright 2002-2021 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-constraint.
#
# logilab-constraint is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 2.1 of the License, or (at your
# option) any later version.
#
# logilab-constraint is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-constraint. If not, see <http://www.gnu.org/licenses/>.
"""Tools to work with finite interval domain interval and constraints

"""


from logilab.constraint.distributors import AbstractDistributor
from logilab.constraint.propagation import (
    AbstractDomain,
    ConsistencyFailure,
    AbstractConstraint,
)


class Interval:
    """representation of an interval
    This class is used to give back results from a FiniteIntervalDomain
    """

    def __init__(self, start, end):
        self._start = start
        self._end = end

    def __repr__(self):
        return f"<Interval [{self._start:.2f} {self._end:.2f}[>"

    def __eq__(self, other):
        return self._start == other._start and self._end == other._end

    def __hash__(self):
        return hash((self.__class__, self._start, self._end))


class FiniteIntervalDomain(AbstractDomain):
    """
    Domain for a variable with interval values.
    """

    def __init__(
        self, lowestMin, highestMax, min_length, max_length=None, resolution=1
    ):
        """
        lowestMin is the lowest value of a low boundary for a variable (inclusive).
        highestMax is the highest value of a high boundary for a variable (exclusive).
        min_length is the minimum width of the interval.
        max_length is the maximum width of the interval.
                   Use None to have max = min.
        resolution is the precision to use for constraint satisfaction. Defaults to 1
        """
        assert highestMax >= lowestMin
        if max_length is None:
            max_length = min_length
        assert 0 <= min_length <= max_length
        assert min_length <= highestMax - lowestMin
        assert resolution > 0
        AbstractDomain.__init__(self)
        self.lowestMin = lowestMin
        self.highestMax = highestMax
        self._min_length = min_length
        max_length = min(max_length, highestMax - lowestMin)
        self._max_length = max_length
        self._resolution = resolution

    def __eq__(self, other):
        return (
            self.lowestMin == other.lowestMin
            and self.highestMax == other.highestMax
            and self._min_length == other._min_length
            and self._max_length == other._max_length
            and self._resolution == other._resolution
        )

    def __hash__(self):
        return hash(
            (
                self.__class__,
                self.lowestMin,
                self.highestMax,
                self._min_length,
                self._max_length,
                self._resolution,
            )
        )

    def getValues(self):
        return list(self.iter_values())

    def iter_values(self):
        length = self._min_length
        while length <= self._max_length:
            start = self.lowestMin
            while start + length <= self.highestMax:
                yield Interval(start, start + length)
                start += self._resolution
            length += self._resolution

    def size(self):
        """computes the size of a finite interval"""
        size = 0
        length = self._min_length
        while length <= self._max_length:
            size += ((self.highestMax - length) - self.lowestMin) / self._resolution + 1
            length += self._resolution
        return size

    def _highestMin(self):
        return self.highestMax - self._min_length

    def _lowestMax(self):
        return self.lowestMin + self._min_length

    lowestMax = property(_lowestMax, None, None, "")

    highestMin = property(_highestMin, None, None, "")

    def copy(self):
        """clone the domain"""
        return FiniteIntervalDomain(
            self.lowestMin,
            self.highestMax,
            self._min_length,
            self._max_length,
            self._resolution,
        )

    def setLowestMin(self, new_lowestMin):
        self.lowestMin = new_lowestMin
        self._valueRemoved()

    def setHighestMax(self, new_highestMax):
        self.highestMax = new_highestMax
        self._valueRemoved()

    def setMinLength(self, new_min):
        self._min_length = new_min
        self._valueRemoved()

    def setMaxLength(self, new_max):
        self._max_length = new_max
        self._valueRemoved()

    def overlap(self, other):
        return other.highestMax > self.lowestMin and other.lowestMin < self.highestMax

    def no_overlap_impossible(self, other):
        return self.lowestMax > other.highestMin and other.lowestMax > self.highestMin

    def hasSingleLength(self):
        return self._max_length == self._min_length

    def _valueRemoved(self):
        if self.lowestMin >= self.highestMax:
            raise ConsistencyFailure(
                "earliest start [%.2f] higher than latest end  [%.2f]"
                % (self.lowestMin, self.highestMax)
            )
        if self._min_length > self._max_length:
            raise ConsistencyFailure(
                "min length [%.2f] greater than max length [%.2f]"
                % (self._min_length, self._max_length)
            )

        self._max_length = min(self._max_length, self.highestMax - self.lowestMin)
        AbstractDomain._valueRemoved(self)

    def __repr__(self):
        return "<FiniteIntervalDomain % 3d from [%.2f, %.2f[ to [%.2f, %.2f[>" % (
            self.size(),
            self.lowestMin,
            self.lowestMax,
            self.highestMin,
            self.highestMax,
        )


##
# Distributors
##


class FiniteIntervalDistributor(AbstractDistributor):
    """Distributes a set of FiniteIntervalDomain
    The distribution strategy is the following:
     - the smallest domain of size > 1 is picked
     - if its max_length is greater than its min_length, a subdomain if size
       min_length is distributed, with the same boundaries
     - otherwise, a subdomain [lowestMin, lowestMax[ is distributed
    """

    def __init__(self):
        AbstractDistributor.__init__(self)

    def _split_values(self, copy1, copy2):
        lm = copy1.lowestMin
        hM = copy1.highestMax
        if copy1.hasSingleLength():
            r = copy1._resolution
            L = copy1._min_length
            m = (hM - L + lm) // (2 * r) * r
            #             copy1.highestMax = copy1.lowestMin + copy1._min_length
            #             copy2.lowestMin += copy2._resolution
            copy1.highestMax = m + L
            copy2.lowestMin = m + r
        else:
            copy1._max_length = copy1._min_length
            copy2._min_length += copy2._resolution

    def _distribute(self, dom1, dom2):
        variable = self.findSmallestDomain(dom1)
        if self.verbose:
            print("Distributing domain for variable", variable)
        splitted = dom1[variable]
        cpy1 = splitted.copy()
        cpy2 = splitted.copy()

        self._split_values(cpy1, cpy2)

        dom1[variable] = cpy1
        dom2[variable] = cpy2

        return cpy1, cpy2


##
# Constraints
##


class AbstractFIConstraint(AbstractConstraint):
    def __init__(self, var1, var2):
        AbstractConstraint.__init__(self, (var1, var2))

    def estimateCost(self, domains):
        return 1

    def __repr__(self):
        return f"<{self.__class__.__name__} {str(self._variables)}>"

    def __eq__(self, other):
        return self.__class__ is other.__class__ and tuple(
            sorted(self._variables)
        ) == tuple(sorted(other._variables))

    def __hash__(self):
        # FIXME: to be able to add constraints in Sets (and compare them)
        # FIXME: improve implementation
        variables = tuple(sorted(self._variables))
        return hash((self.__class__.__name__, variables))

    def narrow(self, domains):
        """narrowing algorithm for the constraint"""
        dom1 = domains[self._variables[0]]
        dom2 = domains[self._variables[1]]

        return self._doNarrow(dom1, dom2)

    def _doNarrow(self, dom1, dom2):
        """virtual method which does the real work"""
        raise NotImplementedError


# FIXME: deal with more than 2 domains at once ?
class NoOverlap(AbstractFIConstraint):
    def _doNarrow(self, dom1, dom2):
        if not dom1.overlap(dom2):
            return 1
        elif dom1.no_overlap_impossible(dom2):
            raise ConsistencyFailure
        elif dom1.lowestMax == dom2.highestMin and dom2.lowestMax > dom1.highestMin:
            dom1.setHighestMax(dom2.highestMin)
            dom2.setLowestMin(dom1.lowestMax)
            return 1
        elif dom1.lowestMax > dom2.highestMin and dom2.lowestMax == dom1.highestMin:
            dom2.setHighestMax(dom1.highestMin)
            dom1.setLowestMin(dom2.lowestMax)
            return 1
        return 0


class StartsBeforeStart(AbstractFIConstraint):
    def _doNarrow(self, dom1, dom2):
        if dom1.lowestMin > dom2.highestMin:
            raise ConsistencyFailure
        if dom1.highestMin < dom2.lowestMin:
            return 1
        return 0


class StartsBeforeEnd(AbstractFIConstraint):
    def _doNarrow(self, dom1, dom2):
        if dom1.lowestMin > dom2.highestMax:
            raise ConsistencyFailure
        if dom1.highestMin < dom2.lowestMax:
            return 1
        return 0


class EndsBeforeStart(AbstractFIConstraint):
    def _doNarrow(self, dom1, dom2):
        if dom1.lowestMax > dom2.highestMin:
            raise ConsistencyFailure
        if dom1.highestMax < dom2.lowestMin:
            return 1
        if dom1.highestMax > dom2.highestMin:
            dom1.setHighestMax(dom2.highestMin)
        return 0


class EndsBeforeEnd(AbstractFIConstraint):
    def _doNarrow(self, dom1, dom2):
        if dom1.lowestMax > dom2.highestMax:
            raise ConsistencyFailure
        if dom1.highestMax < dom2.lowestMax:
            return 1
        if dom1.highestMax > dom2.highestMax:
            dom1.setHighestMax(dom2.highestMax)
        return 0


class StartsAfterStart(AbstractFIConstraint):
    def _doNarrow(self, dom1, dom2):
        if dom1.highestMin < dom2.lowestMin:
            raise ConsistencyFailure
        if dom1.lowestMin > dom2.highestMin:
            return 1
        if dom1.lowestMin < dom2.lowestMin:
            dom1.setLowestMin(dom2.lowestMin)
        return 0


class StartsAfterEnd(AbstractFIConstraint):
    def _doNarrow(self, dom1, dom2):
        if dom1.highestMin < dom2.lowestMax:
            raise ConsistencyFailure
        if dom1.lowestMin > dom2.highestMax:
            return 1
        if dom1.lowestMin < dom2.lowestMax:
            dom1.setLowestMin(dom2.lowestMax)
        if dom2.highestMax > dom1.highestMin:
            dom2.setHighestMax(dom1.highestMin)
        return 0


class EndsAfterStart(AbstractFIConstraint):
    def _doNarrow(self, dom1, dom2):
        if dom1.highestMax < dom2.lowestMin:
            raise ConsistencyFailure
        if dom1.lowestMax > dom2.highestMin:
            return 1
        return 0


class EndsAfterEnd(AbstractFIConstraint):
    def _doNarrow(self, dom1, dom2):
        if dom1.highestMax < dom2.lowestMax:
            raise ConsistencyFailure
        if dom1.lowestMax > dom2.highestMax:
            return 1
        return 0
