# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

import datetime

from sql import Literal
from sql.aggregate import Sum

from trytond.model import (
    ModelView, ModelSQL, ModelStorage, DeactivableMixin, fields, Unique)
from trytond.pyson import Not, Bool, Eval, If
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.tools import reduce_ids, grouped_slice

__all__ = ['Work', 'WorkContext']


class Work(DeactivableMixin, ModelSQL, ModelView):
    'Work'
    __name__ = 'timesheet.work'
    name = fields.Char('Name',
        states={
            'invisible': Bool(Eval('origin')),
            'required': ~Eval('origin'),
            },
        depends=['origin'],
        help="The main identifier of the work.")
    origin = fields.Reference('Origin', selection='get_origin',
        states={
            'invisible': Bool(Eval('name')),
            'required': ~Eval('name'),
            },
        depends=['name'],
        help="Use to relate the time spent to other records.")
    duration = fields.Function(fields.TimeDelta('Timesheet Duration',
            'company_work_time', help="Total time spent on this work."),
        'get_duration')
    timesheet_start_date = fields.Date('Timesheet Start',
        domain=[
            If(Eval('timesheet_start_date') & Eval('timesheet_end_date'),
                ('timesheet_start_date', '<=', Eval('timesheet_end_date')),
                ()),
            ],
        depends=['timesheet_end_date'],
        help="Restrict adding lines before the date.")
    timesheet_end_date = fields.Date('Timesheet End',
        domain=[
            If(Eval('timesheet_start_date') & Eval('timesheet_end_date'),
                ('timesheet_end_date', '>=', Eval('timesheet_start_date')),
                ()),
            ],
        depends=['timesheet_start_date'],
        help="Restrict adding lines after the date.")
    company = fields.Many2One('company.company', 'Company', required=True,
        select=True, help="Make the work belong to the company.")
    timesheet_lines = fields.One2Many('timesheet.line', 'work',
        'Timesheet Lines',
        depends=['active'],
        states={
            'readonly': Not(Bool(Eval('active'))),
            },
        help="Spend time on this work.")
    # Self referring field to use for aggregation in graph view
    work = fields.Function(fields.Many2One('timesheet.work', 'Work'),
        'get_work')

    @classmethod
    def __setup__(cls):
        super(Work, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('origin_unique', Unique(t, t.origin, t.company),
                'The origin must be unique per company.'),
            ]
        cls._error_messages.update({
                'mismatch_company': ('The company of the work "%(work)s" '
                    'is different than the origin\'s company'),
                })

    @classmethod
    def __register__(cls, module_name):
        table_h = cls.__table_handler__(module_name)
        table = cls.__table__()
        cursor = Transaction().connection.cursor()

        super(Work, cls).__register__(module_name)

        # Migration from 4.0: remove required on name
        table_h.not_null_action('name', 'remove')

        # Migration from 4.0: remove parent, left and right
        if table_h.column_exist('parent'):
            id2name = {}
            id2parent = {}
            cursor.execute(*table.select(
                    table.id, table.parent, table.name))
            for id_, parent, name in cursor:
                id2name[id_] = name
                id2parent[id_] = parent

            for id_, name in id2name.items():
                parent = id2parent[id_]
                while parent:
                    name = '%s\\%s' % (id2name[parent], name)
                    parent = id2parent[parent]
                cursor.execute(*table.update(
                        [table.name], [name],
                        where=table.id == id_))
            table_h.drop_column('parent')
        table_h.drop_column('left')
        table_h.drop_column('right')

        # Migration from 4.0: remove timesheet_available
        if table_h.column_exist('timesheet_available'):
            cursor.execute(*table.delete(
                    where=table.timesheet_available == False))
            table_h.drop_column('timesheet_available')

    @staticmethod
    def default_company():
        return Transaction().context.get('company')

    @classmethod
    def _get_origin(cls):
        'Return list of Model names for origin Reference'
        return []

    @classmethod
    def get_origin(cls):
        Model = Pool().get('ir.model')
        models = cls._get_origin()
        models = Model.search([
                ('model', 'in', models),
                ])
        return [('', '')] + [(m.model, m.name) for m in models]

    @classmethod
    def get_duration(cls, works, name):
        pool = Pool()
        Line = pool.get('timesheet.line')
        transaction = Transaction()
        cursor = transaction.connection.cursor()
        context = transaction.context

        table_w = cls.__table__()
        line = Line.__table__()
        ids = [w.id for w in works]
        durations = dict.fromkeys(ids, None)
        where = Literal(True)
        if context.get('from_date'):
            where &= line.date >= context['from_date']
        if context.get('to_date'):
            where &= line.date <= context['to_date']
        if context.get('employees'):
            where &= line.employee.in_(context['employees'])

        query_table = table_w.join(line, 'LEFT',
            condition=line.work == table_w.id)

        for sub_ids in grouped_slice(ids):
            red_sql = reduce_ids(table_w.id, sub_ids)
            cursor.execute(*query_table.select(table_w.id, Sum(line.duration),
                    where=red_sql & where,
                    group_by=table_w.id))
            for work_id, duration in cursor.fetchall():
                # SQLite uses float for SUM
                if duration and not isinstance(duration, datetime.timedelta):
                    duration = datetime.timedelta(seconds=duration)
                durations[work_id] = duration
        return durations

    def get_work(self, name):
        return self.id

    def get_rec_name(self, name):
        if isinstance(self.origin, ModelStorage):
            return self.origin.rec_name
        else:
            return self.name

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
            ('name',) + tuple(clause[1:]),
            ] + [
                ('origin.rec_name',) + tuple(clause[1:]) + (origin,)
                for origin in cls._get_origin()]

    @classmethod
    def copy(cls, works, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('timesheet_lines', None)
        return super(Work, cls).copy(works, default=default)

    @classmethod
    def validate(cls, works):
        super(Work, cls).validate(works)
        for work in works:
            if work.origin and not work._validate_company():
                cls.raise_user_error('mismatch_company', {
                        'work': work.rec_name,
                        })

    def _validate_company(self):
        return True

    @classmethod
    def search_global(cls, text):
        for record, rec_name, icon in super(Work, cls).search_global(text):
            icon = icon or 'tryton-clock'
            yield record, rec_name, icon

    @property
    def hours(self):
        if not self.duration:
            return 0
        return self.duration.total_seconds() / 60 / 60


class WorkContext(ModelView):
    'Work Context'
    __name__ = 'timesheet.work.context'
    from_date = fields.Date('From Date',
        help="Do not take into account lines before the date.")
    to_date = fields.Date('To Date',
        help="Do not take into account lines after the date.")
