/*
 * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
 *
 * Copyright 2015 Canonical Ltd.
 *
 * 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>
 */

using PulseAudio;

/**
 * A VolumeWarning that uses PulseAudio to
 * (a) implement sound_system_set_multimedia_volume() and
 * (b) keep the multimedia_active and multimedia_volume properties up-to-date
 */
public class VolumeWarningPulse : VolumeWarning
{
	public VolumeWarningPulse (IndicatorSound.Options options,
	                           PulseAudio.GLibMainLoop pgloop) {
		base(options);

		_pgloop = pgloop;
		pulse_reconnect();
	}

	~VolumeWarningPulse () {
		clear_timer(ref _pulse_reconnect_timer);
		clear_timer(ref _update_sink_timer);
		clear_timer(ref _update_sink_inputs_timer);
		pulse_disconnect();
	}

        protected override void preshow () {
		/* showing the dialog can change the sink input index (bug #1484589)
		 * so cache it here for later use in sound_system_set_multimedia_volume() */
                _target_sink_index = _multimedia_sink_index;
        }

	protected override void sound_system_set_multimedia_volume (PulseAudio.Volume volume) {
		var index = _target_sink_index;

		GLib.return_if_fail (_pulse_context != null);
		GLib.return_if_fail (index != PulseAudio.INVALID_INDEX);
		GLib.return_if_fail (volume != PulseAudio.Volume.INVALID);

		unowned CVolume cvol = CVolume ();
		cvol.set (1, volume);
		GLib.message ("setting multimedia volume to %s", cvol.to_string());
		_pulse_context.set_sink_volume_by_index (index, cvol);
	}

	/***
	****  PulseAudio: Tracking the active multimedia sink input
	***/

	private unowned PulseAudio.GLibMainLoop _pgloop = null;
	private PulseAudio.Context _pulse_context = null;
	private uint _pulse_reconnect_timer = 0;
	private uint _update_sink_timer = 0;
	private uint _update_sink_inputs_timer = 0;
        private GenericSet<uint32> _pending_sink_inputs = new GenericSet<uint32>(direct_hash, direct_equal);

	private uint soon_interval_msec = 500;

	private uint32 _target_sink_index           = PulseAudio.INVALID_INDEX;
	private uint32 _multimedia_sink_index       = PulseAudio.INVALID_INDEX;
	private uint32 _multimedia_sink_input_index = PulseAudio.INVALID_INDEX;

	/***/

	private void update_multimedia_volume () {

		GLib.return_if_fail(_pulse_context != null);
		GLib.return_if_fail(_multimedia_sink_index != PulseAudio.INVALID_INDEX);

		_pulse_context.get_sink_info_by_index(_multimedia_sink_index, (c,i) => {
			if (i != null)
				multimedia_volume = i.volume.max();
		});
	}

	private void update_multimedia_volume_soon () {

		if (_update_sink_timer == 0) {
			_update_sink_timer = Timeout.add (soon_interval_msec, () => {
				_update_sink_timer = 0;
				update_multimedia_volume ();
				return Source.REMOVE;
			});
		}
	}

	private void set_multimedia_sink_index(uint32 index) {

		if (index == PulseAudio.INVALID_INDEX) {
			_multimedia_sink_index = PulseAudio.INVALID_INDEX;
			multimedia_volume = PulseAudio.Volume.INVALID;
		} else if (_multimedia_sink_index != index) {
			_multimedia_sink_index = index;
			multimedia_volume = PulseAudio.Volume.INVALID;
			update_multimedia_volume_soon();
		}
	}

	/***/

	private bool is_active_multimedia (SinkInputInfo i) {

		if (i.corked != 0)
			return false;

		var key = PulseAudio.Proplist.PROP_MEDIA_ROLE;
		var media_role = i.proplist.gets(key);
		if (media_role != "multimedia")
			return false;

		return true;
	}

	private void clear_multimedia () {
		_multimedia_sink_input_index = PulseAudio.INVALID_INDEX;
		set_multimedia_sink_index (PulseAudio.INVALID_INDEX);
		multimedia_active = false;
	}

	private void on_sink_input_info (Context c, SinkInputInfo? i, int eol) {

		if (i == null)
			return;

		if (is_active_multimedia (i)) {
			GLib.message ("on_sink_input_info() setting multimedia sink input index to %d, sink index to %d", (int)i.index, (int)i.sink);
			_multimedia_sink_input_index = i.index;
			set_multimedia_sink_index (i.sink);
			multimedia_active = true;
		}
		else if (i.index == _multimedia_sink_input_index) {
			clear_multimedia();
		}
	}

	private void update_all_sink_inputs () {
		_pulse_context.get_sink_input_info_list (on_sink_input_info);
	}
	private void update_sink_input (uint32 index) {
		_pulse_context.get_sink_input_info (index, on_sink_input_info);
	}

	private void update_sink_input_soon (uint32 index) {

		_pending_sink_inputs.add(index);

		if (_update_sink_inputs_timer == 0) {
			_update_sink_inputs_timer = Timeout.add (soon_interval_msec, () => {
				_pending_sink_inputs.foreach((i) => update_sink_input(i));
				_pending_sink_inputs.remove_all();
				_update_sink_inputs_timer = 0;
				return Source.REMOVE;
			});
		}
	}

        private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index) {
		switch (t & Context.SubscriptionEventType.FACILITY_MASK)
		{
			case Context.SubscriptionEventType.SINK:
				// if something happens to the sink connected to our mm sink input,
				// get its updated info to keep our multimedia volume up-to-date
				if ((index == _multimedia_sink_index) && (index != PulseAudio.INVALID_INDEX))
					update_multimedia_volume_soon();
				break;

			case Context.SubscriptionEventType.SINK_INPUT:
				switch (t & Context.SubscriptionEventType.TYPE_MASK)
				{
					// if a SinkInput changed, get its updated info
					// to keep our multimedia indices up-to-date
					case Context.SubscriptionEventType.NEW:
					case Context.SubscriptionEventType.CHANGE:
						update_sink_input_soon(index);
						break;

					// if the multimedia sink input was removed,
					// reset our mm fields and look for a new mm sink input
					case Context.SubscriptionEventType.REMOVE:
						if (index == _multimedia_sink_input_index) {
							clear_multimedia();
							update_all_sink_inputs ();
						}
						break;

					default:
						GLib.debug ("Sink input event not known.");
						break;
				}
                                break;

			default:
				break;
		}
	}

	private void pulse_context_state_callback (Context c) {
		switch (c.get_state ()) {
			case Context.State.READY:
				c.set_subscribe_callback (context_events_cb);
				c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
				             PulseAudio.Context.SubscriptionMask.SINK_INPUT);
				update_all_sink_inputs ();
				break;

			case Context.State.FAILED:
			case Context.State.TERMINATED:
				pulse_reconnect_soon ();
				break;

			default:
				break;
		}
	}

	private void pulse_disconnect () {
		if (_pulse_context != null) {
			_pulse_context.disconnect ();
			_pulse_context = null;
		}
	}

	private void pulse_reconnect_soon () {
		if (_pulse_reconnect_timer == 0) {
			_pulse_reconnect_timer = Timeout.add_seconds (2, () => {
				_pulse_reconnect_timer = 0;
				pulse_reconnect();
				return Source.REMOVE;
			});
		}
	}

	void pulse_reconnect () {
		pulse_disconnect();

		var props = new Proplist ();
		props.sets (Proplist.PROP_APPLICATION_NAME, "Ubuntu Audio Settings");
		props.sets (Proplist.PROP_APPLICATION_ID, "com.canonical.settings.sound");
		props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control");
		props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1");

		_pulse_context = new PulseAudio.Context (_pgloop.get_api(), null, props);
		_pulse_context.set_state_callback (pulse_context_state_callback);

		var server_string = Environment.get_variable ("PULSE_SERVER");
		if (_pulse_context.connect (server_string, Context.Flags.NOFAIL, null) < 0)
			GLib.warning ("pa_context_connect() failed: %s\n", PulseAudio.strerror(_pulse_context.errno()));
	}
}