from .. import constants

from . import transformable
from . import coord
import numpy 

#----------------------------------------------------------------------------
# gds_size_info class
#----------------------------------------------------------------------------

__all__ = ["gds_size_info",
           "size_info",
           "size_info_from_coord",
           "size_info_from_numpyarray",
           "size_info_from_point_list"]
           

class gds_size_info(transformable.gds_transformable):
        
        """ object which contains size info about its parent """
        def __init__(self, left=None, right=None, top=None, bottom=None):
                self.__left = left
                self.__right = right
                self.__top = top
                self.__bottom = bottom
                
        def __is_initialized(self):
                """ checks whether the internal data makes sense """
                return not ((self.__left == None) or
                                (self.__right == None) or
                                (self.__top == None)or
                                (self.__bottom == None))

        # internal properties
        def get_left(self): return self.__left
        def set_left(self, value):
                self.__left = value
                if not self.__right == None:
                        self.__right = max(self.__left, self.__right)
        left = property(get_left, set_left)
        """ leftmost x-coordinate """
        
        def get_right(self): return self.__right
        def set_right(self, value):
                self.__right = value
                if not self.__left == None:
                        self.__left = min(self.__left, self.__right)
        right = property(get_right, set_right)
        """ rightmost x coordinate """

        def get_top(self): return self.__top
        def set_top(self, value):
                self.__top = value
                if not self.__bottom == None:
                        self.__bottom = min(self.__top, self.__bottom)
        top = property(get_top, set_top)
        """ highest y coordinate """

        def get_bottom(self): return self.__bottom
        def set_bottom(self, value):
                self.__bottom = value
                if not self.__top == None:
                        self.__top = max(self.__top, self.__bottom)
        bottom = property(get_bottom, set_bottom)
        """ lowest y coordinate """

        # virtual properties
        def get_center(self):
                if not self.__is_initialized(): return None
                return (0.5 * (self.__left + self.__right), 0.5 * (self.__bottom + self.__top))
        def set_center(self, value):
                # change center but keep height and width
                if self.__is_initialized():
                        wo2 = 0.5 * (self.__right - self.__left)
                        self.__left = value[0] - wo2
                        self.__right = value[0] + wo2
                        ho2 = 0.5 * (self.__top - self.__bottom)
                        self.__top = value[1] + ho2
                        self.__bottom = value[1] - ho2
                else:
                        self.__left = value[0]
                        self.__right = value[0]
                        self.__top = value[1]
                        self.__bottom = value[1]
        center = property(get_center, set_center)
        """ center coordinate """

        # virtual properties
        def get_size(self):
                if not self.__is_initialized(): return (0.0, 0.0)
                return (self.__right - self.__left, self.__top - self.__bottom)
        def set_size(self, value):
                # change width and height but keep the center
                if self.__is_initialized():
                        cw = 0.5 * (value[0] - self.__right + self.__left)
                        self.__left -= cw
                        self.__right += cw
                        ch = 0.5 * (value[1] - self.__top + self.__bottom)
                        self.__bottom -= ch
                        self.__top += ch
        size = property(get_size, set_size)
        """ size: (width, height)"""

        def get_width(self):
                if not self.__is_initialized(): return 0.0
                return self.__right - self.__left
        def set_width(self, value):
                # change width but keep center
                if self.__is_initialized():
                        cw = 0.5 * (value - self.__right + self.__left)
                        self.__left -= cw
                        self.__right += cw
        width = property(get_width, set_width)
        """ width """
        
        def get_height(self):
                if not self.__is_initialized(): return 0.0
                return self.__top - self.__bottom
        def set_height(self, value):
                # change height but keep center
                if self.__is_initialized():
                        ch = 0.5 * (value - self.__top + self.__bottom)
                        self.__bottom -= ch
                        self.__top += ch
        height = property(get_height, set_height)
        """ height """

        # read-only properties
        def get_topleft(self): return (self.__left, self.__top)
        topleft = property(get_topleft)
        """ topleft coordinate """
        def get_topright(self): return (self.__right, self.__top)
        topright = property(get_topright)
        """ top right coordinate """
        def get_bottomleft(self): return (self.__left, self.__bottom)
        bottomleft = property(get_bottomleft)
        """ bottom left coordinate """

        def get_bottomright(self): return (self.__right, self.__bottom)
        bottomright = property(get_bottomright)
        """ bottom right coordinate """


        def get_box(self):
                if not self.__is_initialized(): return None
                from . import shape
                return shape.gds_shape([(self.__left, self.__bottom) , (self.__right, self.__top)])
        box = property(get_box)
        """ box: gds_shape(bottom_left, top_right) """

        def __bounding_box_array__(self):
                """ numpy array with the corner point of the enclosing rectangle """
                if not self.__is_initialized(): return None
                return  numpy.array([(self.__left, self.__bottom) ,
                              (self.__right, self.__bottom) ,
                              (self.__right, self.__top)   ,
                              (self.__left, self.__top)])
        def get_bounding_box(self):
                if not self.__is_initialized(): return None
                from . import shape
                return  shape.gds_shape([(self.__left, self.__bottom) ,
                                            (self.__right, self.__bottom) ,
                                            (self.__right, self.__top)   ,
                                            (self.__left, self.__top)], True)
        bounding_box = property(get_bounding_box)
        """ shape with enclosing rectangle """
        
        def get_area(self):
                return self.width * self.height
        area = property(get_area)

        # check interaction with other objects
        def __contains__(self, other):
                """ checks whether point(s) is in bounding box """
                from . import shape
                if not self.__is_initialized(): return False
                if isinstance(other, (tuple, coord.coord2)):
                        return (other[0] <= self.__right) and (other[0] >= self.__left) and (other[1] <= self.__top) and (other[1] >= self.__bottom)
                elif isinstance(other, shape.gds_shape):
                        for c in other:
                                if not c in self: return False
                        return True
                elif isinstance(other, gds_size_info):
                        return (other.__right <= self.__right) and (other.__left >= self.__left) and (other.__top <= self.__top) and (other.__bottom >= self.__bottom)
                else:
                        raise TypeError("Unsupported type " + str(type(other)) + " in gds_size_info.__contains__()")
                
                
        def encloses(self, point):
                """ checks whether point is in bounding box """
                return self.__contains__(point)

        def snap_to_grid(self, grids_per_unit = None):
                """ snaps the size_info object to the grid """
                if not self.__is_initialized(): return self
                if grids_per_unit == None: grids_per_unit = constants.get_grids_per_unit()
                self.__left = constants.snap_value(self.__left, grids_per_unit)
                self.__right = constants.snap_value(self.__right, grids_per_unit)
                self.__top = constants.snap_value(self.__top, grids_per_unit)
                self.__bottom = constants.snap_value(self.__bottom, grids_per_unit)
                return self

        def expand_to_grid(self, grids_per_unit = None):
                """ expands the size_info object to the grid """
                if not self.__is_initialized(): return self
                if grids_per_unit == None: grids_per_unit = constants.get_grids_per_unit()
                self.__left = floor(self.__left * grids_per_unit)/grids_per_unit
                self.__right = ceil(self.__right * grids_per_unit)/grids_per_unit
                self.__top = ceil(self.__top * grids_per_unit)/grids_per_unit
                self.__bottom = floor(self.__bottom * grids_per_unit)/grids_per_unit
                return self

        # combine with another size_info object
        def __add__(self, other):
                """ gives the sizeinfo of the box enclosing the union of both boxes """
                if self.__is_initialized() and other.__is_initialized():
                        left = min(self.__left, other.__left)
                        right = max(self.__right, other.__right)
                        bottom = min(self.__bottom, other.__bottom)
                        top = max(self.__top, other.__top)
                elif other.__is_initialized():
                        left = other.__left
                        right = other.__right
                        bottom = other.__bottom
                        top = other.__top
                elif self.__is_initialized():
                        left = self.__left
                        right = self.__right
                        bottom = self.__bottom
                        top = self.__top
                else:        
                        left, right, top, bottom = None, None, None, None

                return gds_size_info(left, right, top, bottom)

        def __iadd__(self, other):
                """ expands the sizeinfo to include the other box """
                if self.__is_initialized() and other.__is_initialized():
                        self.__left = min(self.__left, other.__left)
                        self.__right = max(self.__right, other.__right)
                        self.__bottom = min(self.__bottom, other.__bottom)
                        self.__top = max(self.__top, other.__top)
                elif other.__is_initialized():
                        self.__left = other.__left
                        self.__right = other.__right
                        self.__bottom = other.__bottom
                        self.__top = other.__top
                return self

        def move(self, coordinate):
                """ translates the center """
                if self.__is_initialized():
                        self.left += coordinate[0]
                        self.right += coordinate[0]
                        self.top += coordinate[1]
                        self.bottom += coordinate[1]
                else:
                        self.left, self.right, self.top, self.bottom = None, None, None, None
                return self

        def move_copy(self, coordinate):
                """ creates a copy of this sizeinfo and moves it """
                if self.__is_initialized():
                        left = self.__left + coordinate[0]
                        right = self.__right +  coordinate[0]
                        top = self.__top + coordinate[1]
                        bottom = self.__bottom + coordinate[1]
                else:
                        left, right, top, bottom = None, None, None, None
                return gds_size_info(left, right, top, bottom)

        # apply a transformation        
        def transform(self, transformation):
                """ transforms the size_info box and makes a new cartesian box over it """
                if self.__is_initialized():
                        BB = transformation.apply_to_array(self.__bounding_box_array__())
                        LB = numpy.min(BB, 0)
                        TR = numpy.max(BB, 0)
                        self.__init__(LB[0], TR[0], TR[1], LB[1])
                return self

        def transform_copy(self, transformation):
                """ transforms a copy of the size_info box and makes a new cartesian box over it """
                if self.__is_initialized():
                        return size_info_from_numpyarray(transformation.apply_to_array(self.__bounding_box_array__()))
                else:
                        return gds_size_info()
                

                
def size_info_from_point_list(point_list):
        """ generate a size info from a point list """
        if len(point_list) == 0: return gds_size_info()
        x = [c[0] for c in point_list]
        y = [c[1] for c in point_list]
        return gds_size_info(min(x), max(x), max(y), min(y))
        
def size_info_from_numpyarray(points):
        """ generate a size_info from a numpy array """
        if len(points) == 0: return gds_size_info()
        LB = numpy.min(points, 0)
        TR = numpy.max(points, 0)
        return gds_size_info(LB[0], TR[0], TR[1], LB[1])

def size_info_from_coord(coord):
        """ generate a size info from a single coordinate """
        return gds_size_info(coord[0], coord[0], coord[1], coord[1])

def size_info(shape):
        """ generate a size_info from a shape-like object """
        from . import shape
        if isinstance(shape, shape.gds_shape):
                return shape.size_info()
        elif isinstance(shape, numpy.ndarray):
                return size_info_from_numpyarray(shape)
        elif isinstance(shape, list):
                return size_info_from_pointlist(shape)
        elif isinstance(shape, coord.coord2):
                return size_info_from_coord(shape)
        else:
                raise TypeError("Invalid type for size_info(): " + str(type(shape)))
                
