# Internal.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 numpy import sign, interp
from copy import deepcopy

from tools import logger_color_red, logger_color_reset, logger_exception
from Meshing.AMeshingTool import AMeshingTool

from Model.Geometry.ProfileXYZ import ProfileXYZ

logger = logging.getLogger()


class InternalMeshing(AMeshingTool):
    def __init__(self):
        super(InternalMeshing, self).__init__()

    ###########
    # Meshing #
    ###########

    def meshing(self, reach,
                step: float = 50,
                limites=[-1, -1],
                linear: bool = True):

        # pre process

        profiles = self.get_profiles(reach, limites)
        if profiles is None:
            return []

        for p in profiles:
            if len(p) < 1:
                return []

        guide_list = self.get_guide_list(reach, profiles)
        if guide_list is None:
            return []

        # we never modify original profiles, we work on the copies
        m_profiles = self.st_to_m(profiles, guide_list)
        new_profiles = self.interpolate_transversal_step(m_profiles, step)

        for new_profiles2 in new_profiles:
            for p in new_profiles2:
                p.hard_purge_aligned_points()

        return new_profiles

    def st_to_m(self, profiles, guide_list):
        guide_list = ["un"] + guide_list + ["np"]
        max_values = [0] * (len(guide_list) - 1)
        max_values_index = [0] * (len(guide_list) - 1)
        # we work between each couple of guidelines
        for j, p in enumerate(profiles):
            for i in range(len(guide_list) - 1):
                index1 = p.named_point_index(guide_list[i])
                index2 = p.named_point_index(guide_list[i+1])
                if index2 - index1 > max_values[i]:
                    max_values[i] = index2 - index1
                    max_values_index[i] = j

        for i in range(len(max_values)):
            for isect in range(max_values_index[i], len(profiles)-1):
                self.compl_sect(profiles[isect],
                                profiles[isect+1],
                                guide_list[i],
                                guide_list[i+1])

            for isect in reversed(range(0, max_values_index[i])):
                self.compl_sect(profiles[isect+1],
                                profiles[isect],
                                guide_list[i],
                                guide_list[i+1])

        return profiles

    def interpolate_transversal_step(self,
                                     profiles,
                                     step):
        # calcul number of intermediate profiles
        np = []
        for i in range(len(profiles)-1):
            np.append(int((profiles[i+1].rk -
                           profiles[i].rk) / step) - 1)
            if np[-1] < 0:
                np[-1] = 0

        new_profiles = []
        for i in range(len(profiles)-1):
            new_profiles2 = []
            d = 1.0/float(np[i]+1)
            for j in range(np[i]):
                p = profiles[i].copy()
                # RATIO entre les deux sections initiales
                dj = float(j+1)*d
                pt1 = profiles[i].points
                pt2 = profiles[i+1].points
                for k in range(len(pt1)):
                    p.points[k].x = pt1[k].x + \
                        dj*(pt2[k].x -
                            pt1[k].x)
                    p.points[k].y = pt1[k].y + \
                        dj*(pt2[k].y -
                            pt1[k].y)
                    p.points[k].z = pt1[k].z + \
                        dj*(pt2[k].z -
                            pt1[k].z)
                    p.rk = profiles[i].rk + \
                        dj*(profiles[i+1].rk -
                            profiles[i].rk)
                    p.name = f'interpol{p.rk}'
                new_profiles2.append(p)
            new_profiles.append(new_profiles2)

        return new_profiles

    def compl_sect(self, sect1, sect2, tag1, tag2):
        # sect1: reference section
        # sect2: section to complete
        # start1 and 2: indices of the first point of the bed to complete
        # end = start + len
        # len1 and 2: number of intervals
        # len1: target len

        start1 = sect1.named_point_index(tag1)
        start2 = sect2.named_point_index(tag1)

        end1 = sect1.named_point_index(tag2)
        end2 = sect2.named_point_index(tag2)

        len1 = end1 - start1
        len2 = end2 - start2

        if len1 < len2:
            self.compl_sect(sect2, sect1, tag1, tag2)
            return
        elif len1 == len2:
            return

        ltot1 = 0.0
        ltot2 = 0.0
        alpha = []
        beta = []
        # remove names
        sect2.points[start2].name = ''
        sect2.points[end2].name = ''
        for i in range(len1):
            ltot1 += sect1.point(start1+i).dist(sect1.point(start1+i+1))
            alpha.append(ltot1)
        for i in range(len2):
            ltot2 += sect2.point(start2+i).dist(sect2.point(start2+i+1))
            beta.append(ltot2)
        if len2 < 1 or ltot2 < 0.0001:
            # unique point (copy len1 times)
            # we are at an edge of the profile
            for i in range(len1-len2):
                p = sect2.point(start2).copy()
                sect2.insert_point(start2, p)
                len2 += 1
        elif ltot1 < 0.0001:
            sect2.add_npoints(len1-len2)
            len2 = len1
        else:  # regular case
            alpha = list(map(lambda x: x/ltot1, alpha))  # target ratios
            beta = list(map(lambda x: x/ltot2, beta))  # current ratios
            for i in range(len1 - len2):
                beta.append(1.0)
            beta.append(0.0)  # beta[-1]
            # loop on the points to insert
            for k in range(len1-len2):
                trouve = False
                for j0 in range(len2):
                    if trouve:
                        break
                    undeplus = True
                    for j1 in range(j0, len2):
                        if undeplus:
                            if beta[j1] <= alpha[j1]:
                                undeplus = False
                                break
                    if undeplus:
                        if j0 == len2:
                            trouve = True
                        else:
                            j1 = j0+1
                            if beta[j0]-alpha[j0] > alpha[j1]-beta[j0]:
                                trouve = True

                    if trouve:
                        len2 += 1
                        for i in reversed(range(j0, len2)):
                            beta[i+1] = beta[i]
                        if (beta[j0] == beta[j0-1]):
                            # rien
                            pass
                        else:
                            ratio = (alpha[j0] - beta[j0-1]) \
                                    / (beta[j0] - beta[j0-1])
                            if ratio < 0.0:
                                logger.warning(f"ratio négatif {ratio}")
                                # on double le point a gauche
                                p = sect2.point(start2+j0-1).copy()
                                sect2.insert_point(start2+j0-1, p)
                                sect2.point(start2+j0-1).name = ''
                                beta[j0] = beta[j0-1]
                            else:
                                p = sect2.point(start2+j0).copy()
                                sect2.insert_point(start2+j0, p)
                                sect2.point(start2+j0+1).name = ''
                                sect2.point(start2+j0+1).x = \
                                    (1.0-ratio) * sect2.point(start2+j0).x + \
                                    ratio * sect2.point(start2+j0+2).x
                                sect2.point(start2+j0+1).y = \
                                    (1.0-ratio) * sect2.point(start2+j0).y + \
                                    ratio * sect2.point(start2+j0+2).y
                                sect2.point(start2+j0+1).z = \
                                    (1.0-ratio) * sect2.point(start2+j0).z + \
                                    ratio * sect2.point(start2+j0+2).z
                                beta[j0] = (1.-ratio)*beta[j0-1] \
                                    + ratio*beta[j0+1]

        if len1 > len2:
            for i in range(len1 - len2):
                p = sect2.point(start2+len2).copy()
                sect2.insert_point(start2+len2, p)
                sect2.point(start2+len2).name = ''
        if tag1 != "un":
            sect2.point(start2).name = tag1
        if tag2 != "np":
            sect2.point(start2+len2).name = tag2

        sect2.modified()

    def update_rk(self, reach, begin_rk, end_rk,
                  step: float = 50,
                  limites=[-1, -1],
                  origin=0,
                  directrices=['un', 'np'],
                  lplan: bool = False,
                  lm: int = 3,
                  linear: bool = False,
                  origin_value=0.0,
                  orientation=0):
        if reach is None or len(reach.profiles) == 0:
            return []

        nprof = reach.number_profiles
        if origin >= nprof or origin < 0:
            logger.warning(
                f"Origin outside reach"
            )
            return reach.get_rk()

        # orientation is the orintation of the bief:
        # 1: up -> downstream
        # 2: down -> upstream
        # else: keep current orientation
        old_orientation = sign(reach.profiles[-1].rk - reach.profiles[0].rk)
        if orientation == 1:
            sgn = 1.0
        elif orientation == 2:
            sgn = -1.0
        else:
            sgn = old_orientation
        if abs(sgn) < 0.5:
            sgn = 1.0

        new_rk = [0.0]*nprof
        new_rk[origin] = origin_value

        frictions = reach._parent.frictions

        old_begin_rk = list(map(lambda x: x * old_orientation,
                            begin_rk))
        old_end_rk = list(map(lambda x: x * old_orientation,
                          end_rk))
        old_rk = list(map(lambda x: x * old_orientation,
                          reach.get_rk()))

        if origin < nprof - 1:
            for i in range(origin+1, nprof):
                # 2D
                d1 = reach.profiles[i-1].named_point(
                    directrices[0]
                ).dist_2d(reach.profiles[i].named_point(directrices[0]))
                d2 = reach.profiles[i-1].named_point(
                    directrices[1]
                ).dist_2d(reach.profiles[i].named_point(directrices[1]))
                new_rk[i] = new_rk[i-1] + (sgn * (d1 + d2) / 2)
        if origin > 0:
            for i in reversed(range(0, origin)):
                # 2D
                d1 = reach.profiles[i+1].named_point(
                    directrices[0]
                ).dist_2d(reach.profiles[i].named_point(directrices[0]))
                d2 = reach.profiles[i+1].named_point(
                    directrices[1]
                ).dist_2d(reach.profiles[i].named_point(directrices[1]))
                new_rk[i] = new_rk[i+1] - (sgn * (d1 + d2) / 2)

        new_begin_rk = interp(old_begin_rk, old_rk, new_rk)
        new_end_rk = interp(old_end_rk, old_rk, new_rk)
        if abs(sgn - old_orientation) > 0.1:  # orientation changed
            return new_rk, new_end_rk, new_begin_rk
        else:
            return new_rk, new_begin_rk, new_end_rk

    def get_guide_list(self, reach, profiles):

        gl = reach.compute_guidelines()

        # we make sure that the lines are in the left-to-right order
        guide_list = [
            [
                x.name
                for x in p.named_points()
                if x.name in gl[0]
            ]
            for p in profiles]

        # we make sure the lines never cross
        for gl in guide_list:
            for i in range(len(guide_list[0])):
                if gl[i] != guide_list[0][i]:
                    return None

        return guide_list[0]

    def get_profiles(self, reach, limites):

        if reach is None or len(reach.profiles) == 0:
            return None

        if limites[0] > limites[1]:
            lim = limites[1]
            limites[1] = limites[0]
            limites[0] = lim
        elif limites[0] == limites[1]:
            return None

        profiles = [reach.profile(i).copy()
                    for i in range(limites[0], limites[1]+1)
                    ]

        return profiles
