// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>
//
// Klipspringer is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Klipspringer 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
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.

package net.avadeaux.klipspringer;

import org.json.*;
import java.util.regex.*;
import java.util.HashMap;

/** Object representation of mix specification given for a track, created from its JSON
  * specification. Times in seconds. */
public class MixSpec {
    /** Crossfade shape function class. */
    public static abstract class Fade {
        public interface Factory {
            Fade get(double[] params);
        }

        /** Crossfade sample value that combines one to fade out with one to fade in, given a point
          * in time between 0 (crossfade starting) and 1 (crossfade ending). Clips if out of
          * range. */
        public abstract int mix(int out, int in, double t);
    }

    private final static double SQRTHALF = Math.sqrt(0.5);

    public final static Fade JUMP_START = new Fade() {
            public int mix(int out, int in, double t) { return in; }
            public String toString() { return "jump_start"; }
        };
    public final static Fade JUMP_END = new Fade() {
            public int mix(int out, int in, double t) { return out; }
            public String toString() { return "jump_end"; }
        };
    public final static Fade LIN_AMP = new Fade() {
            public int mix(int out, int in, double t) {
                return (int) Math.max(Math.min(in*t + out*(1-t), Integer.MAX_VALUE), Integer.MIN_VALUE);
            }
            public String toString() { return "lin_amp"; }
        };
    public final static Fade LIN_POW = new Fade() {
            public int mix(int out, int in, double t) {
                return (int) Math.max(Math.min(in*Math.sqrt(t) + out*Math.sqrt(1-t), Integer.MAX_VALUE), Integer.MIN_VALUE);
            }
            public String toString() { return "lin_pow"; }
        };
    public final static Fade COMBINE = new Fade() {
            public int mix(int out, int in, double t) {
                return (int) Math.max(Math.min(in*SQRTHALF + out*SQRTHALF, Integer.MAX_VALUE), Integer.MIN_VALUE);
            }
            public String toString() { return "combine"; }
        };

    private final static HashMap<String, Fade.Factory> facts = new HashMap<String, Fade.Factory>();
    static {
        facts.put(JUMP_START.toString(), new Fade.Factory() { public Fade get(double[] params) { return JUMP_START; } });
        facts.put(JUMP_END.toString(), new Fade.Factory() { public Fade get(double[] params) { return JUMP_END; } });
        facts.put(LIN_AMP.toString(), new Fade.Factory() { public Fade get(double[] params) { return LIN_AMP; } });
        facts.put(LIN_POW.toString(), new Fade.Factory() { public Fade get(double[] params) { return LIN_POW; } });
        facts.put(COMBINE.toString(), new Fade.Factory() { public Fade get(double[] params) { return COMBINE; } });
        facts.put("exp", new Fade.Factory() {
                public Fade get(double[] params) {
                    final double a = params[0];
                    return new Fade() {
                        public int mix(int out, int in, double t) {
                            double g = 1 - Math.pow(2, -a * t);
                            double f = Math.sqrt(1-g*g);
                            return (int) Math.max(Math.min(out*f + in*g, Integer.MAX_VALUE), Integer.MIN_VALUE);
                        }
                        public String toString() { return a == (long) a ? "exp,"+(long) a : "exp,"+a; }
                    };
                }
            });
    }

    private final static Pattern timeP = Pattern.compile("(?:(?:([0-9]+):)?(?:([0-9]+):))?([0-9]+(?:\\.[0-9]*)?)");

    private static double num(String s) {
        if (s == null || s.length() == 0) { return 0; }
        return Double.parseDouble(s);
    }

    private static double time(JSONObject o, String key, double dflt) {
        if (!o.has(key)) { return dflt; }
        return time(o.optString(key));
    }

    private static double time(String s) {
        Matcher m = timeP.matcher(s);
        if (!m.matches()) { throw new IllegalArgumentException("Invalid time value: "+s); }
        return num(m.group(1))*3600 + num(m.group(2))*60 + num(m.group(3));
    }

    private static MixSpec defaultInstance = new MixSpec(0, 0, Double.NaN, JUMP_START);

    public static MixSpec getInstance() { return defaultInstance; }

    public static MixSpec getInstance(JSONObject json) {
        double skip = time(json, "skip", 0);
        double end = time(json, "end", Double.NaN);
        if (!json.has("fadein")) {
            return new MixSpec(skip, 0, end, JUMP_START);
        } else {
            JSONArray fadeInSpec = json.getJSONArray("fadein");
            double fadeTime = time(fadeInSpec.optString(0));
            String fadeType = fadeInSpec.optString(1);
            double[] params = new double[fadeInSpec.length()-2];
            for (int i = 0; i < params.length; i++) {
                params[i] = fadeInSpec.getDouble(i+2);
            }
            return new MixSpec(skip, fadeTime, end, fade(fadeType, params));
        }
    }

    public static Fade fade(String fadeType, double[] params) {
        Fade.Factory fact = facts.get(fadeType);
        if (fact == null) { throw new IllegalArgumentException("Unrecognized fade type: "+fadeType); }
        return fact.get(params);
    }

    public final double skip, fadeIn, end;
    public final Fade fade;

    public MixSpec(double skip, double fadeIn, double end, Fade fade) {
        this.skip = skip;
        this.fadeIn = fadeIn;
        this.end = end;
        this.fade = fade;
    }
}
