# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *   Copyright (c) 2025 Samuel Abels <knipknap@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 uuid
import json
import FreeCAD
from FreeCAD import Base
from typing import Optional, Union, Mapping, List
from ...assets import Asset, AssetUri, AssetSerializer


class Machine(Asset):
    """Represents a machine with various operational parameters."""

    asset_type: str = "machine"
    API_VERSION = 1

    UNIT_CONVERSIONS = {
        "hp": 745.7,  # hp to W
        "in-lbf": 0.112985,  # in-lbf to N*m
        "inch/min": 25.4,  # inch/min to mm/min
        "rpm": 1.0 / 60.0,  # rpm to 1/s
        "kW": 1000.0,  # kW to W
        "Nm": 1.0,  # Nm to N*m
        "mm/min": 1.0,  # mm/min to mm/min
    }

    def __init__(
        self,
        label: str = "Machine",
        max_power: Union[int, float, FreeCAD.Units.Quantity] = 2,
        min_rpm: Union[int, float, FreeCAD.Units.Quantity] = 3000,
        max_rpm: Union[int, float, FreeCAD.Units.Quantity] = 60000,
        max_torque: Optional[Union[int, float, FreeCAD.Units.Quantity]] = None,
        peak_torque_rpm: Optional[Union[int, float, FreeCAD.Units.Quantity]] = None,
        min_feed: Union[int, float, FreeCAD.Units.Quantity] = 1,
        max_feed: Union[int, float, FreeCAD.Units.Quantity] = 2000,
        id: Optional[str] = None,
    ) -> None:
        """
        Initializes a Machine object.

        Args:
            label: The label of the machine.
            max_power: The maximum power of the machine (kW or Quantity).
            min_rpm: The minimum RPM of the machine (RPM or Quantity).
            max_rpm: The maximum RPM of the machine (RPM or Quantity).
            max_torque: The maximum torque of the machine (Nm or Quantity).
            peak_torque_rpm: The RPM at which peak torque is achieved
                             (RPM or Quantity).
            min_feed: The minimum feed rate of the machine
                      (mm/min or Quantity).
            max_feed: The maximum feed rate of the machine
                      (mm/min or Quantity).
            id: The unique identifier of the machine.
        """
        self.id = id or str(uuid.uuid1())
        self._label = label

        # Initialize max_power (W)
        if isinstance(max_power, FreeCAD.Units.Quantity):
            self._max_power = max_power.getValueAs("W").Value
        elif isinstance(max_power, (int, float)):
            self._max_power = max_power * self.UNIT_CONVERSIONS["kW"]
        else:
            self._max_power = 2000.0

        # Initialize min_rpm (1/s)
        if isinstance(min_rpm, FreeCAD.Units.Quantity):
            try:
                self._min_rpm = min_rpm.getValueAs("1/s").Value
            except (Base.ParserError, ValueError):
                self._min_rpm = min_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
        elif isinstance(min_rpm, (int, float)):
            self._min_rpm = min_rpm * self.UNIT_CONVERSIONS["rpm"]
        else:
            self._min_rpm = 3000 * self.UNIT_CONVERSIONS["rpm"]

        # Initialize max_rpm (1/s)
        if isinstance(max_rpm, FreeCAD.Units.Quantity):
            try:
                self._max_rpm = max_rpm.getValueAs("1/s").Value
            except (Base.ParserError, ValueError):
                self._max_rpm = max_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
        elif isinstance(max_rpm, (int, float)):
            self._max_rpm = max_rpm * self.UNIT_CONVERSIONS["rpm"]
        else:
            self._max_rpm = 60000 * self.UNIT_CONVERSIONS["rpm"]

        # Initialize min_feed (mm/min)
        if isinstance(min_feed, FreeCAD.Units.Quantity):
            self._min_feed = min_feed.getValueAs("mm/min").Value
        elif isinstance(min_feed, (int, float)):
            self._min_feed = min_feed
        else:
            self._min_feed = 1.0

        # Initialize max_feed (mm/min)
        if isinstance(max_feed, FreeCAD.Units.Quantity):
            self._max_feed = max_feed.getValueAs("mm/min").Value
        elif isinstance(max_feed, (int, float)):
            self._max_feed = max_feed
        else:
            self._max_feed = 2000.0

        # Initialize peak_torque_rpm (1/s)
        if isinstance(peak_torque_rpm, FreeCAD.Units.Quantity):
            try:
                self._peak_torque_rpm = peak_torque_rpm.getValueAs("1/s").Value
            except (Base.ParserError, ValueError):
                self._peak_torque_rpm = peak_torque_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
        elif isinstance(peak_torque_rpm, (int, float)):
            self._peak_torque_rpm = peak_torque_rpm * self.UNIT_CONVERSIONS["rpm"]
        else:
            self._peak_torque_rpm = self._max_rpm / 3

        # Initialize max_torque (N*m)
        if isinstance(max_torque, FreeCAD.Units.Quantity):
            self._max_torque = max_torque.getValueAs("Nm").Value
        elif isinstance(max_torque, (int, float)):
            self._max_torque = max_torque
        else:
            # Convert 1/s to rpm
            peak_rpm_for_calc = self._peak_torque_rpm * 60
            self._max_torque = (
                self._max_power * 9.5488 / peak_rpm_for_calc if peak_rpm_for_calc else float("inf")
            )

    def get_id(self) -> str:
        """Returns the unique identifier for the Machine instance."""
        return self.id

    def to_dict(self) -> dict:
        """Returns a dictionary representation of the Machine."""
        return {
            "version": self.API_VERSION,
            "id": self.id,
            "label": self.label,
            "max_power": self._max_power,  # W
            "min_rpm": self._min_rpm,  # 1/s
            "max_rpm": self._max_rpm,  # 1/s
            "max_torque": self._max_torque,  # Nm
            "peak_torque_rpm": self._peak_torque_rpm,  # 1/s
            "min_feed": self._min_feed,  # mm/min
            "max_feed": self._max_feed,  # mm/min
        }

    def to_bytes(self, serializer: AssetSerializer) -> bytes:
        """Serializes the Machine object to bytes using to_dict."""
        data_dict = self.to_dict()
        json_str = json.dumps(data_dict)
        return json_str.encode("utf-8")

    @classmethod
    def from_dict(cls, data_dict: dict, id: str) -> "Machine":
        """Creates a Machine instance from a dictionary."""
        machine = cls(
            label=data_dict.get("label", "Machine"),
            max_power=data_dict.get("max_power", 2000.0),  # W
            min_rpm=data_dict.get("min_rpm", 3000 * cls.UNIT_CONVERSIONS["rpm"]),  # 1/s
            max_rpm=data_dict.get("max_rpm", 60000 * cls.UNIT_CONVERSIONS["rpm"]),  # 1/s
            max_torque=data_dict.get("max_torque", None),  # Nm
            peak_torque_rpm=data_dict.get("peak_torque_rpm", None),  # 1/s
            min_feed=data_dict.get("min_feed", 1.0),  # mm/min
            max_feed=data_dict.get("max_feed", 2000.0),  # mm/min
            id=id,
        )
        return machine

    @classmethod
    def from_bytes(
        cls,
        data: bytes,
        id: str,
        dependencies: Optional[Mapping[AssetUri, Asset]],
    ) -> "Machine":
        """
        Deserializes bytes into a Machine instance using from_dict.
        """
        # If dependencies is None, it's fine as Machine doesn't use it.
        data_dict = json.loads(data.decode("utf-8"))
        return cls.from_dict(data_dict, id)

    @classmethod
    def dependencies(cls, data: bytes) -> List[AssetUri]:
        """Returns a list of AssetUri dependencies parsed from the serialized data."""
        return []  # Machine has no dependencies

    @property
    def max_power(self) -> FreeCAD.Units.Quantity:
        return FreeCAD.Units.Quantity(self._max_power, "W")

    @property
    def min_rpm(self) -> FreeCAD.Units.Quantity:
        return FreeCAD.Units.Quantity(self._min_rpm, "1/s")

    @property
    def max_rpm(self) -> FreeCAD.Units.Quantity:
        return FreeCAD.Units.Quantity(self._max_rpm, "1/s")

    @property
    def max_torque(self) -> FreeCAD.Units.Quantity:
        return FreeCAD.Units.Quantity(self._max_torque, "Nm")

    @property
    def peak_torque_rpm(self) -> FreeCAD.Units.Quantity:
        return FreeCAD.Units.Quantity(self._peak_torque_rpm, "1/s")

    @property
    def min_feed(self) -> FreeCAD.Units.Quantity:
        return FreeCAD.Units.Quantity(self._min_feed, "mm/min")

    @property
    def max_feed(self) -> FreeCAD.Units.Quantity:
        return FreeCAD.Units.Quantity(self._max_feed, "mm/min")

    @property
    def label(self) -> str:
        return self._label

    @label.setter
    def label(self, label: str) -> None:
        self._label = label

    def get_min_rpm_value(self) -> float:
        """Helper method to get minimum RPM value for display/testing."""
        return self._min_rpm * 60

    def get_max_rpm_value(self) -> float:
        """Helper method to get maximum RPM value for display/testing."""
        return self._max_rpm * 60

    def get_peak_torque_rpm_value(self) -> float:
        """Helper method to get peak torque RPM value for display/testing."""
        return self._peak_torque_rpm * 60

    def validate(self) -> None:
        """Validates the machine parameters."""
        if not self.label:
            raise AttributeError("Machine name is required")
        if self._peak_torque_rpm > self._max_rpm:
            err = ("Peak Torque RPM {ptrpm:.2f} must be less than max RPM " "{max_rpm:.2f}").format(
                ptrpm=self._peak_torque_rpm * 60, max_rpm=self._max_rpm * 60
            )
            raise AttributeError(err)
        if self._max_rpm <= self._min_rpm:
            raise AttributeError("Max RPM must be larger than min RPM")
        if self._max_feed <= self._min_feed:
            raise AttributeError("Max feed must be larger than min feed")

    def get_torque_at_rpm(self, rpm: Union[int, float, FreeCAD.Units.Quantity]) -> float:
        """
        Calculates the torque at a given RPM.

        Args:
            rpm: The RPM value (int, float, or Quantity).

        Returns:
            The torque at the given RPM in Nm.
        """
        if isinstance(rpm, FreeCAD.Units.Quantity):
            try:
                rpm_hz = rpm.getValueAs("1/s").Value
            except (Base.ParserError, ValueError):
                rpm_hz = rpm.Value * self.UNIT_CONVERSIONS["rpm"]
        else:
            rpm_hz = rpm * self.UNIT_CONVERSIONS["rpm"]
        max_torque_nm = self._max_torque
        peak_torque_rpm_hz = self._peak_torque_rpm
        peak_rpm_for_calc = peak_torque_rpm_hz * 60
        rpm_for_calc = rpm_hz * 60
        torque_at_current_rpm = (
            self._max_power * 9.5488 / rpm_for_calc if rpm_for_calc else float("inf")
        )
        if rpm_for_calc <= peak_rpm_for_calc:
            torque_at_current_rpm = (
                max_torque_nm / peak_rpm_for_calc * rpm_for_calc
                if peak_rpm_for_calc
                else float("inf")
            )
        return min(max_torque_nm, torque_at_current_rpm)

    def set_max_power(self, power: Union[int, float], unit: Optional[str] = None) -> None:
        """Sets the maximum power of the machine."""
        unit = unit or "kW"
        if unit in self.UNIT_CONVERSIONS:
            power_value = power * self.UNIT_CONVERSIONS[unit]
        else:
            power_value = FreeCAD.Units.Quantity(power, unit).getValueAs("W").Value
        self._max_power = power_value
        if self._max_power <= 0:
            raise AttributeError("Max power must be positive")

    def set_min_rpm(self, min_rpm: Union[int, float, FreeCAD.Units.Quantity]) -> None:
        """Sets the minimum RPM of the machine."""
        if isinstance(min_rpm, FreeCAD.Units.Quantity):
            try:
                min_rpm_value = min_rpm.getValueAs("1/s").Value
            except (Base.ParserError, ValueError):
                min_rpm_value = min_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
        else:
            min_rpm_value = min_rpm * self.UNIT_CONVERSIONS["rpm"]
        self._min_rpm = min_rpm_value
        if self._min_rpm < 0:
            raise AttributeError("Min RPM cannot be negative")
        if self._min_rpm >= self._max_rpm:
            self._max_rpm = min_rpm_value + 1.0 / 60.0

    def set_max_rpm(self, max_rpm: Union[int, float, FreeCAD.Units.Quantity]) -> None:
        """Sets the maximum RPM of the machine."""
        if isinstance(max_rpm, FreeCAD.Units.Quantity):
            try:
                max_rpm_value = max_rpm.getValueAs("1/s").Value
            except (Base.ParserError, ValueError):
                max_rpm_value = max_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
        else:
            max_rpm_value = max_rpm * self.UNIT_CONVERSIONS["rpm"]
        self._max_rpm = max_rpm_value
        if self._max_rpm <= 0:
            raise AttributeError("Max RPM must be positive")
        if self._max_rpm <= self._min_rpm:
            self._min_rpm = max(0, max_rpm_value - 1.0 / 60.0)

    def set_min_feed(
        self,
        min_feed: Union[int, float, FreeCAD.Units.Quantity],
        unit: Optional[str] = None,
    ) -> None:
        """Sets the minimum feed rate of the machine."""
        unit = unit or "mm/min"
        if unit in self.UNIT_CONVERSIONS:
            min_feed_value = min_feed * self.UNIT_CONVERSIONS[unit]
        else:
            min_feed_value = FreeCAD.Units.Quantity(min_feed, unit).getValueAs("mm/min").Value
        self._min_feed = min_feed_value
        if self._min_feed < 0:
            raise AttributeError("Min feed cannot be negative")
        if self._min_feed >= self._max_feed:
            self._max_feed = min_feed_value + 1.0

    def set_max_feed(
        self,
        max_feed: Union[int, float, FreeCAD.Units.Quantity],
        unit: Optional[str] = None,
    ) -> None:
        """Sets the maximum feed rate of the machine."""
        unit = unit or "mm/min"
        if unit in self.UNIT_CONVERSIONS:
            max_feed_value = max_feed * self.UNIT_CONVERSIONS[unit]
        else:
            max_feed_value = FreeCAD.Units.Quantity(max_feed, unit).getValueAs("mm/min").Value
        self._max_feed = max_feed_value
        if self._max_feed <= 0:
            raise AttributeError("Max feed must be positive")
        if self._max_feed <= self._min_feed:
            self._min_feed = max(0, max_feed_value - 1.0)

    def set_peak_torque_rpm(
        self, peak_torque_rpm: Union[int, float, FreeCAD.Units.Quantity]
    ) -> None:
        """Sets the peak torque RPM of the machine."""
        if isinstance(peak_torque_rpm, FreeCAD.Units.Quantity):
            try:
                peak_torque_rpm_value = peak_torque_rpm.getValueAs("1/s").Value
            except (Base.ParserError, ValueError):
                peak_torque_rpm_value = peak_torque_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
        else:
            peak_torque_rpm_value = peak_torque_rpm * self.UNIT_CONVERSIONS["rpm"]
        self._peak_torque_rpm = peak_torque_rpm_value
        if self._peak_torque_rpm < 0:
            raise AttributeError("Peak torque RPM cannot be negative")

    def set_max_torque(
        self,
        max_torque: Union[int, float, FreeCAD.Units.Quantity],
        unit: Optional[str] = None,
    ) -> None:
        """Sets the maximum torque of the machine."""
        unit = unit or "Nm"
        if unit in self.UNIT_CONVERSIONS:
            max_torque_value = max_torque * self.UNIT_CONVERSIONS[unit]
        else:
            max_torque_value = FreeCAD.Units.Quantity(max_torque, unit).getValueAs("Nm").Value
        self._max_torque = max_torque_value
        if self._max_torque <= 0:
            raise AttributeError("Max torque must be positive")

    def dump(self, do_print: bool = True) -> Optional[str]:
        """
        Dumps machine information to console or returns it as a string.

        Args:
            do_print: If True, prints the information to the console.
                      If False, returns the information as a string.

        Returns:
            A formatted string containing machine information if do_print is
            False, otherwise None.
        """
        min_rpm_value = self._min_rpm * 60
        max_rpm_value = self._max_rpm * 60
        peak_torque_rpm_value = self._peak_torque_rpm * 60

        output = ""
        output += f"Machine {self.label}:\n"
        output += f"  Max power: {self._max_power:.2f} W\n"
        output += f"  RPM: {min_rpm_value:.2f} RPM - {max_rpm_value:.2f} RPM\n"
        output += f"  Feed: {self.min_feed.UserString} - " f"{self.max_feed.UserString}\n"
        output += (
            f"  Peak torque: {self._max_torque:.2f} Nm at " f"{peak_torque_rpm_value:.2f} RPM\n"
        )
        output += f"  Max_torque: {self._max_torque} Nm\n"

        if do_print:
            print(output)
        return output
