import collections
import math
import time
import datetime
import json
#import pickle
import inspect
import pandas

from poobrains import Markup, app, request, abort, flash, redirect, g, session, locked_cached_property, new_session

import poobrains.helpers
import poobrains.errors
import poobrains.form
import poobrains.storage
import poobrains.auth
import poobrains.commenting

from . import base

def dynamic_datasets():

    datasets = dict([(name.lower(), cls) for name, cls in Dataset.class_children_keyed().items()]) # collects *all* descendants of Dataset including StoredDataset and its descendants

    for name in [name.lower() for name in datasets['storeddataset'].class_children_keyed().keys()]: # remove descendants of StoredDataset
        del(datasets[name])
    del(datasets['storeddataset'])

    return datasets


#def get_dataset_parameters(dataset_name):
#    """ get parameters for dynamic datasets' `fill` function """





def load_dataset(handle):
    if handle.startswith('_'): # underscore marks dynamic datasets in URLs

        name, *args = handle.split('.')
        name = name[1:] # remove leading underscore
        
        valid_datasets = dynamic_datasets()
        if len(name) > 0 and name in valid_datasets:

            cls = valid_datasets[name]

            #cls.permissions['read'].check(g.user) # make sure everything below this is only executed if user is allowed to see the results (i.e. don't do remote API requests etc if user isn't allowed to see the results anyhow

            ds = cls()
            ds.fill(*args)

        else:
            raise poobrains.errors.ExposedError('Unknown dynamic dataset: %s' % func)

    else:

        ds = StoredDataset.load(handle)
        ds.permissions['read'].check(g.user)

    return ds


def dataset_choices():

    dynamic = []
    for name, cls in dynamic_datasets().items():

        try:
            cls.permissions['read'].check(g.user)
            name = '_%s' % name
            dynamic.append((name, cls.__name__))
        except poobrains.auth.AccessDenied:
            pass

    stored = []
    for dataset in StoredDataset.list('read', g.user):
        stored.append((dataset.name, dataset.title))

    choices = [(dynamic, 'Dynamic Datasets'), (stored, 'Stored Datasets')]
    return choices


def dynamic_dataset_parameters(dataset_class):

    r = {}
    signature = inspect.signature(dataset_class.fill)
    for param in signature.parameters.values():

        if param.name == 'self':
            continue # next loop iteration

        if param.annotation == param.empty:
            param_type = str

        else:
            param_type = param.annotation

        r[param.name] = {
            'type': param_type,
            'default': param.default
        }

    return r


def validate_handle_not_in_session(handle):
    if handle in session['editor-sessions']:
        raise poobrains.errors.ValidationError("Editor Session named '%s' already exists!" % handle)
#

class Dataset(poobrains.auth.Protected):

    title = None # to override Renderable's @property
    description = poobrains.md.MarkdownString()

    def __init__(self, name=None, title=None, description=None, data=None, label_x=None, label_y=None):

        self._name = name
        self.title = title or type(self).__name__
        self.description = description or ''
        self.label_x = label_x or 'X'
        self.label_y = label_y or 'Y'

        if not data is None:
            self.data = data
        else:
            self.data = pandas.DataFrame()


    def __len__(self):
        return len(self.data)


    @property
    def name(self):
        return self._name or type(self).__name__.lower()


    @name.setter
    def name(self, value):
        self._name = value


    @locked_cached_property
    def ref_id(self):
        return "dataset-%s" % self.name


    def datapoint_id(self, x):
        return "dataset-%s-%s" % (self.name, x)
    
    
    def render(self, mode=None):

        if mode == 'json':
            return self.data.to_json()

        return super(Dataset, self).render(mode=mode)
   

    def plot(self, kind='scatter'):

        if not kind in plot_kinds:
            raise ValueError('wrong plot kind: %s' % kind)

        return plot_kinds[kind](dataset=self).render('raw')


    def save(self, name=None, owner=None):

        """
        Convert this Dataset into a StoredDataset and save it.
        """

        ds = StoredDataset()

        #ds.owner = owner or poobrains.auth.User.get(poobrains.auth.User.id == 1)
        ds.owner = owner or g.user
        
        now = int(time.time())
        ds.name = name or poobrains.helpers.clean_string("%s-%d" % (self.name, now))
        ds.title = '%s@%s' % (self.title, str(datetime.datetime.fromtimestamp(now)))
        ds.label_x = self.label_x
        ds.label_y = self.label_y
        ds.description = self.description
        ds.data = self.data
        ds.save(force_insert=True)

        return ds



class EditorNewSessionFieldset(poobrains.form.Fieldset):

    handle = poobrains.form.fields.Text(required=True, validators=[validate_handle_not_in_session])
    new_session = poobrains.form.Button(label='Create', type='submit')


    def process(self, submit, instance):

        app.debugger.set_trace()

        handle = self.fields['handle'].value

        instance.owner = g.user
        instance.name = handle
        instance.title = handle.capitalize()
        instance.label_x = 'x'
        instance.label_y = 'y'
        instance.save()

        return instance.url('edit')


class EditorLoadFieldset(poobrains.form.Fieldset):

    key = poobrains.form.fields.Text()
    dataset = poobrains.form.fields.Select(choices=dataset_choices)
    load = poobrains.form.Button(label='Load', type='submit')


    def process(self, submit, instance):

        app.debugger.set_trace()
        ds = load_dataset(self.fields['dataset'].value)
        
        session['editor-sessions'][instance.name]['data'] = session['editor-sessions'][instance.name]['data'].append(ds.data)



class DataEditor(poobrains.auth.BoundForm):

    def __init__(self, instance, mode='add'):

        super().__init__(instance, mode=mode)

        app.debugger.set_trace()
        if mode == 'add':
            self.new = EditorNewSessionFieldset()

        else:

            if not self.instance.name in session['editor-sessions']:

                # set up a fresh editor session holding current data
                editor_session = {
                    'handle': self.instance.name,
                    'action': None,
                    'action_data': {},
                    'title': self.instance.title,
                    'description': self.instance.description,
                    'data': self.instance.data,
                }

                session['editor-sessions'][self.instance.name] = editor_session 

            else:
                editor_session = session['editor-sessions'][self.instance.name]
   
            self.load = EditorLoadFieldset(self.handle_string)

            if not editor_session['data'].empty:
               self.plot = Plot(dataset=Dataset(data=editor_session['data']))


    def process(self, submit):

        app.debugger.set_trace()

        if submit.startswith('new.'):
            self.fields['new'].process(submit, self.instance)

        elif submit.startswith('load.'):
            self.fields['load'].process(submit, self.instance)

        return self


class StoredDataset(Dataset, poobrains.commenting.Commentable):

    title = poobrains.storage.fields.CharField()
    description = poobrains.md.MarkdownField(null=True)
    label_x = poobrains.storage.fields.CharField(verbose_name="Label for the x-axis")
    label_y = poobrains.storage.fields.CharField(verbose_name="Label for the y-axis")
    data = poobrains.storage.fields.TextField() # Fuck it, just use JSON
    #data = poobrains.storage.fields.BlobField(form_widget=None)

    form_add = DataEditor
    form_edit = DataEditor

    def __init__(self, *args, **kwargs):

        self._data = None
        return super(Dataset, self).__init__(*args, **kwargs)


    def __getattribute__(self, name):

        if name == 'data':
            #app.debugger.set_trace()
            if self._data is None:
                if 'data' in self.__data__:
                    self._data = pandas.DataFrame.from_dict(json.loads(self.__data__['data']))
                else:
                    self._data = pandas.DataFrame()
                    self.__data__['data'] = self._data.to_dict()

            return self._data
        return super(StoredDataset, self).__getattribute__(name)

    
    def __setattr__(self, name, value):

        if name == 'data':

            if type(value) == pandas.DataFrame:
                #value = pickle.dumps(value)
                #value = json.dumps(value)
                value = value.to_json()

        super(StoredDataset, self).__setattr__(name, value)


    @locked_cached_property
    def ref_id(self):

        return "dataset-%s" % self.name


    def save(self, **kwargs):

        dataobj = self.data
        #self.data = dataobj.to_json()
        r = super(Dataset, self).save(**kwargs)
        self.data = dataobj
        return r


#    def view(self, mode='teaser', handle=None, **kwargs):
#
#        if mode in ['add', 'edit']:
#
#            editor = DataEditor()
#            return editor.view('full')
#
#        super().view(mode=mode, handle=handle, **kwargs)


@app.expose('/svg/plot')
class Plot(base.SVG):

    padding = None
    width = None
    height = None
    inner_width = None
    inner_height = None
    plot_width = None
    plot_height = None
    description_height = None

    dataset = None
    length = None

    # TODO: deprecate all this by just statically computed bounds?
    min_x = None
    max_x = None
    min_y = None
    max_y = None
    span_x = None
    span_y = None
    
    bounds = None

    class Meta:

        modes = collections.OrderedDict([
            ('teaser', 'read'),
            ('full', 'read'),
            ('raw', 'read'),
            ('json', 'read'),
            ('inline', 'read')
        ])

    def __init__(self, handle=None, mode=None, dataset=None, **kwargs):

        super(Plot, self).__init__(handle=handle, mode=mode, **kwargs)

        app.debugger.set_trace()

        if handle is None and dataset is None:
            abort(404, "No dataset selected")
        
        self.padding = app.config['SVG_PLOT_PADDING']
        self.plot_width = app.config['SVG_PLOT_WIDTH']
        self.plot_height = app.config['SVG_PLOT_HEIGHT']
        self.description_height = app.config['SVG_PLOT_DESCRIPTION_HEIGHT']
        self.width = self.plot_width + (2 * self.padding)
        self.height = self.plot_height + self.description_height + (3 * self.padding)
        self.inner_width = self.width - (2 * self.padding)
        self.inner_height = self.height - (2 * self.padding)

        if not dataset is None:
            self.dataset = dataset

        else:
            self.dataset = load_dataset(handle)


    def is_dataframe(self, x):
        return type(x) is pandas.DataFrame


    def cast_value(self, value):

        cls = type(value)

        if cls in [int, float]:
            return value

        casters = {
            numpy.datetime64: lambda x: pandas.to_datetime(x).timestamp()
        }

        if type(value) in casters:
            return casters[type(value)](value)

        raise ValueError('Value of type %s not castable for plot: %s' % (cls, value))


    @locked_cached_property
    def min_x(self):

        mins = []
        if isinstance(self.dataset.data, pandas.DataFrame):
            submins = []
            for subset_index in self.dataset.data:
                submins.append(min(self.dataset.data[subset_index].index))

                mins.append(min(submins))

        else:
            mins.append(min(self.dataset.data.index))

        return min(mins)


    @locked_cached_property
    def max_x(self):

        maxes = []

        for subset_index in self.dataset.data:
            maxes.append(max(self.dataset.data[subset_index].index))

        return max(maxes)


    @locked_cached_property
    def min_y(self):

        mins = []

        for subset_index in self.dataset.data:
            mins.append(min(self.dataset.data[subset_index]))

        return min(mins)


    @locked_cached_property
    def max_y(self):

        maxes = []

        for subset_index in self.dataset.data:
            maxes.append(max(self.dataset.data[subset_index]))

        return max(maxes)


    def normalize_x(self, value):

        if self.span_x == 0.0:
            return self.plot_width / 2.0

        return (value - self.min_x) * (self.plot_width / self.span_x)


    def normalize_y(self, value):

        if self.span_y == 0.0:
            return self.plot_height / 2.0

        return self.plot_height - (value - self.min_y) * (self.plot_height / self.span_y)


    @locked_cached_property
    def label_x(self):

        #return u' / '.join([dataset.label_x for dataset in self.datasets])
        return self.dataset.label_x


    @locked_cached_property
    def label_y(self):

        #return u' / '.join([dataset.label_y for dataset in self.datasets])
        return self.dataset.label_y


    @locked_cached_property
    def has_grid(self):

        #app.debugger.set_trace()
        valid_dtypes = [
           int,
           float
        ]

        for dtype in valid_dtypes:

            x = False
            y = False

            if self.dataset.data.index.dtype in valid_dtypes:
                x = True # x axis is numerical, can be gridded

            if dtype in self.dataset.data.dtypes.values:
                y = True # y axais is numeric, can be gridded

            if x and y:
                return True

        return False


    @locked_cached_property
    def span_x(self):

        if not self.has_grid:
            return 0 # why, tho?

        return max(self.dataset.data.index) - min(self.dataset.data.index)


    @locked_cached_property
    def span_y(self):

        if not self.has_grid:
            return 0 # why, tho?

        absolute_minimum = None
        absolute_maximum = None

        for column in self.dataset.data.columns:
            for value in self.dataset.data[column].values:
                if absolute_minimum is None or value < absolute_minimum:
                    absolute_minimum = value
                if absolute_maximum is None or value > absolute_maximum:
                    absolute_maximum = value

        if not absolute_minimum is None:
            return absolute_maximum - absolute_minimum


    @locked_cached_property
    def grid_x(self):

        #app.debugger.set_trace()
        if self.span_x == 0:
            return [self.min_x]

        grid_step = 10 ** (int(math.log10(self.span_x)) - 1)

        offset = (self.min_x % grid_step) * grid_step # distance from start of plot to first line on the grid
        start = self.min_x + offset

        x = start
        coords = [x]
        while x <= self.max_x:
            coords.append(x)
            x += grid_step

        return coords


    @locked_cached_property
    def grid_y(self):

        if self.span_y == 0:
            return [self.min_y]

        grid_step = 10 ** (int(math.log10(self.span_y)) - 1)
        #offset = (self.min_y % grid_step) * grid_step # distance from start of plot to first line on the grid
        offset = grid_step - self.min_y % grid_step
        start = self.min_y + offset

        y = start
        coords = [y]
        while y <= self.max_y:
            coords.append(y)
            y += grid_step

        return coords


@app.expose('/svg/lineplot')
class LinePlot(Plot):
    pass


@app.expose('/svg/barchart')
class BarChart(Plot):
    
    @locked_cached_property
    def bar_width(self):
        #return 10
        return (self.plot_width - self.padding) / self.length - 1


plot_kinds = {
    'scatter': Plot,
    'line': LinePlot,
    'bar': BarChart
}


@new_session
def editor_session(session):
    session['editor-sessions'] = {}








#class PlotKindFieldset(poobrains.form.Fieldset):
#
#    def __init__(self, editor_handle, **kwargs):
#        super(PlotKindFieldset, self).__init__(**kwargs)
#
#        self.editor_handle = editor_handle
#        action = session['editor-sessions'][self.editor_handle]['action']
#
#        self.kind = poobrains.form.fields.Select(choices=[(kind, cls.__name__) for (kind, cls) in plot_kinds.items()], default=session['editor-sessions'][editor_handle]['plot_kind'])
#
#        self.apply = poobrains.form.Button(type='submit', label='Apply')
#
#
#    def process(self, submit, instance):
#        session['editor-sessions'][instance.handle_string]['plot_kind'] = self.fields['kind'].value
#        instance.dataset.plot_kind = self.fields['kind'].value

class EditorLoadFieldset_old(poobrains.form.Fieldset):

    title = "Add new dataset to session"

    dataset = poobrains.form.fields.Select(choices=dataset_choices)
    dataset_name = poobrains.form.fields.Text(label='Dataset name')
    dataset_title = poobrains.form.fields.Text(label='Dataset title')
    dataset_description = poobrains.form.fields.TextArea(label='Dataset description')
    load_dataset = poobrains.form.Button(label='Load', type='submit')


    def __init__(self, editor_handle, **kwargs):

        datasets = dynamic_datasets()

        super(EditorLoadFieldset, self).__init__(**kwargs)

        self.editor_handle = editor_handle
        editor_session = session['editor-sessions'][self.editor_handle]

        if editor_session['action'].startswith('load.dynamic.'):

            dataset_name = editor_session['action'].split('.')[2]
            self.fields['dataset'].value = '_%s' % dataset_name
            self.fields['dataset'].readonly = True

            self.fields['dataset_name'].value = editor_session['action_data']['dataset_name']
            self.fields['dataset_name'].readonly = True

            self.fields['dataset_description'].value = editor_session['action_data']['dataset_description']
            self.fields['dataset_description'].readonly = True

            if dataset_name in datasets:

                dataset_class = datasets[dataset_name]
                self.summon_parameter_fieldset(dataset_class)


            else:
                flash('Unknown dynamic dataset: %s' % dataset_name, 'error')


    def summon_parameter_fieldset(self, dataset_class):

            subform = poobrains.form.Fieldset(name='dynamic')
            params = dynamic_dataset_parameters(dataset_class)

            for param_name, param_info in params.items():
                param_type = poobrains.form.types.lookup_table[param_info['type']]()
                subform.add_field(poobrains.form.fields.Text(name=param_name, label=param_name, type=param_type, default=param_info['default']))

            subform.cancel = poobrains.form.Button(type='submit', label='Cancel')

            self.add_field(subform)


    def process(self, submit, instance):

        datasets = dynamic_datasets()
        action = session['editor-sessions'][self.editor_handle]['action']
        dataset_name = self.fields['dataset'].value
        if action == 'load':
    
            if submit == '%s.load_dataset' % self.name:

                session['editor-sessions'][self.editor_handle]['action_data'] = {
                    'dataset_name': self.fields['dataset_name'].value,
                    'dataset_description': self.fields['dataset_description'].value
                }

                if dataset_name.startswith('_'):

                    dataset_name = dataset_name[1:]
                    if not dataset_name in datasets:
                        flash('Unknown dynamic dataset: %s' % dataset_name, 'error')
                    else:
                        session['editor-sessions'][instance.handle_string]['action'] = 'load.dynamic.%s' % dataset_name
                        self.summon_parameter_fieldset(datasets[dataset_name])
                        self.fields['dataset'].readonly = True
                        self.fields['dataset_name'].readonly = True
                else: # directly loadable dataset

                    try:
                        dataset = StoredDataset.load(dataset_name)
                        dataset.permissions['read'].check(g.user)
                        dataset.name = self.fields['dataset_name'].value
                        instance.datasets[dataset.name] = dataset
                        session['editor-sessions'][instance.handle_string]['action'] = None
                        session['editor-sessions'][instance.handle_string]['data'][dataset.name]['title'] = dataset.title
                        session['editor-sessions'][instance.handle_string]['data'][dataset.name]['description'] = dataset.description
                        session['editor-sessions'][instance.handle_string]['data'][dataset.name]['data'] = dataset.data.to_dict()
                    except StoredDataset.DoesNotExist:
                        flash('Unknown stored dataset: %s' % dataset_name, 'error')

        if action.startswith('load.dynamic.'):

            if submit == '%s.dynamic.cancel' % self.name:
                session['editor-sessions'][instance.handle_string]['action'] = 'load'
                del(self.fields['dynamic']) # actively remove the fieldset so it doesn't get rendered in the response directly to this request
                self.fields['dataset'].readonly = False

            elif submit == '%s.load_dataset' % self.name:

                dataset_name = dataset_name[1:]
                dataset_class = datasets[dataset_name]
                params = dynamic_dataset_parameters(dataset_class)

                param_values = {}
                for param_name in params:
                    if param_name in self.fields['dynamic'].fields:
                        param_values[param_name] = self.fields['dynamic'].fields[param_name].value

                if len(param_values) == len(params):

                    dataset = dataset_class()
                    dataset.fill(**param_values)
                    dataset.name = self.fields['dataset_name'].value
                    dataset.title = self.fields['dataset_title'].value
                    dataset.description = self.fields['dataset_description'].value

                    dataset.permissions['read'].check(g.user)
                    instance.dataset = dataset
                    session['editor-sessions'][instance.handle_string]['action'] = None
                    session['editor-sessions'][instance.handle_string]['datasets'][dataset.name] = {
                        'title': dataset.title,
                        'description': dataset.description,
                        'data': dataset.data.to_dict(),
                        'plot_kind': 'scatter',
                    }
                    del(self.fields['dynamic'])
                    self.fields['dataset'].readonly = False

                else:
                    flash('Incomplete parameters for dynamic dataset %s!' % dataset_name, 'error')

#@app.expose('/svg/plot/editor/', force_secure=True)
class DataEditor_old(poobrains.auth.ProtectedForm):

    #save_dataset = poobrains.form.Button(label="Save", type="submit")


    def __init__(self, handle=None, mode=None, **kwargs):

        super(DataEditor, self).__init__(handle=handle, mode=mode, **kwargs)
 
        self.menu_actions = poobrains.rendering.Menu('data-editor-sessions', title='Editor sessions')
        for editor_session_id in session['editor-sessions'].keys():
            self.menu_actions.append(self.__class__.url(mode='full', handle=editor_session_id), editor_session_id)


        if not self.handle_string or not self.handle_string in session['editor-sessions']: # new editor session
            self.new = EditorNewSessionFieldset(self.handle_string)

        else:

            editor_session = session['editor-sessions'][self.handle_string]

            self.editor_action = editor_session['action']

            self.dataset = Dataset()
            self.dataset.name = self.handle_string
            #self.dataset.plot_kind = editor_session['plot_kind'] or 'scatter'
            self.dataset.plot_kind = 'scatter'
            self.dataset.title = editor_session['title']
            self.dataset.description = editor_session['description']
            self.dataset.data = pandas.DataFrame.from_dict(editor_session['data'])

            #if len(self.dataset.data):
            #    self.plot_kind = PlotKindFieldset(self.handle_string)


            if editor_session['action'] is None:
                editor_session['action'] = 'load'

            if editor_session['action'].startswith('load'):
                #self.add_field(EditorLoadFieldset(self.handle_string))
                self.load = EditorLoadFieldset(self.handle_string)


            self.delete_session = poobrains.form.Button(type='submit', label='Delete session')
            self.save_dataset = poobrains.form.Button(type='submit', label='Save dataset')


    def process(self, submit):

        if submit.startswith('new.'):
            return self.fields['new'].process(submit, self)

        elif submit.startswith('load.'):
            self.fields['load'].process(submit, self)
            if len(self.dataset):
                #self.plot_kind = PlotKindFieldset(self.handle_string)
                self.plot = Plot(dataset=self.dataset)

        elif submit.startswith('plot_kind.'):
            self.fields['plot_kind'].process(submit, self)

        elif submit == 'save_dataset':
            stored_ds = self.dataset.save()
            return redirect(stored_ds.url('edit'))

        elif submit == 'delete_session':
            del(session['editor-sessions'][self.handle_string])
            return redirect(self.__class__.url('full'))

        return self
