# bijoe - BI dashboard
# Copyright (C) 2015  Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import re
import json
import hashlib
import datetime
import decimal
from collections import OrderedDict
import copy

from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils import six
from django.core.cache import cache
from django.http import Http404
from django.conf import settings

from ..utils import get_warehouses, human_join
from ..engine import Engine
from .ods import Workbook


class Visualization(object):
    def __init__(self, cube, representation, measure, drilldown_x=None, drilldown_y=None,
                 filters=None, loop=None):
        self.cube = cube
        self.representation = representation

        self.measure = measure
        self.drilldown_x = drilldown_x
        self.drilldown_y = drilldown_y
        self.filters = filters or {}
        self.loop = loop

    @property
    def drilldown(self):
        drilldown = []
        if self.loop:
            drilldown.append(self.loop)
        if self.drilldown_x:
            drilldown.append(self.drilldown_x)
        if self.drilldown_y:
            drilldown.append(self.drilldown_y)
        return drilldown

    def to_json(self):
        return {
            'warehouse': self.cube.engine.warehouse.name,
            'cube': self.cube.name,
            'representation': self.representation,
            'measure': self.measure and self.measure.name,
            'drilldown_x': self.drilldown_x and self.drilldown_x.name,
            'drilldown_y': self.drilldown_y and self.drilldown_y.name,
            'filters': self.filters,
            'loop': self.loop and self.loop.name,
        }

    def copy(self):
        return Visualization(self.cube, self.representation, measure=self.measure,
                             drilldown_x=self.drilldown_x, drilldown_y=self.drilldown_y,
                             filters=copy.deepcopy(self.filters), loop=self.loop)

    @classmethod
    def from_json(cls, d, request=None):
        for warehouse in get_warehouses(request=request):
            if d['warehouse'] == warehouse.name:
                break
        else:
            raise Http404('warehouse %s not found' % d['warehouse'])
        engine = Engine(warehouse)
        try:
            cube = engine[d['cube']]
        except KeyError:
            raise Http404('cube %s not found' % d['cube'])
        representation = d['representation']
        measure = cube.measures[d['measure']]
        drilldown_x = cube.dimensions[d['drilldown_x']] if 'drilldown_x' in d else None
        drilldown_y = cube.dimensions[d['drilldown_y']] if 'drilldown_y' in d else None
        filters = d.get('filters', {})
        loop = d.get('loop')
        if loop:
            loop = cube.dimensions[loop]
        return cls(cube, representation, measure, drilldown_x=drilldown_x, drilldown_y=drilldown_y,
                   filters=filters, loop=loop)

    @classmethod
    def from_form(cls, cube, form):
        cleaned_data = form.cleaned_data
        filters = {}
        for kw, values in cleaned_data.iteritems():
            if values and kw.startswith('filter__'):
                dimension_name = kw[8:]
                filters[dimension_name] = values
        measure = cleaned_data.get('measure', [])
        measure = measure and cube.measures[measure]
        drilldown_x = cleaned_data.get('drilldown_x')
        drilldown_x = drilldown_x and cube.dimensions[drilldown_x]
        drilldown_y = cleaned_data.get('drilldown_y')
        drilldown_y = drilldown_y and cube.dimensions[drilldown_y]
        loop = cleaned_data.get('loop')
        loop = loop and cube.dimensions[loop]
        return cls(cube, cleaned_data['representation'],
                   measure,
                   drilldown_x=drilldown_x,
                   drilldown_y=drilldown_y,
                   filters=filters, loop=loop)

    @property
    def key(self):
        keys = [self.cube.engine.warehouse.name, self.cube.name]
        if self.loop:
            keys.append(self.loop.name)
        for kw, value in self.filters.iteritems():
            if value is None:
                continue
            elif isinstance(value, (dict, list, tuple)):
                # multiple values
                if isinstance(value, dict):
                    value = value.items()
                keys.append('$'.join([kw] + sorted(map(six.text_type, value))))
            else:
                # scalar values
                keys.append(u'%s$%s' % (kw, six.text_type(value)))
        keys += [dim.name for dim in self.drilldown]
        keys += [self.measure.name]
        key = '$'.join(v.encode('utf8') for v in keys)
        return hashlib.md5(key).hexdigest()

    def stringified(self):
        data = self.cached()
        for row in data:
            for cell in row:
                value = cell['value']
                if cell['type'] == 'percent':
                    try:
                        value = ('%4.2f' % float(value)).replace('.', ',') + u' %'
                    except:
                        value = _('Not applicable')
                elif value is not None and cell['type'] == 'duration':
                    s = ''
                    if value.days:
                        s += '%d jour(s)' % value.days
                    if value.seconds / 3600:
                        s += ' %d heure(s)' % (value.seconds / 3600)
                    if not s:
                        s = 'moins d\'1 heure'
                    value = s
                elif value is not None and cell['type'] == 'bool':
                    value = _('Yes') if value else _('No')
                elif value is None and cell['type'] in ('duration','integer'):
                    value = 0
                elif value is None and cell['type'] != 'integer':
                    value = _('None')
                cell['value'] = value
        return data

    def data(self):
        return self.cube.query(self.filters.items(),
                               [dim.name for dim in self.drilldown],
                               [self.measure.name])

    def cached(self):
        key = self.key
        data = cache.get(key)
        if data is None:
            data = list(self.data())
            if settings.BIJOE_CACHE:
                cache.set(key, data)
        return data

    def table(self):
        table = []
        if len(self.drilldown) == 2:
            if self.measure.type == 'integer':
                default = 0
            elif self.measure.type == 'duration':
                default = '0 s'
            elif self.measure.type == 'percent':
                default = '0 %'
            else:
                raise NotImplementedError(self.measure.type)

            x_labels = [x.label for x in self.drilldown_x.members]
            y_labels = [y.label for y in self.drilldown_y.members]
            used_x_label = set()
            used_y_label = set()

            grid = {(x, y): default for x in x_labels for y in y_labels}

            for row in self.stringified():
                x_label = unicode(row[0]['value'])
                y_label = unicode(row[1]['value'])
                used_x_label.add(x_label)
                used_y_label.add(y_label)
                grid[(x_label, y_label)] = row[2]['value']

            table.append([''] + [x for x in x_labels if x in used_x_label])
            for y in y_labels:
                if y not in used_y_label:
                    continue
                table.append([y] + [grid[(x, y)] for x in x_labels if x in used_x_label])
            if self.measure.expression.lower().startswith('count('):
                # ajout des totaux horizontaux
                table[0].append(_('Total'))
                for row in table[1:]:
                    row.append(sum(v or 0 for v in row[1:]))
                table.append([_('Total')])
                for i in range(1, len(table[0])):
                    table[-1].append(sum([
                        row[i] or 0 for row in table[1:-1]]))
            return table
        elif self.drilldown_x:
            table.append([self.drilldown_x.label])
            table.append([self.measure.label])
            for row in self.stringified():
                table[0].append(row[0]['value'])
                table[1].append(row[1]['value'])
        elif self.drilldown_y:
            table.append([self.drilldown_y.label, self.measure.label])
            for row in self.stringified():
                table.append([
                    row[0]['value'],
                    row[1]['value']
                ])
        else:
            value = self.stringified()[0][0]['value']
            table.append([self.measure.label, value])
        return table

    def javascript(self):
        l = []
        l.append('var measure = %s;' % json.dumps(self.measure.to_json()))
        l.append('var loop = %s;' % json.dumps(self.loop.to_json() if self.loop else None))
        l.append('var drilldown = %s;' % json.dumps([dim.to_json() for dim in self.drilldown]))
        l.append('var data = %s;' % json.dumps(self.json_data()))
        return mark_safe('\n'.join(l))

    def json_data(self):
        json_data = []
        for row in self.data():
            coords = []
            for cell in row[:len(self.drilldown)]:
                coords.append(cell)
            measures = []
            for cell in row[len(self.drilldown):]:
                if isinstance(cell['value'], decimal.Decimal):
                    cell['value'] = float(cell['value'])
                if isinstance(cell['value'], datetime.timedelta):
                    cell['value'] = cell['value'].days + cell['value'].seconds / 86400.
                measures.append(cell)
            json_data.append({'coords': coords, 'measures': measures})
        return json_data

    def ods(self):
        workbook = Workbook()

        full_title = self.title()

        for table in self:
            sheet_name = re.sub('[^a-zA-Z ]', '', table.table_title)
            sheet = workbook.add_sheet(sheet_name)

            sheet.write(0, 0, full_title)
            for j, row in enumerate(table.table()):
                for i, value in enumerate(row):
                    sheet.write(j + 1, i, 0 if value is None else value)
        return workbook

    def title(self):
        l = []
        if self.measure:
            l.append(self.measure.label)
        if self.drilldown_x:
            l.append(self.drilldown_x.label)
        if self.drilldown_y:
            l.append(self.drilldown_y.label)
        if self.loop:
            l.append(self.loop.label)
        return u', '.join(l)

    def __iter__(self):
        if self.loop:
            members = list(self.loop.members)
            d = list(self.cube.query(self.filters.items(), [self.loop.name],
                                     [self.measure.name]))
            names = [unicode(x[0]['value']) for x in d]
            members = [m for m in members if unicode(m.label) in names]
            for member in members:
                table = self.copy()
                table.loop = None
                table.filters[self.loop.name] = [member.id]
                table.table_title = unicode(member.label)
                yield table
        else:
            self.table_title = self.title()
            yield self
