# 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 decimal import Decimal

from trytond.model import (
    ModelSQL, ModelStorage, ModelView, fields, sequence_ordered)
from trytond.modules.product import round_price
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, If


class Template(metaclass=PoolMeta):
    __name__ = "product.template"

    components = fields.One2Many(
        'product.component', 'parent_template', "Components")

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.type.selection.append(('kit', "Kit"))

    @classmethod
    def _cost_price_method_domain_per_type(cls):
        types_cost_method = super()._cost_price_method_domain_per_type()
        types_cost_method['kit'] = [('cost_price_method', '=', 'fixed')]
        return types_cost_method

    @fields.depends('type', 'cost_price_method')
    def on_change_type(self):
        super().on_change_type()
        if self.type == 'kit':
            self.cost_price_method = 'fixed'

    @classmethod
    def copy(cls, templates, default=None):
        pool = Pool()
        Component = pool.get('product.component')
        if default is None:
            default = {}
        else:
            default = default.copy()

        copy_components = 'components' not in default
        default.setdefault('components', None)
        new_templates = super().copy(templates, default)
        if copy_components:
            old2new = {}
            to_copy = []
            for template, new_template in zip(templates, new_templates):
                to_copy.extend(
                    c for c in template.components if not c.parent_product)
                old2new[template.id] = new_template.id
            if to_copy:
                Component.copy(to_copy, {
                        'parent_template': (lambda d:
                            old2new[d['parent_template']]),
                        })
        return new_templates


class Product(metaclass=PoolMeta):
    __name__ = "product.product"

    components = fields.One2Many(
        'product.component', 'parent_product', "Components")

    def get_multivalue(self, name, **pattern):
        pool = Pool()
        Uom = pool.get('product.uom')
        value = super().get_multivalue(name, **pattern)
        if name == 'cost_price' and self.type == 'kit':
            value = Decimal(0)
            for component in self.components_used:
                cost_price = component.product.get_multivalue(
                    'cost_price', **pattern)
                cost_price = Uom.compute_price(
                    component.product.default_uom, cost_price, component.unit)
                value += cost_price * Decimal(str(component.quantity))
            value = round_price(value)
        return value

    @property
    def components_used(self):
        if self.components:
            yield from self.components
        else:
            for component in self.template.components:
                if not component.parent_product:
                    yield component

    @classmethod
    def get_quantity(cls, products, name):
        pool = Pool()
        Uom = pool.get('product.uom')
        kits = [p for p in products if p.type == 'kit']
        quantities = super().get_quantity(products, name)
        for kit in kits:
            qties = []
            for component in kit.components_used:
                component_qty = Uom.compute_qty(
                    component.product.default_uom,
                    getattr(component.product, name),
                    component.unit, round=False)
                if not component.fixed:
                    component_qty /= component.quantity
                qties.append(component_qty)
            quantities[kit.id] = kit.default_uom.floor(min(qties, default=0))
        return quantities

    @classmethod
    def copy(cls, products, default=None):
        pool = Pool()
        Component = pool.get('product.component')
        if default is None:
            default = {}
        else:
            default = default.copy()

        copy_components = 'components' not in default
        if 'template' in default:
            default.setdefault('components', None)
        new_products = super().copy(products, default)
        if 'template' in default and copy_components:
            template2new = {}
            product2new = {}
            to_copy = []
            for product, new_product in zip(products, new_products):
                if product.components:
                    to_copy.extend(product.components)
                    template2new[product.template.id] = new_product.template.id
                    product2new[product.id] = new_product.id
            if to_copy:
                Component.copy(to_copy, {
                        'parent_product': (lambda d:
                            product2new[d['parent_product']]),
                        'parent_template': (lambda d:
                            template2new[d['parent_template']]),
                        })
        return new_products


class ComponentMixin(sequence_ordered(), ModelStorage):

    parent_type = fields.Function(fields.Selection(
            'get_product_types', "Parent Type"), 'on_change_with_parent_type')
    product = fields.Many2One(
        'product.product', "Product", required=True,
        domain=[
            ('components', '=', None),
            ('template.components', '=', None),
            If(Eval('parent_type') == 'kit',
                ('type', '=', 'goods'),
                ()),
            ])
    product_unit_category = fields.Function(
        fields.Many2One('product.uom.category', "Product Unit Category"),
        'on_change_with_product_unit_category')
    quantity = fields.Float("Quantity", digits='unit', required=True)
    unit = fields.Many2One('product.uom', "Unit", required=True,
        domain=[
            If(Bool(Eval('product_unit_category')),
                ('category', '=', Eval('product_unit_category')),
                ('category', '!=', -1)),
            ],
        depends={'product'})
    fixed = fields.Boolean("Fixed",
        help="Check to make the quantity of the component independent "
        "of the kit quantity.")

    @classmethod
    def get_product_types(cls):
        pool = Pool()
        Product = pool.get('product.product')
        return Product.fields_get(['type'])['type']['selection']

    def on_change_with_parent_type(self, name):
        raise NotImplementedError

    @property
    def parent_uom(self):
        raise NotImplementedError

    @fields.depends('product', 'unit', 'quantity',
        methods=['on_change_with_product_unit_category'])
    def on_change_product(self):
        if self.product:
            self.product_unit_category = (
                self.on_change_with_product_unit_category())
            if (not self.unit
                    or self.unit.category != self.product_unit_category):
                self.unit = self.product.default_uom

    @fields.depends('product')
    def on_change_with_product_unit_category(self, name=None):
        return self.product.default_uom.category if self.product else None

    def get_line(self, Line, quantity, unit, **values):
        pool = Pool()
        Uom = pool.get('product.uom')
        line = Line(product=self.product, **values)
        line.unit = self.unit
        if self.fixed:
            line.quantity = self.quantity
        else:
            quantity = Uom.compute_qty(
                unit, quantity, self.parent_uom, round=False)
            line.quantity = self.unit.round(quantity * self.quantity)
        return line

    def get_rec_name(self, name):
        pool = Pool()
        Lang = pool.get('ir.lang')
        lang = Lang.get()
        return (lang.format_number_symbol(
                self.quantity, self.unit, digits=self.unit.digits)
            + ' %s' % self.product.rec_name)

    @classmethod
    def search_rec_name(cls, name, clause):
        return [
            ('product.rec_name', *clause[1:]),
            ]


class Component(ComponentMixin, ModelSQL, ModelView):
    "Product Component"
    __name__ = "product.component"

    parent_template = fields.Many2One(
        'product.template', "Parent Product",
        required=True, ondelete='CASCADE',
        domain=[
            If(Bool(Eval('parent_product')),
                ('products', '=', Eval('parent_product')),
                ()),
            ])
    parent_product = fields.Many2One(
        'product.product', "Parent Variant", ondelete='CASCADE',
        domain=[
            If(Bool(Eval('parent_template')),
                ('template', '=', Eval('parent_template')),
                ()),
            ])

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.update(['parent_template', 'parent_product'])

    @fields.depends(
        'parent_product', '_parent_parent_product.template')
    def on_change_parent_product(self):
        if self.parent_product:
            self.parent_template = self.parent_product.template

    @fields.depends(
        'parent_template', '_parent_parent_template.type',
        'parent_product', '_parent_parent_product.type')
    def on_change_with_parent_type(self, name=None):
        if self.parent_product:
            return self.parent_product.type
        elif self.parent_template:
            return self.parent_template.type

    @property
    def parent_uom(self):
        if self.parent_product:
            return self.parent_product.default_uom
        elif self.parent_template:
            return self.parent_template.default_uom

    def get_rec_name(self, name):
        return super().get_rec_name(name) + (
            ' @ %s' % (
                self.parent_product.rec_name if self.parent_product
                else self.parent_template.rec_name))

    @classmethod
    def search_rec_name(cls, name, clause):
        return super().search_rec_name(name, clause) + [
            ('parent_product.rec_name',) + tuple(clause[1:]),
            ('parent_template.rec_name',) + tuple(clause[1:]),
            ]
