"""
This file demonstrates a bokeh applet, which can be viewed directly
on a bokeh-server. See the README.md file in this directory for
instructions on running.
"""

import logging

logging.basicConfig(level=logging.DEBUG)

import numpy as np
from scipy.stats.mstats import theilslopes
from helper_funcs import load_ts_from_coords
from spatial_funcs import geom_sr_from_point, geom_sr_from_bbox
import datetime
import fiona
from config_study_sites import study_sites
from collections import OrderedDict
import pandas as pd
import time

from bokeh.plotting import figure
from bokeh.io import gridplot
from bokeh.models import Plot, ColumnDataSource, HoverTool, BoxAnnotation, Dict, GridPlot
from bokeh.properties import Instance
from bokeh.server.app import bokeh_app
from bokeh.server.utils.plugins import object_page
from bokeh.models.widgets import HBox, Slider, TextInput, VBoxForm, RadioButtonGroup, \
    Dropdown, Button, VBox, Select, Paragraph, DataTable


class SlidersApp(HBox):
    """An example of a browser-based, interactive plot with slider controls."""

    extra_generated_classes = [["SlidersApp", "SlidersApp", "HBox"]]

    radio_btns = Instance(RadioButtonGroup)
    inputs = Instance(VBoxForm)

    text = Instance(TextInput)
    txt_output_slope = Instance(TextInput)
    txt_output_nobs = Instance(TextInput)

    year_start = Instance(Slider)
    year_end = Instance(Slider)
    month_start = Instance(Slider)
    month_end = Instance(Slider)

    plot = Instance(Plot)
    plot_lower = Instance(Plot)
    hover = Instance(HoverTool)

    source_base = Instance(ColumnDataSource)
    source = Instance(ColumnDataSource)
    source_active = Instance(ColumnDataSource)
    source_water_bounds = Instance(ColumnDataSource)
    source_tundra_bounds = Instance(ColumnDataSource)
    source_plotbounds_active = Instance(ColumnDataSource)

    txt_studysite = Instance(TextInput)
    txt_coords = Instance(TextInput)
    txt_comment = Instance(TextInput)

    add_button_submit = Instance(Button)
    #data_dropdown = Instance(Dropdown)
    selector_studysites = Instance(Select)
    selector_examples = Instance(Select)

    paragraph = Instance(Paragraph)
    data_table = Instance(DataTable)
    #lc_range = Instance(Dict)


    @classmethod
    def create(cls, data={}):
        """One-time creation of app's objects.
        This function is called once, and is responsible for
        creating all objects (plots, datasources, etc)
        """
        obj = cls()

        # Create data

        # TODO: take dynamic loader for initialisation
        coords = (248912.221319, 7785716.38894) # burn
        data = prepare_data(get_datafolder('NS', coords),coords)

        x = data['imdates']
        y = data['tcb']
        y = np.ma.masked_values(y, 0)
        slp, ict, cil, ciu = theilslopes(y, x)
        y_mod = x*slp + ict
        y_diff = y - y_mod

        datestr, yr, mth, dt = calc_dates_from_imdates(x)

        obj.source_base = ColumnDataSource(data=data)

        obj.source = ColumnDataSource(data=dict(x=x,
                                                y=y,
                                                y_mod=y_mod,
                                                y_mod_new=y_mod,
                                                y_diff=y_diff,
                                                dt=dt,
                                                yr=yr,
                                                mth=mth,
                                                datestr=datestr,
                                                flist=data['flist']))

        obj.source_active = ColumnDataSource(data=dict(x=x, y=y, dt=dt, ydiff=[]))

        # Data for definition of background_plot_bounds
        obj.source_water_bounds = ColumnDataSource(
            data={'tcb': (1.86900161e-02,1.07140526e-01),
                        'tcg': (-6.27548173e-02,-7.36424141e-03),
                        'tcw': (-4.73380089e-04, 4.14514430e-02),
                        'ndvi': (-5.91476917e-01,-1.64228976e-02),
                        'ndwi': (2.09970891e-01,7.54860103e-01),
                        'ndmi': (2.23491907e-01,8.30612063e-01)}
        )

        obj.source_tundra_bounds = ColumnDataSource(
            data={'tcb': (0.24277386,  0.38576081),
                        'tcg': (0.01137961,  0.13659704),
                        'tcw': (-0.22351126, -0.11089928),
                        'ndvi': (0.37258583,  0.65635675),
                        'ndwi': (-0.67397082, -0.43988082),
                        'ndmi': (-0.14433968,  0.20968285)}
        )

        obj.source_plotbounds_active = ColumnDataSource(data=dict(lower=[1.07140526e-01, 0.38576081],
                                                                  upper=[1.86900161e-02, 0.24277386],
                                                                  left=[datetime.datetime(1960,1,1)]*2,
                                                                  right=[datetime.datetime(2050,1,1)]*2,
                                                                  color=['#3690c0', '#238b45']))
        # Create Widgets
        obj.radio_btns = RadioButtonGroup(
            labels=['tcb', 'tcg', 'tcw', 'ndvi', 'ndwi', 'ndmi'],
            active=0
        )

        #############################################
        # Textbox to print information
        obj.txt_output_slope = TextInput(
            title="Slope", name='slope', value=str(slp)
        )
        # Textbox to print information
        obj.txt_output_nobs = TextInput(
            title="nObservations", name='nobs', value=str(slp)
        )

        #############################################
        # add data
        # TEXTBOXES
        obj.text = TextInput(
            title="title", name='title', value='tcb'
        )

        # Textbox to insert name of study site for new time-series
        obj.txt_studysite = TextInput(
            title="Study Site", name='study_site', value=None
        )
        # Textbox to insert coordinates for new time-series
        obj.txt_coords = TextInput(
            title="Coords", name='coords', value=None
        )
        # Textbox to insert comment for new time-series
        obj.txt_comment = TextInput(
            title="Comment", name='comment', value=None
        )

        # Button - INACTIVE, because of bug?
        obj.add_button_submit = Button(
            label="submit", name='test')

        # TODO: Selection does not work properly yet
        # Select menu

        obj.selector_examples = Select(
            options = ["Select", "Lake Drainage /w vegetation", "Lake Drainage w/o vegetation",
                       "Coastal Erosion", "Fire Event"], value ="Select",
            title="Examples")

        obj.selector_studysites = Select(
            options = study_sites.keys(), value="NS", title="Study Site")

        # SLIDERS
        obj.year_start = Slider(
            title="Start Year", name='year_start',
            value=min(yr), start=min(yr), end=max(yr), step=1
        )
        obj.year_end = Slider(
            title="End Year", name='year_end',
            value=max(yr), start=min(yr), end=max(yr), step=1
        )
        obj.month_start = Slider(
            title="Start Month", name='month_start',
            value=6, start=1, end=12, step=1
        )
        obj.month_end = Slider(
            title="End Month", name='month_end',
            value=9, start=1, end=12, step=1
        )
        #############################################
        obj.paragraph = Paragraph(text='bla')
        #############################################

        # Set-up Figure
        toolset = "pan,reset,resize,save,wheel_zoom,box_zoom"
        # Generate a figure container
        timedelta = datetime.timedelta(days=360)
        obj.plot = figure(title_text_font_size="12pt",
                      plot_height=350,
                      plot_width=750,
                      title=obj.radio_btns.labels[obj.radio_btns.active],
                      tools=toolset,
                      y_range=[y.min()-0.2, y.max()+0.2],
                      x_range=[dt.min()-timedelta, dt.max()+timedelta],
                      x_axis_type="datetime"
        )


        obj.plot_lower = figure(title_text_font_size="12pt",
                      plot_height=200,
                      plot_width=750,
                      title=obj.radio_btns.labels[obj.radio_btns.active] + ": deviation from trend",
                      tools=toolset,
                      y_range=[y.min()-0.2, y.max()+0.2],
                      x_range=[dt.min()-timedelta, dt.max()+timedelta],
                      x_axis_type="datetime"
        )

        #obj.plot = plot
        #############################################
        # First data-setup
        obj.change_data_active()
        obj.make_plot()


        hbox_dataLoading = HBox(children = [obj.selector_studysites, obj.txt_coords], name='hbox_dataLoading')
        # Set-up of plot structure and panels
        obj.inputs = VBoxForm(
            children=[obj.selector_examples,hbox_dataLoading,
                obj.year_start, obj.year_end, obj.month_start, obj.month_end
            ]
        )

        obj.children.append(obj.inputs)

        # right panel
        gp = GridPlot(children=[[obj.plot], [obj.plot_lower]])
        obj.children.append(VBox(obj.radio_btns,
                                 gp,
                                 HBox(obj.txt_output_slope, obj.txt_output_nobs)))

        return obj


    def setup_input_data(self):
        data_index = 0
        coords = (248912.221319, 7785716.38894) # burn
        self.data = {data_index : prepare_data(get_datafolder('NS', coords),coords)}
        data_index += 1


    def make_plot(self):
        # Plot the line by the x,y values in the source property

        # Upper Plot
        self.plot.scatter(x='dt', y='y', source=self.source, alpha=0.3, name='scatter_active')
        self.plot.scatter('dt', 'y', source=self.source_active, alpha=0.9)
        self.plot.line('dt', 'y_mod', source=self.source, alpha=0.3)
        self.plot.line('dt', 'y_mod_new', source=self.source, alpha=0.9, line_width=1.5)
        # Plot background colors for data ranges of "stable LC-classes
        self.plot.quad(name='quad',top='upper', bottom='lower',
                       left='left',
                       right='right',
                       source=self.source_plotbounds_active,
                       color='color',
                       alpha=0.15)

        # Lower Plot
        self.plot_lower.line(x=[self.plot.x_range.start, self.plot.x_range.end], y=[0, 0],
                             line_color='black', line_dash='dashed')
        self.plot_lower.scatter(x='dt', y='y_diff', source=self.source, alpha=0.3, fill_alpha=0.1)
        self.plot_lower.scatter(x='dt', y='y_diff', source=self.source_active, alpha=0.9, fill_alpha=0.1)
        self.plot_lower.segment(x0='dt', y0=0,x1='dt', y1='y_diff', source=self.source, alpha=0.1, line_width=1.5)
        self.plot_lower.segment(x0='dt', y0=0,x1='dt', y1='y_diff', source=self.source_active, alpha=0.9, line_width=1.5)

        for p in [self.plot]:
            p.grid.minor_grid_line_color = 'black'
            p.grid.minor_grid_line_alpha = 0.1

        # Hover Tool
        hover = HoverTool(
        tooltips=[("Value", "@y{0.000}"),
                  ("Date:", "@datestr"),
                  ("File:","@flist"),
                  ],
        line_policy="none",
        point_policy="snap_to_data",
        # TODO: hover should be preserved for each ds
        names = ['scatter_active'],
        name='hover_tool'
        )
        self.plot.add_tools(hover)


    def setup_events(self):
        """Attaches the on_change event to the value property of the widget.
        The callback is set to the input_change method of this app.
        """
        super(SlidersApp, self).setup_events()
        if not self.text:
            return


        # Text box event registration
        self.text.on_change('value', self, 'input_change')

        self.txt_coords.on_change('value', self, 'get_coords')

        # Dataset change event
        self.radio_btns.on_click(self.radio_button_change)

        # Slider event registration
        for w in ["year_start", "year_end", "month_start", "month_end"]:
            getattr(self, w).on_change('value', self, 'input_change')

        # Select Menu change registration
        self.selector_studysites.on_change('value', self, 'select_handler')
        self.selector_examples.on_change('value', self, 'select_handler_examples')

        # add data button
        if not self.add_button_submit:
            return


    ##########################################################
    # EVENT HANDLERS
    # Change Text-Box
    def input_change(self, obj, attrname, old, new):
        """Executes whenever the input form changes.
        It is responsible for updating the plot, or anything else you want.
        """
        self.change_data_index()
        self.change_data_active()


    # Change Radio Button/Tabs
    def radio_button_change(self, active):
        """
        Event handler for radio button clicks
        """
        self.plot.title = self.radio_btns.labels[active]
        self.plot_lower.title = self.radio_btns.labels[active] + ": deviation from trend"
        self.change_data_index() # change used index according to radio button
        self.change_data_active() # check for settings


    def select_handler(self, obj, attrname, old, new):
        # inactive at the moment
        print "Selector: ", self.selector_studysites.value

    def select_handler_examples(self, obj, attrname, old, new):
        if self.selector_examples.value == "Select":
            return
        d = OrderedDict(names=["Lake Drainage /w vegetation", "Lake Drainage w/o vegetation",
                               "Coastal Erosion", "Fire Event"],
                                study_sites=['NS', 'NS', 'NS', 'NS'],
                                coords=[(598351, 7706506), (449049, 7828652), (472281, 7865167), (594607, 7666726)])
        df = pd.DataFrame(d, index=d['names'])
        ss, coords = df.loc[self.selector_examples.value, ["study_sites", "coords"]]

        self.change_dataset(ss, coords)
        self.change_data_index()
        self.change_data_active()


    def get_coords(self, obj, attrname, old, new):
        coords = np.array(self.txt_coords.value.split(','), dtype=np.float32)

        self.change_dataset(self.selector_studysites.value, coords)
        self.change_data_index()
        self.change_data_active()


    def change_dataset(self, study_site, coords):
        """
        Event Handler for change in "coords" Text box, which triggers data loading
        :param coords:
        :return:
        """
        # TODO: FIX - not working yet!
        #self.make_loading_window() # show inactivation window

        try:
            data = prepare_data(get_datafolder(study_site, coords), coords)
        except:
            print "Error"
            return
        self.source_base.data = data=data
        x = self.source_base.data['imdates']
        layer = self.radio_btns.labels[self.radio_btns.active]
        y = self.source_base.data[layer]
        y = np.ma.masked_values(y, 0)
        slp, ict, cil, ciu = theilslopes(y, x)
        y_mod = x*slp + ict
        y_diff = y - y_mod
        datestr, yr, mth, dt = calc_dates_from_imdates(x)

        self.source.data = dict(x=x, y=y, y_mod=y_mod, y_mod_new=y_mod, y_diff=y_diff, yr=yr, mth=mth,
                                datestr=datestr, dt=dt, flist=np.array(data['flist']))

        self.source_active.data = dict(x=x, y=y, dt=dt, y_diff=y_diff)


        timedelta = datetime.timedelta(days=360)
        self.plot.x_range.start = dt.min() - timedelta
        self.plot.x_range.end = dt.max() + timedelta

        self.change_data_index()
        self.change_data_active()


        #print "Renderer: ",self.plot.renderers[4]

        # TODO: set slider to temporal bounds of dataset


    def change_data_index(self):
        """
        Function changes the y-value according to the selected index from RadioButton Panel
        Update of source data and background plots
        :return:
        """
        # update input-data - !!! only works for complete replacement of source dataset (Bug?) !!!
        layer = self.radio_btns.labels[self.radio_btns.active]
        # get source data of selected index
        y = np.array(self.source_base.data[layer], np.float32).copy()
        # get upper and lower boundaries of "stable" classes water and tundra
        water_bnds = np.array(self.source_water_bounds.data[layer]).copy()
        tundra_bnds = np.array(self.source_tundra_bounds.data[layer]).copy()

        self.source.data = dict(x=self.source.data['x'],
                                y=y,
                                y_mod=self.source.data['y_mod'],
                                yr=self.source.data['yr'],
                                mth=self.source.data['mth'],
                                dt=self.source.data['dt'],
                                y_mod_new=self.source.data['y_mod_new'],
                                y_diff=self.source.data['y_diff'],
                                datestr=self.source.data['datestr'],
                                flist=self.source.data['flist'])

        self.source_plotbounds_active.data = dict(lower=[water_bnds[0], tundra_bnds[0]],
                                                  upper=[water_bnds[1], tundra_bnds[1]],
                                                  left=self.source_plotbounds_active.data['left'],
                                                  right=self.source_plotbounds_active.data['right'],
                                                  color=self.source_plotbounds_active.data['color'])

        print "Renderer: ",self.plot.renderers[4]
        #hover = self.plot.select(dict(name='hover_tool')).renderers = [self.plot.renderers[4]]
        #hover.renderers = [self.plot.renderers[4]]
        #self.plot.add_tools(hover)
        #print "Hover: ",self.plot.select(dict(name='hover_tool')).names


    def change_data_active(self):
        """
        Function to immediately update if Slider value have changed
        """

        # get data
        y = np.array(self.source.data['y'])
        x = np.array(self.source.data['x'])
        yr = np.array(self.source.data['yr'])
        mth = np.array(self.source.data['mth'])
        dt = np.array(self.source.data['dt'])

        # remove inactive values
        y = np.ma.masked_values(y, 0)
        y = np.ma.masked_invalid(y)

        # calculate slopes of full data
        slp, ict, cil, ciu = theilslopes(y, x)
        y_mod = x*slp + ict
        #y_diff = y -y_mod

        # get positions of "active" values
        idx = np.where((yr >= self.year_start.value) & (yr <=self.year_end.value) &
                       (mth >= self.month_start.value) & (mth <= self.month_end.value))[0]



        # calculate slopes of active data
        slp, ict, cil, ciu = theilslopes(y[idx], x[idx])
        y_mod_new = x*slp + ict
        y_diff = y -y_mod_new

        # update datasets
        self.source.data = dict(x=x, y=y,  y_mod=y_mod, yr=yr, mth=mth, dt=np.array(dt),
                                y_mod_new=y_mod_new, y_diff=y_diff, datestr=np.array(self.source.data['datestr']),
                                flist=np.array(self.source.data['flist']))

        self.source_active.data = dict(x=x[idx], y=y[idx], dt=dt[idx], y_diff=y_diff[idx])

        self.txt_output_slope.value = "{0:.4f}".format(slp*3650)
        self.txt_output_nobs.value = str((~y[idx].mask).sum())
        # update plot-range of y-values
        self.plot.y_range.start = max(y.min()-0.2, -1)
        self.plot.y_range.end = min(y.max()+0.2, 1)
        self.plot_lower.y_range.start = max(y_diff.min()-0.1, -1)
        self.plot_lower.y_range.end = min(y_diff.max()+0.1, 1)

    # TODO: FIX
    def make_loading_window(self):
        """
        Function to temporary indicate that a new dataset is being loaded : Not working yet!!!
        """
        print "Loading Data"
        self.plot.title = "Loading"
        self.source_active.data = dict(x=[], y=[], dt=[], y_diff=[])


def calc_dates_from_imdates(x):
    """
    reorganized data from ordinal image date to sevral other formats
    :param x:
    :return:
    """
    datestr = np.array([datetime.datetime.fromordinal(idt).strftime('%Y-%m-%d') for idt in x])
    yr = np.array([datetime.datetime.fromordinal(idt).year for idt in x])
    mth = np.array([datetime.datetime.fromordinal(idt).month for idt in x])
    dt = np.array([datetime.datetime.fromordinal(idt) for idt in x])
    return datestr, yr, mth, dt


def prepare_data(path, coords):
    data = load_ts_from_coords(path, coords)
    tc, ndvi, ndwi, ndmi = [data['indices'][idx] for idx in ['tc', 'ndvi', 'ndwi', 'ndmi']]
    ndvi, ndwi, ndmi, tcb, tcg, tcw = [d.squeeze() for d in [ndvi, ndwi, ndmi, tc[:,0], tc[:,1], tc[:,2]]]

    data=dict(tcb=tcb, tcg=tcg, tcw=tcw, ndvi=ndvi, ndwi=ndwi, ndmi=ndmi,
              imdates=data['imdates'], sensor=data['imdates'],
              flist=data['flist'],
              imdates_doy=['imdates_doy'])
    return data


def get_ullr_from_json(json):
    crd = np.array(json['geometry']['coordinates'][0][:4])
    ulx, uly, lrx, lry = crd[[0,2]].ravel()
    return ulx, uly, lrx, lry


def get_datafolder(study_site, coords):
    epsg = study_sites[study_site]['epsg']
    geom_pt, sr_pt = geom_sr_from_point(coords[0], coords[1], epsg)
    with fiona.open(study_sites[study_site]['fishnet_file']) as src:
        for s in src:
            ulx, uly, lrx, lry = get_ullr_from_json(s)
            geom_bbox, osr_bbox = geom_sr_from_bbox(ulx, uly, lrx, lry, epsg)
            if geom_bbox.Intersects(geom_pt):
                row, path = (s['properties']['row'], s['properties']['path'])
                break
    pdir = study_sites[study_site]['processing_dir'] + r'\{0}_{1}_{2}'.format(study_site, row, path)
    return pdir


# The following code adds a "/bokeh/sliders/" url to the bokeh-server. This
# URL will render this sine wave sliders app. If you don't want to serve this
# applet from a Bokeh server (for instance if you are embedding in a separate
# Flask application), then just remove this block of code.
@bokeh_app.route("/bokeh/sliders/")
@object_page("sin")
def make_sliders():
    app = SlidersApp.create()
    return app
