####################################################################################################
#
# PyOpenGLng - An OpenGL Python Wrapper with a High Level API.
# Copyright (C) 2014 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################

""" Implements 1D and 2D Interval Arithmetic.
"""

####################################################################################################

__ALL__ = ['Interval',
           'IntervalInt',
           'IntervalIntSupOpen',
           'Interval2D',
           'Interval2DInt',
           ]

####################################################################################################

import math
import sys

####################################################################################################

from .Functions import middle

####################################################################################################

empty_interval_string = '[empty]'

####################################################################################################

class Interval(object):

    """ One-dimension Interval
    """

    ##############################################

    # Fixme: better name than array ?

    def __init__(self, *args):

        """ Initialise an interval

          - Interval(inf, sup)
          - else args must support the __getitem__ interface, e.g.:
            - Interval((inf, sup))
            - Interval([inf, sup])
            - Interval(interval_instance)
        """

        array = self._check_arguments(args)
        
        self.inf = array[0]
        self.sup = array[1]
        
        if self.inf > self.sup: # None > None = False
            raise ValueError("inf <= sup condition is false [%g, %g]" % (self.inf, self.sup))

    ##############################################

    def _check_arguments(self, args):

        size = len(args)
        if size == 1:
            array = args[0]
        elif size == 2:
            array = args
        else:
            raise ValueError("Args size > 2")
        
        return array

    ##############################################

    def copy(self):

        """ Return a clone of the interval
        """

        return self.__class__(self.inf, self.sup)

    ##############################################

    def __getitem__(self, index):

        if isinstance(index, slice):
            if index.start is None:
                lower = 0
            else:
                lower = index.start
            
            if index.stop is None:
                upper = 1
            else:
                upper = index.stop -1
            
            if lower == 0 and upper == 1:
                return self.inf, self.sup
            elif lower == 0 and upper == 0:
                return self.inf
            elif lower == 1 and upper == 1:
                return self.sup
            else:
                raise IndexError("Wrong slice")
        
        elif index == 0:
            return self.inf
        elif index == 1:
            return self.sup
        else:
            raise IndexError("Index is out of range")

    ##############################################

    def __repr__(self):

        return str(self.__class__) + ' ' + str(self)

    ##############################################

    def __str__(self):

        if self.is_empty():
            return empty_interval_string
        else:
            return '[{}, {}]'.format(self.inf, self.sup)

    ##############################################

    def is_empty(self):

        return self.inf is None and self.sup is None

    ##############################################

    @staticmethod
    def _intersection(i1, i2):

        if i1.intersect(i2):
            return (max((i1.inf, i2.inf)),
                    min((i1.sup, i2.sup)))
        else:
            return None, None

    def __and__(i1, i2):

        """ Return the intersection of i1 and i2
        """

        return i1.__class__(i1._intersection(i1, i2))

    def __iand__(self, i2):

        """ Update the interval with its intersection with i2
        """

        self.inf, self.sup = self._intersection(self, i2)
        return self

    ##############################################

    @staticmethod
    def _union(i1, i2):

        return (min((i1.inf, i2.inf)),
                max((i1.sup, i2.sup)))

    def __or__(i1, i2):

        """ Return the union of i1 and i2
        """

        return i1.__class__(i1._union(i1, i2))

    def __ior__(self, i2):

        """ Update the interval with its union with i2
        """

        self.inf, self.sup = self._union(self, i2)
        return self

    ##############################################

    def __eq__(i1, i2):

        """ Equality test
        """

        return i1.inf == i2.inf and i1.sup == i2.sup

    ##############################################

    def __lt__(i1, i2):

        """ Test if i1.sup < i2.inf
        """

        return i1.sup < i2.inf

    ##############################################

    def __gt__(i1, i2):

        """ Test if i1.inf > i2.sup
        """

        return i1.inf > i2.sup

    ##############################################

    def __iadd__(self, dx):

        """ Shift the interval of dx
        """

        self.inf += dx
        self.sup += dx
        return self

    ##############################################

    def __add__(self, dx):

        """ Construct a new interval shifted of dx
        """

        return self.__class__((self.inf + dx,
                               self.sup + dx))

    ##############################################

    def __mul__(self, scale):

        """ Construct a new interval scaled by scale
        """

        return self.__class__((self.inf * scale,
                               self.sup * scale))

    ##############################################

    def __isub__(self, dx):

        """ Shift the interval of -dx
        """

        self.inf -= dx
        self.sup -= dx
        return self

    ##############################################

    def __sub__(self, dx):


        """ Construct a new interval shifted of -dx
        """

        return self.__class__((self.inf - dx,
                               self.sup - dx))

   ###############################################

    def intersect(i1, i2):

        """ Does the interval intersect with i2?
        """

        return ((i1.inf <= i2.sup and i2.inf <= i1.sup) or
                (i2.inf <= i1.sup and i1.inf <= i2.sup))

    ##############################################

    def __contains__(self, x):

        """ Is *x* in the interval?
        """

        if x is None:
            return False
        else:
            return self.inf <= x and x <= self.sup

    ##############################################

    def is_included_in(i1, i2):

        """ Is the interval included in i1?
        """

        # print '(%f <= %f and %f <= %f)' % (i2.inf, i1.inf, i1.sup, i2.sup)

        return i2.inf <= i1.inf and i1.sup <= i2.sup

    ##############################################

    def is_outside_of(i1, i2):

        """ Is the interval outside of i2?
        """

        return i1.inf < i2.inf or i2.sup < i1.sup

    ##############################################

    def map_in(self, interval_reference):

        """ Construct a new interval shifted of interval_reference.inf
        """

        return self - interval_reference.inf

    ##############################################

    def map_x_in(self, x):

        """ Return x - inf
        """

        return x - self.inf

    ##############################################

    def unmap_x_in(self, x):

        """ Return x + inf
        """

        return x + self.inf

    ##############################################

    # Fixme: length -> size ?

    def length(self):

        """ Return sup - inf
        """

        return self.sup - self.inf

    ##############################################

    def zero_length(self):

        """ Return sup == inf
        """

        return self.sup == self.inf

    ##############################################

    def middle(self):

        """ Return interval middle
        """

        return middle(self.inf, self.sup)

    ##############################################

    def enlarge(self, dx):

        """ Enlarge the interval of dx
        """

        self.inf -= dx
        self.sup += dx
        
        return self

####################################################################################################

class IntervalInt(Interval):

    """ One-dimension Integer Interval
    """

    ##############################################

    # Fixme: better name than array ?

    def __init__(self, *args):

        """ Initialise an interval

        array must support the __getitem__ interface
        """

        array = self._check_arguments(args)
        
        if None not in array:
            array = [int(x) for x in array[:2]] # Fixme: rint ?
        # Fixme: else:
        
        super(IntervalInt, self).__init__(array)

    ##############################################

    def __str__(self):

        if self.is_empty():
            return empty_interval_string
        else:
            return '[%i, %i]' % (self.inf, self.sup)

    ##############################################

    def length(self):

        """ Return sup - inf +1
        """

        return self.sup - self.inf +1

    ##############################################

    def length_float(self):

        """ Return sup - inf
        """

        return self.sup - self.inf

####################################################################################################

class IntervalIntSupOpen(IntervalInt):

    """ One-dimension Integer Interval [inf, sup[
    """

    ##############################################

    def __init__(self, *args):

        """ Initialise an interval

        array must support the __getitem__ interface
        """

        array = self._check_arguments(args)
        
        super(IntervalInt, self).__init__(args)

    ##############################################

    def __str__(self):

        if self.is_empty():
            return empty_interval_string
        else:
            if self.sup == sys.maxsize:
                sup_str = '+oo'
            else:
                sup_str = str(self.sup)
            return '[{}, {}['.format(self.inf, sup_str)

    ##############################################

    def intersect(i1, i2):

        """ Does the interval intersect with i2?
        """

        return ((i1.inf < i2.sup and i2.inf < i1.sup) or
                (i2.inf < i1.sup and i1.inf < i2.sup))

   ###############################################

    def minus(i1, i2):

        """ Return i1 - i1 intersect i2
        """

        if i1.intersect(i2):
            
            # i2.inf < i1.inf < i1.sup < i2.sup
            if i1.is_included_in(i2):
                return (IntervalIntSupOpen(None, None),)
            
            # i1.inf < i2.inf < i2.sup < i1.sup
            elif i2.is_included_in(i1):
                r = []
                if i1.inf != i2.inf:
                    r.append(IntervalIntSupOpen(i1.inf, i2.inf))
                if i2.sup != i1.sup:
                    r.append(IntervalIntSupOpen(i2.sup, i1.sup))
                return tuple(r)
            
            # i1.inf < i2.inf < i1.sup < i2.sup
            elif i1.inf <= i2.inf:
                return (IntervalIntSupOpen(i1.inf, i2.inf),)
            
            # i2.inf < i1.inf < i2.sup < i1.sup
            else:
                return (IntervalIntSupOpen(i2.sup, i1.sup),)
        
        else:
            return (IntervalIntSupOpen(None, None),)

   ###############################################

    def split(i1, i2):

        """ Split
        """

        if i1.intersect(i2):
            
            # i2.inf < i1.inf < i1.sup < i2.sup
            if i1.is_included_in(i2):
                return ((i2, False),)
            
            # i1.inf < i2.inf < i2.sup < i1.sup
            elif i2.is_included_in(i1):
                r = []
                if i1.inf != i2.inf: r.append((IntervalIntSupOpen(i1.inf, i2.inf), True))
                r.append((i2, False))
                if i2.sup != i1.sup: r.append((IntervalIntSupOpen(i2.sup, i1.sup), True))
                return tuple(r)
            
            # i1.inf < i2.inf < i1.sup < i2.sup
            elif i1.inf <= i2.inf:
                return ((IntervalIntSupOpen(i1.inf, i2.inf), True),
                        (i2, False))
            
            # i2.inf < i1.inf < i2.sup < i1.sup
            else:
                return ((i2, False),
                        (IntervalIntSupOpen(i2.sup, i1.sup), True))
        
        else:
            return None

#################################################################################

class Interval2D(object):

    """ Two-dimension Interval
    """

    ##############################################

    def __init__(self, x, y):

        """ Initialise a 2D interval

        x and y must support the __getitem__ interface
        """

        self.x = Interval(x)
        self.y = Interval(y)

    ##############################################

    def copy(self):

        """ Return a clone of the interval
        """

        return self.__class__(self.x, self.y)

    ##############################################

    def __setitem__(self, index, interval):

        if index == 0:
            self.x = interval
        elif index == 1:
            self.y = interval
        else:
            raise IndexError("Index is out of range")

    ##############################################

    def __getitem__(self, index):

        if index == 0:
            return self.x
        elif index == 1:
            return self.y
        else:
            raise IndexError("Index is out of range")

    ##############################################

    def __str__(self):

        return str(self.x) + '*' + str(self.y)

    ##############################################

    def __repr__(self):

        return str(self.__class__) + ' ' + str(self)

    ##############################################

    def is_empty(self):

        return self.x.is_empty() or self.y.is_empty()

    ##############################################

    def __and__(i1, i2):

        """ Return the intersection of i1 and i2
        """

        return i1.__class__(i1.x & i2.x,
                            i1.y & i2.y)

    def __iand__(self, i2):

        """ Update the interval with its intersection with i2
        """

        self.x &= i2.x
        self.y &= i2.y
        return self

    ##############################################

    # Union

    def __or__(i1, i2):

        """ Return the union of i1 and i2
        """

        return i1.__class__(i1.x | i2.x,
                            i1.y | i2.y)

    def __ior__(self, i2):

        """ Update the interval with its union with i2
        """

        self.x |= i2.x
        self.y |= i2.y
        return self

    ##############################################

    def __eq__(i1, i2):

        """ Equality test
        """

        return i1.x == i2.x and i1.y == i2.y

    ##############################################

    def shift(self, dx, dy):

        # Fixme: deprecated

        """ Shift the interval of dx
        """

        self.x += dx
        self.y += dy
        return self

    ##############################################

    def __iadd__(self, dxy):

        """ Shift the interval of *dxy*.
        """

        self.x += dx
        self.y += dy
        return self

    ##############################################

    def __add__(self, dxy):

        """ Construct a new interval shifted by *dxy*.
        """

        return self.__class__(self.x + dxy[0], self.y  + dxy[1])

    ##############################################

    def __imul__(self, scale):

        """ Scale the interval by *scale*.
        """

        self.x += scale
        self.y += scale
        return self

    ##############################################

    def __mul__(self, scale):

        """ Construct a new interval scaled by scale
        """

        return self.__class__(self.x * scale, self.y * scale)

    ##############################################

    def intersect(self, i2):

        """ Does the interval intersect with i2?
        """

        return (self.x.intersect(i2.x) and
                self.y.intersect(i2.y))

    ##############################################

    def is_included_in(self, i2):

        """ Is the interval included in i2?
        """

        return (self.x.is_included_in(i2.x) and
                self.y.is_included_in(i2.y))

    ##############################################

    def map_in(self, interval_reference):

        """ Construct a new interval shifted of interval_reference.inf
        """

        return self.__class__(self.x.map_in(interval_reference.x),
                              self.y.map_in(interval_reference.y))

    ##############################################

    def map_xy_in(self, x, y):

        """ Return x - y.inf, y - y.inf
        """

        return(self.x.map_x_in(x),
               self.y.map_x_in(y))

    ##############################################

    def unmap_xy_in(self, x, y):

        """ Return x + y.inf, y + y.inf
        """

        return(self.x.unmap_x_in(x),
               self.y.unmap_x_in(y))

    ##############################################

    def size(self):

        """ Return the horizontal and vertical size 
        """

        return self.x.length(), self.y.length()

    ##############################################

    def area(self):

        """ Return the area
        """

        return self.x.length() * self.y.length()

    ##############################################

    def diagonal(self):

        """ Return the diagonal length
        """

        return math.sqrt((self.x.length())**2 + (self.y.length())**2)

    ##############################################

    def bounding_box(self):

        """ Return the corresponding bounding box (x.inf, y.inf, x.sup, y.sup)
        """

        return (self.x.inf, self.y.inf,
                self.x.sup, self.y.sup)

    ##############################################

    def middle(self):

        """ Return interval middle
        """

        return self.x.middle(), self.y.middle()

    ##############################################

    def enlarge(self, dx):

        """ Enlarge the interval of dx
        """

        self.x.enlarge(dx)
        self.y.enlarge(dx)
        
        return self

####################################################################################################

class IntervalInt2D(Interval2D):

    """ Two-dimension Integer Interval
    """

    ##############################################

    def __init__(self, x, y):

        """ Initialise a 2D Integer interval

        x and y must support the __getitem__ interface
        """

        self.x = IntervalInt(x)
        self.y = IntervalInt(y)

####################################################################################################
#
# End
#
####################################################################################################
