from __future__ import absolute_import
from __future__ import print_function
import numpy as np
import logging
import re

from scipy.spatial import Delaunay, ConvexHull
from scipy.special import factorial
from scipy.spatial.qhull import QhullError

from lmatools.lasso import empirical_charge_density as cd

# logger = logging.getLogger('FlashAutorunLogger')
class Flash(object):
    def __init__(self, points):
        self.points = points

class FlashMetadata(object):

    def __init__(self, headerText):
        #Search the header for info on how the data is written

        self.header = headerText

        isColumnHeaderLine = r"^Data:(.*)"
        matchDataFormatLine = re.compile(isColumnHeaderLine, re.IGNORECASE | re.MULTILINE)

        isDataStartTime = r"^Data start time:(.*)"
        matchDataStartTimeLine = re.compile(isDataStartTime, re.IGNORECASE | re.MULTILINE)

        secAnalyzedLine = r"^Number of seconds analyzed:(.*)"
        matchSecAnalyzedLine = re.compile(secAnalyzedLine, re.IGNORECASE | re.MULTILINE)


        startTimeMatch = matchDataStartTimeLine.search(headerText)
        if startTimeMatch:
            #Looking to deal with something like: " 06/28/04 23:50:00"
            dateAndTime = startTimeMatch.group(1).split()
            self.startmonth, self.startday, self.startyear = [ int(datePart) for datePart in dateAndTime[0].split('/') ]
            self.starthour, self.startminute, self.startsecond = [ int(timePart) for timePart in dateAndTime[1].split(':') ]
            if self.startyear < 1000 and self.startyear > 70:
                self.startyear += 1900
            else:
                self.startyear += 2000

        secAnalyzedMatch=matchSecAnalyzedLine.search(headerText)
        if secAnalyzedMatch:
            self.sec_analyzed = int(secAnalyzedMatch.group(1))


        # formatMatch=matchDataFormatLine.search(headerText)
        # if formatMatch:
        #     columns = formatMatch.group(1).split(',')
        #     self.columns = [columnName.strip() for columnName in columns]

 
def barotropic_rho(z):
    rho = 1.225e9 #kg/km^3
    H = 8. #km
    return rho*np.exp(-z/H) 
    
def poly_area(x,y):
    """ Calculate the area of a non-self-intersecting planar polygon.
        x0y1 - x0y0 + x1y2 - x2y1 + x2y3 - x2y2 + ... + xny0 - x0yn 
    """
    det = x[:-1]*y[1:] - x[1:]*y[:-1] # determinant
    area = det.sum()
    area += x[-1]*y[0] - x[0]*y[-1] # wrap-around terms in determinant
    area *= 0.5
    return area
    

def hull_volume(xyz):
    """ Calculate the volume of the convex hull of 3D (X,Y,Z) LMA data.
        xyz is a (N_points, 3) array of point locations in space. """
    assert xyz.shape[1] == 3
        
    tri = Delaunay(xyz[:,0:3])
    vertices = tri.points[tri.vertices]
    
    # This is the volume formula in 
    # https://github.com/scipy/scipy/blob/master/scipy/spatial/tests/test_qhull.py#L106
    # Except the formula needs to be divided by ndim! to get the volume, cf., 
    # http://en.wikipedia.org/wiki/Simplex#Geometric_properties
    # Credit Pauli Virtanen, Oct 14, 2012, scipy-user list
    
    q = vertices[:,:-1,:] - vertices[:,-1,None,:]
    simplex_volumes = (1.0 / factorial(q.shape[-1])) * np.fromiter(
           (np.linalg.det(q[k,:,:]) for k in range(tri.nsimplex)) , dtype=float)
    # print vertices.shape # number of simplices, points per simplex, coords
    # print q.shape
    
    # The simplex volumes have negative values since they are oriented 
    # (think surface normal direction for a triangle
    volume=np.sum(np.abs(simplex_volumes))
    return volume, vertices, simplex_volumes

##############ADDED 01/05/2017 ###############
def energy(area, separation, zinit, constant, eta):
    #Charge separation computed from 27th and 73rd percentiles of 
    #flash altitude source locations - marks where the most sources are typically
    #found from synthetic flashes generated in the NSSL COMMAS.
    #
    #eta = 0.01 is recommended and is a ballpark neutrlization efficiency as found in Salinas et al. [In Progress - 060220]

    distance = separation #np.abs(random)
    density  = cd.rho_retrieve(area, distance, zinit, separation, False, None) #None - No constant charge density specified
    rho,w    = density.calculate()
    return(eta*w)
##############################################   

def calculate_flash_stats(flash, min_pts=2):
    logger = logging.getLogger('FlashAutorunLogger')
    
    Re = 6378.137*1000;           #Earth's radius in m
    pi = np.pi
    
    flash.pointCount = flash.points.shape[0]
    
    fl_id = np.unique(flash.points['flash_id'])
    assert (fl_id.shape[0] == 1)
    flash.id = fl_id[0]
    
    
    lat = np.asarray(flash.points['lat'],dtype=float) 
    lon = np.asarray(flash.points['lon'],dtype=float) 
    alt = np.asarray(flash.points['alt'], dtype=float)
    # 
    # # mean location of all points
    latavg, lonavg, altavg = lat.mean(), lon.mean(), alt.mean()
    x = Re * (np.radians(lonavg) - np.radians(lon)) * np.cos(np.radians(latavg))
    y = Re * (np.radians(latavg) - np.radians(lat))
    z = altavg - alt
    # r_sq = x**2.0 + y**2.0 + z**2.0
    # sigma_sq = r_sq.sum()/r_sq.shape[0]
    # sigma = np.std(r_sq)

    separation     = np.abs(np.percentile(alt,73) - np.percentile(alt,27))
    flash_init_idx = np.argmin(flash.points['time'])
    zinit = alt[flash_init_idx] #in meters
    area  = 0.0
    eta   = 0.01

    if flash.pointCount > 2:
        try:
            # find the convex hull and calculate its area
            cvh = ConvexHull(np.vstack((x,y)).T)
            # NOT cvh.area - it is the perimeter in 2D. 
            # cvh.area is the surface area in 3D.
            area = cvh.volume
        except IndexError:
            # tends to happen when a duplicate point causes the point count to
            # drop to 2, leading to a degenerate polygon with no area
            logger.warning('Setting area to 0 for flash with points %s, %s' % (x, y))
            area=0.0
        except KeyError:
            # hull indexing has problems here
            logger.warning('Setting area to 0 for flash with points %s, %s' % (x, y))
            area=0.0
           
    if area == 0.0:
        energy_estimate = 0.
    else:        
        energy_estimate = energy(area, separation, zinit, False, eta)
            
    volume = 0.0
    if flash.pointCount > 3:
        # Need four points to make at least one tetrahedron.
        try:
            volume, vertices, simplex_volumes = hull_volume(np.vstack((x,y,z)).T)
        except QhullError:
            # this can happen with a degenerate first simplex - all points are
            # coplanar to machine precision. Try again, after adding a tiny amount
            # to the first point.
            print("Perturbing one source to help triangulation for flash with {0} points".format(flash.pointCount))
            # we can tolerate perturbing by no more than 1 m
            machine_eps = 1.0 # np.finfo(x.dtype).eps
            perturb = 2*machine_eps*np.random.random(size=3)-machine_eps
            x[0] += perturb[0]
            y[0] += perturb[1]
            z[0] += perturb[2]
            volume, vertices, simplex_volumes = hull_volume(np.vstack((x,y,z)).T)
    
    flash_init_idx = np.argmin(flash.points['time'])
    
    ###ROUGH APPROXIMATION FOR NOW: #######################
    air_density = barotropic_rho(alt[flash_init_idx]*1e-3)
    if volume == 0.:
        specific_energy = 0.
    else:
        specific_energy = energy_estimate / ((volume / 1.0e9) * air_density)
    #######################################################
    
    flash.start   = flash.points[flash_init_idx]['time']
    flash.end     = flash.points['time'].max()
    flash.duration = flash.end - flash.start
    flash.area    = area / 1.0e6  # km^2, 1000x1000
    flash.initLat = lat[flash_init_idx] 
    flash.initLon = lon[flash_init_idx]
    flash.initStd = 0.0
    flash.initAlt = alt[flash_init_idx]
    flash.initPts = (int(flash_init_idx),)
    flash.ctralt  = altavg
    flash.ctrlat  = latavg
    flash.ctrlon  = lonavg
    flash.volume  = volume / 1.0e9 # km^3, 1000x1000x1000 m
    #CHANGED 03-20-17
    flash.total_energy    = energy_estimate    #flash.energy ---> flash.tot_energy
    flash.specific_energy = specific_energy  #flash.tot_energy ---> flash.specific_energy