/* tween.vala
 *
 * Copyright 2020 James Westman <james@flyingpimonster.net>
 *
 * This program 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.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */


/**
 * Handles tweening for animations.
 *
 * A tween object keeps track of a widget's animation states, each of which is
 * a double. To start an animation, call start() with the name of the
 * animation, the end value, and the duration. All interpolation is linear.
 *
 * #Tween also uses tick callbacks to make sure the widget is redrawn every
 * tick as long as at least one animation is active.
 *
 * If GTK animations are disabled, then no interpolation will be done and
 * animations will be immediately set to their end value.
 *
 * In Vala, an animation's current value can be retrieved using array access
 * syntax, like this: `tween["animation-name"]`. The same syntax can be used
 * to start an animation. In this case, :duration will be used as the
 * duration.
 */
public class Camera.Tween : Object {
    private struct TweenVal {
        bool started;
        int64 start;
        int64 duration;
        double from;
        double to;

        public bool is_active(int64 current_time) {
            return started
                && duration > 0
                && current_time >= start
                && current_time <= start + duration;
        }

        public double get_val(int64 current_time) {
            double percent;
            double diff;

            if (duration <= 0) {
                return to;
            }

            percent = (current_time - start) / (double) duration;
            percent = percent.clamp(0, 1);
            diff = to - from;
            return percent * diff + from;
        }

        public int64 time_left(int64 current_time) {
            return int64.max(start + duration - current_time, 0);
        }
    }


    /**
     * If this is %TRUE, the widget will be resized during the animation
     * rather than redrawn.
     */
    public bool resize { get; set; default=false; }

    public int num_tweens { get; construct; }

    public weak Gtk.Widget widget { get; construct; }


    private uint tick_callback_id;
    private bool callback_active;

    private TweenVal[] tweens;


    /**
     * Creates a new Tween object that manages animations for the widget.
     */
    public Tween(Gtk.Widget widget, int num_tweens) {
        Object(widget: widget, num_tweens: num_tweens);
    }


    construct {
        this.tweens = new TweenVal[num_tweens];
        this.callback_active = false;
    }


    /**
     * Starts an animation.
     *
     * Over @duration milliseconds, the value will be interpolated from its
     * current value to @to. If the animation has never been started, if the
     * duration is 0, or if GTK animations are disabled, then the value will be
     * set immediately.
     */
    public void start(int slot, double to, int64 duration) {
        int64 start_time = this.get_time();
        TweenVal old;

        if (animations_disabled()) {
            this.tweens[slot] = TweenVal() {
                started = true,
                start = start_time,
                duration = 0,
                from = to,
                to = to
            };

            this.tick_action();

            return;
        }

        // milliseconds to microseconds
        duration *= 1000;

        old = this.tweens[slot];
        if (old.started) {
            int64 time_left = old.time_left(start_time);
            double val = old.get_val(start_time);

            this.tweens[slot] = TweenVal() {
                started = true,
                start = start_time,
                duration = duration - time_left,
                from = val,
                to = to
            };
        } else {
            this.tweens[slot] = TweenVal() {
                started = true,
                start = start_time,
                duration = 0,
                from = to,
                to = to
            };
        }

        if (this.is_active() && !this.callback_active) {
            this.tick_callback_id = this.widget.add_tick_callback(on_tick_callback);
            this.callback_active = true;
        }

        this.tick_action();
    }


    /**
     * Gets an animation's current value.
     */
    public double get(int slot) {
        TweenVal? val = this.tweens[slot];
        return val != null ? val.get_val(this.get_time()) : 0;
    }


    /**
     * Starts an animation with #CameraTween:duration as the duration.
     */
    public void set(int slot, double val) {
        this.start(slot, val, 250);
    }


    /**
     * Determines whether any tween is currently active.
     */
    public bool is_active() {
        int64 current_time = this.get_time();

        foreach (TweenVal val in this.tweens) {
            if (val.is_active(current_time)) {
                return true;
            }
        }

        return false;
    }


    private void tick_action() {
        if (this.resize) {
            this.widget.queue_resize();
        } else {
            this.widget.queue_draw();
        }
    }


    private bool on_tick_callback(Gtk.Widget widget, Gdk.FrameClock clock) {
        this.tick_action();

        if (!is_active()) {
            this.callback_active = false;
            return false;
        } else {
            return true;
        }
    }


    private int64 get_time() {
        var clock = this.widget.get_frame_clock();
        return clock != null ? clock.get_frame_time() : 0;
    }


    private static bool animations_disabled() {
        return !Gtk.Settings.get_default().gtk_enable_animations;
    }
}


