/*
 * Copyright 2015 Canonical Ltd.
 * Copyright 2021 Robert Tari
 *
 * 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; version 3.
 *
 * 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/>.
 *
 * Authors:
 *      Charles Kerr <charles.kerr@canonical.com>
 *      Robert Tari <robert@tari.in>
 */

using PulseAudio;

public abstract class VolumeWarning : Object
{
    // true if headphones are in use
    public bool headphones_active { get; set; default = false; }

    // true if the warning dialog is being shown
    public bool active { get; protected set; default = false; }

    // true if we're playing unapproved loud multimedia over headphones
    public bool high_volume { get; protected set; default = false; }

    public enum Key {
        VOLUME_UP,
        VOLUME_DOWN
    }

    public void user_keypress (Key key) {
        if ((key == Key.VOLUME_DOWN) && active) {
            _notification.close ();
            on_user_response (IndicatorSound.WarnNotification.Response.CANCEL);
        }
    }

    protected VolumeWarning (IndicatorSound.Options options) {

        _options = options;

        init_high_volume ();
        init_approved ();

        _notification.user_responded.connect ((n, r) => on_user_response (r));
    }

    ~VolumeWarning () {
        clear_timer (ref _approved_timer);
    }

    /***
    ****
    ***/

    // true if the user has approved high volumes recently
    protected bool approved { get; set; default = false; }

    // true if multimedia is currently playing
    protected bool multimedia_active { get; set; default = false; }

    /* Cached value of the multimedia volume reported by pulse.
       Setting this only updates the cache -- to change the volume,
       use sound_system_set_multimedia_volume.
       NB: This PulseAudio.Volume is typed as uint to unconfuse valac. */
    protected uint multimedia_volume { get; set; default = PulseAudio.Volume.INVALID; }

    protected abstract void sound_system_set_multimedia_volume (PulseAudio.Volume volume);

    protected void clear_timer (ref uint timer) {
        if (timer != 0) {
            Source.remove (timer);
            timer = 0;
        }
    }

    private IndicatorSound.Options _options;

    /**
    *** HIGH VOLUME PROPERTY
    **/

    private void init_high_volume () {
        const string self_keys[] = {
            "multimedia-volume",
            "multimedia-active",
            "headphones-active",
            "high-volume-approved"
        };
        foreach (var key in self_keys)
            this.notify[key].connect (() => update_high_volume ());

        const string options_keys[] = {
            "loud-volume",
            "loud-warning-enabled"
        };
        foreach (var key in options_keys)
            _options.notify[key].connect (() => update_high_volume ());

        update_high_volume ();
    }

    private void update_high_volume () {

        var newval = _options.loud_warning_enabled
            && headphones_active
            && multimedia_active
            && !approved
            && (multimedia_volume != PulseAudio.Volume.INVALID)
            // from the sound specs:
            // "Whenever you increase volume,..., such that acoustic output would be MORE than 85 dB
            && (multimedia_volume > _options.loud_volume);

        if (high_volume != newval) {
            debug ("changing high_volume from %d to %d", (int)high_volume, (int)newval);
            if (newval && !active)
                activate ();
            high_volume = newval;
        }
    }

    /**
    *** HIGH VOLUME APPROVED PROPERTY
    **/

    private Settings _settings = new Settings ("org.ayatana.indicator.sound");
    private const string TTL_KEY = "warning-volume-confirmation-ttl";
    private uint _approved_timer = 0;
    private int64 _approved_at = 0;
    private int64 _approved_ttl_usec = 0;

    private void approve_high_volume () {
        _approved_at = GLib.get_monotonic_time ();
        update_approved ();
        update_approved_timer ();
    }

    private void init_approved () {
        _settings.changed[TTL_KEY].connect (() => update_approved_cache ());
        update_approved_cache ();
    }
    private void update_approved_cache () {
        _approved_ttl_usec = _settings.get_int (TTL_KEY);
        _approved_ttl_usec *= 1000000;

        update_approved ();
        update_approved_timer ();
    }
    private void update_approved_timer () {

        clear_timer (ref _approved_timer);

        if (_approved_at == 0)
            return;

        int64 expires_at = _approved_at + _approved_ttl_usec;
        int64 now = GLib.get_monotonic_time ();
        if (expires_at > now) {
            var seconds_left = 1 + ((expires_at - now) / 1000000);
            _approved_timer = Timeout.add_seconds ((uint)seconds_left, () => {
                _approved_timer = 0;
                update_approved ();
                return Source.REMOVE;
            });
        }
    }
    private void update_approved () {
        var new_approved = calculate_approved ();
        if (approved != new_approved) {
            debug ("changing approved from %d to %d", (int)approved, (int)new_approved);
            approved = new_approved;
        }
    }
    private bool calculate_approved () {
        int64 now = GLib.get_monotonic_time ();
        return (_approved_at != 0)
            && (_approved_at + _approved_ttl_usec >= now);
    }

    // NOTIFICATION

    private IndicatorSound.WarnNotification _notification = new IndicatorSound.WarnNotification ();
    private PulseAudio.Volume _ok_volume = PulseAudio.Volume.INVALID;

    protected virtual void preshow () {}

    private void activate () {
        preshow ();
        _ok_volume = multimedia_volume;

        _notification.show ();
        this.active = true;

        // lower the volume to just the warning level
        // from the sound specs:
        // "Whenever you increase volume,..., such that acoustic output would be MORE than 85 dB
        sound_system_set_multimedia_volume (_options.loud_volume);
    }

    private void on_user_response (IndicatorSound.WarnNotification.Response response) {

        this.active = false;

        if (response == IndicatorSound.WarnNotification.Response.OK) {
            approve_high_volume ();
            sound_system_set_multimedia_volume (_ok_volume);
        } else {
            this.cancel_pressed (VolumeWarning.volume_to_double(_options.loud_volume));
        }

        _ok_volume = PulseAudio.Volume.INVALID;
    }

    private static double volume_to_double (PulseAudio.Volume vol)
    {
        double tmp = (double)(vol - PulseAudio.Volume.MUTED);
        return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED);
    }

    public signal void cancel_pressed (double cancel_volume);
}
