from __future__ import print_function
import region as rr
from .shape import *
from numpy import array, empty, ndarray, append, integer, require
import os as os
from .region_utils import *
import warnings

class CXCRegion(object):

    # This provides a mapping between shape name and the class of the object to create
    _shape_classes = { 'annulus'       : Annulus,
                       'box'           : Box,
                       'circle'        : Circle,
                       'ellipse'       : Ellipse,
                       'field'         : Field,
                       'pie'           : Pie,
                       'point'         : Point,
                       'polygon'       : Polygon,
                       'rectangle'     : Rectangle,
                       'rotbox'        : Rotbox,
                       #'rotrectangle'  : Rotrectangle,
                       'sector'        : Sector 
                     }


    def __init__(self, regstr=None, colname=None, component=1):
        """
        CXCRegion constructor

        Parameters
        ----------
           regstr - inputs can be either a region string to be parsed, 
              a filename, or None

              Input file can either be a DS9 region file or FITS file containing
              a region subspace.

              None will return an empty CXCRegion.
              
           colname - the name of the column to extract from file subspace 
              ** filename is required

           component - the subspace component to extract the column from
              ** default == 1

        Returns
        --------
           CXCRegion based on input(s)
           

        Raises
        --------
           IOError


        Examples
        --------
           1)  reg = CXCRegion()
           2)  reg = CXCRegion("circle(10,10,5)")
           3)  reg = CXCRegion("image.fits", colname="POS", component=2)
           4)  reg = CXCRegion("box.reg")
           5)  reg = CXCRegion("region(box.reg)")
           6)  reg = CXCRegion("bounds(region(box.reg))")
           7)  reg = CXCRegion("bounds(circle(10,10,5))")
           8)  reg = CXCRegion( <Shape object> )
        """

        self.__clear__()

        # return an empty Region
        if regstr is None:
            self.__region = rr.regCreateEmptyRegion()
            return 

        intype = str(type(regstr))

        if 'region.shape' in intype:
            shape = regstr
            self.__region = rr.regCreateEmptyRegion()
            self._add_shape( shape.name, shape.xpoints, shape.ypoints, shape.radii, shape.angles, shape.include, shape.logic )
            return

        if 'str' in intype:

            # for column name exists and regstr == filename
            # extract Region from data subspace
            if colname is not None:
                if not os.path.exists(regstr):
                    raise IOError("File " + regstr + " does not exist")
                else:
                    self.__region = self.__extract_from_subspace(regstr, colname, component)

                if self.__region is None:
                    raise IOError("Unable to extract subspace region from ", regstr)

            else:
                # get the pyRegion 
                try:
                    self.__region = rr.regParse(regstr)
                except:
                    self.__region = rr.regParse("region("+regstr+")")

        if 'Region' in intype:
            self.__region = regstr

        if (rr.regGetNoShapes(self.__region) != 0 ):
            self.__setup_shapes(self.__region)


    def __setup_shapes(self, reg):
        """
        Creates the correct shape based on valid shape name and adds it to the
        CXCRegion's shapes list.
        """

        # clear shapes
        if self.shapes:
            del self.__shapes
            self.__shapes = []

        # get pyShape pointers
        shapes = rr.regGetShapes(reg)

        if shapes is None:
            return

        # use pyShape pointers to populate shapes list for this region
        for ii in range(0, len(shapes)):

            (inc, shape_name) = rr.regShapeGetName(shapes[ii])
            new_shape_class = self._shape_classes[shape_name.lower()]
 
            # set up logic operators
            _shape = new_shape_class(shapes[ii])
            if ii == 0:
                _shape.logic = opNOOP
            else:
                if _shape.component == self.__shapes[ii-1].component:
                    _shape.logic = opAND
                else:
                    _shape.logic = opOR
                    
            # add new Shape to Region shapes list
            self.__shapes.append( _shape )
            

    def __extract_from_subspace(self, filename, colname, component):
        """
        Retrieves the region from the file's subspace column and component.

        """
        import cxcdm as dm
        regptr = None

        try:
            blk = dm.dmBlockOpen(filename)
        except:
            raise IOError("Unable to open " + filename)

        if "MASK" in dm.dmBlockGetName(blk):
            dm.dmBlockClose(blk)
            raise RuntimeError("MASK files are not supported at this time")

        try:
            dm.dmBlockSetSubspaceCpt(blk, component)
        except:
            dm.dmBlockClose(blk)
            raise RuntimeError("Subspace component " + str(component) +" does not exist") 

        try:
            col = dm.dmSubspaceColOpen(blk, colname)
        except:
            dm.dmBlockClose(blk)
            raise IOError("Column " + colname + " does not exist")

        regptr = dm.dmSubspaceColGetRegion(col)

        dm.dmBlockClose(blk)

        return regptr


    def __clear__(self):
        """
        Resets the region pointer and shapes list

        """
        self.__region = None
        self.__shapes = []


    def __del__(self):
        """
        Deletes the data in this CXCRegion before removal

        """
        self.__clear__()


    def __str__( self ):
        """
        Constructs the string value from the shapes and logic

          ** Note: An empty CXCRegion will display a blank line

        """
        retval = "".join( [shape.logic.str+str(shape) for shape in self.__shapes ] )
        return(retval)


    def __repr__(self):
        """
        Constructs the string value from the shapes and logic

          ** Note: An empty CXCRegion will display a blank line

        """
        return self.__str__()


    def __add__(self, other):
        """
        Logically OR (Union) two regions together
        """
        if not isinstance( other, type(self)): 
            raise TypeError("Cannot perform region logic with {} type".format(type(other)) )

        pyreg = rr.regUnionRegion(self.__region, other.__region)
        if pyreg is None:
            return None

        newreg = CXCRegion(pyreg)

        return newreg


    def __mul__(self, other):
        """
        Logically AND (Intersect) two regions together
        """
        if not isinstance( other, type(self)):
            raise TypeError("Cannot perform region logic with {} type".format(type(other)) )

        pyreg = rr.regIntersectRegion(self.__region, other.__region)

        newreg = CXCRegion(pyreg)

        return newreg


    def __sub__(self, other):
        """
        Subtraction is actually a short-hand or AND NOT.
        Other is inverted and then AND'ed with self.
        """
        if not isinstance( other, type(self)):
            raise TypeError("Cannot perform region logic with {} type".format(type(other)) )

        invreg = rr.regInvert(other.__region)
        pyreg = rr.regIntersectRegion(self.__region, invreg) 
        
        newreg = CXCRegion(pyreg)

        return newreg


    def __neg__(self):
        """
        Invert region
        """
        pyreg = rr.regInvert(self.__region)

        newreg = CXCRegion(pyreg)

        return newreg


    def __copy__(self):
        """
        Return a copy of the region
        """
        pyreg = rr.regCopyRegion(self.__region)

        newreg = CXCRegion(pyreg)

        return newreg


    def __eq__(self, other):
        """
        Is this region equal to another (shape-by-shape comparison)
        """
        if not isinstance( other, type(self)):
            raise TypeError("Cannot perform region logic with {} type".format(type(other)) )

        retval = rr.regCompareRegion(self.__region, other.__region)
        return (retval != 0)


    def __and__(self, other ):
        """
        shape & shape ==> shape * shape 
        """
        return self*other


    def __or__(self, other):
        """ 
        shape | shape => shape + shape 
        """
        return self+other


    def __xor__(self,other):
        """ 
        shape ^ shape ==> (a-b)+(b-a)
        """
        return (self-other)+(other-self)

        
    def __invert__(self):
        """
        ~shape == !shape
        """
        return -self


    def __len__(self):
        """
        Returns the number of shapes in the region
        """
        return len(self.__shapes)


    def __getitem__(self, index ):
        """
        Returns a new region with the shape at input index
        """
        return self.get_shape_region(index)
        

    def __contains__(self, other):
        """
        Checks to see if a shape is contained within a region
        """        
        if not isinstance( other, type(self)):
            raise TypeError("Cannot perform region logic with {} type".format(type(other)) )
    
        if len(other) < 1:
            raise IndexError("Other region must contain a single shape")

        other_shape = rr.regGetShape(other._get_region_ptr(), 0)
        shapes = rr.regGetShapes(self.__region)

        for ii in range(0,len(self.__shapes)) :
            this_shape = shapes[ii]
            contains = rr.regCompareShape(this_shape, other_shape)
            if contains:
                return True

        return False


    def _get_region_ptr(self):
        return self.__region


    @property
    def shapes(self):
        """A list of the shapes in the region"""
        return self.__shapes


    def print_shapes(self):
        """
        Prints the list index and shape(s) contained within the region

        Parameters
        ----------
           None

        Returns
        --------
           None

        Raises
        --------
           None

        """

        index = 0
        for shape in self.__shapes:
            print(index, ":  ", shape)
            index += 1

   
    def area(self, bin=1.0, pixel=False):
        """
        Calculates the geometric or pixellated area of the region


        Parameters
        ----------
           bin (optional)     - bin size; default == 1.0
           pixel (optional)   - if True, calculate the pixellated area
                                default == False

        Returns
        --------
           Area of the shapes in the region

        Raises
        --------
           None

        """

        return rr.regArea(self.__region, bin, pixel)


    def extent(self):
        """
        Calculates the bounding box around the region


        Parameters
        ----------
           None

        Returns
        --------
           (x0, y0, x1, y1) - the lower left and upper right coordinates 
                              of the bounding box 

        Raises
        --------
           None

        """

        (x0, y0, x1, y1) =  rr.regExtent(self.__region)

        return { 'x0' : x0, 
                 'y0' : y0,
                 'x1' : x1,
                 'y1' : y1 }


    def is_inside(self, xx, yy):
        """
        Determines if (x, y) coordinate is inside the region


        Parameters
        ----------
           xx - X point
           yy - Y point

        Returns
        --------
           True if (x,y) coordinate is inside the region, False if not.

        Raises
        --------
           None

        """
        retval = False

        if rr.regInsideRegion(self.__region, xx, yy):
            retval = True

        return retval

    
    def _add_shape(self, shape_name, xpoints, ypoints, radii, angles, inc_flag,
                  logic_operator):

        """
        Append a shape to the region

        Parameters
        ----------
           shape_name  - name of shape
           xpoints     - X coordinate
           ypoints     - Y coordinate
           radii       - redius length
           angles      - angle of rotation
           inc_flag    - incINCLUDE or incEXCLUDE
           logic_operator - opAND, opOR, or opNOOP

        Returns
        --------
           None

        Raises
        --------
           None

        """
        def _newarray(someval):
            'SL-164: Force array data to be a contiguous array, even if already an array'
            reqs = ['C_CONTIGUOUS', 'OWNDATA']
            retval = require(someval, requirements=reqs)
            return retval
        
        # make sure x, y, radii, and angles inputs are numpy.arrays
        xpoints = _newarray(xpoints)
            
        ypoints = _newarray(ypoints)
                
        if radii is not None:
            radii = _newarray(radii)

        if angles is not None:
            angles = _newarray(angles)

        nmax = xpoints.size

        logic = 0
        if isinstance(logic_operator, tuple):
            logic = logic_operator.val
        else:
            logic = logic_operator

        include = 0
        if isinstance(inc_flag, tuple):
            include = inc_flag.val
        else:
            include = inc_flag

        # append newly created pyShape to pyRegion
        rr.regAppendShape(self.__region, shape_name, include, logic, 
                          xpoints, ypoints, nmax, radii, angles, 0, 0) 

        self.__setup_shapes(self.__region)


    def edit(self, index=None, dx=0, dy=0, rotate=0, stretch=1, pad=0):
        """
        Updates a single shape at a given index, or all shapes in the region if 
        no index is entered 

        Parameters
        ----------
           index (optional)   - shape index number
           dx (optional)      - amount to shift x-points by
           dy (optional)      - amount to shift y-points by
           rotate (optional)  - amount to rotate
                                ** only applies to shapes with rotation angle(s)
           stretch (optional) - amount to multiply the radii by
           pad (optional)     - amount to add to the radii

        Returns
        --------
           New, updated CXCRegion

        Raises
        -------
           Warning if there in nothing to update


        """
        if ( (dx == 0) and (dy == 0) and (rotate == 0) and (stretch == 1) and (pad == 0) ):
            warnings.warn("Warning: Nothing to update.")
            return

        if stretch < 0.0:
            warnings.warn("Warning: Stretch amount cannot be less than zero. \nContinuing edit without stretch.")

        shapes = self.__shapes
        newreg = CXCRegion()

        for ii in range( len(shapes) ):
            shape = shapes[ii]

            if (index is None) or (index == ii):
                # Edit this shape
                if (dx != 0):
                    xpoints = array([ xx+dx for xx in shape.xpoints ])
                else:
                    xpoints = shape.xpoints

                if (dy != 0):
                    ypoints = array([ yy+dy for yy in shape.ypoints ])
                else:
                    ypoints = shape.ypoints

                radii = None
                if (stretch > 0.0) and (shape.radii is not None):
                    radii = array([])

                    for rad in shape.radii:
                        result = rad*stretch+pad

                        if result < 0:
                            warnings.warn("Warning: Padded radii cannot be less than zero. \nContinuing edit without pad.")
                            radii = shape.radii
                            break
                        else:
                            radii = append(radii, result)
                else:
                    radii = shape.radii

                angles = None
                if (rotate != 0) and (shape.angles is not None):
                    angles = array([ aa+rotate for aa in shape.angles ])
                else:
                    angles = shape.angles
            else:
                # Do not edit this shape..
                xpoints = shape.xpoints
                ypoints = shape.ypoints
                radii   = shape.radii
                angles  = shape.angles
        
            # restore shape in region
            newreg._add_shape(shape.name, xpoints, ypoints, radii, angles, shape.include, shape.logic )

        return newreg


    def _delete_shape(self, index):
        """
        Removes a shape from the region


        Parameters
        ----------
           index - shape number in the list

        Returns
        --------
           None
        
        Raises
        --------
           TypeError
           IndexError

        """
       
        if index is None:
            raise IndexError("Index cannot be None.")

        if not isinstance(index, (int, integer)):
            raise TypeError("Index must be an integer type.")

        rr.regFreeShape(self.__region, index)
        del self.__shapes[index]


    def write(self, filename, fits=False, newline=False, clobber=False):
        """
        Writes the region to a file

        Parameters
        ----------
           filename  - name of the output file
           fits      - if True, write region as FITS file;
                       if False, write region as text
           newline   - add newline to text file after each OR operator
           clobber   - if True and filename exists, the file is deleted
                       

        Returns
        --------
           None

        Raises
        --------
           None

        """
        import cxcdm as dm

        if os.path.exists(filename):
            if clobber is True:
                os.unlink(filename)
            else:
                raise RuntimeError("Unable to write to file; file exists and clobber=False")
            
        if fits is True:
            ds = dm.dmDatasetCreate(filename)
            dd = dm.dmDescriptor()
            blk = dm.dmTableWriteRegion(ds, "REGION", dd, self.__region)
            dm.dmDatasetClose(ds)
        else:
            as_str = str(self)
            if newline:  # Replace OR (+) with new lines.  Have to keep *'s
                as_str = as_str.replace(opOR.str, "\n")
            
            with open(filename, "w") as fp:
                fp.write( as_str )
                fp.write("\n")


    def apply_transform(self, xfunction):
        """
        Create a copy of the current region, shape-by-shape, and 
        apply the transformation function to the parameters.
        
        The x,y points are transformed by applying the xfunction
        directly to the values.
        
        Radii and angles are transformed by computing the scaling 
        function for a slightly shifted location and finding the shift 
        and rotation in the transformed coordinates.

        Parameters
        ----------
           xfunction - the Transform function

        Returns
        -------
           CXCRegion - a transformed copy of the this region

        """
        from math import atan2, hypot, degrees
        _dy = 1/3600.0

        newreg = CXCRegion()

        for ii in range( len(self) ):
            shape = self.__shapes[ii]

            reg_math = shape.logic
            reg_inc = shape.include

            copy_xx = array([ xx for xx in shape.xpoints ])
            copy_yy = array([ yy for yy in shape.ypoints ])

            # List wrap zip function for P2 & P3 support
            xypoints = list( zip(copy_xx, copy_yy) )

            # Apply the function to the x,y values directly
            xform_xy = xfunction(xypoints)
            xform_xx, xform_yy = zip(*xform_xy)
            
            xform_xx = array(xform_xx)
            xform_yy = array(xform_yy)

            # Get the scale and the angle
            copy_yy = array([ yy+_dy for yy in shape.ypoints ])            


            # List wrap zip function for P2 & P3 support
            xypoints = list( zip(copy_xx, copy_yy) )
            xform_xy = xfunction(xypoints)

            delt_xx, delt_yy = zip(*xform_xy)

            xform_rr = None
            if shape.radii is not None:
                stretch = hypot((delt_xx[0]-xform_xx[0]),(delt_yy[0]-xform_yy[0]))/_dy
                xform_rr = array([ rad*stretch for rad in shape.radii])

            xform_aa = None
            if shape.angles is not None:
                rotate = degrees(atan2((delt_xx[0]-xform_xx[0]),(delt_yy[0]-xform_yy[0])))
                xform_aa = array([ aa+rotate for aa in shape.angles ])

            newreg._add_shape( shape.name, xform_xx, xform_yy, xform_rr, xform_aa, reg_inc, reg_math )
            
        return newreg


    def get_shape_region(self, index):
        """
        Returns a new region containing the shape at the given index

        Parameters
        ----------
           index - shape index number or a slice in the format start:stop:step

        Returns
        --------
           CXCRegion
 
        Raises
        --------
           IndexError
           TypeError

        """
       
        if index is None:
            raise IndexError("Index cannot be None.")

        if isinstance(index, slice):
            if index.start is not None and not isinstance(index.start,(int, integer)):
                raise TypeError("Slice start must be an integer type.")
            if index.stop is not None and not isinstance(index.stop,(int, integer)):
                raise TypeError("Slice stop must be an integer type.")
            if index.step is not None and not isinstance(index.step,(int, integer)):
                raise TypeError("Slice step must be an integer type.")
        elif not isinstance(index, (int, integer)):
            raise TypeError("Index/indices must be an integer type.")

        shape_list = self.__shapes[index]
        if not isinstance(shape_list, list):
            shape_list = [shape_list]

        newreg = CXCRegion()
            
        for shape in shape_list:
            newreg._add_shape( shape.name, shape.xpoints, shape.ypoints, shape.radii, shape.angles, shape.include, shape.logic )

        return newreg


    def index(self, other):
        """
        Checks to see if a shape is contained within a region.  If so, returns
        a list of indicies.

          ** Note: If searching for an excluded shape, be sure to include the minus operator with the Shape.
                a = circle(0,0,10)
                b = box(-5,-5,10,10)
                r1 = a-b
                r1.index(b)   ==> outputs an empty list '[]'
                r1.index(-b)  ==> outputs index '[1]'


        Parameters
        ----------
           other - region to check against


        Returns
        --------
           True if shape is in the region, False if not.

        Raises
        --------
           TypeError
           IndexError

        """
        if not isinstance( other, type(self)):
            raise TypeError("Cannot perform region logic with {} type".format(type(other)) )
    
        if len(other) != 1:
            raise IndexError("Other region must contain a single shape")
        
        other_shape = rr.regGetShape(other._get_region_ptr(), 0)
        shapes = rr.regGetShapes(self.__region)
        retval = []

        for ii in range(0,len(self.__shapes)) :
            this_shape = shapes[ii]
            contains = rr.regCompareShape(this_shape, other_shape)
            if contains:
                retval.append( ii )

        return retval





def ShapeReg( name, xpoints, ypoints, radii, angles ):
    """
    Global function for creating single-shape regions
    """
    newreg = CXCRegion()
    newreg._add_shape( name, xpoints, ypoints, radii, angles, incINCLUDE, opNOOP)
    return newreg


def circle( xx, yy, radii ):
    """
    circle( x_center, y_center, radius )
    """
    if radii < 0:
        raise ValueError("Radius of circle must be positive")
    return ShapeReg( "circle", xx, yy, radii, None )


def ellipse( xx, yy, major, minor, angle=None ):
    """
    ellipse( x_center, y_center, semi_major_axis, semi_minor_axis, [rotation_angle])

       Note: angle is measured in degrees from the +X axis
    """
    if major < 0:
        raise ValueError("Semi-major axis of ellipse must be positive")
    if minor < 0:
        raise ValueError("Semi-minor axis of ellipse must be positive")

    return ShapeReg( "ellipse", xx, yy, [major, minor], angle )


def box( xx, yy, xlen, ylen, angle=None ):
    """
    box( x_center, y_center, x_length, y_length)

       Note: angle is measured in degrees from the +X axis
    """
    if angle == 0:
        angle = None

    if xlen < 0:
        raise ValueError("X-length of box must be positive")
    if ylen < 0:
        raise ValueError("Y-length of box must be positive")

    return ShapeReg( "box", xx, yy, [xlen, ylen], angle )


def rotbox( xx, yy, xlen, ylen, angle ):
    """
    rotbox( x_center, y_center, x_length, y_length, rotation_angle)

       Note: angle is measured in degrees from the +X axis
    """
    if xlen < 0:
        raise ValueError("X-length of rotbox must be positive")
    if ylen < 0:
        raise ValueError("Y-length of rotbox must be positive")

    return ShapeReg( "rotbox", xx, yy, [xlen, ylen], angle )


def annulus( xx, yy, inner, outer ):
    """
    annulus( x_center, y_center, inner_radius, outer_radius )
    """
    if inner < 0:
        raise ValueError("Inner radius of annulus must be positive")
    if outer < 0:
        raise ValueError("Outer radius of annulus must be positive")

    return ShapeReg( "annulus", xx, yy, [inner, outer], None )


def field():
    """
    field()
    """
    return ShapeReg("field", None, None, None, None )


def pie( xx, yy, inner, outer, start, stop ):
    """
    pie( x_center, y_center, inner_radius, outer_radius, start_angle, stop_angle )

       Note: angles are measured in degrees from the +X axis
    """
    if inner < 0:
        raise ValueError("Inner radius of pie must be positive")
    if outer < 0:
        raise ValueError("Outer radius of pie must be positive")

    return ShapeReg( "pie", xx, yy, [inner, outer], [start, stop] )


def point(xx, yy):
    """
    point( x_center, y_center)
    """
    return ShapeReg( "point", xx, yy, None, None)


def rectangle(llxx, llyy, urxx, uryy, angle=None ):
    """
    rectangle( lower_left_x, lower_left_y, upper_right_x, upper_right_y)
    """
    if angle == 0:
        angle = None

    return ShapeReg( "rectangle", [llxx, urxx], [llyy, uryy], None, angle )


#def rotrectangle(llxx, llyy, urxx, uryy, angle ):
#    """
#    rotrectangle( lower_left_x, lower_left_y, upper_right_x, upper_right_y, rotation_angle)
#    """
#    return ShapeReg( "rotrectangle", [llxx, urxx], [llyy, uryy], None, angle )


def sector( xx, yy, start, stop ):
    """
    sector( x_center, y_center, start_angle, stop_angle

       Note: angles are measured in degrees from +X axis
    """
    return ShapeReg( "sector", xx, yy, None, [start, stop] )


def polygon( *args ):
    """
    polygon(x1, y1, x2, y2, x3, y3,...)
    - or -
    polygon( x_array, y_array ) 
    
    
    Must contain at least 3 points, and same number of x-y pairs.
    An extra point is added if last point is not equal to 1st point.

    """
    if len(args) == 2:
        xx = args[0]
        yy = args[1]
    else:
        xx = [args[i] for i in range(0,len(args),2) ]
        yy = [args[i] for i in range(1,len(args),2) ]

    if (len(xx) != len(yy)):
        raise RuntimeError("Polygon must have equal number of x,y pairs")
    if (len(xx) < 3):
        raise RuntimeError("At least 3 points are needed to make a Polygon")
    return ShapeReg( "polygon", xx, yy, None, None )
