# /**********************************************************************
# *                                                                     *
# * 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 os
import platform
import subprocess
from typing import Dict, List

import FreeCAD
import openpyxl
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet

import PVPlantResources
import PVPlantSite

if FreeCAD.GuiUp:
    from PySide.QtCore import QT_TRANSLATE_NOOP


__title__ = "PVPlant Mechanical BOQ"
__author__ = "Javier Braña"
__url__ = "http://www.sogos-solar.com"

# Constants
SCALE = 0.001  # Millimeters to meters
THIN_BORDER = Border(
    top=Side(border_style="thin", color="7DA4B8"),
    left=Side(border_style="thin", color="7DA4B8"),
    right=Side(border_style="thin", color="7DA4B8"),
    bottom=Side(border_style="thin", color="7DA4B8")
)
HEADER_FILL = PatternFill("solid", fgColor="7DA4B8")
HEADER_FONT = Font(name='Arial', size=10, bold=True, color="FFFFFF")
DATA_FONT = Font(name='Arial', size=10)
CENTER_ALIGN = Alignment(horizontal="center", vertical="center")



# Estilos:
thin = Side(border_style="thin", color="7DA4B8")
double = Side(border_style="double", color="ff0000")
border_thin = Border(top=thin, left=thin, right=thin, bottom=thin)
border_fat = Border(top=thin, left=thin, right=thin, bottom=thin)
# fill = PatternFill("solid", fgColor="DDDDDD")
# fill = GradientFill(stop=("000000", "FFFFFF"))

scale = 0.001 # milimeters to meter


def open_file(path: str) -> None:
    """Open a file or directory using the system's default handler"""
    if platform.system() == "Windows":
        os.startfile(path)
    elif platform.system() == "Darwin":
        subprocess.Popen(["open", path])
    else:
        subprocess.Popen(["xdg-open", path])

def style_range(ws, cell_range, border=Border(), fill=None, font=None, alignment=None):
    """
    Apply styles to a range of cells as if they were a single cell.

    :param ws:  Excel worksheet instance
    :param range: An excel range to style (e.g. A1:F20)
    :param border: An openpyxl Border
    :param fill: An openpyxl PatternFill or GradientFill
    :param font: An openpyxl Font object
    """

    top = Border(top=border.top)
    left = Border(left=border.left)
    right = Border(right=border.right)
    bottom = Border(bottom=border.bottom)

    first_cell = ws[cell_range.split(":")[0]]
    if alignment:
        first_cell.alignment = alignment

    rows = ws[cell_range]
    if font:
        first_cell.font = font

    for cell in rows[0]:
        cell.border = cell.border + top
    for cell in rows[-1]:
        cell.border = cell.border + bottom

    for row in rows:
        l = row[0]
        r = row[-1]
        l.border = l.border + left
        r.border = r.border + right
        if fill:
            for c in row:
                c.fill = fill


def create_sheet_headers(sheet: Worksheet, headers: List[str], widths: Dict[str, float]) -> None:
    """Create standardized sheet headers."""
    for col, header in enumerate(headers, start=1):
        cell = sheet.cell(row=1, column=col, value=header)
        cell.fill = HEADER_FILL
        cell.font = HEADER_FONT
        cell.alignment = CENTER_ALIGN

    for col, width in widths.items():
        sheet.column_dimensions[col].width = width

    sheet.row_dimensions[1].height = 30
    style_range(sheet, f'A1:{chr(64 + len(headers))}1',
                border=THIN_BORDER,
                fill=HEADER_FILL,
                font=HEADER_FONT,
                alignment=CENTER_ALIGN)


def spreadsheetBOQFrames(sheet: Worksheet, selection: List[FreeCAD.DocumentObject]) -> None:
    """Generate frames information sheet."""
    headers = [
        'Index', 'Frame', 'Frame Type', 'X (m)', 'Y (m)', 'Z (m)',
        'Angle N-S', 'Angle L-W', 'Nº Poles'
    ]
    widths = {'A': 8, 'B': 30, 'C': 20, 'D': 15, 'E': 15,
              'F': 15, 'G': 15, 'H': 15, 'I': 10}

    create_sheet_headers(sheet, headers, widths)

    for idx, obj in enumerate(selection, start=2):
        placement = obj.Placement
        sheet[f'A{idx}'] = idx - 1
        sheet[f'B{idx}'] = obj.Label
        sheet[f'C{idx}'] = obj.Setup.Label
        sheet[f'D{idx}'] = placement.Base.x * SCALE
        sheet[f'E{idx}'] = placement.Base.y * SCALE
        sheet[f'F{idx}'] = placement.Base.z * SCALE
        sheet[f'G{idx}'] = placement.Rotation.toEuler()[0]
        sheet[f'H{idx}'] = placement.Rotation.toEuler()[1]
        sheet[f'I{idx}'] = obj.Setup.NumberPole.Value

        style_range(sheet, f'A{idx}:I{idx}',
                    border=THIN_BORDER,
                    font=DATA_FONT,
                    alignment=CENTER_ALIGN)

def spreadsheetBOQPoles(sheet: Worksheet, selection: List[FreeCAD.DocumentObject]) -> None:
    """Generate poles information sheet."""
    headers = [
        'Frame', 'Pole', 'Pole Type', 'X (m)', 'Y (m)', 'Z Frame Attach (m)',
        'Z Aerial Head (m)', 'Pole Length (m)', 'Aerial Length (m)',
        'Terrain Enter Length (m)'
    ]
    widths = {chr(65 + i): 20 for i in range(10)}
    widths['A'] = 30
    widths['B'] = 10

    create_sheet_headers(sheet, headers, widths)
    sheet.row_dimensions[2].height = 5


    import MeshPart as mp
    from Mechanical.Frame import PVPlantFrame
    # Data:
    terrain = PVPlantSite.get().Terrain.Mesh  # Shape
    poles_data = []

    for frame in selection:
        try:
            poles = frame.Shape.SubShapes[1].SubShapes[0].SubShapes
            num_poles = int(frame.Setup.NumberPole.Value)
            sequence = frame.Setup.PoleSequence

            if len(sequence) < num_poles:
                sequence = PVPlantFrame.getarray(frame.Setup.PoleSequence, num_poles)

            for pole_idx in range(num_poles):
                pole = poles[pole_idx]
                pole_type = frame.Setup.PoleType[sequence[pole_idx]]
                center = pole.BoundBox.Center

                poles_data.append({
                    'frame': frame.Label,
                    'number': pole_idx + 1,
                    'type': pole_type.Label,
                    'center': center,
                    'head_z': pole.BoundBox.ZMax,
                    'length': pole_type.Height.Value
                })
        except Exception as e:
            FreeCAD.Console.PrintError(f"Error processing frame {frame.Label}: {str(e)}\n")

    # Project points on terrain
    try:
        import MeshPart
        points = [data['center'] for data in poles_data]
        terrain_z = MeshPart.projectPointsOnMesh(points, terrain, FreeCAD.Vector(0, 0, 1))
        for data, z in zip(poles_data, terrain_z):
            data['terrain_z'] = z.z
    except Exception as e:
        FreeCAD.Console.PrintError(f"Terrain projection failed: {str(e)}\n")
        for data in poles_data:
            data['terrain_z'] = 0

    # Write data to sheet
    row = 3
    group_from = row
    f = poles_data[0]['frame']
    for i, data in enumerate(poles_data):
        if f != data["frame"]:
            style_range(sheet, 'A' + str(group_from) + ':F' + str(row - 1),
                        border=Border(top=thin, left=thin, right=thin, bottom=thin),
                        font=Font(name='Quicksand', size=11, ),
                        alignment=Alignment(horizontal="center", vertical="center"))
            sheet.merge_cells('A' + str(group_from) + ':A' + str(row - 1))
            style_range(sheet, 'A' + str(group_from) + ':A' + str(row - 1),
                        border=Border(top=thin, left=thin, right=thin, bottom=thin),
                        font=Font(name='Quicksand', size=11, ),
                        alignment=Alignment(horizontal="center", vertical="center"))
            # sheet['A{0}'.format(row)] = ""
            sheet.row_dimensions[row].height = 5
            row += 1
            f = data["frame"]
            group_from = row

        sheet['A{0}'.format(row)] = data['frame']
        sheet['B{0}'.format(row)] = data['number']
        sheet['C{0}'.format(row)] = data['type']
        sheet['D{0}'.format(row)] = round(data['center'].x, 0) * scale
        sheet['D{0}'.format(row)].number_format = "0.000"
        sheet['E{0}'.format(row)] = round(data['center'].y, 0) * scale
        sheet['E{0}'.format(row)].number_format = "0.000"
        try:
            sheet['F{0}'.format(row)] = round(data['terrain_z'].z, 0) * scale
            sheet['F{0}'.format(row)].number_format = "0.000"
        except:
            pass

        sheet['G{0}'.format(row)] = round(data['head_z']) * scale
        sheet['G{0}'.format(row)].number_format = "0.000"
        sheet['H{0}'.format(row)] = data["length"] * scale
        sheet['H{0}'.format(row)].number_format = "0.000"
        sheet['I{0}'.format(row)] = '=G{0}-F{0}'.format(row)
        sheet['I{0}'.format(row)].number_format = "0.000"
        sheet['J{0}'.format(row)] = '=H{0}-I{0}'.format(row)
        sheet['J{0}'.format(row)].number_format = "0.000"

        style_range(sheet, 'A' + str(row) + ':J' + str(row),
                    border=Border(top=thin, left=thin, right=thin, bottom=thin),
                    font=Font(name='Quicksand', size=11, ),
                    alignment=Alignment(horizontal="center", vertical="center"))
        row += 1

def spreadsheetBOQPanelCollision(sheet, sel):
    # Headers:
    sheet['A1'] = 'Frame'
    sheet['B1'] = 'Nombre'
    sheet['C1'] = 'X'
    sheet['D1'] = 'Y'
    sheet['E1'] = 'Z'
    sheet['G1'] = 'Ángulo E-O'
    sheet['H1'] = 'Nº Hincas'

    sheet.column_dimensions['A'].width = 30
    sheet.column_dimensions['B'].width = 8
    sheet.column_dimensions['C'].width = 20
    sheet.column_dimensions['D'].width = 20
    sheet.column_dimensions['E'].width = 20
    sheet.column_dimensions['F'].width = 20
    sheet.column_dimensions['G'].width = 20
    sheet.column_dimensions['H'].width = 20
    sheet.column_dimensions['I'].width = 20
    sheet.row_dimensions[1].height = 40
    style_range(sheet, 'A1:I1',
                border=Border(top=thin, left=thin, right=thin, bottom=thin),
                fill=PatternFill("solid", fgColor="7DA4B8"),
                font=Font(name='Quicksand', size=11, b=True, color="FFFFFF"),
                alignment=Alignment(horizontal="center", vertical="center"))
    sheet['A2'] = ""
    sheet.row_dimensions[2].height = 5

    # Data:
    for frame_ind in range(0, len(sel)):
        frame = sel[frame_ind]
        sheet['A{0}'.format(ind + 2)] = ind
        sheet['B{0}'.format(ind + 2)] = sel[ind].Label
        sheet['C{0}'.format(ind + 2)] = sel[ind].Placement.Base.x * scale
        sheet['D{0}'.format(ind + 2)] = sel[ind].Placement.Base.y * scale
        sheet['E{0}'.format(ind + 2)] = sel[ind].Placement.Base.z * scale
        sheet['F{0}'.format(ind + 2)] = sel[ind].Placement.Rotation.toEuler()[0]
        sheet['G{0}'.format(ind + 2)] = sel[ind].Placement.Rotation.toEuler()[1]
        sheet['H{0}'.format(ind + 2)] = sel[ind].NumberPole.Value

class CommandBOQMechanical:
    def GetResources(self):
        return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "boqm.svg")),
                'Accel': "R, M",
                'MenuText': QT_TRANSLATE_NOOP("Placement", "BOQ Mecánico"),
                'ToolTip': QT_TRANSLATE_NOOP("Placement", "Calcular el BOQ de la")}

    def Activated(self):
        doc = FreeCAD.ActiveDocument

        if not doc or not doc.FileName:
            FreeCAD.Console.PrintError("Document must be saved first\n")
            return

        try:
            sel = [obj for obj in FreeCAD.ActiveDocument.Objects if (hasattr(obj, "Proxy") and (obj.Proxy.Type == "Tracker"))]

            if not sel:
                FreeCAD.Console.PrintWarning("No valid trackers found\n")
                return

            path = os.path.dirname(doc.FileName)
            filename = os.path.join(path, "BOQ_Mechanical.xlsx")

            sel = sorted(sel, key=lambda x: x.Label)

            wb = openpyxl.Workbook()
            wb.remove(wb.active)  # Remove default sheet

            frames_sheet = wb.create_sheet("Frames Information")
            spreadsheetBOQFrames(frames_sheet, sel)

            poles_sheet  = wb.create_sheet("Poles information")
            spreadsheetBOQPoles(poles_sheet , sel)

            wb.save(filename)
            FreeCAD.Console.PrintMessage(f"Report generated: {filename}\n")
            open_file(path)

        except Exception as e:
                FreeCAD.Console.PrintError(f"Error generating BOQ: {str(e)}\n")

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

