# Profile.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 -*-

import logging

from tools import timer
from shapely import geometry

from Model.Geometry.Point import Point
from Model.Except import NotImplementedMethodeError

logger = logging.getLogger()


class Profile(object):
    def __init__(self, id: int = -1, num: int = 0,
                 rk: float = 0.0, name: str = "",
                 code1: int = 0, code2: int = 0,
                 _type: str = "", reach=None,
                 status=None, owner_scenario=-1):
        super(Profile, self).__init__(
            id=id, status=status,
            owner_scenario=owner_scenario
        )

        self._num = int(num)
        self._code1 = int(code1)
        self._code2 = int(code2)
        self._rk = float(rk)
        self._name = str(name)
        self._reach = reach
        self._sl = None

        self._points: List[Point] = []

        self._profile_type = _type

    def __len__(self):
        return len(self.points)

    @property
    def number_points(self):
        return len(self.points)

    def _get_points_list(self):
        return list(self._points)

    @property
    def points(self):
        if not isinstance(self._points, list):
            self._points = self._get_points_list()

        return list(
            filter(
                lambda p: not p.is_deleted(),
                self._points
            )
        )

    def point(self, index):
        return self.points[index]

    def named_point(self, name):
        if name == "un":
            return self.points[0]
        elif name == "np":
            return self.points[-1]
        elif name == "talweg":
            zm = self.z_min()
            return next((p for p in self.points if p.z == zm), None)
        else:
            return next((p for p in self.points if p.name == name), None)

    def named_point_index(self, name):
        if name == "un":
            return 0
        elif name == "np":
            return len(self.points) - 1
        elif name == "talweg":
            zm = self.z_min()
            return next(
                (p for p in enumerate(self.points) if p[1].z == zm),
                None
            )[0]
        else:
            return next(
                (p for p in enumerate(self.points) if p[1].name == name),
                None
            )[0]

    @property
    def reach(self):
        return self._reach

    @property
    def num(self):
        """
        Returns:
            Number of profile.
        """
        return self._num

    @num.setter
    def num(self, value: int):
        self._num = int(value)
        self.modified()

    @property
    def code1(self):
        """
        Returns:
            Interpolation code 1.
        """
        return self._code1

    @code1.setter
    def code1(self, value: int):
        self._code1 = int(value)
        self.modified()

    @property
    def code2(self):
        """
        Returns:
            Interpolation code 2.
        """
        return self._code2

    @code2.setter
    def code2(self, value: int):
        self._code2 = int(value)
        self.modified()

    @property
    def nb_points(self):
        return len(self.points)

    @property
    def rk(self):
        """
        Returns:
            Kilometer point.
        """
        return self._rk

    @rk.setter
    def rk(self, value: float):
        self._rk = float(value)
        self.modified()

    @property
    def name(self):
        """
        Returns:
            Profile name.
        """
        return self._name

    @name.setter
    def name(self, value: str):
        self._name = value.strip()
        self.modified()

    @property
    def sl(self):
        """
        Returns:
            Profile sediment layers.
        """
        return self._sl

    @sl.setter
    def sl(self, value):
        self._sl = value
        self.modified()

    @property
    def profile_type(self):
        """
        Returns:
            Profile type.
        """
        return self._profile_type

    @profile_type.setter
    def profile_type(self, value: str):
        self._profile_type = value
        self.modified()

    def point(self, i: int):
        if i < len(self.points):
            return self.points[i]

        return None

    def named_points(self):
        """List of named point

        Returns:
            The list of named point
        """
        return [point for point in self.points
                if point.is_named()]

    def insert_point(self, index: int, point: Point):
        """Insert point at index.

        Args:
            index: The index of new point.
            point: The point.

        Returns:
            Nothing.
        """
        if point in self._points:
            point.set_as_not_deleted()
        else:
            self._points.insert(index, point)

        self.modified()

    def delete_i(self, indexes: list):
        list(
            map(
                lambda e: e[1].set_as_deleted(),
                filter(
                    lambda e: e[0] in indexes,
                    enumerate(self.points)
                )
            )
        )

        self.modified()

    def delete_points(self, points):
        """Delete some elements in profile list

        Args:
            points: The list of profile to delete

        Returns:
            Nothing.
        """
        list(
            map(
                lambda p: p.set_as_deleted(),
                points
            )
        )
        self.modified()

    # Move

    def move_up_point(self, index: int):
        if index < len(self._points):
            next = index - 1

            p = self._points
            p[index], p[next] = p[next], p[index]
            self.modified()

    def move_down_point(self, index: int):
        if index >= 0:
            prev = index + 1

            p = self._points
            p[index], p[prev] = p[prev], p[index]
            self.modified()

    # Sort

    @timer
    def sort(self, column, is_reversed: bool = False):
        def predicate(p): return p.x
        if column == 'y':
            def predicate(p): return p.y
        elif column == 'z':
            def predicate(p): return p.z

        self._points = sorted(
            self._points,
            key=predicate,
            reverse=is_reversed
        )
        self.modified()

    @timer
    def sort_with_indexes(self, indexes: list):
        if len(self._points) != len(indexes):
            logger.critical("Indexes list do not correspond to point list")

        self._points = list(
            map(
                lambda x: x[1],
                sorted(
                    enumerate(self._points),
                    key=lambda x: indexes[x[0]]
                )
            )
        )
        self.modified()

    @timer
    def reverse(self):
        self._points.reverse()
        self.modified()

    # Sediment Layers

    def get_sl(self):
        """Get sediment layer height of points

        Get sediment layer of points (without spesific point sl)

        Returns:
            List of sediment layers height
        """
        res = []
        psl = [point.sl for point in self.points]

        # Compute max number of layers
        sl_max = 0
        for sl in psl:
            n = 0 if sl is None else len(sl)
            sl_max = max(n, sl_max)

        # Create list of height for each sl and each layer
        for i in range(0, sl_max):
            cur = []
            # Compute new layer line for each sl
            for sl in psl:
                if sl is not None and i < len(sl):
                    cur.append(sl.get(i).height)
                else:
                    cur.append(0)
            # Add layer line to result
            res.append(cur)

        return res

    # Abstract method, must be implemented for in non abstract class
    def get_station(self):
        raise NotImplementedMethodeError(self, self.get_station)

    # Computation method

    # Abstract method for width approximation
    def width_approximation(self):
        raise NotImplementedMethodeError(self, self.width_approximation)

    # Abstract method for width approximation
    def get_water_limits(self, z):
        raise NotImplementedMethodeError(self, self.get_water_limits)

    def wet_points(self, z):
        raise NotImplementedMethodeError(self, self.wet_point)

    def wet_width(self, z):
        raise NotImplementedMethodeError(self, self.wet_width)

    def wet_perimeter(self, z):
        raise NotImplementedMethodeError(self, self.wet_perimeter)

    def wet_area(self, z):
        raise NotImplementedMethodeError(self, self.wet_area)

    def wet_radius(self, z):
        raise NotImplementedMethodeError(self, self.wet_radius)

    def get_nb_wet_areas(self, z):
        raise NotImplementedMethodeError(self, self.get_nb_wet_areas)
