/*
 *  Copyright (c) 2024 - 2025 by Paul Hertz <ignotus@gmail.com>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

package net.paulhertz.pixelaudio.curves;

import java.util.List;
import java.util.function.DoubleUnaryOperator;

import processing.core.PVector;

// TODO evaluate for deletion: used very little, but possibly good for future use.

/**
 *
 * Treats a gesture defined by (time, x, y) samples as a parametric curve.
 * <code>
 *    u ∈ [0,1] → (t(u), x(u), y(u))
 * </code>
 * <p>
 * Under the hood:
 * <ul>
 *    <li>timesMs: int[] of time offsets (ms), ascending, timesMs[0] == 0</li>
 *    <li>points:  PVector list (x,y) of same length</li>
 * </ul>
 * </p><p>
 * You can:
 * <ul>
 *    <i>sample with a linear parameter: f(u) = u</i>
 *    <li>pass a warp function f(u) to speed up/slow down traversal</li>
 * </ul> 
 * </p><p>
 * Exponential warp (speeding up over time)<br>
 * <pre>
 *    DoubleUnaryOperator expWarp = u -> (float_ Math.pow(u, 2.0);   // u^2
 *    GestureParametric.Sample s = gp.sample(0.5f, expWarp);
 * </pre>
 * </p><p>
 * Log-like warp (fast at start, slow at end)<br>
 * <pre>
 *    DoubleUnaryOperator sqrtWarp = u -> (float_ Math.sqrt(u);   // 
 *    GestureParametric.Sample s = gp.sample(0.5f, sqrtWarp);
 * </pre>
 *  Something like f(u) = (e^{k *  u} - 1) / (e^{k} - 1) for more dramatic warps.
 *  
 */
public final class PAGestureParametric {

    /**
     * Data storage class for PAGestureParametric.
     */
    public static final class Sample {
        public final float tMs; // interpolated time in ms
        public final float x;
        public final float y;

        public Sample(float tMs, float x, float y) {
            this.tMs = tMs;
            this.x = x;
            this.y = y;
        }
    }

    private final List<PVector> points;
    private final int[] timesMs;
    private final int count;
    private final float totalDurationMs;

    /**
     * @param points  gesture points (x,y)
     * @param timesMs time offsets (ms), same length as points,
     *                ascending, timesMs[0] == 0
     */
    public PAGestureParametric(List<PVector> points, int[] timesMs) {
        if (points == null || timesMs == null) {
            throw new IllegalArgumentException("points and timesMs must not be null");
        }
        if (points.size() != timesMs.length) {
            throw new IllegalArgumentException("points.size() must equal timesMs.length");
        }
        if (points.isEmpty()) {
            throw new IllegalArgumentException("points must not be empty");
        }

        this.points = points;
        this.timesMs = timesMs;
        this.count = points.size();
        this.totalDurationMs = timesMs[count - 1];
    }

    /** Total gesture duration in ms. */
    public float getTotalDurationMs() {
        return totalDurationMs;
    }

    /**
     * Sample with linear parameterization: f(u) = u.
     * u is clamped to [0,1].
     */
    public Sample sample(float u) {
        return sample(u, null);
    }

    /**
     * Sample with an optional warp function f(u) → [0,1].
     * If warp is null, identity is used.
     */
    public Sample sample(float u, DoubleUnaryOperator warp) {
        if (u < 0f) u = 0f;
        if (u > 1f) u = 1f;

        double s = (warp != null) ? warp.applyAsDouble(u) : u;
        if (s < 0.0) s = 0.0;
        if (s > 1.0) s = 1.0;

        // Target time in ms
        float tTarget = (float) (s * totalDurationMs);

        // Find segment i such that timesMs[i] <= tTarget <= timesMs[i+1]
        int i = findSegment(tTarget);
        if (i >= count - 1) {
            // At or beyond last sample; clamp to last point
            PVector plast = points.get(count - 1);
            return new Sample(totalDurationMs, plast.x, plast.y);
        }

        int t0 = timesMs[i];
        int t1 = timesMs[i + 1];
        PVector p0 = points.get(i);
        PVector p1 = points.get(i + 1);

        float v;
        if (t1 == t0) {
            v = 0f;
        } else {
            v = (tTarget - t0) / (float) (t1 - t0);
            if (v < 0f) v = 0f;
            if (v > 1f) v = 1f;
        }

        float x = lerp(p0.x, p1.x, v);
        float y = lerp(p0.y, p1.y, v);

        return new Sample(tTarget, x, y);
    }

    // ------------------------------------------------------------------------
    // Internals
    // ------------------------------------------------------------------------

    /**
     * Find the largest i such that timesMs[i] <= tTarget.
     * Returns count-1 if tTarget is beyond the last sample.
     */
    private int findSegment(float tTarget) {
        // Simple linear scan is fine for modest sizes.
        // Replace with binary search if gestures get long.
        int i = 0;
        while (i < count - 1 && timesMs[i + 1] <= tTarget) {
            i++;
        }
        return i;
    }

    private static float lerp(float a, float b, float u) {
        return a + (b - a) * u;
    }
}
