/*
 * Copyright (C) 2008 OpenedHand Ltd.
 * Copyright (C) 2009,2010,2011,2012 Nokia Corporation.
 * Copyright (C) 2012 Openismus GmbH
 *
 * Author: Jorn Baayen <jorn@openedhand.com>
 *         Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
 *                               <zeeshan.ali@nokia.com>
 *         Jens Georg <jensg@openismus.com>
 *
 * Rygel is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Rygel 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

using Gst;
using GUPnP;

public class Rygel.Playbin.Player : GLib.Object, Rygel.MediaPlayer {
    private const string TRANSFER_MODE_STREAMING = "Streaming";
    private const string TRANSFER_MODE_INTERACTIVE = "Interactive";
    private const string PROTOCOL_INFO_TEMPLATE = "http-get:%s:*:%s";

    private const string[] protocols = { "http-get", "rtsp" };
    private const string[] mime_types = {
                                        "audio/mpeg",
                                        "application/ogg",
                                        "audio/x-vorbis",
                                        "audio/x-vorbis+ogg",
                                        "audio/ogg",
                                        "audio/x-ms-wma",
                                        "audio/x-ms-asf",
                                        "audio/x-flac",
                                        "audio/x-flac+ogg",
                                        "audio/flac",
                                        "audio/x-mod",
                                        "audio/x-wav",
                                        "audio/x-ac3",
                                        "audio/x-m4a",
                                        "audio/L16;rate=44100;channels=2",
                                        "audio/L16;rate=44100;channels=1",
                                        "audio/L16;channels=2;rate=44100",
                                        "audio/L16;channels=1;rate=44100",
                                        "audio/L16;rate=44100",
                                        "image/jpeg",
                                        "image/png",
                                        "video/x-theora",
                                        "video/x-theora+ogg",
                                        "video/x-oggm",
                                        "video/ogg",
                                        "video/x-dirac",
                                        "video/x-wmv",
                                        "video/x-wma",
                                        "video/x-msvideo",
                                        "video/x-3ivx",
                                        "video/x-3ivx",
                                        "video/x-matroska",
                                        "video/x-mkv",
                                        "video/mpeg",
                                        "video/mp4",
                                        "video/x-ms-asf",
                                        "video/x-xvid",
                                        "video/x-ms-wmv" };
    private static Player player;

    public dynamic Element playbin { get; private set; }

    private string _playback_state = "NO_MEDIA_PRESENT";
    public string playback_state {
        owned get {
            return this._playback_state;
        }

        set {
            Gst.State state, pending;

            this.playbin.get_state (out state, out pending, Gst.MSECOND);

            debug ("Changing playback state to %s.", value);

            switch (value) {
                case "STOPPED":
                    if (state != State.NULL || pending != State.VOID_PENDING) {
                        this._playback_state = "TRANSITIONING";
                        this.playbin.set_state (State.NULL);
                    } else {
                        this._playback_state = value;
                    }
                break;
                case "PAUSED_PLAYBACK":
                    if (state != State.PAUSED || pending != State.VOID_PENDING) {
                        this._playback_state = "TRANSITIONING";
                        this.playbin.set_state (State.PAUSED);
                    } else {
                        this._playback_state = value;
                    }
                break;
                case "PLAYING":
                    if (state != State.PLAYING ||
                        pending != State.VOID_PENDING) {
                        this._playback_state = "TRANSITIONING";
                        this.playbin.set_state (State.PLAYING);
                    } else {
                        this._playback_state = value;
                    }
                break;
                default:
                break;
            }
        }
    }

    private string transfer_mode = null;

    private bool uri_update_hint = false;
    private string? _uri = null;
    public string? uri {
        owned get {
            return _uri;
        }

        set {
            this._uri = value;
            this.playbin.set_state (State.READY);
            this.playbin.uri = value;
            if (value != "") {
                switch (this._playback_state) {
                    case "NO_MEDIA_PRESENT":
                        this.playbin.set_state (State.READY);
                        this._playback_state = "STOPPED";
                        this.notify_property ("playback-state");
                        break;
                    case "STOPPED":
                        this.playbin.set_state (State.READY);
                        break;
                    case "PAUSED_PLAYBACK":
                        this.playbin.set_state (State.PAUSED);
                        break;
                    case "PLAYING":
                        this.playbin.set_state (State.PLAYING);
                        break;
                    default:
                        break;
                }
            } else {
                this._playback_state = "NO_MEDIA_PRESENT";
                this.notify_property ("playback-state");
            }
            debug ("URI set to %s.", value);
        }
    }

    private string _mime_type = "";
    public string? mime_type {
        owned get {
            return this._mime_type;
        }

        set {
            this._mime_type = value;
        }
    }

    private string _metadata = "";
    public string? metadata {
        owned get {
            return this._metadata;
        }

        set {
            this._metadata = value;
        }
    }

    private string _content_features = "";
    private ProtocolInfo protocol_info;
    public string? content_features {
        owned get {
            return this._content_features;
        }

        set {
            var pi_string = PROTOCOL_INFO_TEMPLATE.printf (this.mime_type,
                                                           value);
            try {
                this.protocol_info = new ProtocolInfo.from_string (pi_string);
                var flags = this.protocol_info.dlna_flags;
                if (DLNAFlags.INTERACTIVE_TRANSFER_MODE in flags) {
                    this.transfer_mode = TRANSFER_MODE_INTERACTIVE;
                } else if (DLNAFlags.STREAMING_TRANSFER_MODE in flags) {
                    this.transfer_mode = TRANSFER_MODE_STREAMING;
                } else {
                    this.transfer_mode = null;
                }
            } catch (Error error) {
                this.protocol_info = null;
                this.transfer_mode = null;
            }
            this._content_features = value;
        }
    }

    public double volume {
        get {
            return this.playbin.volume;
        }

        set {
            this.playbin.volume = value;
            debug ("volume set to %f.", value);
        }
    }

    public int64 duration {
        get {
            var format = Format.TIME;
            int64 dur;

            if (this.playbin.query_duration (ref format, out dur)) {
                return dur;
            } else {
                return 0;
            }
        }
    }

    public int64 position {
        get {
            var format = Format.TIME;
            int64 pos;

            if (this.playbin.query_position (ref format, out pos)) {
                return pos;
            } else {
                return 0;
            }
        }
    }

    private Player () {
        this.playbin = ElementFactory.make ("playbin2", null);
        this.setup_playbin ();
    }

    public Player.wrap (Gst.Element playbin) {
        this.playbin = playbin;
        this.setup_playbin ();
    }

    public static Player get_default () {
        if (player == null) {
            player = new Player ();
        }

        return player;
    }

    public bool seek (ClockTime time) {
        return this.playbin.seek (1.0,
                                  Format.TIME,
                                  SeekFlags.FLUSH,
                                  Gst.SeekType.SET,
                                  (int64) time,
                                  Gst.SeekType.NONE,
                                  -1);
    }

    public string[] get_protocols () {
        return protocols;
    }

    public string[] get_mime_types () {
        return mime_types;
    }

    private bool is_rendering_image () {
        dynamic Element typefind;

        typefind = (this.playbin as Gst.Bin).get_by_name ("typefind");
        Caps caps = typefind.caps;
        var structure = caps.get_structure (0);

        return structure.get_name () == "image/jpeg" ||
               structure.get_name () == "image/png";
    }

    private void bus_handler (Gst.Bus bus,
                              Message message) {
        switch (message.type) {
        case MessageType.STATE_CHANGED:
            if (message.src == this.playbin) {
                State old_state, new_state, pending;

                message.parse_state_changed (out old_state,
                                             out new_state,
                                             out pending);
                if (old_state == State.READY && new_state == State.PAUSED) {
                    this.notify_property ("duration");
                    if (this.uri_update_hint) {
                        this.uri_update_hint = false;
                        string uri = this.playbin.uri;
                        if (this._uri != uri) {
                            // uri changed externally
                            this._uri = this.playbin.uri;
                            this.notify_property("uri");
                            this.metadata = this.generate_basic_didl ();
                        }
                    }
                }

                debug ("%d %d %d", old_state, new_state, pending);

                if (pending == State.VOID_PENDING) {
                    switch (new_state) {
                        case State.PAUSED:
                            this.playback_state = "PAUSED_PLAYBACK";
                            break;
                        case State.NULL:
                            this.playback_state = "STOPPED";
                            break;
                        case State.PLAYING:
                            this.playback_state = "PLAYING";
                            break;
                        default:
                            break;
                    }
                }
            }
            break;
        case MessageType.EOS:
            if (!this.is_rendering_image ()) {
                debug ("EOS");
                this.playback_state = "STOPPED";
            } else {
                debug ("Content is image, ignoring EOS");
            }

            break;
        case MessageType.ERROR:
            Error error;
            string error_message;

            message.parse_error (out error, out error_message);

            warning ("Error from GStreamer element %s: %s",
                     this.playbin.name,
                     error_message);
            warning ("Going to STOPPED state");

            this.playback_state = "STOPPED";

            break;
        }
    }

    private void on_source_setup (Element pipeline, dynamic Element source) {
        if (source.get_type ().name () == "GstSoupHTTPSrc" &&
            this.transfer_mode != null) {
            debug ("Setting transfer mode to %s", this.transfer_mode);

            var structure = new Structure.empty ("Extra Headers");
            structure.set_value ("transferMode.dlna.org", this.transfer_mode);

            source.extra_headers = structure;
        }
    }

    private void on_uri_notify (ParamSpec pspec) {
        this.uri_update_hint = true;
    }

    /**
     * Generate basic DIDLLite information.
     *
     * This is used when the URI gets changed externally. DLNA requires that a
     * minimum DIDLLite is always present if the URI is not empty.
     */
    private string generate_basic_didl () {
        var writer = new DIDLLiteWriter (null);
        var item = writer.add_item ();
        item.id = "1";
        item.parent_id = "-1";
        item.upnp_class = "object.item";
        var resource = item.add_resource ();
        resource.uri = this._uri;
        var file = File.new_for_uri (this.uri);
        item.title = file.get_basename ();

        return writer.get_string ();
    }

    private void setup_playbin () {
        // Needed to get "Stop" events from the playbin.
        // We can do this because we have a bus watch
        this.playbin.auto_flush_bus = false;
        assert (this.playbin != null);

        this.playbin.source_setup.connect (this.on_source_setup);
        this.playbin.notify["uri"].connect (this.on_uri_notify);

        // Bus handler
        var bus = this.playbin.get_bus ();
        bus.add_signal_watch ();
        bus.message.connect (this.bus_handler);
    }
}
