import os
from django.contrib.gis.geos import Point, LinearRing, fromstr
from math import pi, sin, tan, sqrt, pow
from django.conf import settings
from django.db import connection
from django.core.cache import cache
from lingcod.common.models import KmlCache
import zipfile
import re
import logging
import inspect
import tempfile

def get_logger(caller_name=None):
    try:
        fh = open(settings.LOG_FILE,'w')
        logfile = settings.LOG_FILE
    except:
        print " NOTICE: settings.LOG_FILE not specified or is not writeable; logging to stdout instead" 
        logfile = None

    try:
        level = settings.LOG_LEVEL
    except AttributeError:
        if settings.DEBUG:
            level = logging.DEBUG
        else:
            level = logging.WARNING 
    
    format = '%(asctime)s %(name)s %(levelname)s %(message)s'
    if logfile:
        logging.basicConfig(level=level, format=format, filename=logfile)
    else:
        logging.basicConfig(level=level, format=format)

    if not caller_name:
        caller = inspect.currentframe().f_back
        caller_name = caller.f_globals['__name__']

    logger = logging.getLogger(caller_name)

    if logfile and settings.DEBUG:
        import sys
        strm_out = logging.StreamHandler(sys.__stdout__)
        logger.addHandler(strm_out)

    return logger

log = get_logger()

def KmlWrap( string ):
    return '<?xml version="1.0" encoding="UTF-8"?> <kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">' + string + '</kml>'
    
    
def LookAtKml( geometry ):
    lookAtParams = ComputeLookAt( geometry )
    return '<LookAt><latitude>%f</latitude><longitude>%f</longitude><range>%f</range><tilt>%f</tilt><heading>%f</heading><altitudeMode>clampToGround</altitudeMode></LookAt>' % (lookAtParams['latitude'], lookAtParams['longitude'], lookAtParams['range'], lookAtParams['tilt'], lookAtParams['heading'] )

def LargestPolyFromMulti(geom): 
    """ takes a polygon or a multipolygon geometry and returns only the largest polygon geometry"""
    if geom.num_geom > 1:
        largest_area = 0.0
        for g in geom: # find the largest polygon in the multi polygon 
            if g.area > largest_area:
                largest_geom = g
                largest_area = g.area
    else:
        largest_geom = geom
    return largest_geom  

def LargestLineFromMulti(geom): 
    """ takes a line or a multiline geometry and returns only the longest line geometry"""
    if geom.num_geom > 1:
        largest_length = 0.0
        for g in geom: # find the largest polygon in the multi polygon 
            if g.length > largest_length:
                largest_geom = g
                largest_length = g.length
    else:
        largest_geom = geom
    return largest_geom  
   
def angle(pnt1,pnt2,pnt3):
    """
    Return the angle in radians between line(pnt2,pnt1) and line(pnt2,pnt3)
    """
    cursor = connection.cursor()
    if pnt1.srid:
        query = "SELECT abs(ST_Azimuth(ST_PointFromText(\'%s\',%i), ST_PointFromText(\'%s\',%i) ) - ST_Azimuth(ST_PointFromText(\'%s\',%i), ST_PointFromText(\'%s\',%i)) )" % (pnt2.wkt,pnt2.srid,pnt1.wkt,pnt1.srid,pnt2.wkt,pnt2.srid,pnt3.wkt,pnt3.srid)
    else:
        query = "SELECT abs(ST_Azimuth(ST_PointFromText(\'%s\'), ST_PointFromText(\'%s\') ) - ST_Azimuth(ST_PointFromText(\'%s\'), ST_PointFromText(\'%s\')) )" % (pnt2.wkt,pnt1.wkt,pnt2.wkt,pnt3.wkt)
    #print query
    cursor.execute(query)
    row = cursor.fetchone()
    return row[0]
    
def angle_degrees(pnt1,pnt2,pnt3):
    """
    Return the angle in degrees between line(pnt2,pnt1) and line(pnt2,pnt3)
    """
    rads = angle(pnt1,pnt2,pnt3)
    return rads * (180/pi)

def spike_ring_indecies(line_ring,threshold=0.01):
    """
    Returns a list of point indexes if ring contains spikes (angles of less than threshold degrees).
    Otherwise, an empty list.
    """
    radian_thresh = threshold * (pi/180)
    spike_indecies = []
    for i,pnt in enumerate(line_ring.coords):
        if(i==0 and line_ring.num_points > 3): # The first point  ...which also equals the last point
            p1_coords = line_ring.coords[len(line_ring.coords) - 2]
        elif(i==line_ring.num_points-1): # The first and last point are the same in a line ring so we're done
            break
        else:
            p1_coords = line_ring.coords[i - 1]
            
        # set up the points for the angle test.
        p1_str = 'POINT (%f %f), %i' % (p1_coords[0], p1_coords[1], settings.GEOMETRY_DB_SRID)
        p1 = fromstr(p1_str)
        p2_str = 'POINT (%f %f), %i' % (pnt[0],pnt[1],settings.GEOMETRY_DB_SRID)
        p2 = fromstr(p2_str)
        p3_coords = line_ring.coords[i + 1]
        p3_str = 'POINT (%f %f), %i' % (p3_coords[0], p3_coords[1], settings.GEOMETRY_DB_SRID)
        p3 = fromstr(p3_str)
        if( angle(p1,p2,p3) <= radian_thresh ):
            spike_indecies.append(i)
    
    return spike_indecies

def remove_spikes(poly,threshold=0.01):
    """
    Looks for spikes (angles < threshold degrees) in the polygons exterior ring.  If there are spikes,
    they will be removed and a polygon (without spikes) will be returned.  If no spikes are found, method
    will return original geometry.
    
    NOTE: This method does not examine or fix interior rings.  So far those haven't seemed to have been a problem.
    """
    line_ring = poly.exterior_ring
    spike_indecies = spike_ring_indecies(line_ring,threshold=threshold)
    if( spike_indecies ):
        for i,org_index in enumerate(spike_indecies):
            if(org_index==0): # special case, must remove first and last point, and add end point that overlaps new first point
                # get the list of points
                pnts = list(line_ring.coords)
                # remove the first point
                pnts.remove(pnts[0])
                # remove the last point
                pnts.remove(pnts[-1])
                # append a copy of the new first point (old second point) onto the end so it makes a closed ring
                pnts.append(pnts[0])
                # replace the old line ring
                line_ring = LinearRing(pnts)
            else:
                line_ring.remove(line_ring.coords[org_index])
        poly.exterior_ring = line_ring
    return poly
    
def clean_geometry(geom):
    """Send a geometry to the cleanGeometry stored procedure and get the cleaned geom back."""
    cursor = connection.cursor()
    query = "select cleangeometry(st_geomfromewkt(\'%s\')) as geometry" % geom.ewkt
    cursor.execute(query)
    row = cursor.fetchone()
    newgeom = fromstr(row[0])
    # sometimes, clean returns a multipolygon
    geometry = LargestPolyFromMulti(newgeom)

    if not geometry.valid or (geometry.geom_type != 'Point' and geometry.num_coords < 2):
        raise Exception("I can't clean this geometry. Dirty, filthy geometry. This geometry should be ashamed.")
    else:
        return geometry

# transforms the geometry to the given srid, checks it's validity and 
# cleans it if necessary, transforms it back into the original srid and
# cleans again if needed before returning 
# Note, it does not scrub the geometry before transforming, so if needed
# call check_validity(geo, geo.srid) first.
def ensure_clean(geo, srid):
    old_srid = geo.srid
    if geo.srid is not srid:
        geo.transform(srid)
    geo = clean_geometry(geo)
    if not geo.valid:
        raise Exception("ensure_clean could not produce a valid geometry.")
    if geo.srid is not old_srid:
        geo.transform(old_srid)
        geo = clean_geometry(geo)
        if not geo.valid:
            raise Exception("ensure_clean could not produce a valid geometry.")
    return geo
   
def ComputeLookAt( geometry ):

    lookAtParams = {}

    DEGREES = pi / 180.0
    EARTH_RADIUS = 6378137.0

    trans_geom = geometry.clone()
    trans_geom.transform(settings.GEOMETRY_DB_SRID) # assuming this is an equal area projection measure in meters

    w = trans_geom.extent[0]
    s = trans_geom.extent[1]
    e = trans_geom.extent[2]
    n = trans_geom.extent[3]
    
    center_lon = trans_geom.centroid.y
    center_lat = trans_geom.centroid.x
    
    lngSpan = (Point(w, center_lat)).distance(Point(e, center_lat)) 
    latSpan = (Point(center_lon, n)).distance(Point(center_lon, s))
    
    aspectRatio = 1.0

    PAD_FACTOR = 1.5 # add 50% to the computed range for padding
    
    aspectUse = max( aspectRatio, min((lngSpan / latSpan),1.0))
    alpha = (45.0 / (aspectUse + 0.4) - 2.0) * DEGREES # computed experimentally;
  
    # create LookAt using distance formula
    if lngSpan > latSpan:
        # polygon is wide
        beta = min(DEGREES * 90.0, alpha + lngSpan / 2.0 / EARTH_RADIUS)
    else:
        # polygon is taller
        beta = min(DEGREES * 90.0, alpha + latSpan / 2.0 / EARTH_RADIUS)
  
    lookAtParams['range'] = PAD_FACTOR * EARTH_RADIUS * (sin(beta) *
        sqrt(1.0 / pow(tan(alpha),2.0) + 1.0) - 1.0)
        
    trans_geom.transform(4326)
    
    lookAtParams['latitude'] = trans_geom.centroid.y
    lookAtParams['longitude'] = trans_geom.centroid.x
    lookAtParams['tilt'] = 0
    lookAtParams['heading'] = 0

    return lookAtParams
    
def get_class(path):
    from django.utils import importlib
    module,dot,klass = path.rpartition('.')
    m = importlib.import_module(module)
    return m.__getattribute__(klass)
    
def kml_errors(kmlstring):
    import feedvalidator
    from feedvalidator import compatibility
    events = feedvalidator.validateString(kmlstring, firstOccurrenceOnly=1)['loggedEvents'] 

    # Three levels of compatibility
    # "A" is most basic level
    # "AA" mimics online validator
    # "AAA" is experimental; these rules WILL change or disappear in future versions
    filterFunc = getattr(compatibility, "AA")
    events = filterFunc(events)

    # there are a few annoyances with feedvalidator; specifically it doesn't recognize 
    # KML ExtendedData element 
    # or our custom 'mm' namespance 
    # or our custom atom link relation
    # or space-delimited Icon states
    # so we ignore all related events
    events = [x for x in events if not (
                (isinstance(x,feedvalidator.logging.UndefinedElement) 
                    and x.params['element']==u'ExtendedData') or
                (isinstance(x,feedvalidator.logging.UnregisteredAtomLinkRel) 
                    and x.params['value']==u'marinemap.update_form') or
                (isinstance(x,feedvalidator.logging.UnregisteredAtomLinkRel) 
                    and x.params['value']==u'marinemap.create_form') or
                (isinstance(x,feedvalidator.logging.UnknownNamespace) 
                    and x.params['namespace']==u'http://marinemap.org') or
                (isinstance(x,feedvalidator.logging.UnknownNamespace) 
                    and x.params['namespace']==u'http://www.google.com/kml/ext/2.2') or
                (isinstance(x,feedvalidator.logging.InvalidItemIconState) 
                    and x.params['element']==u'state' and ' ' in x.params['value']) or
                (isinstance(x,feedvalidator.logging.UnregisteredAtomLinkRel) 
                    and x.params['element']==u'atom:link' and 'workspace' in x.params['value'])
                )]

    from feedvalidator.formatter.text_plain import Formatter
    output = Formatter(events)

    if output:
        errors = []
        for i in range(len(output)):
            errors.append( (events[i],events[i].params,output[i],kmlstring.splitlines()[events[i].params['backupline']]) )
        return errors
    else:
        return None

def hex8_to_rgba(hex8):
    """
    Takes an 8 digit hex color string (used by Google Earth) and converts it to RGBA colorspace
    * 8-digit hex codes use AABBGGRR (R - red, G - green, B - blue, A - alpha transparency)
    """
    hex8 = str(hex8.replace('#',''))
    if len(hex8) != 8:
        raise Exception("Hex8 value must be exactly 8 digits")
    hex_values = [hex8[i:i+2:1] for i in xrange(0, len(hex8), 2)]
    rgba_values = [int(x,16) for x in hex_values]
    rgba_values.reverse()
    return rgba_values
    
from django.utils.importlib import import_module

def load_session(request, session_key):
    if session_key and session_key != '0':
        engine = import_module(settings.SESSION_ENGINE)
        request.session = engine.SessionStore(session_key)

def valid_browser(ua):
    """
    Returns boolean depending on whether we support their browser
    based on their HTTP_USER_AGENT

    Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7
    Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-us) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10
    Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b7) Gecko/20100101 Firefox/4.0b7
    """
    supported_browsers = [
            ('Firefox', 3, 5, 'Mac'),
            ('Firefox', 4, 0, 'Mac'),
            ('Safari', 3, 1, 'Mac'),
            ('Chrome', 6, 0, 'Mac'),
            ('Firefox', 3, 5, 'Windows'),
            ('Firefox', 4, 0, 'Windows'),
            ('Chrome', 1, 0, 'Windows'),
            ('IE', 8, 0, 'Windows'),
    ]

    from lingcod.common import uaparser

    bp = uaparser.browser_platform(ua)
    if not bp.platform:
        log.warn("Platform is None: UA String is '%s'" % ua)

    for sb in supported_browsers:
        if bp.family == sb[0] and \
           ((bp.v1 == sb[1] and bp.v2 >= sb[2]) or bp.v1 > sb[1]) and \
           bp.platform == sb[3]:
               return True

    return False

class KMZUtil:
    """
    Recursively adds a directory to a zipfile
    modified from http://stackoverflow.com/questions/458436/adding-folders-to-a-zip-file-using-python
    
    from lingcod.common.utils import ZipUtil
    zu = ZipUtil()
    filename = 'TEMP.zip'
    directory = 'kmldir' # containing doc.kml, etc
    zu.toZip(directory, filename)
    """
    def toZip(self, file, filename):
        zip_file = zipfile.ZipFile(filename, 'w')
        if os.path.isfile(file):
            zip_file.write(file)
        else:
            self.addFolderToZip(zip_file, file)
        zip_file.close()

    def addFolderToZip(self, zip_file, folder): 
        if not folder or folder == '':
            folder_path = '.'
        else:
            folder_path = folder

        # first add doc.kml - IMPORTANT that it be the first file added!
        doc = os.path.join(folder,'doc.kml')
        if os.path.exists(doc):
            #print 'File added: ' + str(doc)
            zip_file.write(doc)

        for file in os.listdir(folder_path):
            full_path = os.path.join(folder, file)
            if os.path.isfile(full_path) and not full_path.endswith("doc.kml"):
                #print 'File added: ' + str(full_path)
                zip_file.write(full_path)
            elif os.path.isdir(full_path):
                #print 'Entering folder: ' + str(full_path)
                self.addFolderToZip(zip_file, full_path)

def isCCW(ring):
    """
    Determines if a LinearRing is oriented counter-clockwise or not
    """
    area = 0.0
    for i in range(0,len(ring)-1):
        p1 = ring[i]
        p2 = ring[i+1]
        area += (p1[1] * p2[0]) - (p1[0] * p2[1])

    if area > 0:
        return False
    else:
        return True


from django.contrib.gis.geos import Polygon
def forceRHR(polygon):
    """
    reverses rings so that polygon follows the Right-hand rule
    exterior ring = clockwise
    interior rings = counter-clockwise
    """
    assert polygon.geom_type == 'Polygon'
    if polygon.empty:
        return poly
    exterior = True
    rings = []
    for ring in polygon:
        assert ring.ring # Must be a linear ring at this point
        if exterior:
            if isCCW(ring):
                ring.reverse()
            exterior = False
        else:
            if not isCCW(ring):
                ring.reverse()
        rings.append(ring)
    poly = Polygon(*rings)
    return poly

def forceLHR(polygon):
    """
    reverses rings so that geometry complies with the LEFT-hand rule
    Google Earth KML requires this oddity
    exterior ring = counter-clockwise
    interior rings = clockwise
    """
    assert polygon.geom_type == 'Polygon'
    assert not polygon.empty
    exterior = True
    rings = []
    for ring in polygon:
        assert ring.ring # Must be a linear ring at this point
        if exterior:
            if not isCCW(ring):
                ring.reverse()
            exterior = False
        else:
            if isCCW(ring):
                ring.reverse()
        rings.append(ring)
    poly = Polygon(*rings)
    return poly

def asKml(input_geom, altitudeMode=None, uid=''):
    """
    Performs three critical functions for creating suitable KML geometries:
     - simplifies the geoms (lines, polygons only)
     - forces left-hand rule orientation
     - sets the altitudeMode shape 
       (usually one of: absolute, clampToGround, relativeToGround)
    """
    if altitudeMode is None:
        try:
            altitudeMode = settings.KML_ALTITUDEMODE_DEFAULT
        except:
            altitudeMode = None

    key = "asKml_%s_%s_%s" % (input_geom.wkt.__hash__(), altitudeMode, uid)
    kmlcache, created = KmlCache.objects.get_or_create(key=key)
    kml = kmlcache.kml_text
    if not created and kml:
        return kml

    log.debug("%s ...no kml cache found...seeding" % key)

    latlon_geom = input_geom.transform(settings.GEOMETRY_CLIENT_SRID, clone=True)

    if latlon_geom.geom_type in ['Polygon','LineString']:
        geom = latlon_geom.simplify(settings.KML_SIMPLIFY_TOLERANCE_DEGREES)
        # Gaurd against invalid geometries due to bad simplification
        # Keep reducing the tolerance til we get a good one
        if geom.empty or not geom.valid: 
            toler = settings.KML_SIMPLIFY_TOLERANCE_DEGREES
            maxruns = 20
            for i in range(maxruns):
                toler = toler / 3.0
                geom = latlon_geom.simplify(toler)
                log.debug("%s ... Simplification failed ... tolerance=%s" % (key,toler))
                if not geom.empty and geom.valid: 
                    break
            if i == maxruns-1:
                geom = latlon_geom
    else:
        geom = latlon_geom

    if geom.geom_type == 'Polygon':
        geom = forceLHR(geom)
        
    kml = geom.kml

    if altitudeMode and geom.geom_type == 'Polygon':
        kml = kml.replace('<Polygon>', '<Polygon><altitudeMode>%s</altitudeMode><extrude>1</extrude>' % altitudeMode)
        # The GEOSGeometry.kml() method always adds a z dim = 0
        kml = kml.replace(',0 ', ',%s ' % settings.KML_EXTRUDE_HEIGHT)

    kmlcache.kml_text = kml
    kmlcache.save()
    return kml

def enable_sharing(group=None):
    """
    Give group permission to share models 
    Permissions are attached to models but we want this perm to be 'global'
    Fake it by attaching the perm to the Group model (from the auth app)
    We check for this perm like: user1.has_perm("auth.can_share_features")
    """
    from django.contrib.auth.models import Permission, Group
    from django.contrib.contenttypes.models import ContentType

    try:
        p = Permission.objects.get(codename='can_share_features')
    except Permission.DoesNotExist:
        gct = ContentType.objects.get(name="group")
        p = Permission.objects.create(codename='can_share_features',name='Can Share Features',content_type=gct)
        p.save()
 
    # Set up default sharing groups
    for groupname in settings.SHARING_TO_PUBLIC_GROUPS:
        g, created = Group.objects.get_or_create(name=groupname)
        g.permissions.add(p)
        g.save()
    
    for groupname in settings.SHARING_TO_STAFF_GROUPS:
        g, created = Group.objects.get_or_create(name=groupname)
        g.permissions.add(p)
        g.save()

    if group:
        # Set up specified group
        group.permissions.add(p)
        group.save()
    return True


'''
Returns a path to desired resource (image file)
Called from within pisaDocument via link_callback parameter (from pdf_report)
'''    
def fetch_resources(uri, rel):
    import os
    import settings
    import datetime
    import random
    import tempfile
    import urllib2
    from django.test.client import Client

    if uri.startswith('http'):
        # An external address assumed to require no authentication
        req = urllib2.Request(uri)
        response = urllib2.urlopen(req)
        content = response.read()
    elif 'staticmap' in uri:
        # A staticmap url .. gets special treatment due to permissions
        from lingcod.staticmap.temp_save import img_from_params
        params = get_params_from_uri(uri)
        content = img_from_params(params, None)
    else:
        # An internal address assumed; use the django test client
        client = Client()
        response = client.get(uri)
        content = response.content
        # alternate way
        # path = os.path.join(settings.MEDIA_ROOT, uri.replace(settings.MEDIA_URL, ""))

    randnum = random.randint(0, 1000000000)
    timestamp = datetime.datetime.now().strftime('%m_%d_%y_%H%M')       
    filename = 'resource_%s_%s.tmp' % (timestamp,randnum)
    pathname = os.path.join(tempfile.gettempdir(),filename)
    fh = open(pathname,'wb')
    fh.write(content)
    fh.close()
    return pathname

'''
Returns a dictionary representation of the parameters attached to the given uri
Called by fetch_resources
'''    
def get_params_from_uri(uri):
    from urlparse import urlparse
    results = urlparse(uri)
    params = {}
    if results.query == '':
        return params
    params_list = results.query.split('&')
    for param in params_list:
        pair = param.split('=')
        params[pair[0]] = pair[1]
    return params

def is_text(s):
    """
    Tests a string to see if it's binary
    borrowed from http://code.activestate.com/recipes/173220-test-if-a-file-or-string-is-text-or-binary/
    """
    import string
    from django.utils.encoding import smart_str
    s = smart_str(s)
    text_characters = "".join(map(chr, range(32, 127)) + list("\n\r\t\b"))
    _null_trans = string.maketrans("", "")

    if "\0" in s:
        return False
    if not s:
        return True 

    # Get the non-text characters (maps a character to itself then
    # use the 'remove' option to get rid of the text characters.)
    t = s.translate(_null_trans, text_characters)

    # If more than 30% non-text characters, then
    # this is considered a binary file
    if float(len(t))/len(s) > 0.30:
        return False
    return True
