# /**********************************************************************
# *                                                                     *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com>  *
# *                                                                     *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL)  *
# * as published by the Free Software Foundation; either version 2 of   *
# * the License, or (at your option) any later version.                 *
# * for detail see the LICENCE text file.                               *
# *                                                                     *
# * 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 Library General Public License for more details.                *
# *                                                                     *
# * You should have received a copy of the GNU Library General Public   *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307*
# * USA                                                                 *
# *                                                                     *
# ***********************************************************************

import FreeCAD
import Part

if FreeCAD.GuiUp:
    import FreeCADGui, os
    from PySide import QtCore, QtGui
    from PySide.QtGui import QListWidgetItem
    from PySide.QtCore import QT_TRANSLATE_NOOP
else:
    # \cond
    def translate(ctxt, txt):
        return txt


    def QT_TRANSLATE_NOOP(ctxt, txt):
        return txt
    # \endcond

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    def _fromUtf8(s):
        return s

import PVPlantResources
import PVPlantSite

version = "0.1.0"


def selectionFilter(sel, objtype):
    fil = []
    for obj in sel:
        if hasattr(obj, "Proxy"):
            if obj.Proxy.__class__ is objtype:
                fil.append(obj)
    return fil


class _PVPlantPlacementTaskPanel:
    '''The editmode TaskPanel for Schedules'''

    def __init__(self, obj=None):
        self.site = PVPlantSite.get()
        self.Terrain = self.site.Terrain
        self.FrameSetups = None
        self.PVArea = None
        self.Area = None
        self.gap_col = .0
        self.gap_row = .0
        self.offsetX = .0
        self.offsetY = .0
        self.Dir = FreeCAD.Vector(0, -1, 0)  # Norte a sur

        # self.form:
        self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui"))
        self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg")))

        self.addFrames()
        self.maxWidth = max([frame.Width.Value for frame in self.site.Frames])

        self.form.buttonPVArea.clicked.connect(self.addPVArea)
        self.form.editGapCols.valueChanged.connect(self.update_inner_spacing)
        self.update_inner_spacing()

    def addPVArea(self):
        sel = FreeCADGui.Selection.getSelection()
        if len(sel) > 0:
            self.PVArea = sel[0]
            self.form.editPVArea.setText(self.PVArea.Label)

    def addFrames(self):
        for frame_setup in self.site.Frames:
            list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups)
            list_item.setCheckState(QtCore.Qt.Checked)

    def update_inner_spacing(self):
        self.form.editInnerSpacing.setText(
            ("{} m".format((self.form.editGapCols.value()  - self.maxWidth / 1000))))

    def createFrameFromPoints(self, dataframe):
        from Mechanical.Frame import PVPlantFrame
        '''try:
            MechanicalGroup = FreeCAD.ActiveDocument.Frames
        except:
            MechanicalGroup = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Frames')
            MechanicalGroup.Label = "Frames"
            FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup)

        if self.form.cbSubfolders.isChecked:
            name = "Frames-" + self.PVArea.Label
            if name in [obj.Name for obj in FreeCAD.ActiveDocument.Frames.Group]:
                MechanicalGroup = FreeCAD.ActiveDocument.getObject(name)[0]
            else:
                group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name)
                group.Label = name
                MechanicalGroup.addObject(group)
                MechanicalGroup = group'''

        doc = FreeCAD.ActiveDocument

        # 1. Obtener o crear el grupo principal 'Frames'
        main_group_name = "Frames"
        main_group = doc.getObject(main_group_name)
        if not main_group:
            main_group = doc.addObject("App::DocumentObjectGroup", main_group_name)
            main_group.Label = main_group_name
            # Asumiendo que existe un grupo 'MechanicalGroup'
            if hasattr(doc, 'MechanicalGroup'):
                doc.MechanicalGroup.addObject(main_group)

        # 2. Manejar subgrupo si es necesario
        group = main_group  # Grupo donde se añadirán los marcos
        if self.form.cbSubfolders.isChecked():  # ¡Corregido: falta de paréntesis!
            subgroup_name = f"Frames-{self.PVArea.Label}"

            # Buscar subgrupo existente
            subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None)

            if not subgroup:
                subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name)
                subgroup.Label = subgroup_name
                main_group.addObject(subgroup)
            group = subgroup

        try:
            placements = dataframe["placement"].tolist()
            types = dataframe["type"].tolist()
            frames = []
            for idx in range(len(placements)):
                newrack = PVPlantFrame.makeTracker(setup=types[idx])
                newrack.Label = "Tracker"
                newrack.Visibility = False
                newrack.Placement = placements[idx]
                group.addObject(newrack)
                frames.append(newrack)
        except:
            placements = dataframe[0]
            frames = []
            for idx in placements:
                print(idx)
                newrack = PVPlantFrame.makeTracker(setup=idx[0])
                newrack.Label = "Tracker"
                newrack.Visibility = False
                newrack.Placement = idx[1]
                groupq.addObject(newrack)
                frames.append(newrack)

        if self.PVArea.Name.startswith("FrameArea"):
            self.PVArea.Frames = frames

    def getProjected(self, shape):
        """ returns projected edges from a shape and a direction """
        if shape.BoundBox.ZLength == 0:
            return Part.Face(Part.Wire(shape.Edges))

        from Utils import PVPlantUtils as utils
        wire = utils.simplifyWire(utils.getProjected(shape))
        return Part.Face(wire.removeSplitter()) if wire.isClosed() else Part.Face(wire)

    def calculateWorkingArea(self):
        self.Area = self.getProjected(self.PVArea.Shape)
        exclusion_areas  = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea")

        if exclusion_areas:
            prohibited_faces = []
            for obj in exclusion_areas:
                face = self.getProjected(obj.Shape.SubShapes[1])
                if face.isValid():
                    prohibited_faces.append(face)
            self.Area = self.Area.cut(prohibited_faces)

    def getAligments(self):
        # TODO: revisar todo esto: -----------------------------------------------------------------
        sel = FreeCADGui.Selection.getSelectionEx()[0]
        refh = None
        refv = None

        if len(sel.SubObjects) == 0:
            return

        elif len(sel.SubObjects) == 1:
            # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma
            refh = refv = sel.SubObjects[0]

        elif len(sel.SubObjects) > 1:
            # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma
            if sel.SubObjects[0].BoundBox.XLength > sel.SubObjects[1].BoundBox.XLength:
                refh = sel.SubObjects[0]
            else:
                refh = sel.SubObjects[1]

            if sel.SubObjects[0].BoundBox.YLength > sel.SubObjects[1].BoundBox.YLength:
                refv = sel.SubObjects[0]
            else:
                refv = sel.SubObjects[1]

        steps = int((refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col)
        startx = int(refv.BoundBox.XMin + self.offsetX - self.gap_col * steps)
        steps = int((refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row)
        starty = int(refh.BoundBox.YMin + self.offsetY + self.gap_row * steps)
        # todo end ----------------------------------------------------------------------------------

        return np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.int64), \
            np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.int64)

    def adjustToTerrain_old(self, coordinates):
        mode = 1
        terrain = self.Terrain.Mesh

        def placeRegion(df):  # TODO: new
            import MeshPart as mp
            from scipy import stats
            linregression = []
            for colnum in df.column.unique().tolist():
                dftmp = df[df["column"] == colnum]
                for id in dftmp["ID"].tolist():
                    data = df.loc[df['ID'] == id]
                    frametype = data["type"].tolist()[0]
                    # col = data["column"]
                    # row = data["row"]
                    base = data["placement"].tolist()[0]

                    yl = frametype.Length.Value / 2
                    ptop = FreeCAD.Vector(base)
                    ptop.y += yl
                    pbot = FreeCAD.Vector(base)
                    pbot.y -= yl
                    line = Part.LineSegment(ptop, pbot).toShape()
                    profilepoints = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))[0]
                    '''else:  # Shape: sumamente lento por lo que quedaría eliminado si no se encuetra otro modo.
                        tmp = terrain.makeParallelProjection(line, FreeCAD.Vector(0, 0, 1))
                        profilepoints = [ver.Point for ver in tmp.Vertexes]'''

                    xx = list()
                    yy = list()
                    zz = list()
                    for pts in profilepoints:
                        xx.append(pts.x)
                        yy.append(pts.y)
                        zz.append(pts.z)
                    slope, intercept, r, p, std_err = stats.linregress(yy, zz)

                    # linregression.append(slope, intercept, r, p, std_err)
                    def myfunc(x):
                        return slope * x + intercept

                    newzz = list(map(myfunc, [yy[0], yy[-1]]))
                    points3D = list()
                    points3D.append(FreeCAD.Vector(xx[0], yy[0], newzz[0]))
                    points3D.append(FreeCAD.Vector(xx[-1], yy[-1], newzz[1]))
                    linregression.append(points3D)

                    # for ind in range(0, len(points3D) - 1):
                    pl = FreeCAD.Placement()
                    pl.Base = (points3D[0] + points3D[1]) / 2
                    rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), points3D[0] - points3D[1])
                    pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0)
                    df.at[id - 1, "placement"] = pl
            df["regression"] = linregression

        # 01. Grouping:
        from scipy.ndimage import label as sclabel
        import pandas as pd
        tmp = []
        for c, col in enumerate(coordinates):
            tmpcol = []
            for n, obj in enumerate(col):
                if obj != 0:
                    tmpcol.append(1)
                else:
                    tmpcol.append(0)
            tmp.append(tmpcol)

        data = {"ID": [],
                "region": [],
                "type": [],
                "column": [],
                "row": [],
                "placement": []}

        arr = np.array(tmp)
        labeled_array, num_features = sclabel(arr)
        id = 1
        for label in range(1, num_features + 1):
            cols, rows = np.where(labeled_array == label)
            unique, counts = np.unique(cols, return_counts=True)
            result = np.column_stack((unique, counts))
            cnt = 0
            for val, count in result:
                for c in range(count):
                    data["ID"].append(id)
                    data["region"].append(label)
                    data["type"].append(coordinates[val][rows[cnt]][0])
                    data["column"].append(val)
                    data["row"].append(rows[cnt])
                    data["placement"].append(coordinates[val][rows[cnt]][1])
                    cnt += 1
                    id += 1
        df = pd.DataFrame(data)
        placeRegion(df)
        return df

    def _setup_terrain_interpolator(self):
        """Prepara interpolador del terreno para ajuste rápido"""
        import numpy as np
        from scipy.interpolate import LinearNDInterpolator

        mesh = self.Terrain.Mesh
        points = np.array([p.Vector for p in mesh.Points])
        bbox = self.Area.BoundBox

        # Filtrar puntos dentro del área de trabajo
        in_bbox = [
            p for p in points
            if bbox.XMin <= p[0] <= bbox.XMax and
               bbox.YMin <= p[1] <= bbox.YMax
        ]

        if not in_bbox:
            return None

        coords = np.array(in_bbox)
        return LinearNDInterpolator(coords[:, :2], coords[:, 2])

    def adjustToTerrain(self, coordinates):
        from scipy.ndimage import label as sclabel
        import pandas as pd
        import numpy as np
        from scipy import stats
        import MeshPart

        # Crear matriz binaria
        arr = np.array([[1 if obj != 0 else 0 for obj in col] for col in coordinates])
        labeled_array, num_features = sclabel(arr)

        # Construir DataFrame optimizado
        data = []
        terrain_interp = self._setup_terrain_interpolator()

        for label in range(1, num_features + 1):
            cols, rows = np.where(labeled_array == label)
            for idx, (col, row) in enumerate(zip(cols, rows)):
                frame_type, placement = coordinates[col][row]
                data.append({
                    'ID': len(data) + 1,
                    'region': label,
                    'type': frame_type,
                    'column': col,
                    'row': row,
                    'placement': placement
                })

        df = pd.DataFrame(data)

        # Ajustar al terreno
        for idx, row in df.iterrows():
            pl = row['placement']
            yl = row['type'].Length.Value / 2

            # Calcular puntos extremos
            top_point = FreeCAD.Vector(pl.x, pl.y + yl, 0)
            bot_point = FreeCAD.Vector(pl.x, pl.y - yl, 0)

            # Usar interpolador si está disponible
            if terrain_interp:
                yy = np.linspace(bot_point.y, top_point.y, 10)
                xx = np.full(10, pl.x)
                zz = terrain_interp(xx, yy)

                if not np.isnan(zz).all():
                    slope, intercept, *_ = stats.linregress(yy, zz)
                    z_top = slope * top_point.y + intercept
                    z_bot = slope * bot_point.y + intercept
                else:
                    z_top = z_bot = 0
            else:
                # Fallback a proyección directa
                line = Part.LineSegment(bot_point, top_point).toShape()
                projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0]
                if len(projected) >= 2:
                    yy = [p.y for p in projected]
                    zz = [p.z for p in projected]
                    slope, intercept, *_ = stats.linregress(yy, zz)
                    z_top = slope * top_point.y + intercept
                    z_bot = slope * bot_point.y + intercept
                else:
                    z_top = z_bot = 0

            # Actualizar placement
            new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top)
            new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot)

            new_pl = FreeCAD.Placement()
            new_pl.Base = (new_top + new_bot) / 2
            new_pl.Rotation = FreeCAD.Rotation(
                FreeCAD.Vector(-1, 0, 0),
                new_top - new_bot
            )
            df.at[idx, 'placement'] = new_pl

        return df

    def isInside(self, frame, point):
        if self.Area.isInside(point, 10, True):
            frame.Placement.Base = point
            cut = frame.cut([self.Area])
            if len(cut.Vertexes) == 0:
                return True
        return False

    def calculateAlignedArray(self):
        import FreeCAD
        pointsx, pointsy = self.getAligments()

        footprints = []
        for frame in self.FrameSetups:
            xx = frame.Length.Value
            yy = frame.Width.Value
            xx_med = xx / 2
            yy_med = yy / 2
            rec = Part.makePolygon([FreeCAD.Vector(-xx_med, -yy_med, 0),
                                    FreeCAD.Vector(xx_med, -yy_med, 0),
                                    FreeCAD.Vector(xx_med, yy_med, 0),
                                    FreeCAD.Vector(-xx_med, yy_med, 0),
                                    FreeCAD.Vector(-xx_med, -yy_med, 0)])
            rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0))
            footprints.append([frame, rec])
        ref = footprints.pop(0)
        xx = ref[0].Length.Value
        yy = ref[0].Width.Value
        xx_med = xx / 2
        yy_med = yy / 2

        # variables for corridors:
        countcols = 0
        countrows = 0
        offsetcols = 0  # ??
        offsetrows = 0  # ??
        valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - yy)

        cols = []
        for x in pointsx:
            col = []
            for y in pointsy:
                found = False
                point = FreeCAD.Vector(x + yy_med + offsetcols, y - xx_med + offsetrows, 0.0)
                if self.isInside(ref[1], point):
                    col.append([ref[0], point])
                    found = True
                    continue
                else:
                    for footprint in footprints:
                        l = int((ref[0].Length - footprint[0].Length) / 2)
                        for i in range(2):
                            point1 = FreeCAD.Vector(point)
                            point1.y = point1.y + l
                            if self.isInside(footprint[1], point1):
                                col.append([footprint[0], point1])
                                found = True
                                break
                            l = -l
                        if found:
                            break
                if not found:
                    col.append(0)
            cols.append(col)

            # if len(col) > 0:
            # code for vertical corridors:
            if self.form.groupCorridor.isChecked():
                if self.form.editColCount.value() > 0:
                    countcols += 1
                    if countcols == self.form.editColCount.value():
                        offsetcols += valcols
                        countcols = 0

        return self.adjustToTerrain(cols)

    def calculateNonAlignedArray(self):
        pointsx, pointsy = self.getAligments()
        if len(pointsx) == 0:
            FreeCAD.Console.PrintWarning("No se encontraron alineaciones X.\n")
            return []

        footprints = []
        for frame in self.FrameSetups:
            l = frame.Length.Value
            w = frame.Width.Value
            l_med = l / 2
            w_med = w / 2
            rec = Part.makePolygon([FreeCAD.Vector(-l_med, -w_med, 0),
                                    FreeCAD.Vector( l_med, -w_med, 0),
                                    FreeCAD.Vector( l_med,  w_med, 0),
                                    FreeCAD.Vector(-l_med,  w_med, 0),
                                    FreeCAD.Vector(-l_med, -w_med, 0)])
            rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0))
            footprints.append([frame, rec])

        corridor = self.form.groupCorridor.isChecked()
        corridor_offset = 0
        count = 0

        cols = []
        for x in pointsx:
            col=[]
            x += corridor_offset
            p1 = FreeCAD.Vector(x, self.Area.BoundBox.YMax, 0.0)
            p2 = FreeCAD.Vector(x, self.Area.BoundBox.YMin, 0.0)
            line = Part.makePolygon([p1, p2])
            inter = self.Area.section([line])
            pts = [ver.Point for ver in inter.Vertexes]
            pts = sorted(pts, key=lambda p: p.y, reverse=True)
            for i in range(0, len(pts), 2):
                top = pts[i]
                bootom = pts[i + 1]
                if top.distanceToPoint(bootom) > footprints[-1][1].BoundBox.YLength:
                    y1 = top.y - (footprints[-1][1].BoundBox.YLength / 2)
                    cp = footprints[-1][1].copy()
                    cp.Placement.Base = FreeCAD.Vector(x + footprints[-1][1].BoundBox.XLength / 2, y1, 0.0)
                    inter = cp.cut([self.Area])
                    vtx = [ver.Point for ver in inter.Vertexes]
                    mod = top.y
                    if len(vtx) != 0:
                        mod = min(vtx, key=lambda p: p.y).y
                        #y1 = cp.Placement.Base.y - mod

                    tmp = optimized_cut(mod - bootom.y, [ftp[1].BoundBox.YLength for ftp in footprints], 500, 'greedy')
                    for opt in tmp[0]:
                        mod -= (footprints[opt][1].BoundBox.YLength / 2)
                        pl = FreeCAD.Vector(x + footprints[opt][1].BoundBox.XLength / 2, mod, 0.0)
                        cp = footprints[opt][1].copy()
                        if self.isInside(cp, pl):
                            col.append([footprints[opt][0], pl])
                            mod -= ((footprints[opt][1].BoundBox.YLength / 2) + 500)
                            Part.show(cp)

                    if corridor and len(col) > 0:
                        count += 1
                        if count == self.form.editColCount.value():
                            corridor_offset += 12000
                            count = 0

            cols.append(cols)
        return self.adjustToTerrain(cols)

    def accept(self):
        from datetime import datetime
        starttime = datetime.now()

        params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document")
        auto_save_enabled = params.GetBool("AutoSaveEnabled")
        params.SetBool("AutoSaveEnabled", False)
        FreeCAD.ActiveDocument.RecomputesFrozen = True

        items = [
            FreeCAD.ActiveDocument.getObject(item.text())
            for i in range(self.form.listFrameSetups.count())
            if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked
        ]

        unique_frames = {frame.Length.Value: frame for frame in items}
        self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True)

        self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value
        self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value
        self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value
        self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value

        FreeCAD.ActiveDocument.openTransaction("Create Placement")
        # 1. Calculate working area:
        self.calculateWorkingArea()
        # 2. Calculate aligned array:
        if self.form.cbAlignFrames.isChecked():
            dataframe = self.calculateAlignedArray()
        else:
            dataframe = self.calculateNonAlignedArray()
        # 3. Adjust to terrain:
        self.createFrameFromPoints(dataframe)

        import Electrical.group as egroup
        import importlib
        importlib.reload(egroup)
        egroup.groupTrackersToTransformers(5000000, self.gap_row + self.FrameSetups[0].Length.Value)


        FreeCAD.ActiveDocument.commitTransaction()
        FreeCAD.ActiveDocument.RecomputesFrozen = False
        params.SetBool("AutoSaveEnabled", auto_save_enabled)

        total_time = datetime.now() - starttime
        print(" -- Tiempo tardado:", total_time)
        FreeCADGui.Control.closeDialog()
        FreeCAD.ActiveDocument.recompute()


def optimized_cut(L_total, piezas, margen=0, metodo='auto'):
    """
    Encuentra la combinación óptima de piezas para minimizar el desperdicio,
    considerando un margen entre piezas.

    Args:
        L_total (int): Longitud total del material.
        piezas (list): Lista de longitudes de los patrones de corte.
        margen (int): Espacio perdido entre piezas consecutivas.
        metodo (str): 'dp' para programación dinámica, 'greedy' para voraz, 'auto' para selección automática.

    Returns:
        tuple: (piezas_seleccionadas, desperdicio)
    """
    # Filtrar piezas inválidas
    piezas = [p for p in piezas if 0 < p <= L_total]
    if not piezas:
        return [], L_total

    # Transformar longitudes y longitud total con margen
    longitudes_aumentadas = [p + margen for p in piezas]
    L_total_aumentado = L_total + margen

    # Selección automática de método
    if metodo == 'auto':
        if L_total_aumentado <= 10000 and len(piezas) <= 100:
            metodo = 'dp'
        else:
            metodo = 'greedy'

    if metodo == 'dp':
        n = len(piezas)
        dp = [0] * (L_total_aumentado + 1)
        parent = [-1] * (L_total_aumentado + 1)  # Almacena índices de piezas usadas

        # Llenar la tabla dp y parent
        for j in range(1, L_total_aumentado + 1):
            for i in range(n):
                p_aum = longitudes_aumentadas[i]
                if p_aum <= j:
                    if dp[j] < dp[j - p_aum] + p_aum:
                        dp[j] = dp[j - p_aum] + p_aum
                        parent[j] = i  # Guardar índice de la pieza

        # Reconstruir solución desde el final
        current = L_total_aumentado
        seleccion_indices = []
        while current > 0 and parent[current] != -1:
            i = parent[current]
            seleccion_indices.append(i)
            current -= longitudes_aumentadas[i]

        # Calcular desperdicio real
        k = len(seleccion_indices)
        if k == 0:
            desperdicio = L_total
        else:
            suma_original = sum(piezas[i] for i in seleccion_indices)
            desperdicio = L_total - suma_original - margen * (k - 1)

        return seleccion_indices, desperdicio

    elif metodo == 'greedy':
        # Crear lista con índices y longitudes aumentadas
        lista_con_indices = [(longitudes_aumentadas[i], i) for i in range(len(piezas))]
        lista_con_indices.sort(key=lambda x: x[0], reverse=True)  # Ordenar descendente

        seleccion_indices = []
        restante = L_total_aumentado

        # Seleccionar piezas vorazmente
        for p_aum, i in lista_con_indices:
            while restante >= p_aum:
                seleccion_indices.append(i)
                restante -= p_aum

        # Calcular desperdicio real
        k = len(seleccion_indices)
        if k == 0:
            desperdicio = L_total
        else:
            suma_original = sum(piezas[i] for i in seleccion_indices)
            desperdicio = L_total - suma_original - margen * (k - 1)

        return seleccion_indices, desperdicio


# Ejemplo de uso
'''if __name__ == "__main__":
    L_total = 100
    piezas = [25, 35, 40, 20, 15, 30, 50]
    margen = 5

    print("Solución óptima con margen (programación dinámica):")
    seleccion, desperd = corte_optimizado(L_total, piezas, margen, 'dp')
    print(f"Piezas usadas: {seleccion}")
    print(f"Margen entre piezas: {margen} cm")
    print(f"Material útil: {sum(seleccion)} cm")
    print(f"Espacio usado por márgenes: {(len(seleccion) - 1) * margen} cm")
    print(f"Desperdicio total: {desperd} cm")

    print("\nSolución aproximada con margen (algoritmo voraz):")
    seleccion_g, desperd_g = corte_optimizado(L_total, piezas, margen, 'greedy')
    print(f"Piezas usadas: {seleccion_g}")
    print(f"Margen entre piezas: {margen} cm")
    print(f"Material útil: {sum(seleccion_g)} cm")
    print(f"Espacio usado por márgenes: {(len(seleccion_g) - 1) * margen} cm")
    print(f"Desperdicio total: {desperd_g} cm")'''


# ----------------------------------------------------------------------------------------------------------------------
# function AdjustToTerrain
#   Take a group of objects and adjust it to the slope and altitude of the terrain mesh. It detects the terrain mesh
#
#   Inputs:
#   1. frames: group of objest to adjust
# ----------------------------------------------------------------------------------------------------------------------
class adjustToTerrainTaskPanel:
    def __init__(self, obj=None):
        self.obj = obj
        self.form = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantPlacementAdjust.ui")

    def accept(self):
        frames = []
        for obj in FreeCADGui.Selection.getSelection():
            if obj.Name.startswith("Tracker"):
                frames.append(obj)
            elif obj.Name.startswith("FrameArea"):
                frames.extend(obj.Frames)
        adjustToTerrain(frames, self.form.comboMethod.currentIndex() == 0)
        self.close()
        return True

    def reject(self):
        self.close()
        return False

    def close(self):
        FreeCADGui.Control.closeDialog()

import numpy as np
from scipy import stats

def get_trend(points):
    """Return the trend of a list of 3D points"""
    x, y, z = zip(*[(point.x, point.y, point.z) for point in points])
    slope, intercept, _, _, _ = stats.linregress(y, z)
    new_z = slope * np.array([y[0], y[-1]]) + intercept
    return [FreeCAD.Vector(x[0], y[0], new_z[0]), FreeCAD.Vector(x[-1], y[-1], new_z[1])]

def getTrend(points): # old
    from scipy import stats
    def getNewZ(x):
        return slope * x + intercept

    xx = list()
    yy = list()
    zz = list()
    for point in points:
        xx.append(point.x)
        yy.append(point.y)
        zz.append(point.z)
    slope, intercept, r, p, std_err = stats.linregress(yy, zz)
    newzz = list(map(getNewZ, [yy[0], yy[-1]]))
    return [FreeCAD.Vector(xx[0], yy[0], newzz[0]),
            FreeCAD.Vector(xx[-1], yy[-1], newzz[1])]


def adjustToTerrain(frames, individual=True):
    from datetime import datetime
    starttime = datetime.now()

    import MeshPart as mp

    FreeCAD.ActiveDocument.openTransaction("Adjust to terrain")
    terrain = PVPlantSite.get().Terrain.Mesh

    if individual:
        for frame in frames:
            length = frame.Setup.Length.Value / 2 + 5000
            p1 = FreeCAD.Vector(-length, 0, 0, )
            p2 = FreeCAD.Vector(length, 0, 0, )
            line = Part.LineSegment(p1, p2).toShape()
            line.Placement = frame.Placement.copy()
            line.Placement.Base.z = 0
            xyz = line.Placement.Rotation.toEulerAngles("XYZ")
            line.Placement.Rotation.setEulerAngles("XYZ", 0, 0, xyz[2])
            pro = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))
            pts = []
            for points in pro:
                pts.extend(points)
            points3D = get_trend(pts)

            pl = FreeCAD.Placement()
            pl.Base = (points3D[0] + points3D[1]) / 2
            rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), points3D[0] - points3D[1])
            pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0)
            frame.Placement = pl
    else:
        import math
        def getLineAngle(line):
            # ángulo en grados = arctan(ángulo en porcentaje / 100%)
            import math
            p1 = FreeCAD.Vector(line.Vertexes[0].Point)
            p2 = FreeCAD.Vector(line.Vertexes[1].Point)
            hi = p2.z - p1.z
            p1.z = 0
            p2.z = 0
            le = p2.sub(p1).Length
            return math.degrees(math.atan2(hi, le))

        cols = getCols(frames)
        for col in cols:
            for group in col:
                # Ver 1 -----------------
                lines = []
                # 1. Generar las líneas de trabajo.
                for frame in group:
                    # 1.1. Corregir los frames que estén fuera de tolerancia:
                    if frame.AngleY < FreeCAD.ActiveDocument.MaximumTiltNegative.Value:
                        frame.AngleY = FreeCAD.ActiveDocument.MaximumTiltNegative.Value
                    if frame.AngleY > FreeCAD.ActiveDocument.MaximumTiltPositive.Value:
                        frame.AngleY = FreeCAD.ActiveDocument.MaximumTiltPositive.Value

                    # 1.2. Generar las líneas con las que se trabajarán:
                    l = frame.Setup.Length / 2
                    pn = FreeCAD.Vector(-l, 0, 0)
                    ps = FreeCAD.Vector( l, 0, 0)
                    line = Part.LineSegment(pn, ps).toShape()
                    line.Placement = frame.Placement.copy()
                    lines.append([frame, line])

                # 2. Poner los tracker en tolerancia:
                cnt = len(lines)
                if cnt > 1:
                    angleLine=[]
                    anglesTwoLines=[]
                    for frame in lines:
                        angleLine.append(frame[0].AngleY.Value)
                    for ind in range(cnt - 1):
                        frame1 = lines[ind]
                        frame2 = lines[ind + 1]
                        vec1 = frame1[1].Vertexes[1].Point.sub(frame1[1].Vertexes[0].Point)
                        vec2 = frame2[1].Vertexes[1].Point.sub(frame2[1].Vertexes[0].Point)
                        anglesTwoLines.append(math.degrees(vec2.getAngle(vec1)))
                    print(angleLine)
                    print(anglesTwoLines)
                    pass

                for ind, frame in enumerate(lines):
                    frame0 = None
                    frame1 = None
                    if ind > 0:
                        frame0 = lines[ind - 1]
                    if ind < (len(group) - 1):
                        frame1 = lines[ind + 1]

                    if (frame0 is None) and (frame1 is None):        # Caso 1: sólo 1 frame por fila
                        # no se hace nada. ya está con todos los parámetros dentro de tolerancia
                        pass
                    elif (frame0 is None) and not (frame1 is None):  # Caso 2: frame es el primero y hay más frames
                        pass
                    elif not (frame0 is None) and (frame1 is None):  # Caso 3: el frame es el último y hay más frames
                        pass
                    else:                                            # Caso 4: el frame está en el médio de varios frames
                        pass

                continue

                # Ver 0 -----------------
                points = []
                # 1. Get lines/points to project on land
                frame1 = group[0]  # Norte
                frame2 = group[-1]  # Sur
                # 1.1. Get the first and last points:

                # TODO: revisar esta parte:
                p0 = FreeCAD.Vector(frame1.Shape.BoundBox.Center.x, frame1.Shape.BoundBox.YMax, 0.0)
                pf = FreeCAD.Vector(frame2.Shape.BoundBox.Center.x, frame2.Shape.BoundBox.YMin, 0.0)

                vec = (pf - p0).normalize()
                points.append(p0)
                for ind in range(0, len(group) - 1):
                    frame1 = group[ind]
                    frame2 = group[ind + 1]
                    vec1 = FreeCAD.Vector(frame1.Placement.Base)
                    vec2 = FreeCAD.Vector(frame2.Placement.Base)
                    vec1.z = 0
                    vec2.z = 0
                    vec3 = vec2.sub(vec1)
                    c = vec3.Length / 2 + (frame1.Setup.Length.Value - frame2.Setup.Length.Value) / 4
                    v = FreeCAD.Vector(vec)
                    v.Length = c
                    v = vec1.add(v)
                    v.z = 0
                    points.append(v)
                points.append(pf)

                # 2. Calculate trend:
                points3D = []
                for ind in range(len(points) - 1):
                    line = Part.LineSegment(points[ind], points[ind + 1]).toShape()
                    pro = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))
                    pts = []
                    for lp in pro:
                        pts.extend(lp)
                    points3D.extend(get_trend(pts))
                # Todo: aplicar aproximación de los vertices:
                # prueba:
                for i in range(0, len(points3D) - 2, 2):
                    # p0 = points3D[i]
                    p1 = points3D[i + 1]
                    p2 = points3D[i + 2]
                    # p3 = points3D[i + 3]

                    l = p1.sub(p2).Length
                    if l > 250:
                        l = (l - 250) / 2
                        if p1.z > p2.z:
                            p1.z -= l
                            p2.z += l
                        else:
                            p1.z += l
                            p2.z -= l

                # 3. Aplicar placement
                for ind, frame in enumerate(group):
                    v1 = points3D[ind * 2]
                    v2 = points3D[ind * 2 + 1]
                    pl = frame.Placement.copy()
                    pl.Base.z = (v1.add(v2) / 2).z
                    rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), v1.sub(v2))
                    pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0)
                    frame.Placement = pl

    FreeCAD.ActiveDocument.commitTransaction()
    total_time = datetime.now() - starttime
    print(" -- Tiempo tardado en ajustar al terreno:", total_time)
    FreeCAD.activeDocument().recompute()


def getRows(objs):
    ''' '''
    def countFrames(columns):
        cnt = 0
        for icol in columns:
            cnt += len(icol)
        return cnt

    if len(objs) == 0:
        return None, None

    cols = getCols(list(objs))
    tmpcols = []
    for col in cols:
        g = []
        for group in col:
            g.extend(group)
        tmpcols.append(g)

    rows = []
    while countFrames(tmpcols) > 0:
        firstCol = max(tmpcols, key=lambda col: col[0].Placement.Base.y)
        compFrame = max(firstCol, key=lambda x: x.Placement.Base.y)
        ind = tmpcols.index(firstCol)
        group = [compFrame,]
        tmpcols[ind].remove(compFrame)
        for i in range(ind - 1, 0, -1):
            if len(tmpcols[i]) == 0:
                break
            frame = tmpcols[i][0]
            framelen = frame.Setup.Length / 2
            compFramelen = compFrame.Setup.Length / 2
            l = max([framelen, compFramelen])
            if abs(compFrame.Placement.Base.y - frame.Placement.Base.y) <= l:
                group.append(frame)
                tmpcols[i].remove(frame)
                compFrame = frame
            else:
                break

        for i in range(ind + 1, len(cols)):
            if len(tmpcols[i]) == 0:
                break
            frame = tmpcols[i][0]
            framelen = frame.Setup.Length / 2
            compFramelen = compFrame.Setup.Length / 2
            l = max([framelen, compFramelen])
            if abs(compFrame.Placement.Base.y - frame.Placement.Base.y) <= l:
                group.append(frame)
                tmpcols[i].remove(frame)
                compFrame = frame
            else:
                break

        if len(group) > 0:
            group = sorted(group, key=lambda x: x.Placement.Base.x)
            rows.append(group)

    return rows, cols


def getCols(objs):
    def getRound(num):
        return round(num / 100, 0)
    xx = set(getRound(obj.Placement.Base.x) for obj in objs)
    xx = sorted(xx)
    columns = []

    for x in xx:
        # 1. identificar los objetos de una columna
        tmpcol = []
        for obj in objs:
            if getRound(obj.Placement.Base.x) == x:
                tmpcol.append(obj)
        tmpcol = sorted(tmpcol, key=lambda obj: getRound(obj.Placement.Base.y), reverse=True)
        for obj in tmpcol:
            objs.remove(obj)

        # 2. dividir los objetos en grupos:
        group = []
        col = []
        for i, f2 in enumerate(tmpcol):
            if i > 0:
                f1 = group[-1]
                d = abs(f1.Placement.Base.y - f2.Placement.Base.y) - \
                    (f1.Setup.Length.Value + f2.Setup.Length.Value) / 2
                if d > 1000:
                    col.append(group.copy())
                    group.clear()
            group.append(f2)
        col.append(group)
        columns.append(col)
    return columns


# en el caso de que no sean perpendiculares a x:

def getCols_old(sel, tolerance=4000, sort=True):
    # TODO: get only frames from de selection
    if not sel:
        return
    if len(sel) == 0:
        return

    cols = []
    while len(sel) > 0:
        obj = sel[0]
        p = obj.Shape.BoundBox.Center
        vec = obj.Shape.SubShapes[1].SubShapes[1].BoundBox.Center - \
              obj.Shape.SubShapes[1].SubShapes[0].BoundBox.Center
        n = FreeCAD.Vector(vec.y, -vec.x, 0)

        # 1. Detectar los objetos que están en una misma columna
        col = []
        newsel = []
        for obj1 in sel:
            if obj1.Shape.BoundBox.isCutPlane(p, n):  # todo: esto no es del todo correcto. buscar otra manera
                col.append(obj1)
            else:
                newsel.append(obj1)
        sel = newsel.copy()
        col = sorted(col, key=lambda k: k.Placement.Base.y, reverse=True)  # Orden Norte - Sur (Arriba a abajo)

        # 2. Detectar y separar los grupos dentro de una misma columna:
        group = []
        newcol = []
        group.append(col[0])
        if len(col) > 1:
            for ind in range(0, len(col) - 1):
                vec1 = FreeCAD.Vector(col[ind].Placement.Base)
                vec1.z = 0
                vec2 = FreeCAD.Vector(col[ind + 1].Placement.Base)
                vec2.z = 0
                distance = abs((vec1 - vec2).Length) - (col[ind].Setup.Width.Value + col[ind + 1].Setup.Width.Value) / 2
                if distance > tolerance:
                    newcol.append(group.copy())
                    group.clear()
                group.append(col[ind + 1])
        newcol.append(group)
        cols.append(newcol)

    if sort:
        cols = sorted(cols, key=lambda k: k[0][0].Placement.Base.x, reverse=False)

    return cols


# -----------------------------------------------------------------------------------------------------------------------
# Convert
# -----------------------------------------------------------------------------------------------------------------------
class _PVPlantConvertTaskPanel:
    '''The editmode TaskPanel for Conversions'''

    def __init__(self):
        self.To = None
        # self.form:
        self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacementConvert.ui"))
        self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "Trace.svg")))
        self.form.buttonTo.clicked.connect(self.addTo)

    def addTo(self):
        sel = FreeCADGui.Selection.getSelection()
        if len(sel) > 0:
            self.To = sel[0]
            self.form.editTo.setText(self.To.Label)

    def accept(self):
        sel = FreeCADGui.Selection.getSelection()
        if sel == self.To:
            return False
        if len(sel) > 0 and self.To is not None:
            FreeCAD.ActiveDocument.openTransaction("Convert to")
            ConvertObjectsTo(sel, self.To)
            return True
        return False


def getHeadsAndSoil(frame=None):
    if frame == None:
        return None
    import MeshPart as mp

    data = {"heads": [],
            "soil": []}
    poles = frame.Shape.SubShapes[1].SubShapes[0].SubShapes
    for pole in poles:
        vec = pole.BoundBox.Center
        vec.z = pole.BoundBox.ZMax
        data["heads"].append(vec)

    data["soil"].extend(mp.projectPointsOnMesh(data["heads"],
                                               FreeCAD.ActiveDocument.Terrain.Mesh,
                                               FreeCAD.Vector(0, 0, 1)))
    return data


def moveFrameHead(obj, head=0, dist=0):
    import math
    print(dist)
    dir = 1 if head == 0 else -1
    base = obj.Placement.Base
    dist /= 2
    base.z += dist
    angles = obj.Placement.Rotation.toEulerAngles("XYZ")
    angley = math.degrees(math.asin(dist/(obj.Setup.Length.Value / 2))) * dir
    print(angley)
    rot = FreeCAD.Rotation(angles[2], angles[1] + angley, angles[0])
    obj.Placement = FreeCAD.Placement(base, rot, FreeCAD.Vector(0, 0, 0))
    obj.recompute()

# ---------------------------------------------------------------------------------------------------------------------
# function ConvertObjectsTo
#
# ---------------------------------------------------------------------------------------------------------------------
def ConvertObjectsTo(sel, objTo):
    if hasattr(objTo, "Proxy"):
        isFrame = objTo.Proxy.__class__ is PVPlantRack._Tracker
        # isFrame = issubclass(objTo.Proxy.__class__, PVPlantRack._Frame)
    isFrame = True

    for obj in sel:
        if isFrame:
            if hasattr(obj, "Proxy"):
                if obj.Proxy.__class__ is PVPlantRack._Tracker:
                    # if issubclass(obj.Proxy.__class__, PVPlantRack._Frame):             # 1. Si los dos son Frames
                    cp = FreeCAD.ActiveDocument.copyObject(objTo, False)
                    cp.Placement = obj.Placement
                    cp.CloneOf = objTo
            else:  # 2. De un objeto no Frame a Frame
                place = FreeCAD.Placement()  # obj.Placement
                place.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1),
                                                  90)  # TODO: rotar conforme a lados más largos
                bb = None
                if obj.isDerivedFrom("Part::Feature"):
                    bb = obj.Shape.BoundBox
                elif obj.isDerivedFrom("Mesh::Feature"):
                    bb = obj.Mesh.BoundBox
                place.Base = bb.Center
                cp = FreeCAD.ActiveDocument.copyObject(objTo, False)
                cp.Placement = place
                if isFrame:
                    cp.CloneOf = objTo
        else:  # 3. De un objeto a otro objeto (cualesquieran que sean)
            place = FreeCAD.Placement()  # obj.Placement
            bb = None
            if obj.isDerivedFrom("Part::Feature"):
                bb = obj.Shape.BoundBox
            elif obj.isDerivedFrom("Mesh::Feature"):
                bb = obj.Mesh.BoundBox
            place.Base = bb.Center
            cp = FreeCAD.ActiveDocument.copyObject(objTo, False)
            cp.Placement = place
            if isFrame:
                cp.CloneOf = objTo
        FreeCAD.ActiveDocument.removeObject(obj.Name)
    FreeCAD.activeDocument().recompute()


## Comandos: -----------------------------------------------------------------------------------------------------------
class CommandPVPlantPlacement:

    def GetResources(self):
        return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "way.svg")),
                'Accel': "P,P",
                'MenuText': QT_TRANSLATE_NOOP("Placement", "Placement"),
                'ToolTip': QT_TRANSLATE_NOOP("Placement", "Crear un campo fotovoltaico")}

    def Activated(self):
        taskd = _PVPlantPlacementTaskPanel(None)
        FreeCADGui.Control.showDialog(taskd)

    def IsActive(self):
        if FreeCAD.ActiveDocument:
            return True
        else:
            return False


class CommandAdjustToTerrain:

    def GetResources(self):
        return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "adjust.svg")),
                'Accel': "P, A",
                'MenuText': QT_TRANSLATE_NOOP("Placement", "Adjust"),
                'ToolTip': QT_TRANSLATE_NOOP("Placement", "Adjust object to terrain")}

    def Activated(self):
        sel = FreeCADGui.Selection.getSelection()
        if len(sel) > 0:
            # adjustToTerrain(sel)
            FreeCADGui.Control.showDialog(adjustToTerrainTaskPanel())
        else:
            print("No selected object")

    def IsActive(self):
        if FreeCAD.ActiveDocument:
            return True
        else:
            return False


class CommandConvert:
    def GetResources(self):
        return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "convert.svg")),
                'Accel': "P, C",
                'MenuText': QT_TRANSLATE_NOOP("Placement", "Convert"),
                'ToolTip': QT_TRANSLATE_NOOP("Placement", "Convertir un objeto en otro")}

    def IsActive(self):
        return (not FreeCAD.ActiveDocument is None and
                not FreeCAD.ActiveDocument.getObject("Site") is None and
                not FreeCAD.ActiveDocument.getObject("Terrain") is None and
                not FreeCAD.ActiveDocument.getObject("TrackerSetup") is None)

    def Activated(self):
        taskd = _PVPlantConvertTaskPanel()
        FreeCADGui.Control.showDialog(taskd)


'''if FreeCAD.GuiUp:
    FreeCADGui.addCommand('PVPlantPlacement', _CommandPVPlantPlacement())
    FreeCADGui.addCommand('PVPlantAdjustToTerrain', _CommandAdjustToTerrain())
    FreeCADGui.addCommand('PVPlantConvertTo', _CommandConvert())'''
