# PointXYZ.py -- Pamhyr
# Copyright (C) 2023-2024  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 math import dist
import numpy as np

from Model.Tools.PamhyrDB import SQLSubModel
from Model.Geometry.Point import Point


class PointXYZ(Point, SQLSubModel):
    _sub_classes = []

    def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0,
                 name: str = "", profile=None, status=None):
        super(PointXYZ, self).__init__(
            name=name, profile=profile, status=status)

        self._x = float(x)
        self._y = float(y)
        self._z = float(z)

    @classmethod
    def _db_create(cls, execute):
        execute("""
          CREATE TABLE geometry_pointXYZ(
            id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
            ind INTEGER NOT NULL,
            name TEXT,
            x INTEGER NOT NULL,
            y INTEGER NOT NULL,
            z INTEGER NOT NULL,
            profile INTEGER NOT NULL,
            sl INTEGER,
            FOREIGN KEY(profile) REFERENCES geometry_profileXYZ(id),
            FOREIGN KEY(sl) REFERENCES sedimentary_layer(id)
          )
        """)

        return cls._create_submodel(execute)

    @classmethod
    def _db_update(cls, execute, version):
        cls._update_submodel(execute, version)

        major, minor, release = version.strip().split(".")
        if major == minor == "0":
            if int(release) < 2:
                execute(
                    """
                    ALTER TABLE geometry_pointXYZ
                    ADD COLUMN sl INTEGER
                    REFERENCES sedimentary_layer(id)
                    """
                )
        return True

    @classmethod
    def _db_load(cls, execute, data=None):
        status = data["status"]
        profile = data["profile"]

        table = execute(
            "SELECT ind, name, x, y, z, sl " +
            "FROM geometry_pointXYZ " +
            f"WHERE profile = {profile.id}"
        )

        # Fill points list with new point
        for row in table:
            ind = row[0]
            name = row[1]
            x = row[2]
            y = row[3]
            z = row[4]
            sl = row[5]

            new = cls(
                name=name,
                x=x, y=y, z=z,
                profile=profile,
                status=status
            )

            if sl == -1 or sl is None:
                new._sl = None
            else:
                new._sl = next(
                    filter(
                        lambda s: s.id == sl,
                        data["sediment_layers_list"].sediment_layers
                    )
                )

            yield ind, new

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

        sl = self._sl.id if self._sl is not None else -1

        sql = (
            "INSERT INTO " +
            "geometry_pointXYZ(ind, name, x, y, z, profile, sl) " +
            "VALUES (" +
            f"{ind}, '{self._db_format(self._name)}', " +
            f"{self.x}, {self.y}, {self.z}, " +
            f"{profile.id}, {sl}" +
            ")"
        )
        execute(sql)

        return True

    @classmethod
    def from_data(cls, header, data):
        point = None
        try:
            if len(header) == 0:
                point = cls(
                    *data
                )
            else:
                valid_header = {'name', 'x', 'y', 'z', 'profile'}
                d = {}
                for i, v in enumerate(data):
                    h = header[i].strip().lower().split(' ')[0]
                    if h in valid_header:
                        d[h] = v

                point = cls(**d)
        except Exception as e:
            raise ClipboardFormatError(header, data)

        return point

    def __repr__(self):
        return f"({self._x}, {self._y}, {self._z}, {self._name})"

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = float(value)
        self._status.modified()

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        self._y = float(value)
        self._status.modified()

    @property
    def z(self):
        return self._z

    @z.setter
    def z(self, value):
        self._z = float(value)
        self._status.modified()

    def is_nan(self):
        """
        Returns:
            True if at least one coordinate is as np.nan
        """
        return (np.isnan(self.x) or
                np.isnan(self.y) or
                np.isnan(self.z))

    def dist(self, p2):
        return PointXYZ.distance(self, p2)

    def dist_2d(self, p2):
        return PointXYZ.distance_2d(self, p2)

    @staticmethod
    def distance_2d(p1, p2):
        """Euclidean distance between p1 and p2.

        Args:
            p1: A XYZ Point
            p2: A XYZ Point

        Returns:
            Euclidean 2D distance between the two points
        """
        return dist((p1.x, p1.y), (p2.x, p2.y))

    @staticmethod
    def distance(p1, p2):
        """Euclidean distance between p1 and p2.

        Args:
            p1: A XYZ Point
            p2: A XYZ Point

        Returns:
            Euclidean 3D distance between the two points
        """
        return dist((p1.x, p1.y, p1.z), (p2.x, p2.y, p2.z))

    @staticmethod
    def areatriangle3d(p1, p2, p3):
        a = PointXYZ.distance(p1, p2)
        b = PointXYZ.distance(p2, p3)
        c = PointXYZ.distance(p3, p1)
        s = (a + b + c) / 2
        return (s*(s-a) * (s-b)*(s-c)) ** 0.5
