# 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 sql import Cast, Literal
from sql.functions import CharLength, Position, Substring

from trytond.i18n import gettext
from trytond.model import (
    DeactivableMixin, ModelSQL, ModelView, Workflow, fields, tree)
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If
from trytond.transaction import Transaction

from .exceptions import PackageError, PackageValidationError


class Configuration(metaclass=PoolMeta):
    __name__ = 'stock.configuration'
    package_sequence = fields.MultiValue(fields.Many2One(
            'ir.sequence', "Package Sequence", required=True,
            domain=[
                ('company', 'in', [
                        Eval('context', {}).get('company', -1), None]),
                ('sequence_type', '=',
                    Id('stock_package', 'sequence_type_package')),
                ]))

    @classmethod
    def multivalue_model(cls, field):
        pool = Pool()
        if field == 'package_sequence':
            return pool.get('stock.configuration.sequence')
        return super(Configuration, cls).multivalue_model(field)

    @classmethod
    def default_package_sequence(cls, **pattern):
        return cls.multivalue_model(
            'package_sequence').default_package_sequence()


class ConfigurationSequence(metaclass=PoolMeta):
    __name__ = 'stock.configuration.sequence'
    package_sequence = fields.Many2One(
        'ir.sequence', "Package Sequence", required=True,
        domain=[
            ('company', 'in', [Eval('company', -1), None]),
            ('sequence_type', '=',
                Id('stock_package', 'sequence_type_package')),
            ])

    @classmethod
    def default_package_sequence(cls):
        pool = Pool()
        ModelData = pool.get('ir.model.data')
        try:
            return ModelData.get_id('stock_package', 'sequence_package')
        except KeyError:
            return None


class MeasurementsMixin:
    __slots__ = ()

    length = fields.Float(
        "Length", digits='length_uom',
        help="The length of the package.")
    length_uom = fields.Many2One(
        'product.uom', "Length UoM",
        domain=[('category', '=', Id('product', 'uom_cat_length'))],
        states={
            'required': Bool(Eval('length')),
            },
        help="The Unit of Measure for the package length.")
    height = fields.Float(
        "Height", digits='height_uom',
        help="The height of the package.")
    height_uom = fields.Many2One(
        'product.uom', "Height UoM",
        domain=[('category', '=', Id('product', 'uom_cat_length'))],
        states={
            'required': Bool(Eval('height')),
            },
        help="The Unit of Measure for the package height.")
    width = fields.Float(
        "Width", digits='width_uom',
        help="The width of the package.")
    width_uom = fields.Many2One(
        'product.uom', "Width UoM",
        domain=[('category', '=', Id('product', 'uom_cat_length'))],
        states={
            'required': Bool(Eval('width')),
            },
        help="The Unit of Measure for the package width.")

    packaging_volume = fields.Float(
        "Packaging Volume", digits='packaging_volume_uom',
        states={
            'readonly': (
                Bool(Eval('length'))
                & Bool(Eval('height'))
                & Bool(Eval('width'))),
            },
        help="The volume of the package.")
    packaging_volume_uom = fields.Many2One(
        'product.uom', "Packaging Volume UoM",
        domain=[('category', '=', Id('product', 'uom_cat_volume'))],
        states={
            'required': Bool(Eval('packaging_volume')),
            },
        help="The Unit of Measure for the packaging volume.")

    packaging_weight = fields.Float(
        "Packaging Weight", digits='packaging_weight_uom',
        help="The weight of the package when empty.")
    packaging_weight_uom = fields.Many2One(
        'product.uom', "Packaging Weight UoM",
        domain=[('category', '=', Id('product', 'uom_cat_weight'))],
        states={
            'required': Bool(Eval('packaging_weight')),
            },
        help="The Unit of Measure for the packaging weight.")

    @fields.depends(
        'packaging_volume', 'packaging_volume_uom',
        'length', 'length_uom',
        'height', 'height_uom',
        'width', 'width_uom')
    def on_change_with_packaging_volume(self):
        pool = Pool()
        ModelData = pool.get('ir.model.data')
        Uom = pool.get('product.uom')

        if not all([self.packaging_volume_uom,
                    self.length, self.length_uom,
                    self.height, self.height_uom,
                    self.width, self.width_uom]):
            if all([
                        self.length,
                        self.height,
                        self.width]):
                return
            return self.packaging_volume

        meter = Uom(ModelData.get_id('product', 'uom_meter'))
        cubic_meter = Uom(ModelData.get_id('product', 'uom_cubic_meter'))

        length = Uom.compute_qty(
            self.length_uom, self.length, meter, round=False)
        height = Uom.compute_qty(
            self.height_uom, self.height, meter, round=False)
        width = Uom.compute_qty(
            self.width_uom, self.width, meter, round=False)

        return Uom.compute_qty(
            cubic_meter, length * height * width, self.packaging_volume_uom)


class Package(tree(), MeasurementsMixin, ModelSQL, ModelView):
    'Stock Package'
    __name__ = 'stock.package'
    _rec_name = 'number'
    number = fields.Char("Number", readonly=True, required=True)
    company = fields.Many2One('company.company', "Company", required=True)
    type = fields.Many2One(
        'stock.package.type', "Type", required=True,
        states={
            'readonly': Eval('state') == 'closed',
            })
    shipment = fields.Reference(
        "Shipment", selection='get_shipment',
        states={
            'readonly': Eval('state') == 'closed',
            },
        domain={
            'stock.shipment.out': [
                ('company', '=', Eval('company', -1)),
                ],
            'stock.shipment.in.return': [
                ('company', '=', Eval('company', -1)),
                ],
            })
    moves = fields.One2Many('stock.move', 'package', 'Moves',
        domain=[
            ('company', '=', Eval('company', -1)),
            ('shipment', '=', Eval('shipment')),
            ('to_location.type', 'in', ['customer', 'supplier']),
            ('state', '!=', 'cancelled'),
            ],
        filter=[
            ('quantity', '!=', 0),
            ],
        add_remove=[
            ('package', '=', None),
            ],
        states={
            'readonly': Eval('state') == 'closed',
            })
    parent = fields.Many2One(
        'stock.package', "Parent", ondelete='CASCADE',
        domain=[
            ('company', '=', Eval('company', -1)),
            ('shipment', '=', Eval('shipment')),
            ],
        states={
            'readonly': Eval('state') == 'closed',
            })
    children = fields.One2Many(
        'stock.package', 'parent', 'Children',
        domain=[
            ('company', '=', Eval('company', -1)),
            ('shipment', '=', Eval('shipment')),
            ],
        states={
            'readonly': Eval('state') == 'closed',
            })
    state = fields.Function(fields.Selection([
                ('open', "Open"),
                ('closed', "Closed"),
                ], "State"), 'on_change_with_state')

    @classmethod
    def __setup__(cls):
        cls.number.search_unaccented = False
        super().__setup__()

    @classmethod
    def __register__(cls, module):
        pool = Pool()
        table_h = cls.__table_handler__(module)
        table = cls.__table__()
        company_exist = table_h.column_exist('company')
        cursor = Transaction().connection.cursor()

        # Migration from 6.8: rename code to number
        if table_h.column_exist('code'):
            table_h.column_rename('code', 'number')

        super().__register__(module)

        # Migration from 5.8: add company
        if not company_exist:
            shipment_id = Cast(Substring(
                    table.shipment,
                    Position(',', table.shipment) + Literal(1)),
                cls.id.sql_type().base)
            for name in cls._get_shipment():
                Shipment = pool.get(name)
                shipment = Shipment.__table__()
                value = (shipment
                    .select(shipment.company,
                        where=shipment.id == shipment_id))
                cursor.execute(*table.update(
                        [table.company], [value],
                        where=table.shipment.like(name + ',%')))

    @classmethod
    def order_number(cls, tables):
        table, _ = tables[None]
        return [CharLength(table.number), table.number]

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

    @staticmethod
    def _get_shipment():
        'Return list of Model names for shipment Reference'
        return [
            'stock.shipment.out',
            'stock.shipment.in.return',
            ]

    @classmethod
    def get_shipment(cls):
        pool = Pool()
        Model = pool.get('ir.model')
        get_name = Model.get_name
        models = cls._get_shipment()
        return [(None, '')] + [(m, get_name(m)) for m in models]

    @fields.depends('shipment')
    def on_change_with_state(self, name=None):
        if (self.shipment
                and self.shipment.state in {'packed', 'done', 'cancelled'}):
            return 'closed'
        return 'open'

    @fields.depends('type')
    def on_change_type(self):
        if self.type:
            for name in dir(MeasurementsMixin):
                if isinstance(getattr(MeasurementsMixin, name), fields.Field):
                    setattr(self, name, getattr(self.type, name))

    @classmethod
    def validate(cls, packages):
        super().validate(packages)
        for package in packages:
            package.check_volume()

    def check_volume(self):
        pool = Pool()
        Uom = pool.get('product.uom')
        Lang = pool.get('ir.lang')
        lang = Lang.get()
        if not self.packaging_volume:
            return
        children_volume = 0
        for child in self.children:
            if child.packaging_volume:
                children_volume += Uom.compute_qty(
                    child.packaging_volume_uom, child.packaging_volume,
                    self.packaging_volume_uom, round=False)
        if self.packaging_volume < children_volume:
            raise PackageValidationError(
                gettext('stock_package.msg_package_volume_too_small',
                    package=self.rec_name,
                    volume=lang.format_number_symbol(
                        self.packaging_volume, self.packaging_volume_uom),
                    children_volume=lang.format_number_symbol(
                        children_volume, self.packaging_volume_uom)))

    @classmethod
    def create(cls, vlist):
        pool = Pool()
        Config = pool.get('stock.configuration')

        vlist = [v.copy() for v in vlist]
        config = Config(1)
        default_company = cls.default_company()
        for values in vlist:
            values['number'] = config.get_multivalue(
                'package_sequence',
                company=values.get('company', default_company)).get()
        return super(Package, cls).create(vlist)

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


class Type(MeasurementsMixin, DeactivableMixin, ModelSQL, ModelView):
    'Stock Package Type'
    __name__ = 'stock.package.type'
    name = fields.Char('Name', required=True)


class Move(metaclass=PoolMeta):
    __name__ = 'stock.move'
    package = fields.Many2One(
        'stock.package', "Package", readonly=True,
        domain=[
            ('company', '=', Eval('company', -1)),
            ])

    @property
    def package_path(self):
        path = []
        package = self.package
        while package:
            path.append(package)
            package = package.parent
        path.reverse()
        return path

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

    @classmethod
    @ModelView.button
    @Workflow.transition('cancelled')
    def cancel(cls, moves):
        cls.write([m for m in moves if m.package], {'package': None})
        super().cancel(moves)


class PackageMixin(object):
    __slots__ = ()
    packages = fields.One2Many('stock.package', 'shipment', 'Packages',
        domain=[
            ('company', '=', Eval('company', -1)),
            ])
    root_packages = fields.One2Many('stock.package',
        'shipment', 'Packages',
        domain=[
            ('company', '=', Eval('company', -1)),
            ],
        filter=[
            ('parent', '=', None),
            ])

    @classmethod
    def check_packages(cls, shipments):
        for shipment in shipments:
            if not shipment.packages:
                continue
            length = sum(len(p.moves) for p in shipment.packages)
            if len(shipment.packages_moves) != length:
                raise PackageError(
                    gettext('stock_package.msg_package_mismatch',
                        shipment=shipment.rec_name))

    @property
    def packages_moves(self):
        raise NotImplementedError

    @classmethod
    def copy(cls, shipments, default=None):
        default = default.copy() if default is not None else {}
        default.setdefault('packages')
        default.setdefault('root_packages')
        return super().copy(shipments, default=default)


class ShipmentOut(PackageMixin, object, metaclass=PoolMeta):
    __name__ = 'stock.shipment.out'

    @classmethod
    def __setup__(cls):
        super().__setup__()
        packages_readonly = If(
            Eval('warehouse_storage') == Eval('warehouse_output'),
            Eval('state') != 'waiting',
            Eval('state') != 'picked')
        for field in [cls.packages, cls.root_packages]:
            field.states['readonly'] = packages_readonly

    @classmethod
    @ModelView.button
    @Workflow.transition('packed')
    def pack(cls, shipments):
        super(ShipmentOut, cls).pack(shipments)
        cls.check_packages(shipments)

    @classmethod
    @ModelView.button
    @Workflow.transition('done')
    def done(cls, shipments):
        super(ShipmentOut, cls).done(shipments)
        cls.check_packages(shipments)

    @property
    def packages_moves(self):
        return [
            m for m in self.outgoing_moves
            if m.state != 'cancelled' and m.quantity]

    def _group_parcel_key(self, lines, line):
        try:
            root_package = line.package_path[0]
        except IndexError:
            root_package = None
        return super()._group_parcel_key(lines, line) + (
            ('root_package', root_package),)

    @fields.depends('carrier')
    def _parcel_weight(self, parcel):
        pool = Pool()
        Uom = pool.get('product.uom')
        weight = super()._parcel_weight(parcel)
        if self.carrier:
            carrier_uom = self.carrier.weight_uom
            packages = {p for l in parcel for p in l.package_path}
            for package in packages:
                if package.packaging_weight:
                    weight += Uom.compute_qty(
                        package.packaging_weight_uom, package.packaging_weight,
                        carrier_uom, round=False)
        return weight


class ShipmentInReturn(PackageMixin, object, metaclass=PoolMeta):
    __name__ = 'stock.shipment.in.return'

    @classmethod
    def __setup__(cls):
        super().__setup__()
        packages_readonly = ~Eval('state').in_(['waiting', 'assigned'])
        for field in [cls.packages, cls.root_packages]:
            field.states['readonly'] = packages_readonly

    @classmethod
    @ModelView.button
    @Workflow.transition('done')
    def done(cls, shipments):
        super(ShipmentInReturn, cls).done(shipments)
        cls.check_packages(shipments)

    @property
    def packages_moves(self):
        return [
            m for m in self.moves
            if m.state != 'cancelled' and m.quantity]
