# Reaction Rate Field Plot: Reaction System Definition
# Written for CHBE444: Design I, University of Maryland
# (C) Ganesh Sriram, 2025
# Originally written in MATLAB in 2017

# Reaction Rate Field Plot: Reaction System Definition
# Written for CHBE444: Design I, University of Maryland
# (C) Ganesh Sriram, 2025
# Originally written in MATLAB in 2017

import numpy as np
import scipy as sci
import pickle


def duplicate_figure(fig):
    pkl = pickle.dumps(fig)  # serialize figure
    fignew = pickle.loads(pkl)  # deserialize into fignew
    axnew = fignew.axes[0]
    return fignew, axnew


def common_tangent(ax, reactor1, reactor2, tol=1e-2, **kwargs):
    """
    Plots a common tangent to the specified reactors, if one exists.

    Example usage:
        common_tangent(ax, reactor1, reactor2, tol=1e-2)

    Arguments:
        ax: axes of the figure on which the AR is to be plotted
        reactor1, reactor2: reactors, should already be simulated
        tol: tolerance, defaults to 1e-2

    """
    x1 = reactor1.x
    y1 = reactor1.y
    x2 = reactor2.x
    y2 = reactor2.y
    m1 = reactor1.slope_y / reactor1.slope_x
    m2 = reactor2.slope_y / reactor2.slope_x

    X1 = x1[:, None]
    Y1 = y1[:, None]
    M1 = m1[:, None]

    X2 = x2[None, :]
    Y2 = y2[None, :]
    M2 = m2[None, :]

    dx = X2 - X1
    dy = Y2 - Y1

    # Avoid division by zero
    invalid = np.isclose(dx, 0.0)
    slope_line = np.empty_like(dx)
    slope_line[~invalid] = dy[~invalid] / dx[~invalid]
    slope_line[invalid] = np.nan

    # Mismatch between line slope and each curve's local slope
    err1 = M1 - slope_line  # curve 1 slope vs line slope
    err2 = M2 - slope_line  # curve 2 slope vs line slope

    # Total squared error
    err = err1**2 + err2**2
    err[invalid] = np.inf

    # Best indices
    flat_idx = np.argmin(err)
    i_best, j_best = np.unravel_index(flat_idx, err.shape)

    if tol < np.inf and err[i_best, j_best] > tol:
        print("No common tangent found within tolerance")
        return
    else:
        # Slope of the best line
        m_best = slope_line[i_best, j_best]

        ax.plot([x1[i_best], x2[j_best]],
                [y1[i_best], y2[j_best]], 'k-')
        return ax, m_best


def plot_convex_hull(ax, boundaries,
                     alpha=0.2, **kwargs):
    """ Plots the convex hull or a given set of boundaries, which could
    be reactors or line objects. Any reactors passed to this function must
    already be simulated.

    Example usage:
        plot_convex_hull(ax, boundaries, alpha=0.2, **kwargs)

    Arguments:
        ax: axes of the figure on which the AR is to be plotted
        boundaries: list containing reactors and line objects
    """

    pts_list = []
    for boundary in boundaries:
        if isinstance(boundary, Reactor):
            xy = boundary.xy
        else:
            xy = np.column_stack((boundary.get_xdata(), boundary.get_ydata()))
        pts_list.append(xy)
    pts = np.vstack(pts_list)
    pts = pts[~np.isnan(pts).any(axis=1)]  # remove NaNs and duplicate points
    pts = np.unique(pts, axis=0)

    hull = sci.spatial.ConvexHull(pts)
    hull_xy = pts[hull.vertices]
    ax.plot(hull_xy[:, 0], hull_xy[:, 1], 'k-')  # hull edges
    ax.fill(hull_xy[:, 0], hull_xy[:, 1], color='#00ff00', alpha=alpha)
    return ax, hull
