# -*- coding: utf-8 -*-
"""
/***************************************************************************
Name                 : Mascaret
Description          : Pre and Postprocessing for Mascaret for QGIS
Date                 : September,2021
copyright            : (C) 2021 by Artelia
email                :
***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import json

import numpy as np
from shapely.geometry import GeometryCollection, LineString, Polygon, mapping
from shapely.ops import unary_union


class ClassResProfil:
    """ """

    def __init__(self, debug=False):
        self.cas_prt = {
            0: "minor bed profile",
            1: "left major bed profile",
            2: "right major bed profile",
            3: "left stockage zone",
            4: "right stockage zone",
        }

        self.dico_res = {}
        self.pk = None
        self.plani, self.ksmaj, self.ksmin = None, None, None
        self.debug = debug
        self.branch = None
        self.prof = None
        self.min_bed = None
        self.maj_bed = None
        self.warning_message = ""
        self.lst_pr = []
        self.mdb = None
        self.id_run = None

    def init_cl(
        self,
        pk,
        prof,
        branch,
        min_bed,
        maj_bed,
        id_run,
        database=None,
        plani=None,
        ksmaj=None,
        ksmin=None,
        zmax=None,
        dico_plani=None,
    ):
        self.pk = pk
        self.id_run = id_run
        self.branch = branch
        self.prof = prof
        self.dico_plani = dico_plani
        self.min_bed = min_bed
        self.maj_bed = maj_bed
        self.warning_message = ""
        self.dico_res = {}
        self.lst_pr = []
        self.zmax = zmax

        if database:
            self.mdb = database
            self.plani, self.ksmaj, self.ksmin = self.get_plani_ks()
        else:
            self.mdb = None
            self.plani, self.ksmaj, self.ksmin = plani, ksmaj, ksmin

    def init_mdb(self, database):
        self.mdb = database

    def get_plani_ks(self):
        """
        Get planimetry value and Strickler coefficient
        :return: plani, ksmaj, ksmin
        """
        plani = None
        ksmaj = None
        ksmin = None
        dict_ks = self.mdb.zone_ks()
        dict_plani = self.mdb.planim_select()
        for i, (minp, maxp) in enumerate(zip(dict_plani["absmin"], dict_plani["absmax"])):
            if minp <= self.pk <= maxp:
                plani = dict_plani["pas"][i]
                break
        for i, (minp, maxp) in enumerate(zip(dict_ks["zoneabsstart"], dict_ks["zoneabsend"])):
            if minp <= self.pk <= maxp:
                ksmaj = dict_ks["majbedcoef"][i]
                ksmin = dict_ks["minbedcoef"][i]
                break
        return plani, ksmaj, ksmin

    def decoup_pr(self, pr, lminor_x, lmaj_x):
        """
        Split profile in :
            minor profile,
            left major profile
            right major profile
            left stockage profile
            right stockage  profile
        :param pr: profile
        :param lminor_x: minor bed zone
        :param lmaj_x: major bed zone
        :return:
        """
        pr_g = []
        pr_d = []
        pr_m = []
        pr_st_g = []
        pr_st_d = []
        for point in pr:
            if point[0] < lminor_x[0]:
                if point[0] < lmaj_x[0]:
                    pr_st_g.append(point)
                else:
                    pr_g.append(point)
            elif point[0] > lminor_x[1]:
                if point[0] > lmaj_x[1]:
                    pr_st_d.append(point)
                else:
                    pr_d.append(point)
            else:
                pr_m.append(point)
        if not pr_m:
            txt = "[ ERROR ] No minor bed profile"
            if self.debug:
                print(txt)
            self.warning_message += txt
            self.warning_message += "\n"
            return [], [], [], [], []
        if not pr_g:
            pr_g = [pr_m[0], pr_m[0]]
        else:
            pr_g = pr_g + [pr_m[0], pr_m[0]]
        if not pr_d:
            pr_d = [pr_m[-1], pr_m[-1]]
        else:
            pr_d = [pr_m[-1], pr_m[-1]] + pr_d
        if not pr_st_g:
            pr_st_g = [pr_g[0], pr_g[0]]
        else:
            pr_st_g = pr_st_g + [pr_g[0], pr_g[0]]
        if not pr_st_d:
            pr_st_d = [pr_d[-1], pr_d[-1]]
        else:
            pr_st_d = [pr_d[-1], pr_d[-1]] + pr_st_d

        return pr_m, pr_g, pr_d, pr_st_g, pr_st_d

    def creat_line_width(self, poly, line, x_fond, z_level):
        """
        Creation linestring with sum of width when MultiLineString
        :param poly: polygone
        :param line: line
        :param x_fond: bottom x
        :param z_level: line level
        :return:
        """

        line = LineString(line)
        geom = line.intersection(poly)
        if geom.is_empty:
            txt = "[ ERROR ] No intersection : {}  {}".format(line, poly)
            if self.debug:
                print(txt)
            self.warning_message += txt
            self.warning_message += "\n"
            line_disc = None
        elif geom.geom_type == "MultiLineString" or geom.geom_type == "GeometryCollection":
            id_tmp = 0
            for j, ll in enumerate(geom.geoms):
                x, y = ll.xy
                if min(x) <= x_fond <= max(x):
                    id_tmp = j
            l_g = 0
            l_d = 0

            for j, ll in enumerate(geom.geoms):
                length = ll.length
                x, y = ll.xy
                if j != id_tmp and max(x) < x_fond:
                    l_g += length
                elif j != id_tmp and min(x) > x_fond:
                    l_d += length
            x, y = geom.geoms[id_tmp].xy
            linf = [(x[0] - l_g, z_level), (x[-1] + l_d, z_level)]
            line_disc = LineString(linf)
        elif geom.geom_type == "LineString":
            line_disc = geom
        else:
            txt = "Geo type non traite : {}".format(geom.geom_type)
            if self.debug:
                print(txt)
            self.warning_message += txt
            self.warning_message += "\n"
            line_disc = None

        return line_disc

    def discret_pr_lg(self, pr, x_fond, zmin, zmax, pas=0.1, id_g=0, id_d=-1, cond_pas_z=True):
        """
        vertical descritization
        :param pas : dz in m or nb of plan
        :param cond_pas_z : True si descritization in m or false in plan
        :param id_g : id_am_g id point left
        :param id_d : id_am_d id point right
        :param pr : profile
        :param x_fond : x bottom
        :param zmin : bottom point of the profile
        :param zmax : max level of the profile

        """
        if id_g == id_d:
            return []
        if id_d == -1:
            limit_pr = pr[id_g:]
        else:
            limit_pr = pr[id_g : id_d + 1]

        wow = []
        pzmax = max(zmax, pr[id_g, 1])
        for coord in limit_pr:
            wow.append([coord[0], coord[1]])
            if pzmax < coord[1]:
                pzmax = coord[1]
        wow = [[pr[id_g, 0], pzmax + 1]] + wow
        wow += [[pr[id_d, 0], pzmax + 1]]

        line_pr = LineString(wow)

        poly = Polygon(line_pr)
        lst_line_disc = []

        if not poly.is_valid:
            return lst_line_disc

        if cond_pas_z:
            pasz = pas
        else:
            pasz = (zmax - zmin) / pas

        z_level = zmin

        while z_level + pasz <= zmax:
            # création de line pour découpé

            z_level = z_level + pasz

            line = [(pr[id_g, 0] - 1, z_level), (pr[id_d, 0] + 1, z_level)]
            # point_fond = Point([x_fond, z_level])
            line_disc = self.creat_line_width(poly, line, x_fond, z_level)
            if line_disc:
                lst_line_disc.append(line_disc)

        # last line:
        line = [(pr[0, 0] - 1, zmax), (pr[-1, 0] + 1, zmax)]
        line_disc = self.creat_line_width(poly, line, x_fond, zmax)
        if line_disc:
            lst_line_disc.append(line_disc)

        return lst_line_disc, poly

    def print_val(self):
        """
        Print val to check
        :return:
        """

        for key in self.dico_res.keys():
            print("************************")
            print("Profil :", self.cas_prt[key])
            for elem, val in self.dico_res[key].items():
                if elem != "poly":
                    print("{} , {}".format(elem, val))

    def get_poly_disc(self, line_disc, pt_bas):
        """
        Generate polygone list of a profile
        :param line_disc: discritzation line
        :param pt_bas: bottom point
        :return:
        """
        # construit plus bas au plus haut
        first_line = list(line_disc[0].coords)
        last_line = list(first_line)
        first_line.append(pt_bas)
        poly_tmp = Polygon(first_line)
        lst_poly = [poly_tmp]

        for line in line_disc[1:]:
            first_line = list(line.coords)
            last_line.reverse()
            tmp_line = first_line + last_line
            poly_tmp = Polygon(tmp_line)
            lst_poly.append(poly_tmp)
            last_line = first_line

        return lst_poly

    def genrete_plani(self, zmax=None):
        pr_m, pr_g, pr_d, pr_st_g, pr_st_d = self.decoup_pr(self.prof, self.min_bed, self.maj_bed)
        self.lst_pr = [(0, pr_m), (1, pr_g), (2, pr_d), (3, pr_st_g), (4, pr_st_d)]
        self.dico_plani = {}

        for num, prof_tmp in self.lst_pr:
            self.dico_plani[num] = {}
            profile = np.array(prof_tmp)
            if (profile[0] == profile[-1]).all():
                # print('No profile')
                continue
            # x_g, z_g = (profile[0, 0], profile[0, 1])
            # x_d, z_d = (profile[-1, 0], profile[-1, 1])
            z_g = profile[0, 1]
            z_d = profile[-1, 1]
            minz = np.min(profile[:, 1])
            id_f = np.where(profile[:, 1] == minz)[0]
            if len(id_f) > 1:
                x_fond = np.mean(profile[id_f, 0])
            else:
                x_fond = profile[id_f[0], 0]
            pt_bas = (x_fond, minz)
            self.dico_plani[num]["pt_bas"] = pt_bas

            if not zmax:
                zmax = max(z_g, z_d)

            line_disc, poly = self.discret_pr_lg(
                profile, x_fond, minz, zmax, pas=self.plani, id_g=0, id_d=-1, cond_pas_z=True
            )
            self.dico_plani[num]["lines"] = line_disc
        return self.dico_plani

    def plani_stock(self, dico_zmax, id_run, database):
        self.mdb = database
        prof = self.mdb.select(
            "profiles",
            order="abscissa",
            where="active",
            list_var=[
                "abscissa",
                "x",
                "z",
                "leftminbed",
                "rightminbed",
                "leftstock",
                "rightstock",
                "zleftminbed",
                "zrightminbed",
                "branchnum",
            ],
        )
        point_bas = {}
        list_insert_plani = []
        list_pt_bas = []

        if prof:
            for id, pk in enumerate(prof["abscissa"]):
                if pk:
                    try:
                        # **********************************************************

                        x = [float(v) for v in prof["x"][id].split()]
                        z = [float(v) for v in prof["z"][id].split()]
                        profil = list(zip(x, z))

                        min_bed = [prof["leftminbed"][id], prof["rightminbed"][id]]
                        if prof["leftstock"][id] and prof["rightstock"][id]:
                            maj_bed = [prof["leftstock"][id], prof["rightstock"][id]]
                        else:
                            maj_bed = [x[0], x[-1]]

                        self.init_cl(
                            pk,
                            profil,
                            prof["branchnum"][id],
                            min_bed,
                            maj_bed,
                            id_run,
                            database=self.mdb,
                        )
                        self.genrete_plani(dico_zmax[str(pk)])

                        point_bas[pk] = {}

                        for id_type, dico in self.dico_plani.items():
                            if "lines" in dico.keys():
                                for ord, line in enumerate(dico["lines"]):
                                    list_insert_plani.append(
                                        [id_run, pk, id_type, ord, json.dumps(mapping(line))]
                                    )
                                point_bas[pk][id_type] = dico["pt_bas"]

                    except Exception as err:
                        if self.debug:
                            txt = "Warning Plani \n" + err
                            return txt
            if self.mdb:
                if point_bas:
                    list_pt_bas = [[id_run, "opt", "pt_bas", json.dumps(point_bas)]]
                self.insert_lst_mdb(list_insert_plani, list_pt_bas)
            return None

    def insert_lst_mdb(self, list_insert, point_bas):
        if list_insert:
            col_tab = ["id_runs", "pknum", "id_type", "id_order", "line"]
            list_insert2 = []
            for lst in list_insert:
                if lst not in list_insert2:
                    list_insert2.append(lst)
            self.mdb.insert_res("runs_plani", list_insert2, col_tab)

        if point_bas:
            col_tab = ["id_runs", "type_res", "var", "val"]
            self.mdb.insert_res("runs_graph", point_bas, col_tab)

    def get_results(self):
        """
        get result of "cross section result" :
            mirror width
            wet section
            wet perimeter
            bottom point
            debitance
            discratization level
        :return:
        """
        if self.dico_plani:
            self.dico_res = dict(self.dico_plani)
        else:
            self.dico_res = dict()
        for num, dico in self.dico_res.items():
            if not dico:
                continue
            if not dico["line"]:
                continue

            line_disc = dico["line"]

            pt_bas = dico["pt_bas"]
            if num == 0:
                ks = self.ksmin
            elif num == 1 or num == 2:
                ks = self.ksmaj
            else:
                ks = 0.0

            lst_poly = self.get_poly_disc(line_disc, pt_bas)
            dico["poly"] = []
            dico["area"] = []
            # dico['perimeter'] = []
            dico["width"] = []
            dico["z"] = []
            dico["debitance"] = []
            last_poly = None
            for id, poly in enumerate(lst_poly):
                if id == 0:
                    last_poly = poly
                    (minx, minz, maxx, maxz) = poly.bounds
                    perimeter = poly.length
                    area = poly.area

                    dico["poly"].append(poly)
                    dico["area"].append(area)
                    # dico['perimeter'].append(perimeter)
                    dico["z"].append(maxz)
                    dico["width"].append(line_disc[id].length)
                    dico["debitance"].append(self.debitance(num, ks, perimeter, area))
                else:
                    # new_poly = cascaded_union([last_poly, poly])
                    new_poly = unary_union([last_poly, poly])

                    if new_poly.is_valid and not new_poly.is_empty:
                        (minx, minz, maxx, maxz) = new_poly.bounds
                        perimeter = new_poly.length
                        area = new_poly.area
                        dico["poly"].append(new_poly)
                        dico["area"].append(area)
                        # dico['perimeter'].append(perimeter)
                        dico["z"].append(maxz)
                        dico["width"].append(line_disc[id].length)

                        dico["debitance"].append(self.debitance(num, ks, perimeter, area))
                        last_poly = new_poly
            if len(dico["poly"]) > 1:
                dico["pr_poly"] = dico["poly"][-1]
                dico["pr_area"] = dico["area"][-1]
                # dico['pr_perimeter'] = dico['perimeter'][-1]
                dico["pr_width"] = dico["width"][-1]
                dico["pr_z"] = dico["z"][-1]
                perimeter = dico["poly"][-1].length
                dico["pr_debitance"] = self.debitance(num, ks, perimeter, dico["area"][-1])
            else:
                dico["pr_poly"] = None
                dico["pr_area"] = 0.0
                # dico['pr_perimeter'] = 0.
                dico["pr_width"] = 0.0
                dico["pr_z"] = 0.0

                dico["pr_debitance"] = 0.0

    def debitance(self, num, ks, perimeter, area):
        """
         D = K.S.R^2/3
         with K the Strickler coeficient,
         S the wet section and R hydraulic radius
         which is the wet section divided by wet perimeter
        :param num: (int) type  of profile zone
        :param ks: (float) Strickler coeficient
        :param perimeter: (float)wet perimeter
        :param area: (float) wet section
        :return: debitance (float
        """
        if num == 3 or num == 4:
            # No debitance when stockage zone
            return None
        if perimeter > 0:
            debitance = ks * area * (area / perimeter) ** (2 / 3)
        else:
            debitance = 0.0
        return debitance


def get_valeurs(z_level, poly):
    res = {}
    if poly:
        if poly.is_valid and not poly.is_empty:
            (minx, miny, maxx, maxy) = poly.bounds

            if z_level < miny:
                res["z"] = z_level
                res["area"] = None
                res["perimeter"] = None
                res["width"] = None
            else:
                delpoly = Polygon(
                    [
                        [minx - 1, z_level],
                        [maxx + 1, z_level],
                        [maxx + 1, max(z_level + 1, maxy + 1)],
                        [minx - 1, max(z_level + 1, maxy + 1)],
                        [minx - 1, z_level],
                    ]
                )
                if not delpoly.is_empty:
                    if not poly.is_valid:
                        # Polygone => multiPolygone
                        poly = poly.buffer(0)
                    polyw = poly.difference(delpoly)
                    if not polyw.is_valid:
                        polyw = GeometryCollection()
                else:
                    polyw = GeometryCollection()
                if not polyw.is_empty:
                    (minx, miny, maxx, maxy) = polyw.bounds
                    res["z"] = z_level
                    res["area"] = polyw.area
                    res["perimeter"] = polyw.length
                    res["width"] = maxx - minx
                else:
                    res["z"] = z_level
                    res["area"] = None
                    res["perimeter"] = None
                    res["width"] = None
        else:
            res["z"] = z_level
            res["area"] = None
            res["perimeter"] = None
            res["width"] = None
    else:
        res["z"] = z_level
        res["area"] = None
        res["perimeter"] = None
        res["width"] = None
    return res


if __name__ == "__main__":
    pass
