"""
Curve management for ChartView.

.. autosummary::
    ~CurveManager
"""

from PyQt5 import QtCore
import logging
import numpy

logger = logging.getLogger(__name__)


class CurveManager(QtCore.QObject):
    """
    Manages curve data, properties, and lifecycle for ChartView.

    This class handles the storage and management of curve data including
    plotting properties and curve identification. It provides signals for
    curve lifecycle events.

    Signals:
        curveAdded: Emitted when a curve is added (curveID)
        curveRemoved: Emitted when a curve is removed (curveID, curveData, count)
        curveUpdated: Emitted when a curve is updated (curveID)
        allCurvesRemoved: Emitted when all curves are removed
    """

    # Signals
    curveAdded = QtCore.pyqtSignal(str)  # Emit curveID when a curve is added
    curveRemoved = QtCore.pyqtSignal(str, dict, int)  # Emit curveID, curveData, count
    curveUpdated = QtCore.pyqtSignal(str)  # Emit curveID
    allCurvesRemoved = QtCore.pyqtSignal()  # No parameters

    def __init__(self, parent=None):
        super().__init__(parent)
        self._curves = {}  # Store curves with curveID as the key

    def curves(self):
        """
        Returns a copy of the currently managed curves.

        Returns:
            dict: Copy of the curves dictionary with curveID as keys
        """
        return dict(self._curves)

    def getCurveData(self, curveID):
        """
        Get curve data by ID.

        Parameters:
            curveID: The unique identifier of the curve

        Returns:
            dict or None: Complete curve data dictionary if found, None otherwise
        """
        return self._curves.get(curveID, None)

    def getCurveXYData(self, curveID):
        """
        Get the X and Y data arrays for a curve.

        Parameters:
            curveID (str): Unique identifier of the curve

        Returns:
            tuple: (x_data, y_data) or (None, None) if not found
        """
        curve_info = self._curves.get(curveID)
        if curve_info:
            data = curve_info.get("data")
            if data:
                return (data[1], data[2])  # (x_data, y_data)
        return (None, None)

    def getCurveLabel(self, curveID):
        """
        Get the display label for a curve.

        Parameters:
            curveID (str): Unique identifier of the curve

        Returns:
            str or None: The curve's label, or None if not found
        """
        curve_info = self._curves.get(curveID)
        return curve_info.get("label") if curve_info else None

    def getCurvePlotObj(self, curveID):
        """
        Get the Matplotlib Line2D object for a curve.

        Parameters:
            curveID (str): Unique identifier of the curve

        Returns:
            matplotlib Line2D object or None: The plot object, or None if not found
        """
        curve_info = self._curves.get(curveID)
        if curve_info:
            data = curve_info.get("data")
            if data and len(data) > 0:
                return data[0]
        return None

    def generateCurveID(self, run_uid, stream_name, y_field):
        """
        Generate a unique curve ID based on run UID, stream name, and field name.

        This ensures curves from the same run but different streams/fields
        have different IDs, even if they share the same scan_id.

        Parameters:
            run_uid (str): The full run UID
            stream_name (str): The stream name (e.g., "primary", "baseline")
            y_field (str): The Y field/detector name (e.g., "det", "I0", etc.)

        Returns:
            str: A unique curve ID in the format: "run_uid_stream_name_y_field"
        """
        curve_id = f"{run_uid}_{stream_name}_{y_field}"
        logger.debug(
            f"generateCurveID - run_uid={run_uid[:7]}, stream_name={stream_name}, y_field={y_field}, curve_id={curve_id}"
        )
        return curve_id

    def addCurve(
        self,
        curveID,
        plot_obj,
        x_data=None,
        y_data=None,
        label=None,
        style_kwargs=None,
        **kwargs,
    ):
        """
        Add a curve to the manager.

        Parameters:
            curveID (str): Unique identifier for the curve (generated by generateCurveID)
            plot_obj: Matplotlib Line2D object for this curve
            x_data (array, optional): X-axis data array
            y_data (array, optional): Y-axis data array (required)
            label (str, optional): Display label for the curve
            style_kwargs (dict, optional): Style properties (color, marker, etc.)
            **kwargs: Additional properties (run_uid, y_field, stream_name, etc.)

        Returns:
            None: Emits curveAdded signal if curve was added
        """
        if y_data is None:
            raise ValueError("y_data must be provided")

        # If x_data is None, generate it from y_data (single-argument plot)
        if x_data is None:
            x_data = numpy.arange(len(y_data))

        curve_data = (plot_obj, x_data, y_data)

        # Store additional metadata
        curve_info = {
            "data": curve_data,
            "original_y_data": numpy.array(y_data).copy(),
            "factor": 1.0,
            "offset": 0.0,
            "derivative": False,
            "label": label,
            "style_kwargs": style_kwargs or {},
            **kwargs,  # run_uid, y_field, stream_name, offset, factor, etc.
        }

        # Add new curve to self._curves dict
        self._curves[curveID] = curve_info
        logger.debug(
            f"Added curve {curveID} to manager: label={label}, has_x={x_data is not None}"
        )
        # Emit signal
        self.curveAdded.emit(curveID)

    def updateCurve(
        self,
        curveID,
        plot_obj=None,
        x_data=None,
        y_data=None,
        label=None,
        style_kwargs=None,
        update_original_data=False,
        **kwargs,
    ):
        """
        Update data for an existing curve.

        Parameters:
            curveID (str): Unique identifier of the curve to update
            plot_obj: New Matplotlib Line2D object (optional, if line needs to be recreated)
            x_data (array, optional): New X-axis data array
            y_data (array, optional): New Y-axis data array. **IMPORTANT**: When
                ``update_original_data=True``, ``y_data`` MUST be raw/untransformed data.
                If transformed data is passed, it will incorrectly overwrite the original
                baseline data.
            label (str, optional): New display label for the curve
            style_kwargs (dict, optional): New style properties (color, marker, etc.)
            update_original_data (bool, optional): If True, update ``original_y_data`` with
                the provided ``y_data`` and reapply any active transformations. This is
                used during live updates to keep ``original_y_data`` synchronized with
                the latest raw data. Requires that ``y_data`` is raw/untransformed.
            **kwargs: Additional properties to update (run_uid, y_field, stream_name, etc.)

        Returns:
            bool: True if curve was found and updated, False otherwise
        """
        if curveID not in self._curves:
            logger.debug(f"Curve {curveID} not found in manager for update")
            return False

        curve_info = self._curves[curveID]

        # Get current data
        current_data = curve_info["data"]
        current_plot_obj = current_data[0]

        # Use provided plot_obj or keep existing
        new_plot_obj = plot_obj if plot_obj is not None else current_plot_obj

        # Get current x_data and y_data
        current_x = current_data[1]
        current_y = current_data[2]

        # Update with provided data or keep existing
        new_x_data = x_data if x_data is not None else current_x
        new_y_data = y_data if y_data is not None else current_y

        # If this is a live update, update original_y_data and reapply transformations
        if update_original_data and y_data is not None:
            # Update the original y_data with the new raw data
            # CRITICAL: y_data MUST be raw/untransformed data here. If transformed data
            # is passed, it will corrupt original_y_data and break future transformations.
            curve_info["original_y_data"] = numpy.array(y_data).copy()

            # Check if any transformations are active
            offset = curve_info.get("offset", 0.0)
            factor = curve_info.get("factor", 1.0)
            derivative = curve_info.get("derivative", False)

            # If transformations are active, reapply them to the new original_y_data
            if offset != 0.0 or factor != 1.0 or derivative:
                transformed_y = self._applyTransformations(
                    curveID, original_y=curve_info["original_y_data"]
                )
                # Update the plot object with transformed data
                if new_plot_obj is not None:
                    new_plot_obj.set_data(new_x_data, transformed_y)
                # Update the data tuple with transformed data
                curve_info["data"] = (new_plot_obj, new_x_data, transformed_y)
            else:
                # No transformations active, update plot object with raw data
                if new_plot_obj is not None:
                    new_plot_obj.set_data(new_x_data, new_y_data)
                # Update data tuple here for consistency
                curve_info["data"] = (new_plot_obj, new_x_data, new_y_data)
        else:
            # Not a live update, just update the data tuple
            curve_info["data"] = (new_plot_obj, new_x_data, new_y_data)

        # Update label and style_kwargs if provided
        if label is not None:
            curve_info["label"] = label
        if style_kwargs is not None:
            curve_info["style_kwargs"] = style_kwargs

        # Update other properties from kwargs
        for key, value in kwargs.items():
            curve_info[key] = value

        # Emit signal
        self.curveUpdated.emit(curveID)
        logger.debug(f"Updated curve {curveID}")
        return True

    def updateCurveOffsetFactor(self, curveID, offset=None, factor=None):
        """Update offset and/or factor for a curve and apply transformation."""
        return self.updateCurveTransformations(curveID, offset=offset, factor=factor)

    def updateCurveDerivative(self, curveID, derivative=None):
        """Update the derivative for a curve and apply transformation."""
        return self.updateCurveTransformations(curveID, derivative=derivative)

    def updateCurveTransformations(
        self, curveID, offset=None, factor=None, derivative=None
    ):
        """
        Update transformation parameters for a curve and apply transformation.

        Parameters:
            curveID (str): Unique identifier of the curve
            offset (float, optional): New offset value (None to keep current)
            factor (float, optional): New factor value (None to keep current)
            derivative (bool, optional): New derivative value (None to keep current)

        Returns:
            bool: True if curve was found and updated, False otherwise
        """
        if curveID not in self._curves:
            logger.debug(
                f"Curve {curveID} not found in manager for transformation update"
            )
            return False

        curve_info = self._curves[curveID]

        # Get original y_data
        original_y = curve_info.get("original_y_data")
        if original_y is None:
            logger.warning(
                f"Curve {curveID} has no original_y_data, cannot apply transformation"
            )
            return False

        # Update parameters (keep current if None)
        if offset is not None:
            curve_info["offset"] = offset
        if factor is not None:
            curve_info["factor"] = factor
        if derivative is not None:
            curve_info["derivative"] = derivative

        # Apply transformations using helper
        transformed_y = self._applyTransformations(curveID, original_y)

        # Get the plot object and x_data
        plot_obj = curve_info["data"][0]
        x_data = curve_info["data"][1]

        # Update plot object data
        plot_obj.set_data(x_data, transformed_y)

        # Update stored data
        curve_info["data"] = (plot_obj, x_data, transformed_y)

        logger.debug(
            f"Updated curve {curveID}: offset={curve_info.get('offset', 0.0)}, "
            f"factor={curve_info.get('factor', 1.0)}, derivative={curve_info.get('derivative', False)}"
        )

        # Emit signal
        self.curveUpdated.emit(curveID)
        return True

    def _applyTransformations(self, curveID, original_y):
        """
        Apply current transformations to original_y data.

        Parameters:
            curveID (str): Curve identifier
            original_y (array): Raw y data to transform

        Returns:
            array: Transformed y data
        """
        curve_info = self._curves.get(curveID)
        if not curve_info:
            return original_y

        # Get current transformation parameters
        offset = curve_info.get("offset", 0.0)
        factor = curve_info.get("factor", 1.0)
        derivative = curve_info.get("derivative", False)

        # Convert to numpy array
        original_y_array = numpy.array(original_y)

        # Apply transformation
        if derivative:
            # Apply derivative transformation
            transformed_y = offset + (factor * numpy.gradient(original_y_array))
        else:
            # Apply simple offset + factor transformation
            transformed_y = offset + (factor * original_y_array)

        return transformed_y

    def removeCurve(self, curveID):
        """
        Remove a curve from the manager.

        Parameters:
            curveID (str): Unique identifier of the curve to remove

        Returns:
            bool: True if curve was found and removed, False otherwise
        """
        if curveID not in self._curves:
            logger.debug(f"Curve {curveID} not found in manager")
            return False

        curve_data = self._curves.pop(curveID)
        count = len(self._curves)

        logger.debug(f"Removed curve {curveID} from manager, {count} curves remaining")

        # Emit signal
        self.curveRemoved.emit(curveID, curve_data, count)
        return True

    def removeAllCurves(self):
        """
        Remove all curves from the manager.

        Returns:
            None: Emits allCurvesRemoved signal
        """
        count = len(self._curves)
        self._curves.clear()

        logger.debug(f"Removed all {count} curves from manager")

        # Emit signal
        self.allCurvesRemoved.emit()
