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

from functools import wraps

from sql.operators import NotIn

from trytond.model import ModelView, ModelSQL, fields, Unique
from trytond.modules import get_module_list, get_module_info
from trytond.wizard import Wizard, StateView, Button, StateTransition, \
    StateAction
from trytond import backend
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.pyson import Eval
from trytond.rpc import RPC

__all__ = [
    'Module', 'ModuleDependency', 'ModuleConfigWizardItem',
    'ModuleConfigWizardFirst', 'ModuleConfigWizardOther',
    'ModuleConfigWizardDone', 'ModuleConfigWizard',
    'ModuleActivateUpgradeStart', 'ModuleActivateUpgradeDone',
    'ModuleActivateUpgrade', 'ModuleConfig',
    ]


def filter_state(state):
    def filter(func):
        @wraps(func)
        def wrapper(cls, modules):
            modules = [m for m in modules if m.state == state]
            return func(cls, modules)
        return wrapper
    return filter


class Module(ModelSQL, ModelView):
    "Module"
    __name__ = "ir.module"
    name = fields.Char("Name", readonly=True, required=True)
    version = fields.Function(fields.Char('Version'), 'get_version')
    dependencies = fields.One2Many('ir.module.dependency',
        'module', 'Dependencies', readonly=True)
    parents = fields.Function(fields.One2Many('ir.module', None, 'Parents'),
        'get_parents')
    childs = fields.Function(fields.One2Many('ir.module', None, 'Childs'),
        'get_childs')
    state = fields.Selection([
        ('not activated', 'Not Activated'),
        ('activated', 'Activated'),
        ('to upgrade', 'To be upgraded'),
        ('to remove', 'To be removed'),
        ('to activate', 'To be activated'),
        ], string='State', readonly=True)

    @classmethod
    def __setup__(cls):
        super(Module, cls).__setup__()
        table = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(table, table.name),
                'The name of the module must be unique!'),
        ]
        cls._order.insert(0, ('name', 'ASC'))
        cls.__rpc__.update({
                'on_write': RPC(instantiate=0),
                })
        cls._error_messages.update({
            'delete_state': ('You can not remove a module that is activated '
                    'or will be activated'),
            'deactivate_dep': ('Some activated modules depend on the ones '
                    'you are trying to deactivate:'),
            })
        cls._buttons.update({
                'activate': {
                    'invisible': Eval('state') != 'not activated',
                    'depends': ['state'],
                    },
                'activate_cancel': {
                    'invisible': Eval('state') != 'to activate',
                    'depends': ['state'],
                    },
                'deactivate': {
                    'invisible': Eval('state') != 'activated',
                    'depends': ['state'],
                    },
                'deactivate_cancel': {
                    'invisible': Eval('state') != 'to remove',
                    'depends': ['state'],
                    },
                'upgrade': {
                    'invisible': Eval('state') != 'activated',
                    'depends': ['state'],
                    },
                'upgrade_cancel': {
                    'invisible': Eval('state') != 'to upgrade',
                    'depends': ['state'],
                    },
                })

    @classmethod
    def __register__(cls, module_name):
        pool = Pool()
        ModelData = pool.get('ir.model.data')
        TableHandler = backend.get('TableHandler')
        sql_table = cls.__table__()
        model_data_sql_table = ModelData.__table__()
        cursor = Transaction().connection.cursor()

        # Migration from 3.6: remove double module
        old_table = 'ir_module_module'
        if TableHandler.table_exist(old_table):
            TableHandler.table_rename(old_table, cls._table)

        super(Module, cls).__register__(module_name)

        # Migration from 4.0: rename installed to activated
        cursor.execute(*sql_table.update(
                [sql_table.state], ['activated'],
                where=sql_table.state == 'installed'))
        cursor.execute(*sql_table.update(
                [sql_table.state], ['not activated'],
                where=sql_table.state == 'uninstalled'))

        # Migration from 4.6: register buttons on ir module
        button_fs_ids = [
            'module_activate_button',
            'module_activate_cancel_button',
            'module_deactivate_button',
            'module_deactivate_cancel_button',
            'module_upgrade_button',
            'module_upgrade_cancel_button',
            ]
        cursor.execute(*model_data_sql_table.update(
                [model_data_sql_table.module], ['ir'],
                where=((model_data_sql_table.module == 'res')
                    & (model_data_sql_table.fs_id.in_(button_fs_ids)))))

    @staticmethod
    def default_state():
        return 'not activated'

    def get_version(self, name):
        return get_module_info(self.name).get('version', '')

    @classmethod
    def get_parents(cls, modules, name):
        parent_names = list(set(d.name for m in modules
                    for d in m.dependencies))
        parents = cls.search([
                ('name', 'in', parent_names),
                ])
        name2id = dict((m.name, m.id) for m in parents)
        return dict((m.id, [name2id[d.name] for d in m.dependencies])
            for m in modules)

    @classmethod
    def get_childs(cls, modules, name):
        child_ids = dict((m.id, []) for m in modules)
        name2id = dict((m.name, m.id) for m in modules)
        childs = cls.search([
                ('dependencies.name', 'in', list(name2id.keys())),
                ])
        for child in childs:
            for dep in child.dependencies:
                if dep.name in name2id:
                    child_ids[name2id[dep.name]].append(child.id)
        return child_ids

    @classmethod
    def delete(cls, records):
        for module in records:
            if module.state in (
                    'activated',
                    'to upgrade',
                    'to remove',
                    'to activate',
                    ):
                cls.raise_user_error('delete_state')
        return super(Module, cls).delete(records)

    @classmethod
    def on_write(cls, modules):
        dependencies = set()

        def get_parents(module):
            parents = set(p.id for p in module.parents)
            for p in module.parents:
                parents.update(get_parents(p))
            return parents

        def get_childs(module):
            childs = set(c.id for c in module.childs)
            for c in module.childs:
                childs.update(get_childs(c))
            return childs

        for module in modules:
            dependencies.update(get_parents(module))
            dependencies.update(get_childs(module))
        return list(dependencies)

    @classmethod
    @ModelView.button
    @filter_state('not activated')
    def activate(cls, modules):
        modules_activated = set(modules)

        def get_parents(module):
            parents = set(p for p in module.parents)
            for p in module.parents:
                parents.update(get_parents(p))
            return parents

        for module in modules:
            modules_activated.update((m for m in get_parents(module)
                    if m.state == 'not activated'))
        cls.write(list(modules_activated), {
                'state': 'to activate',
                })

    @classmethod
    @ModelView.button
    @filter_state('activated')
    def upgrade(cls, modules):
        modules_activated = set(modules)

        def get_childs(module):
            childs = set(c for c in module.childs)
            for c in module.childs:
                childs.update(get_childs(c))
            return childs

        for module in modules:
            modules_activated.update((m for m in get_childs(module)
                    if m.state == 'activated'))
        cls.write(list(modules_activated), {
                'state': 'to upgrade',
                })

    @classmethod
    @ModelView.button
    @filter_state('to activate')
    def activate_cancel(cls, modules):
        cls.write(modules, {
                'state': 'not activated',
                })

    @classmethod
    @ModelView.button
    @filter_state('activated')
    def deactivate(cls, modules):
        pool = Pool()
        Module = pool.get('ir.module')
        Dependency = pool.get('ir.module.dependency')
        module_table = Module.__table__()
        dep_table = Dependency.__table__()
        cursor = Transaction().connection.cursor()
        for module in modules:
            cursor.execute(*dep_table.join(module_table,
                    condition=(dep_table.module == module_table.id)
                    ).select(module_table.state, module_table.name,
                    where=(dep_table.name == module.name)
                    & NotIn(
                        module_table.state, ['not activated', 'to remove'])))
            res = cursor.fetchall()
            if res:
                cls.raise_user_error('deactivate_dep',
                        error_description='\n'.join(
                            '\t%s: %s' % (x[0], x[1]) for x in res))
        cls.write(modules, {'state': 'to remove'})

    @classmethod
    @ModelView.button
    @filter_state('to remove')
    def deactivate_cancel(cls, modules):
        cls.write(modules, {'state': 'not activated'})

    @classmethod
    @ModelView.button
    @filter_state('to upgrade')
    def upgrade_cancel(cls, modules):
        cls.write(modules, {'state': 'activated'})

    @classmethod
    def update_list(cls):
        'Update the list of available packages'
        count = 0
        module_names = get_module_list()

        modules = cls.search([])
        name2module = dict((m.name, m) for m in modules)
        cls.delete([m for m in modules
                if m.state != 'activated' and m.name not in module_names])

        # iterate through activated modules and mark them as being so
        for name in module_names:
            if name in name2module:
                module = name2module[name]
                tryton = get_module_info(name)
                cls._update_dependencies(module, tryton.get('depends', []))
                continue

            tryton = get_module_info(name)
            if not tryton:
                continue
            module, = cls.create([{
                        'name': name,
                        'state': 'not activated',
                        }])
            count += 1
            cls._update_dependencies(module, tryton.get('depends', []))
        return count

    @classmethod
    def _update_dependencies(cls, module, depends=None):
        pool = Pool()
        Dependency = pool.get('ir.module.dependency')
        Dependency.delete([x for x in module.dependencies
            if x.name not in depends])
        if depends is None:
            depends = []
        # Restart Browse Cache for deleted dependencies
        module = cls(module.id)
        dependency_names = [x.name for x in module.dependencies]
        to_create = []
        for depend in depends:
            if depend not in dependency_names:
                to_create.append({
                        'module': module.id,
                        'name': depend,
                        })
        if to_create:
            Dependency.create(to_create)


class ModuleDependency(ModelSQL, ModelView):
    "Module dependency"
    __name__ = "ir.module.dependency"
    name = fields.Char('Name')
    module = fields.Many2One('ir.module', 'Module', select=True,
       ondelete='CASCADE', required=True)
    state = fields.Function(fields.Selection([
                ('not activated', 'Not Activated'),
                ('activated', 'Activated'),
                ('to upgrade', 'To be upgraded'),
                ('to remove', 'To be removed'),
                ('to activate', 'To be activated'),
                ('unknown', 'Unknown'),
                ], 'State', readonly=True), 'get_state')

    @classmethod
    def __setup__(cls):
        super(ModuleDependency, cls).__setup__()
        table = cls.__table__()
        cls._sql_constraints += [
            ('name_module_uniq', Unique(table, table.name, table.module),
                'Dependency must be unique by module!'),
        ]

    @classmethod
    def __register__(cls, module_name):
        TableHandler = backend.get('TableHandler')

        # Migration from 3.6: remove double module
        old_table = 'ir_module_module_dependency'
        if TableHandler.table_exist(old_table):
            TableHandler.table_rename(old_table, cls._table)

        super(ModuleDependency, cls).__register__(module_name)

    def get_state(self, name):
        pool = Pool()
        Module = pool.get('ir.module')
        dependencies = Module.search([
                ('name', '=', self.name),
                ])
        if dependencies:
            return dependencies[0].state
        else:
            return 'unknown'


class ModuleConfigWizardItem(ModelSQL, ModelView):
    "Config wizard to run after activating a module"
    __name__ = 'ir.module.config_wizard.item'
    action = fields.Many2One('ir.action', 'Action', required=True,
        readonly=True)
    sequence = fields.Integer('Sequence', required=True)
    state = fields.Selection([
        ('open', 'Open'),
        ('done', 'Done'),
        ], string='State', required=True, select=True)

    @classmethod
    def __setup__(cls):
        super(ModuleConfigWizardItem, cls).__setup__()
        cls._order.insert(0, ('sequence', 'ASC'))

    @classmethod
    def __register__(cls, module_name):
        TableHandler = backend.get('TableHandler')
        cursor = Transaction().connection.cursor()
        pool = Pool()
        ModelData = pool.get('ir.model.data')
        model_data = ModelData.__table__()

        # Migration from 3.6: remove double module
        old_table = 'ir_module_module_config_wizard_item'
        if TableHandler.table_exist(old_table):
            TableHandler.table_rename(old_table, cls._table)
        cursor.execute(*model_data.update(
                columns=[model_data.model],
                values=[cls.__name__],
                where=(model_data.model ==
                    'ir.module.module.config_wizard.item')))

        super(ModuleConfigWizardItem, cls).__register__(module_name)

    @staticmethod
    def default_state():
        return 'open'

    @staticmethod
    def default_sequence():
        return 10


class ModuleConfigWizardFirst(ModelView):
    'Module Config Wizard First'
    __name__ = 'ir.module.config_wizard.first'


class ModuleConfigWizardOther(ModelView):
    'Module Config Wizard Other'
    __name__ = 'ir.module.config_wizard.other'

    percentage = fields.Float('Percentage', readonly=True)

    @staticmethod
    def default_percentage():
        pool = Pool()
        Item = pool.get('ir.module.config_wizard.item')
        done = Item.search([
            ('state', '=', 'done'),
            ], count=True)
        all = Item.search([], count=True)
        return done / all


class ModuleConfigWizardDone(ModelView):
    'Module Config Wizard Done'
    __name__ = 'ir.module.config_wizard.done'


class ModuleConfigWizard(Wizard):
    'Run config wizards'
    __name__ = 'ir.module.config_wizard'

    class ConfigStateAction(StateAction):

        def __init__(self):
            StateAction.__init__(self, None)

        def get_action(self):
            pool = Pool()
            Item = pool.get('ir.module.config_wizard.item')
            Action = pool.get('ir.action')
            items = Item.search([
                ('state', '=', 'open'),
                ], limit=1)
            if items:
                item = items[0]
                Item.write([item], {
                        'state': 'done',
                        })
                return Action.get_action_values(item.action.type,
                    [item.action.id])[0]

    start = StateTransition()
    first = StateView('ir.module.config_wizard.first',
        'ir.module_config_wizard_first_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('OK', 'action', 'tryton-ok', default=True),
            ])
    other = StateView('ir.module.config_wizard.other',
        'ir.module_config_wizard_other_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Next', 'action', 'tryton-forward', default=True),
            ])
    action = ConfigStateAction()
    done = StateView('ir.module.config_wizard.done',
        'ir.module_config_wizard_done_view_form', [
            Button('OK', 'end', 'tryton-ok', default=True),
            ])

    def transition_start(self):
        res = self.transition_action()
        if res == 'other':
            return 'first'
        return res

    def transition_action(self):
        pool = Pool()
        Item = pool.get('ir.module.config_wizard.item')
        items = Item.search([
                ('state', '=', 'open'),
                ])
        if items:
            return 'other'
        return 'done'

    def end(self):
        return 'reload menu'


class ModuleActivateUpgradeStart(ModelView):
    'Module Activate Upgrade Start'
    __name__ = 'ir.module.activate_upgrade.start'
    module_info = fields.Text('Modules to update', readonly=True)


class ModuleActivateUpgradeDone(ModelView):
    'Module Activate Upgrade Done'
    __name__ = 'ir.module.activate_upgrade.done'


class ModuleActivateUpgrade(Wizard):
    "Activate / Upgrade modules"
    __name__ = 'ir.module.activate_upgrade'

    start = StateView('ir.module.activate_upgrade.start',
        'ir.module_activate_upgrade_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Start Upgrade', 'upgrade', 'tryton-ok', default=True),
            ])
    upgrade = StateTransition()
    done = StateView('ir.module.activate_upgrade.done',
        'ir.module_activate_upgrade_done_view_form', [
            Button('OK', 'config', 'tryton-ok', default=True),
            ])
    config = StateAction('ir.act_module_config_wizard')

    @classmethod
    def check_access(cls):
        # Use new transaction to prevent lock when activating modules
        with Transaction().new_transaction():
            super(ModuleActivateUpgrade, cls).check_access()

    @staticmethod
    def default_start(fields):
        pool = Pool()
        Module = pool.get('ir.module')
        modules = Module.search([
                ('state', 'in', ['to upgrade', 'to remove', 'to activate']),
                ])
        return {
            'module_info': '\n'.join(x.name + ': ' + x.state
                for x in modules),
            }

    def __init__(self, session_id):
        pass

    def _save(self):
        pass

    def transition_upgrade(self):
        pool = Pool()
        Module = pool.get('ir.module')
        Lang = pool.get('ir.lang')
        with Transaction().new_transaction():
            modules = Module.search([
                ('state', 'in', ['to upgrade', 'to remove', 'to activate']),
                ])
            update = [m.name for m in modules]
            langs = Lang.search([
                ('translatable', '=', True),
                ])
            lang = [x.code for x in langs]
        if update:
            pool.init(update=update, lang=lang)
        return 'done'


class ModuleConfig(Wizard):
    'Configure Modules'
    __name__ = 'ir.module.config'

    start = StateAction('ir.act_module_form')

    @staticmethod
    def transition_start():
        return 'end'
