class sdo_geometry(object):
    '''
    Process an Oracle SDO_GEOMETRY object as returned by cx_Oracle.
    '''
    # definitions
    geom_types = {'00': 'UNKNOWN_GEOMETRY', # UNKNOWN_GEOMETRY
                  '01': 'POINT', # POINT
                  '02': 'LINESTRING', # LINE or CURVE
                  '03': 'POLYGON', # POLYGON
                  '04': 'GEOMETRYCOLLECTION', # COLLECTION
                  '05': 'MULTIPOINT', # MULTIPOINT
                  '06': 'MULTILINESTRING', # MULTILINE or MULTICURVE
                  '07': 'MULTIPOLYGON'} # MULTIPOLYGON
    
    # SDO_ETYPES
    # first element of triplet in SDO_ELEM_INFO
    sdo_etype = {0: 'UNSUPPORTED_GEOMETRY_ETYPE',
                 1: 'POINT_ETYPE',
                 2: 'LINE_ETYPE', 
                 4: 'COMPOUND_LINESTRING_ETYPE', 
                 1003: 'EXTERIOR_CLOSED_SHAPE_ETYPE', 
                 2003: 'INTERIOR_CLOSED_SHAPE_ETYPE',
                 1005: 'COMPOUND_EXTERIOR_CLOSED_SHAPE_ETYPE', 
                 2005: 'COMPOUND_INTERIOR_CLOSED_SHAPE_ETYPE'}
    
    # SDO_INTERPRETATIONS
    # second element of triplet in SDO_ELEM_INFO
    # applies to points - sdo_etype 1
    sdo_interpretation_point = {0: 'ORIENTED_POINT', 
                                1: 'SIMPLE_POINT'
                                # n > 1: point cluster with n points
                                }
    
    # applies to lines - sdo_etype 2
    sdo_interpretation_line = {1: 'STRAIGHT_SEGMENTS', 
                               2: 'CURVED_SEGMENTS'}
    
    # applies to polygons - sdo_etypes 1003 and 2003            
    sdo_interpretation_multi = {1: 'SIMPLE_POLY', 
                                2: 'ARCS_POLY', 
                                3: 'RECTANGLE', 
                                4: 'CIRCLE'} 
    
    # complex geometries - sdo_etypes 4, 1005, 2005 always have n > 1
    # n is the number of contiguous subelements
    # subsequent subelements each define one element
    
    
        
    # init function
    def __init__(self, sdo_geometry_obj=None, debug=False, strict=False):
        '''
        Read the geometry from the sdo_geometry object.
        Debug - produce some debug output.
        Strict - if False (default), convert geometry to a supported type where possible,
        e.g. Oriented Point to Point
        '''
        if debug:
            print 'Debugging on.'
        # read the geometry from the sdo_geometry object
        self.geometry = sdo_geometry_obj
        try:
            self.g_type = str(int(self.geometry.__getattribute__('SDO_GTYPE')))
            self.g_srid = int(self.geometry.__getattribute__('SDO_SRID'))
            self.g_point = [self.geometry.__getattribute__('SDO_POINT').X, self.geometry.__getattribute__('SDO_POINT').Y, self.geometry.__getattribute__('SDO_POINT').Z]
            self.g_eleminfo_arr = self.geometry.__getattribute__('SDO_ELEM_INFO')
            self.g_ords_arr = self.geometry.__getattribute__('SDO_ORDINATES')
        except AttributeError:
            if debug:
                print 'Not a geometry'
            return None
        self.dims = self.get_dims()
        self.topology = self.has_topology()
        self.gtype = self.get_gtype()
        #self.wkb = self.get_wkb()
        self.valid = False
        self.wkt = self.get_wkt(debug)
        self.coord_dim = self.st_coorddim()
        #self.is_valid = self.st_isvalid()
    
    # functions
    def get_dims(self):
        '''
        Return dimensions of the geometry.
        This is extracted from the first character of the SDO_ETYPE value
        '''
        return int(self.g_type[0])
    
    def st_coorddim(self):
        '''
        Return dimensions of the geometry.
        This is extracted from the first character of the SDO_ETYPE value
        This function is a synonym of get_dims
        '''
        return self.get_dims()
    
    def has_topology(self):
        '''
        Return true if the geometry has topology, false if it doesn't, otherwise None
        This is extracted from the second character of the SDO_ETYPE value
        '''
        if 0 <= int(self.g_type[1]) <= 1:
            return int(self.g_type[1])
        else:
            return None
            
    def get_geometry_text(self):
        '''
        Return the type of geometry.
        This is extracted from the third and fourth characters of the SDO_ETYPE value
        '''
        return self.geom_types[self.g_type[2:4]]
        
    def get_gtype(self):
        '''
        Return the type of geometry.
        This is extracted from the third and fourth characters of the SDO_ETYPE value
        '''
        return int(self.g_type[2:4])
               
    def get_srid(self):
        '''
        Return the srid of the data.
        This is as defined in the database and may be an Oracle specific format (not EPSG).
        '''
        return self.g_srid    
    
    def get_num_elements(self):
        '''
        Return the total number of elements in the SDO_ORDINATES array.
        These may be used more than once (end and start adjacent elements).
        '''
        if self.g_eleminfo_arr:
            return len(self.g_eleminfo_arr)
        else:
            return None
            
    def get_etype(self):
        '''
        Return the SDO_ETYPE value, if it is defined.
        '''
        if not self.g_eleminfo_arr:
            return None
        else:
            return int(self.g_eleminfo_arr[1])

    def get_interpretation(self):
        '''
        Return the SDO_INTERPRETATION value, if it is defined.
        '''
        if not self.g_eleminfo_arr:
            return None
        else:
            return int(self.g_eleminfo_arr[2])
            
    def get_point_text(self, point):
        '''
        Convert a point (2d or 3d list) into WKT text.
        '''
        if self.dims == 2:
            return '%.12f %.12f' % (point[0], point[1])
        else:
            return '%.12f %.12f %.12f' % (point[0], point[1], point[2])
            
    def to_points(self, l,n):
        '''
        Convert a list l into a list of smaller lists of dimension n.
        '''
        return [l[i:i+n] for i in xrange(0, len(l), n)]
        
    def points_to_WKT(self, points_list):
        '''
        Convert a list of points into WKT text format.
        This can then be used in simple or multi WKT.
        e.g. [x1, y1,...xn,yn] to 'x1 y1, ..., xn yn'
        '''
        wkt_text = ''
        for point in points_list:
            wkt_text += self.get_point_text(point)+','
        wkt_text = wkt_text[:-1]
        return wkt_text
            
    def make_arrays(self, debug=False):
        '''
        Convert the ordinates to an array of points using information from
        SDO_ELEM_INFO
        '''
        num_triplets = len(self.g_eleminfo_arr)/3
        triplets = self.to_points(self.g_eleminfo_arr, 3)
        if debug:
            print 'sets:',num_triplets, '\ntriplets:', triplets
        start_positions = []
        end_positions = []
        elem_type = []
        elements = []
        elem_text = []
        for i, triplet in enumerate(triplets):
            print i, triplet
            if i == 0: # first element
                start_positions.append(0)
            else:
                start_positions.append(int(triplets[i][0]) - 1)
            if i != num_triplets - 1: # intermediate element
                end_positions.append(int(triplets[i+1][0]) - 1)
            else:
                end_positions.append(len(self.g_ords_arr)) # last element
            elem_type.append(int(triplets[i][1]))
            if debug:
                print 'start:', start_positions, 'end:', end_positions, 'length:', len(self.g_ords_arr)
            elements.append(self.g_ords_arr[start_positions[i]:end_positions[i]])
            points = self.to_points(elements[i], self.dims)
            elem_text.append(self.points_to_WKT(points))
        if debug:
            print 'elements:', len(elements), '\nelem_text:', len(elem_text)
        return elem_text, elem_type
            
    def is_valid(self):
        '''
        Return True for valid geometry, False for invalid geometry, None for unsupported
        '''
        # Place holder for now.
        return None
    
    def get_ewkt(self):
        return 'SRID=%d:%s' % (self.get_srid(), self.get_wkt())

    def get_wkt(self, debug=False):
        '''
        Calculate the WKT for the geometry.
        Point geometry may require only SDO_POINT, all other geometries require the use of SDO_ELEM_INFO and SDO_ORDINATES.
        Geometry may be simple or complex.  Simple geometries are defined in one SDO_ELEM_INFO triplet, Complex geometries require multiple SDO_ELEM_INFO triplets.
        '''
        geom_type = self.get_geometry_text()
        if geom_type == 'UNKNOWN_GEOMETRY':
            return None
        
        elif geom_type == 'POINT':
            '''
            WKT - POINT(x y)
            '''
            if self.g_point:
                # case 1 - simple point
                # point_text = '%s(%s)' % (geom_type, self.get_point_text(self.g_point))
                point_text = self.get_point_text(self.g_point)
                if debug:
                    print point_text

            else:
                # case 2 - simple point - extract point from sdo_ordinates
                if  len(self.g_eleminfo_arr) == 3:
                    points = self.to_points(self.g_ords_arr, self.dims)

                # case 3 - oriented point - doesn't seem to be supported in OGC WKT
                # truncate to first point
                else:
                    points = self.to_points(self.g_ords_arr, self.dims)
                    points = points[0]
                    
            if not point_text:
                point_text = self.points_to_WKT(points)
            self.valid = True
            return '%s(%s)' % (geom_type, point_text)  
                
        
        elif geom_type == 'LINESTRING':
            '''
            WKT - LINESTRING(x1 y1,x2 y2,...,xn yn)
            simple element, with a single SDO_ELEM_INFO triplet.
            each point is listed sequentially in the SDO_ORDINATES
            direct conversion to WKT
            '''
            
            # validity check - may need to expand
            if self.get_etype() != 2 or len(self.g_eleminfo_arr) != 3:
                self.valid = False
                return None
            # straight segments
            if self.get_interpretation() == 1 or (self.get_interpretation() == 2 and strict == False):
                points = self.to_points(self.g_ords_arr, self.dims)
                ls_text = self.points_to_WKT(points)
                self.valid = True
                return '%s(%s)' % (geom_type, ls_text)
            # curved segments
            elif self.get_interpretation() == 2 and strict == True:
                # to do
                return None
                
            # compound linestrings - mix of straight and curved elements, each with a SDO_ELEM_INFO triplet, and each overlapping the last by one point
            
        elif geom_type == 'POLYGON':
            '''
            WKT - POLYGON((x1 y1,x2 y2,...,xn yn,x1 y1))
            WKT - POLYGON((x1 y1,x2 y2,...,xn yn,x1 y1)(i1 j1, 12 j2,...,in jn,i1 j1))
            May include more than one element if there are internal holes
            There can be only one external ring, this must be listed counterclockwise
            There may be zero or more internal rings, these must be listed clockwise, and each has one additional SDO_ELEM_INFO triplet.
            Simple case
               Simple element, with a single SDO_ELEM_INFO triplet.
            Complex case
               More than 1 SDO_ELEM_INFO triplet, first for external ring, other(s) for internal rings.
            The triple can have the following values
            [0] - 1 - starting position (base 1)
            [1] - 1003 or 2003 (SDO_ETYPE)
            [2] - 1, 2, 3, or 4 (SDO_INTERPRETATION)
            Each point is listed sequentially in the SDO_ORDINATES
            The last point in a ring is the same as first point.
            '''
            
            # validity check - may need to expand
            if (self.get_etype() != 1003 and self.get_etype() != 2003) or len(self.g_eleminfo_arr) != 3:
                self.valid = False
                return None
            
            if self.get_interpretation() == 1 or (self.get_interpretation() == 2 and strict == False): 
            # straight segments
                elem_text, elem_type = self.make_arrays(debug)
                poly_text = ''
                for elem in elem_text:
                    poly_text = '%s(%s)' % (poly_text, elem)
                self.valid = True
                return '%s(%s)' % (geom_type, poly_text)
            elif self.get_interpretation() == 2:
                # curved segments
                return None
            elif self.get_interpretation() == 3:
                # rectangle - 2 points
                return None
            elif self.get_interpretation() == 4:
                # circle - 3 points
                return None
            else:
                # invalid
                return None

                
            
        elif geom_type == 'GEOMETRYCOLLECTION':
            '''
            Container for other valid geometries:
            WKT - GEOMETRYCOLLECTION(geom1,geom2,..,geomn)
            SDO_ELEM_ARRAY triples will define different geometries.  
            Need to watch for termination of multi geometries & complex geometries.
            Use recursion for each element.
            '''
            elem_text, elem_type = self.make_arrays(debug)
            for i, elem in enumerate(elem_text):
                if elem_type[i] == 1:
                    pass
                else:
                    pass
                           
            pass 
            
        elif geom_type == 'MULTIPOINT': # not tested yet !!!
            '''
            WKT - MULTIPOINT(x1 y1, ... , xn yn)
            MULTIPOINT((x1 y1), ... , (xn yn))
            MultiPoint - series of points
            A single SDO_ELEM_INFO is used
            [0] SDO_STARTING_OFFSET === 1
            [1] SDO_ETYPE = 1
            [2] SDO_INTERPRETATION = number of points in array
            '''
            
            # validity check - may need to expand
            if self.get_etype() != 1:
                valid = False
                return None
            
            else:
                # num_Points = self.get_interpretation()
                points = self.to_points(self.g_eleminfo_arr, self.dims)
                mp_text = self.points_to_WKT(points)
                self.valid = True
                return '%s(%s)' % (geom_type, mp_text)
            
        elif geom_type == 'MULTILINESTRING':
            '''
            WKT - MULTILINESTRING((x1 y1, x2 y2,...,xn yn)(i1 j1, i2 j2,..., in jn))
            MultiLineString - series of line strings
            Each line string is defined by one SDO_ELEM_INFO triplet
            '''
            
            # validity check - may need to expand
            if self.get_etype() != 2:
                valid = False
                return None
            
            else:
                # this is identical to polygons
                if self.get_interpretation() == 1 or (self.get_interpretation() == 2 and strict == False):
                    elem_text, elem_type = self.make_arrays(debug)
                    ml_text = ''
                    for elem in elem_text:
                        ml_text = '%s(%s)' % (ml_text, elem)
                    self.valid = True
                    return '%s(%s)' % (geom_type, ml_text)
                else:
                    # curved segments
                    return None
            
             
            
        elif geom_type == 'MULTIPOLYGON':
            '''
            WKT - MULTIPOLYGON(((x1 y1, ... , x1 y1)((x2 y2, ..., x2 y2)(x3 y3, ..., x3 y3)))
            MultiPolygon - a number of polygons, each with one external ring and zero or more internal rings.
            External rings have SDO_ETYPE of 1003 and are grouped with subsequent internal rings, which have SDO_ETYPE of 2003
            so SDO_ELEM_INFO like [1,1003,1, 1,1003,1, 1,2003,1] maps to (note parenthesis (( ()  (()()) )
            MULTIPOLYGON(((x1 y1, ... , x1 y1)((x2 y2, ..., x2 y2)(x3 y3, ..., x3 y3)))
            SDO_ELEM_INFO[1] == 1003 => start new outer ring
            SDO_ELEM_INFO[2] => start position
            '''
            # reality check
            if self.get_interpretation() == 1 or (self.get_interpretation() == 2 and strict == False):
                pass
            elem_text, elem_type = self.make_arrays(debug)
            mp_text = ''
            for i, elem in enumerate(elem_text):
                if elem_type[i] == 1003: # outer ring
                    # start with '(('
                    mp_text += '('
                elif elem_type == 2003: # inner ring
                    # start with '('
                    pass
                mp_text = '%s(%s)' % (mp_text, elem)
                if i == len(elem_type) - 1: # last element
                    mp_text += ')'
                elif elem_type[i+1] == 1003: # terminate outer ring
                    mp_text += ')'
            self.valid = True
            return '%s(%s)' % (geom_type, mp_text)
        else:
            return None
    
