# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from collections import defaultdict

from odoo import _, api, exceptions, fields, models

from odoo.addons.base_sparse_field.models.fields import Serialized


class StockLocation(models.Model):
    _inherit = "stock.location"

    tray_type_id = fields.Many2one(
        comodel_name="stock.location.tray.type", ondelete="restrict"
    )
    cell_in_tray_type_id = fields.Many2one(
        string="Cell Tray Type", related="location_id.tray_type_id", readonly=True
    )
    tray_cell_contains_stock = fields.Boolean(
        compute="_compute_tray_cell_contains_stock",
        help="Used to know if a cell of a Tray location is empty.",
    )
    tray_matrix = Serialized(string="Cells", compute="_compute_tray_matrix")
    cell_name_format = fields.Char(
        string="Name Format for Cells",
        default=lambda self: self._default_cell_name_format(),
        help="Cells sub-locations generated in a tray will be named"
        " after this format. Replacement fields between curly braces are used"
        " to inject positions. {x}, {y}, and {z} will be replaced by their"
        " corresponding position. Complex formatting (such as padding, ...)"
        " can be done using the format specification at "
        " https://docs.python.org/3/library/string.html#formatstrings",
    )

    def _default_cell_name_format(self):
        return "x{x:0>2}y{y:0>2}"

    @api.depends("quant_ids.quantity")
    def _compute_tray_cell_contains_stock(self):
        for location in self:
            if not location.cell_in_tray_type_id:
                # Not a tray cell so the value is irrelevant,
                # best to skip them for performance.
                location.tray_cell_contains_stock = False
                continue
            quants = location.quant_ids.filtered(lambda r: r.quantity > 0)
            location.tray_cell_contains_stock = bool(quants)

    @api.depends("quant_ids.quantity", "tray_type_id", "location_id.tray_type_id")
    def _compute_tray_matrix(self):
        for location in self:
            if not (location.tray_type_id or location.cell_in_tray_type_id):
                location.tray_matrix = {}
                continue
            location.tray_matrix = location._tray_matrix_for_widget()

    def _tray_matrix_for_widget(self):
        selected = self._tray_cell_coords()
        cells = self._tray_cell_matrix()
        return {
            # x, y: position of the selected cell
            "selected": selected,
            # 0 is empty, 1 is not
            "cells": cells,
        }

    def action_tray_matrix_click(self, coordX, coordY):
        self.ensure_one()
        if self.cell_in_tray_type_id:
            tray = self.location_id
        else:
            tray = self
        location = self.search(
            [
                ("id", "child_of", tray.ids),
                # we receive positions counting from 0 but they are stored
                # in the "human" format starting from 1
                ("posx", "=", coordX + 1),
                ("posy", "=", coordY + 1),
            ]
        )
        location.ensure_one()
        view = self.env.ref("stock.view_location_form")
        action = self.env.ref("stock.action_location_form").read()[0]
        action.update(
            {
                "res_id": location.id,
                "view_mode": "form",
                "view_type": "form",
                "view_id": view.id,
                "views": [(view.id, "form")],
            }
        )
        return action

    @api.model_create_multi
    def create(self, vals_list):
        records = super().create(vals_list)
        records._update_tray_sublocations()
        return records

    def _check_before_add_tray_type(self):
        if not self.tray_type_id and self.child_ids:
            raise exceptions.UserError(
                _("Location %s has sub-locations, it cannot be converted" " to a tray.")
                % (self.display_name)
            )

    def write(self, vals):
        for location in self:
            trays_to_update = False
            if "tray_type_id" in vals:
                location._check_before_add_tray_type()
                new_tray_type_id = vals.get("tray_type_id")
                trays_to_update = location.tray_type_id.id != new_tray_type_id
            # short-circuit this check if we already know that we have to
            # update trays
            if not trays_to_update and "cell_name_format" in vals:
                new_format = vals.get("cell_name_format")
                trays_to_update = location.cell_name_format != new_format
            super(StockLocation, location).write(vals)
            if trays_to_update:
                self._update_tray_sublocations()
            elif "posz" in vals and location.tray_type_id:
                # On initial generation (when tray_to_update is true),
                # the sublocations are already generated with the pos z.
                location.child_ids.write({"posz": vals["posz"]})
        return True

    @api.constrains("active")
    def _tray_check_active(self):
        for record in self:
            if record.active:
                continue
            # We cannot disable any cell of a tray (entire tray)
            # if at least one of the cell contains stock.
            # We cannot disable a tray, a shuffle or a view if
            # at least one of their tray contain stock.
            if record.cell_in_tray_type_id:
                parent = record.location_id
            else:
                parent = record
            # Add the record to the search: as it has been set inactive, it
            # will not be found by the search.
            locs = self.search([("id", "child_of", parent.id)]) | record
            if any(
                (loc.tray_type_id or loc.cell_in_tray_type_id)
                and loc.tray_cell_contains_stock
                for loc in locs
            ):
                raise exceptions.ValidationError(
                    _(
                        "Tray locations cannot be archived when "
                        "they contain products."
                    )
                )

    def tray_cell_center_position(self):
        """Return the center position in mm of a cell

        The returned position is a tuple with the number of millimeters
        from the bottom-left corner. Tuple: (left, bottom)
        """
        if not self.cell_in_tray_type_id:
            return 0, 0
        posx = self.posx
        posy = self.posy
        cell_width = self.cell_in_tray_type_id.width_per_cell
        cell_depth = self.cell_in_tray_type_id.depth_per_cell
        # posx and posy start at one, we want to count from 0
        from_left = (posx - 1) * cell_width + (cell_width / 2)
        from_bottom = (posy - 1) * cell_depth + (cell_depth / 2)
        return from_left, from_bottom

    def _tray_cell_coords(self):
        if not self.cell_in_tray_type_id:
            return []
        return [self.posx - 1, self.posy - 1]

    def _tray_cell_matrix(self):
        assert self.tray_type_id or self.cell_in_tray_type_id
        if self.tray_type_id:
            location = self
        else:  # cell
            location = self.location_id
        cells = location.tray_type_id._generate_cells_matrix()
        for cell in location.child_ids:
            if cell.tray_cell_contains_stock:
                # 1 means used
                cells[cell.posy - 1][cell.posx - 1] = 1
        return cells

    def _format_tray_sublocation_name(self, x, y, z):
        template = self.cell_name_format or self._default_cell_name_format()
        # using format_map allows to have missing replacement strings
        return template.format_map(defaultdict(str, x=x, y=y, z=z))

    def _update_tray_sublocations(self):
        values = []
        for location in self:
            tray_type = location.tray_type_id

            try:
                location.child_ids.write({"active": False})
            except exceptions.ValidationError:
                # trap this check (_tray_check_active) to display a
                # contextual error message
                raise exceptions.UserError(
                    _("Trays cannot be modified when " "they contain products.")
                )

            if not tray_type:
                continue

            # create accepts several records now
            posz = location.posz or 0
            for row in range(1, tray_type.rows + 1):
                for col in range(1, tray_type.cols + 1):
                    cell_name = location._format_tray_sublocation_name(col, row, posz)
                    subloc_values = {
                        "name": cell_name,
                        "posx": col,
                        "posy": row,
                        "posz": posz,
                        "location_id": location.id,
                        "company_id": location.company_id.id,
                    }
                    values.append(subloc_values)
        if values:
            self.create(values)

    def _create_tray_xmlids(self, module):
        """Create external IDs for generated cells

        If the tray location has one. Used for the demo/test data. It will not
        handle properly changing the tray format as the former cells will keep
        the original xmlid built on x and y, the new ones will not be able to
        use them. As these xmlids are meant for the demo data and the tests,
        it is not a problem and should not be used for other purposes.

        Called from stock_location_tray/demo/stock_location_demo.xml.
        """
        xmlids_to_create = []

        def has_ref(xmlid):
            ModelData = self.env["ir.model.data"]
            __, res_id = ModelData.xmlid_to_res_model_res_id(xmlid)
            return bool(res_id)

        for location in self:
            if not location.cell_in_tray_type_id:
                continue
            tray = location.location_id
            tray_external_id = tray.get_external_id().get(tray.id)
            if not tray_external_id:
                continue
            if "." not in tray_external_id:
                continue
            namespace, tray_name = tray_external_id.split(".")
            if module != namespace:
                continue
            tray_external = self.env["ir.model.data"].browse(
                self.env["ir.model.data"]._get_id(module, tray_name)
            )
            cell_external_id = "{}_x{}y{}".format(
                tray_name, location.posx, location.posy
            )
            cell_xmlid = "{}.{}".format(module, cell_external_id)
            if not has_ref(cell_xmlid):
                xmlids_to_create.append(
                    {
                        "name": cell_external_id,
                        "module": module,
                        "model": self._name,
                        "res_id": location.id,
                        "noupdate": tray_external.noupdate,
                    }
                )

        self.env["ir.model.data"].create(xmlids_to_create)
