# SolverParametersList.py -- Pamhyr
# Copyright (C) 2023-2026  INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

# -*- coding: utf-8 -*-

from copy import copy
from tools import trace, timer
from Solver.Solvers import solver_type_list

from Model.Scenario import Scenario
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Tools.PamhyrList import PamhyrModelList


class Parameter(SQLSubModel):
    def __init__(self, id: int = -1,
                 name="", value="",
                 status=None, owner_scenario=-1):
        super(Parameter, self).__init__(
            id=id, status=status,
            owner_scenario=owner_scenario
        )

        self._name = name
        self._value = value

    @classmethod
    def _db_create(cls, execute, ext=""):
        execute(f"""
          CREATE TABLE solver_parameter{ext} (
            pamhyr_id INTEGER NOT NULL,
            ind INTEGER NOT NULL,
            name TEXT NOT NULL,
            value TEXT NOT NULL,
            solver TEXT NOT NULL,
            {Scenario.create_db_add_scenario()},
            {Scenario.create_db_add_scenario_fk()},
            PRIMARY KEY(pamhyr_id, scenario)
          )
        """)

        return cls._create_submodel(execute)

    @classmethod
    def _db_update(cls, execute, version, data=None):
        major, minor, release = version.strip().split(".")

        if major == minor == "0":
            release = int(release)
            if release < 3:
                execute(
                    "UPDATE solver_parameter SET name='mage_implicitation' " +
                    "WHERE name='mage_implication'"
                )
                execute(
                    "UPDATE solver_parameter SET name='mage_iteration_type' " +
                    "WHERE name='mage_iter_type'"
                )

            def insert(name, value, ind):
                return execute(
                    "INSERT INTO " +
                    "solver_parameter(ind, name, value, solver) " +
                    "VALUES (" +
                    f"{ind}, " +
                    f"'{cls._db_format(name)}', " +
                    f"'{cls._db_format(value)}', " +
                    f"'{cls._db_format(solver)}'" +
                    ")"
                )

            if release < 4:
                cls._db_update_to_0_0_4(execute, insert)

            if release < 7:
                cls._db_update_to_0_0_7(execute, insert)

            if release < 14:
                cls._db_update_to_0_0_14(execute, data)

        return cls._update_submodel(execute, version, data)

    @classmethod
    def _db_update_to_0_0_4(cls, execute, insert):
        solvers = execute(
            "SELECT DISTINCT solver FROM solver_parameter"
        )

        for solver in solvers:
            if solver != "mage8":
                continue

            data = execute("SELECT ind, name, value " +
                           "FROM solver_parameter " +
                           f"WHERE solver = '{solver}' " +
                           "ORDER BY ind ACS")

            execute(
                "DELETE FROM solver_parameter " +
                f"WHERE solver = '{solver}'"
            )

            ind = 0
            lst = []
            for v in data:
                insert(v[0], v[1], ind)
                ind += 1

    @classmethod
    def _db_update_to_0_0_7(cls, execute, insert):
        solvers = execute(
            "SELECT DISTINCT solver FROM solver_parameter"
        )

        for solver in solvers:
            if solver != "mage8":
                continue

            insert("mage_init_internal", "N", ind)
            ind += 1

            new = [
                ("mage_sediment_masse_volumique", "2650.0"),
                ("mage_sediment_angle_repos", "40.0"),
                ("mage_sediment_porosity", "0.40"),
                ("mage_distance_han", "0.0"),
                ("mage_distance_chargement_d50", "100.0"),
                ("mage_distance_chargement_sigma", "100.0"),
                ("mage_methode_modification_geometrie", "1"),
                ("mage_shields_critique", "1"),
                ("mage_shields_correction", "1"),
                ("mage_capacite_solide", "1"),
                ("mage_pas_de_temps_charriage", "1"),
                ("mage_facteur_multiplicateur", "1.0"),
            ]

            for v in new:
                insert(v[0], v[1], ind)
                ind += 1

    @classmethod
    def _db_update_to_0_0_14(cls, execute, data):
        table = "solver_parameter"

        cls.update_db_add_pamhyr_id(execute, table, data)
        Scenario.update_db_add_scenario(execute, table)

        cls._db_create(execute, ext="_tmp")
        execute(
            f"INSERT INTO {table}_tmp " +
            "(pamhyr_id, ind, name, value, solver, scenario) " +
            "SELECT pamhyr_id, ind, name, value, solver, scenario " +
            f"FROM {table}"
        )

        execute(f"DROP TABLE {table}")
        execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")

    @classmethod
    def _db_load(cls, execute, data=None):
        status = data["status"]
        scenario = data["scenario"]
        loaded = data['loaded_pid']
        solver = data['solver']

        new = []
        if scenario is None:
            return new

        table = execute(
            "SELECT pamhyr_id, ind, name, value, scenario " +
            "FROM solver_parameter " +
            f"WHERE solver = '{solver}' " +
            f"AND scenario = {scenario.id} " +
            f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
        )

        for row in table:
            it = iter(row)

            pid = next(it)
            ind = next(it)
            name = next(it)
            value = next(it)
            owner = next(it)

            new.append(
                (
                    ind,
                    cls(
                        id=pid, name=name, value=value,
                        status=status, owner_scenario=owner
                    )
                )
            )

            loaded.add(pid)

        data["scenario"] = scenario.parent
        new += cls._db_load(execute, data)
        data["scenario"] = scenario

        return new

    def _db_save(self, execute, data=None):
        ind = data["ind"]
        solver = data["solver"]

        execute(
            "INSERT INTO " +
            "solver_parameter(pamhyr_id, ind, " +
            "name, value, solver, scenario) " +
            "VALUES (" +
            f"{self.pamhyr_id}, " +
            f"{ind}, " +
            f"'{self._db_format(self.name)}', " +
            f"'{self._db_format(self.value)}', " +
            f"'{self._db_format(solver)}', " +
            f"{self._status.scenario_id}" +
            ")"
        )

        return True

    @property
    def name(self):
        return self._name

    @property
    def value(self):
        return self._value

    def __getitem__(self, key):
        if key == "name":
            return self._name
        elif key == "value":
            return self._value

        return None

    def __setitem__(self, key, value):
        if key == "name":
            self._name = str(value)
        elif key == "value":
            self._value = str(value)

        self.modified()

    @classmethod
    def from_tuple(cls, data, status):
        new = cls(status=status)
        new["name"] = data[0]
        new["value"] = data[1]

        return new


class SolverParametersList(PamhyrModelList):
    _sub_classes = [Parameter]

    def __init__(self, solver_type=None, status=None):
        super(SolverParametersList, self).__init__()

        self._status = status
        self._solver = solver_type
        self._lst = list(
            map(
                lambda t: Parameter.from_tuple(t, self._status),
                solver_type.default_parameters()
            )
        )

    @classmethod
    def _db_create(cls, execute, ext=""):
        return cls._create_submodel(execute)

    @classmethod
    def _db_load(cls, execute, data=None):
        status = data["status"]
        scenario = data["scenario"]
        loaded = data['loaded_pid']

        solvers = execute("SELECT DISTINCT solver FROM solver_parameter")
        new = {}

        if scenario is None:
            return new

        for solver in solvers:
            solver = solver[0]
            if solver not in solver_type_list:
                continue

            st = solver_type_list[solver]
            n = cls(solver_type=st, status=status)

            data['solver'] = solver
            lst = Parameter._db_load(execute, data)

            n._lst = list(map(lambda t: t[1], sorted(lst)))

            new[solver] = n

        return new

    def _db_save(self, execute, data=None):
        t = self._solver._type
        execute(
            "DELETE FROM solver_parameter " +
            f"WHERE solver = '{t}' " +
            f"AND scenario = {self._status.scenario_id}"
        )

        data["solver"] = t

        ind = 0
        for param in self._lst:
            data["ind"] = ind
            param._db_save(execute, data)
            ind += 1

        return True

    @property
    def parameters(self):
        return self.lst

    def get(self, index):
        return self._lst[index]

    def get_by_key(self, key):
        try:
            return next(
                filter(
                    lambda p: p["name"] == key,
                    self._lst
                )
            )["value"]
        except Exception:
            return None

    def set(self, index, new):
        self._lst[index] = new
        new.modified()

    def set_value(self, pid, key, value, owner):
        for p in self._lst:
            if p["name"] == key:
                p["value"] = value
                self._status.modified()
                return

        self._lst.append(
            Parameter(
                id=-1,
                name=key,
                value=value,
                status=self._status,
                owner_scenario=self._status.scenario_id
            )
        )
        self._status.modified()

    def new(self, index):
        n = Parameter(status=self._status)
        self._lst.insert(index, n)
        self._status.modified()
        return n
