from __future__ import division
import json
import pandas as pd
import numpy as np
import math
from datetime import datetime
from scipy.interpolate import interp1d
from matplotlib import path
import matplotlib.pyplot as plt
from hydroinform.core import lazy_property


class HydroTs(object):
    def __init__(self, TimeSeries):
        self.time_series= TimeSeries
        self.__np_values = TimeSeries.values
        self.__np_index = TimeSeries.index.values
        self.__years = TimeSeries.index.year
        self.__values_range = range(len(self.__np_values))


    def EventFreq(self, Comparer):
        EventCount = {}
        EventLength = {}
        currentyear = self.__years[0]
        NumberOfEvents = 0;
        CurrentEventLengt = []
        InEvent = False

        for i in self.__values_range:
            #We got a new year. Store data
            if (self.__years[i] != currentyear):
                EventCount[currentyear]= NumberOfEvents
                EventLength[currentyear]= CurrentEventLengt
                currentyear = self.__years[i];
                NumberOfEvents = 0;
                CurrentEventLengt = [];
                CurrentEventLengt.append(0);
    
            if (Comparer(self.__np_values[i])):
            #This is a new event
                if not InEvent:
                    NumberOfEvents+=1
                    InEvent = True
                    CurrentEventLengt.append(0)
            #Increase number of days in event
                CurrentEventLengt[len(CurrentEventLengt) - 1]+=1
            else:
                InEvent = False
    
        if not currentyear in EventCount:
            EventCount[currentyear] = NumberOfEvents
            EventLength[currentyear] = CurrentEventLengt
        #First average the length on years, then average the averages
        #Average length on years:
        ayear=np.mean([v for k,v in EventCount.items()])
        lengthMoreThenZero=[v for k,v in EventLength.items() if len(v)>0]
        if len(lengthMoreThenZero)== 0:
            return [ayear, 0]
        return [ayear, np.mean([np.mean(v) for v in lengthMoreThenZero])]   

    @lazy_property
    def Q95(self):
        self.__set_percentiles()
        return self._lazy_Q95

    @lazy_property
    def Q90(self):
        self.__set_percentiles()
        return self._lazy_Q90


    @lazy_property
    def Q75(self):
        self.__set_percentiles()
        return self._lazy_Q75

    @lazy_property
    def Q50(self):
        self.__set_percentiles()
#        Q50 =self.time_series.median()
        return self._lazy_Q50

    @lazy_property
    def Q25(self):
        self.__set_percentiles()
        return self._lazy_Q25


    def __set_percentiles(self):
        q=[0.05, 0.1, 0.25,0.5,0.75]
        p= np.quantile(self.__np_values, q, interpolation ='linear')
        self._lazy_Q95 =p[0]
        self._lazy_Q90 =p[1]
        self._lazy_Q75=p[2]
        self._lazy_Q50=p[3]
        self._lazy_Q25=p[4]

    def fre25(self):
        return self.EventFreq(lambda v : v > self.Q25)[0]

    def fre75(self):
        return self.EventFreq(lambda v : v > self.Q75)[0]

    def fre1(self):
        return self.EventFreq(lambda v : v > self.Q50)[0]

    def fre3(self):
        return self.EventFreq(lambda v : v > 3*self.Q50)[0]

    def fre7(self):
        return self.EventFreq(lambda v : v > 7*self.Q50)[0]

    """
    Calculates the average number of timesteps where the value is above a threshold
    """
    def duration(self, Threshold):
        Durations = []
        Durations.append(0)

        for i in self.__values_range:
            if (self.__np_values[i] > Threshold):
                Durations[-1]+=1
            elif self.__np_values[i] <= Threshold and Durations[-1] > 0:
                Durations.append(0)
    
        if Durations[-1] == 0:
            del Durations[-1]
        if (len(Durations) == 0):
            return 0
        return np.mean(Durations)

    def values_above(self, Threshold):
        Durations = []
        Durations.append([])

        for i in self.__values_range:
            if (self.__np_values[i] > Threshold):
                Durations[-1].append(self.__np_values[i])
            elif self.__np_values[i] <= Threshold and len(Durations[-1]) > 0:
                Durations.append([])
    
        if len(Durations[-1]) == 0:
            del Durations[-1]
        return Durations




    """
    Calculates the average number of timesteps where the value is below a threshold
    """
    def DurationBelow(self, Threshold):
        Durations = []
        Durations.append(0)

        for i in self.__values_range:
            if (self.__np_values[i] < Threshold):
                Durations[-1]+=1
            elif self.__np_values[i] >= Threshold and Durations[-1] > 0:
                Durations.append(0)
    
        if Durations[-1] == 0:
            del Durations[-1]
        if (len(Durations) == 0):
            return 0
        return np.mean(Durations)


    def DVFIEQR(self, sin = 2):
        if self.Q50==0:
            return 0
        return 0.217 + 0.103 * sin + 0.02 * self.Q90/self.Q50 * self.EventFreq(lambda v : v > self.Q50)[0];


    def DVPIEQR(self):
        return 0.546 + 0.02 * self.fre25() - 0.019 * self.dur3() - 0.025 * self.fre75()


    def DFFV_EQR(self, sin = 2):
        p25 = self.Q75;
        p75 = self.Q25;
        return 0.811 * self.BaseFlowIndex() + 0.058 * sin + 0.05 * self.EventFreq(lambda v : v >= p75)[0] - 0.319 - 0.0413 * self.EventFreq(lambda v : v <= p25)[0];
    
    
    def medianmin(self):
        return self.time_series.resample("A").min().median()

    def medianmax(self):
        return self.time_series.resample("A").max().median()

    def medmin(self):
        return self.medianmin()/self.Q50

    def medmax(self):
        return self.medianmax()/self.Q50

    def mamin(self):
        return self.time_series.resample("A").min().mean()/self.Q50

    def mamax(self):
        return self.time_series.resample("A").max().mean()/self.Q50

    def mamax7(self):
        return self.__mamax( 7)

    def mamax30(self):
        return self.__mamax( 30)

    def fremedmin(self):
        val =self.medmin()
        return self.EventFreq( lambda v : v < val)[0]

    def mf(self):
        return self.time_series.mean()

    def cv(self):
        return self.time_series.var()

    def q1(self):
        return self.time_series.quantile(0.01, interpolation='linear')/self.Q50

    def q10(self):
        return self.time_series.quantile(0.1, interpolation='linear')/self.Q50

    def q25(self):
        return self.time_series.quantile(0.25, interpolation='linear')/self.Q50

    def q75(self):
        return self.time_series.quantile(0.75, interpolation='linear')/self.Q50

    def q90(self):
        return self.time_series.quantile(0.90, interpolation='linear')/self.Q50

    def durmedmin(self):
        val =medmin(self.time_series)
        return self.Duration(self.time_series, val)

    def dur75(self):
        val =self.Q75()
        return self.DurationBelow( val)

    def dur1(self):
        val =self.Q50
        return self.duration( val)

    def dur3(self):
        val =self.Q50
        return self.duration( 3*val)

    def dur7(self):
        val =self.Q50
        return self.duration( 7*val)

    def dur25(self):
        val = self.Q25(self.time_series)
        return self.Duration(self.time_series, val)

    def pea1(self):
        val = self.Q50
        events = self.values_above( val)
        return np.mean([np.mean(t) for t in events])/val

    def pea3(self):
        val =self.Q50
        return np.mean([t for t in self.time_series if t>3*val])/val

    def pea7(self):
        val =self.Q50
        return np.mean([t for t in self.time_series if t>7*val])/val

    def pea25(self):
        val = self.Q25(self.time_series)
        return np.mean([t for t in self.time_series if t>val])/val

    def norises(self):
        toreturn =0
        for i in range(0,len(self.time_series)-1):
            if(self.time_series.values[i]<self.time_series.values[i+1]):
                toreturn+=1
        return toreturn/len(self.time_series)

    def nofalls(self):
        toreturn =0
        for i in range(0,len(self.time_series)-1):
            if(self.time_series.values[i]>self.time_series.values[i+1]):
                toreturn+=1
        return toreturn/len(self.time_series)


    def EQR_DFFV(self):
        return 0.38 - 0.01 * self.dur75() + 0.12 * self.pea1() - 0.1*self.dur7()

    def EQR_DFFV2(self):
        return 2.85 +1.531*self.mamin() - 0.031* self.fremedmin() - 9.206*self.norises()


    def __mamax(self, windowsize):
        maximums=[]
        for year in range(self.time_series.index[0].year,self.time_series.index[-1].year+1):
            maximums.append(self.time_series[str(year)].rolling(windowsize, win_type='triang').sum().max())
        maximums =[max for max in maximums if not math.isnan(max)]
        return np.mean(maximums)/self.Q50


    """
    /// Calculates the Base flow Index(BFI) from http://nora.nerc.ac.uk/6050/1/IH_108.pdf
    /// The self.TimeSeries must contain daily values
    """
    def BaseFlowIndex(self):
        if len(self.time_series) == 0:
            return -1;

        nDays = 5

        localcount = 0
        nDaysMin = []
        locallist = [];
        for i in self.__values_range:
            if (localcount == nDays):
                min1 = min(locallist)
                nDaysMin.append([ i - nDays + locallist.index(min1) , min1])
                locallist = []
                localcount = 0
            locallist.append(self.__np_values[i]);
            localcount+=1
    

        Ordinates = []
        nDaysMin09 =[l[1]*0.9 for l in nDaysMin]
        for i in range(0, len(nDaysMin) - 2):
            centralvalue = nDaysMin09[i + 1];
            if centralvalue < nDaysMin[i][1]  and centralvalue < nDaysMin[i + 2][1]:
                Ordinates.append(nDaysMin[i + 1])
    

        interpolated = []

        if len(Ordinates) < 2:
            return 0

        i1 = Ordinates[0][0]
        i2 = Ordinates[-1][0]

        interpolated = np.interp(range(i1, i2),[o[0] for o in Ordinates],[o[1] for o in Ordinates])


        Vb = sum(interpolated)
        vals = self.__np_values[i1:i2 - i1+1]
        Va = sum(vals)


        #Why?
##        vv = 0;
##        for i in range(1,len(vals)):
##            vv += (vals[i] + vals[i -1]) / 2;
    
        vvb = 0;
        for i in range (0,len(Ordinates)-1):
            vvb += (Ordinates[i][1] + Ordinates[i + 1][1]) / 2 * (Ordinates[i + 1][0] - Ordinates[i][0]);

        return Vb / Va;

class GroundWaterLayer(object):
    def __init__(self, gvk, dkmlag,dkmomr):
        self.feature = gvk
        self.insidecoords =[]
        self.holes=[]
        self.dkmlag=dkmlag
        self.dkmomr = dkmomr

        lastarea=-1
        for i in range(0, len(gvk.parts)):
            if i == len(gvk.parts)-1:
                end = len(gvk.points)-1
            else:
                end=gvk.parts[i+1]
            points=gvk.points[gvk.parts[i]:end]
            x=[p[0] for p in points]
            y=[p[1] for p in points]
            p=path.Path(points)
            area = 0.5*(np.dot(x,np.roll(y,1))-np.dot(y,np.roll(x,1)))
            if area<0: #Negative area means counter clock wise points which represents holes.
                self.holes.append(p)
            elif area>lastarea: #We have found a larger polgon. Use that
                self.path=p
                lastarea=area

        self.sum=0
        
    def plot(self):
        self.outeredge()
        plt.plot([p.x for p in self.insidecoords],[p.y for p in self.insidecoords],'x')
        plt.plot(self.path.vertices[:,0],self.path.vertices[:,1])
        for hole in self.holes:
            plt.plot(hole.vertices[:,0],hole.vertices[:,1])
            
        #Directions
        plt.plot([p.x for p in self.south],[p.y-250 for p in self.south],'^')
        plt.plot([p.x+250 for p in self.east],[p.y for p in self.east],'<')
        plt.plot([p.x-250 for p in self.west],[p.y for p in self.west],'>')
        plt.plot([p.x for p in self.north],[p.y+250 for p in self.north],'v')
        
        plt.gca().axis('equal')
    

    def remove_holes(self):
        toremove=[]
        for hole in self.holes:
            for p in self.insidecoords:
                if hole.contains_point((p.x,p.y)):
                    toremove.append(p)
        
        for p in toremove:
            self.insidecoords.remove(p)
        
    def outeredge(self):
        self.north=[]
        self.south=[]
        self.east =[]
        self.west=[]
        dict_coord={}
        dict_coord_row={}
        for c in self.insidecoords:
            if not c.col in dict_coord:
                dict_coord[c.col]=[]
            dict_coord[c.col].append(c.row)
            if not c.row in dict_coord_row:
                dict_coord_row[c.row]=[]
            dict_coord_row[c.row].append(c.col)
        for c in self.insidecoords:
            if not c.row+1 in dict_coord[c.col]:
                self.north.append(c)
            if not c.row-1 in dict_coord[c.col]:
                self.south.append(c)
            if not c.col+1 in dict_coord_row[c.row]:
                self.east.append(c)
            if not c.col-1 in dict_coord_row[c.row]:
                self.west.append(c)
                
class GroundWaterBody(object):
    def __init__(self, name):
        self.name = name
        self.GroundWaterLayers = []
    
    def sort_layers(self):
        #remove layers without dfs-points
        self.GroundWaterLayers = [b for b in self.GroundWaterLayers if len(b.insidecoords)>0]
        
        if len(self.GroundWaterLayers)>0:
            self.GroundWaterLayers.sort(key=lambda gwl:gwl.insidecoords[0].layer, reverse=True)            
            self.points_from_above=self.__project()
            self.GroundWaterLayers.sort(key=lambda gwl:gwl.insidecoords[0].layer)            
            self.points_from_below=self.__project()
        else:
            self.points_from_above=[]
            self.points_from_below=[]

 
    def __project(self):            
        points_from_above=[]
        points_from_above.extend(self.GroundWaterLayers[0].insidecoords)
    
        if len(self.GroundWaterLayers)>1:
            dict_coord={}
            for c in points_from_above:
                if not c.col in dict_coord:
                    dict_coord[c.col]=[]
                dict_coord[c.col].append(c.row)
                  
            for i in range(1,len(self.GroundWaterLayers)):
                for c in self.GroundWaterLayers[i].insidecoords:
                    if not c.col in dict_coord:
                        dict_coord[c.col]=[]
                    if not c.row in dict_coord[c.col]:
                        points_from_above.append(c)
                        dict_coord[c.col].append(c.row)
        return points_from_above
            

        
    def plot(self):
        from mpl_toolkits.mplot3d import Axes3D
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')
        for gwb in self.GroundWaterLayers:
            xs=[]
            ys=[]
            zs=[]
            for p in gwb.insidecoords:
                xs.append(p.x)
                ys.append(p.y)
                zs.append(p.layer)
                ax.scatter(xs, ys, zs)

        ax.set_xlabel('X Label')
        ax.set_ylabel('Y Label')
        ax.set_zlabel('Z Label')
        
        plt.show()



def qh_function(H, A, H0, N):
    return A *  max(0, H-H0)^N


def read_sensorthings(url):
    """
    Reads a pandas time series from a VandWeb URL
    """
    jData = read_json_data(url)
    l=list(map(lambda s:s['Result'], jData['value']))
    d=list(map(lambda s:pd.Timestamp(s['ResultTime']), jData['value']))
    return pd.Series(l, d)

def read_hydroinform_ts(sensorid, start=datetime(2020,1,1), end=datetime.now(), zoomlevel=3):
    """
    Returns a time series from hydroinform
    """
    url = r'http://hydroinform.cloudapp.net/api/TimeSeries/' + str(sensorid) + '?start=' + str(start) + '&end=' + str(end) + '&zoomlevel=' + str(zoomlevel) 
    jdata = read_json_data(url)
    l=list(map(lambda s:s['Value'], jdata))
    d=list(map(lambda s:pd.Timestamp(s['Time']), jdata))
    return pd.Series(l, d)

def read_json_data(url):
    """
    Gets json data from a url
    """
    import requests
    myResponse = requests.get(url)
    
    # For successful API call, response code will be 200 (OK)
    if(myResponse.ok):
        jData = json.loads(myResponse.content)
        return jData
    else:
      # If response code is not ok (200), print the resulting http error code with description
        myResponse.raise_for_status()