#pragma once

// subtitleeditor -- a tool to create or edit subtitle
//
// https://kitone.github.io/subtitleeditor/
// https://github.com/kitone/subtitleeditor/
//
// Copyright @ 2005-2018, kitone
//
// 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/>.

#include <gstreamermm.h>
// missing plugins
#include <gst/pbutils/missing-plugins.h>
#include "gstreamer_utility.h"
// std
#include <iomanip>
#include <iostream>

// Class to help with gstreamer(mm)
class MediaDecoder : virtual public sigc::trackable {
 public:
  MediaDecoder(guint timeout = 0) : m_watch_id(0), m_timeout(timeout) {
  }

  virtual ~MediaDecoder() {
    destroy_pipeline();
  }

  void create_pipeline(const Glib::ustring &uri) {
    se_dbg_msg(SE_DBG_PLUGINS, "uri=%s", uri.c_str());

    if (m_pipeline)
      destroy_pipeline();

    m_pipeline = Gst::Pipeline::create("pipeline");

    Glib::RefPtr<Gst::FileSrc> filesrc = Gst::FileSrc::create("filesrc");

    Glib::RefPtr<Gst::DecodeBin> decodebin = Gst::DecodeBin::create("decoder");

    decodebin->signal_pad_added().connect(
        sigc::mem_fun(*this, &MediaDecoder::on_pad_added));

    try {
      m_pipeline->add(filesrc);
      m_pipeline->add(decodebin);

      filesrc->link(decodebin);
    } catch (const std::runtime_error &ex) {
      std::cerr << ex.what() << std::endl;
      // FIXME destroy pipeline ?
    }
    filesrc->set_uri(uri);

    // Bus watching
    Glib::RefPtr<Gst::Bus> bus = m_pipeline->get_bus();
    m_watch_id =
        bus->add_watch(sigc::mem_fun(*this, &MediaDecoder::on_bus_message));

    // m_pipeline->set_state(Gst::STATE_PAUSED);
    if (m_pipeline->set_state(Gst::STATE_PLAYING) ==
        Gst::STATE_CHANGE_FAILURE) {
      se_dbg_msg(SE_DBG_PLUGINS,
                 "Failed to change the state of the pipeline to PLAYING");
    }
  }

  void destroy_pipeline() {
    se_dbg(SE_DBG_PLUGINS);

    if (m_connection_timeout)
      m_connection_timeout.disconnect();

    if (m_pipeline) {
      m_pipeline->get_bus()->remove_watch(m_watch_id);
      m_pipeline->set_state(Gst::STATE_NULL);
    }

    m_watch_id = 0;
    m_pipeline = Glib::RefPtr<Gst::Pipeline>();
  }

  virtual void on_pad_added(const Glib::RefPtr<Gst::Pad> &newpad) {
    se_dbg(SE_DBG_PLUGINS);

    Glib::RefPtr<Gst::Caps> caps_null;
    Glib::RefPtr<Gst::Caps> caps = newpad->query_caps(caps_null);
    se_dbg_msg(SE_DBG_PLUGINS, "newpad->caps: %s", caps->to_string().c_str());

    const Gst::Structure structure = caps->get_structure(0);
    if (!structure)
      return;

    Glib::RefPtr<Gst::Element> sink = create_element(structure.get_name());
    if (sink) {
      // Add bin to the pipeline
      m_pipeline->add(sink);

      // Set the new sink tp PAUSED as well
      Gst::StateChangeReturn retst = sink->set_state(Gst::STATE_PAUSED);
      if (retst == Gst::STATE_CHANGE_FAILURE) {
        std::cerr << "Could not change state of new sink: " << retst
                  << std::endl;
        se_dbg_msg(SE_DBG_PLUGINS, "Could not change the state of new sink");
        m_pipeline->remove(sink);
        return;
      }
      // Get the ghostpad of the sink bin
      Glib::RefPtr<Gst::Pad> sinkpad = sink->get_static_pad("sink");

      Gst::PadLinkReturn ret = newpad->link(sinkpad);

      if (ret != Gst::PAD_LINK_OK && ret != Gst::PAD_LINK_WAS_LINKED) {
        std::cerr << "Linking of pads " << newpad->get_name() << " and "
                  << sinkpad->get_name() << " failed." << std::endl;
        se_dbg_msg(SE_DBG_PLUGINS, "Linking of pads failed");
      } else {
        se_dbg_msg(SE_DBG_PLUGINS, "Pads linking with success");
      }
    } else {
      se_dbg_msg(SE_DBG_PLUGINS, "create_element return an NULL sink");
    }
  }

  // BUS MESSAGE
  virtual bool on_bus_message(const Glib::RefPtr<Gst::Bus> & /*bus*/,
                              const Glib::RefPtr<Gst::Message> &msg) {
    se_dbg_msg(SE_DBG_PLUGINS, "type='%s' name='%s'",
               GST_MESSAGE_TYPE_NAME(msg->gobj()),
               GST_OBJECT_NAME(GST_MESSAGE_SRC(msg->gobj())));

    switch (msg->get_message_type()) {
      case Gst::MESSAGE_ELEMENT:
        return on_bus_message_element(
            Glib::RefPtr<Gst::MessageElement>::cast_static(msg));
      case Gst::MESSAGE_EOS:
        return on_bus_message_eos(
            Glib::RefPtr<Gst::MessageEos>::cast_static(msg));
      case Gst::MESSAGE_ERROR:
        return on_bus_message_error(
            Glib::RefPtr<Gst::MessageError>::cast_static(msg));
      case Gst::MESSAGE_WARNING:
        return on_bus_message_warning(
            Glib::RefPtr<Gst::MessageWarning>::cast_static(msg));
      case Gst::MESSAGE_STATE_CHANGED:
        return on_bus_message_state_changed(
            Glib::RefPtr<Gst::MessageStateChanged>::cast_static(msg));
      default:
        break;
    }
    return true;
  }

  virtual bool on_bus_message_error(Glib::RefPtr<Gst::MessageError> msg) {
    check_missing_plugins();

    Glib::ustring error =
        (msg) ? Glib::ustring(msg->parse_debug()) : Glib::ustring();

    dialog_error(_("Media file could not be played.\n"), error);
    // Critical error, cancel the work.
    on_work_cancel();
    return true;
  }

  virtual bool on_bus_message_warning(Glib::RefPtr<Gst::MessageWarning> msg) {
    check_missing_plugins();

    Glib::ustring error =
        (msg) ? Glib::ustring(msg->parse_debug()) : Glib::ustring();
    dialog_error(_("Media file could not be played.\n"), error);

    return true;
  }

  virtual bool on_bus_message_state_changed(
      Glib::RefPtr<Gst::MessageStateChanged> msg) {
    if (m_timeout > 0)
      return on_bus_message_state_changed_timeout(msg);
    return true;
  }

  virtual bool on_bus_message_eos(Glib::RefPtr<Gst::MessageEos>) {
    m_pipeline->set_state(Gst::STATE_PAUSED);
    on_work_finished();
    return true;
  }

  virtual bool on_bus_message_element(Glib::RefPtr<Gst::MessageElement> msg) {
    check_missing_plugin_message(msg);
    return true;
  }

  virtual void on_work_finished() {
    // FIXME
  }

  virtual void on_work_cancel() {
    // FIXME
  }

  virtual Glib::RefPtr<Gst::Element> create_element(const Glib::ustring &) {
    return Glib::RefPtr<Gst::Element>();
  }

  virtual bool on_timeout() {
    return false;
  }

  // utility
  Glib::ustring time_to_string(gint64 pos) {
    return Glib::ustring::compose(
        "%1:%2:%3",
        Glib::ustring::format(std::setfill(L'0'), std::setw(2),
                              Gst::get_hours(pos)),
        Glib::ustring::format(std::setfill(L'0'), std::setw(2),
                              Gst::get_minutes(pos)),
        Glib::ustring::format(std::setfill(L'0'), std::setw(2),
                              Gst::get_seconds(pos)));
  }

 protected:
  bool on_bus_message_state_changed_timeout(
      Glib::RefPtr<Gst::MessageStateChanged> msg) {
    se_dbg(SE_DBG_PLUGINS);

    // We only update when it is the pipeline object
    if (msg->get_source()->get_name() != "pipeline")
      return true;

    Gst::State old_state, new_state, pending;

    msg->parse(old_state, new_state, pending);

    if (old_state == Gst::STATE_PAUSED && new_state == Gst::STATE_PLAYING) {
      if (!m_connection_timeout)
        m_connection_timeout = Glib::signal_timeout().connect(
            sigc::mem_fun(*this, &MediaDecoder::on_timeout), m_timeout);
    } else if (old_state == Gst::STATE_PLAYING &&
               new_state == Gst::STATE_PAUSED) {
      if (m_connection_timeout)
        m_connection_timeout.disconnect();
    }
    return true;
  }

  void check_missing_plugin_message(
      const Glib::RefPtr<Gst::MessageElement> &msg) {
    se_dbg(SE_DBG_PLUGINS);

    if (!msg)
      return;
    GstMessage *gstmsg = GST_MESSAGE(msg->gobj());
    if (!gstmsg)
      return;
    if (!gst_is_missing_plugin_message(gstmsg))
      return;

    gchar *description = gst_missing_plugin_message_get_description(gstmsg);
    if (!description)
      return;

    se_dbg_msg(SE_DBG_PLUGINS, "missing plugin msg '%s'", description);

    m_missing_plugins.push_back(description);
    g_free(description);
    return;
  }

  bool check_missing_plugins() {
    if (m_missing_plugins.empty())
      return false;

    dialog_missing_plugins(m_missing_plugins);
    m_missing_plugins.clear();
    return true;
  }

  // Display a message for missing plugins.
  void dialog_missing_plugins(const std::list<Glib::ustring> &list) {
    Glib::ustring plugins;

    std::list<Glib::ustring>::const_iterator it = list.begin();
    std::list<Glib::ustring>::const_iterator end = list.end();

    while (it != end) {
      plugins += *it;
      plugins += "\n";
      ++it;
    }

    Glib::ustring msg =
        _("GStreamer plugins missing.\n"
          "The playback of this movie requires the following decoders "
          "which are not installed:");

    dialog_error(msg, plugins);

    se_dbg_msg(SE_DBG_UTILITY, "%s %s", msg.c_str(), plugins.c_str());
  }

 protected:
  guint m_watch_id;
  Glib::RefPtr<Gst::Pipeline> m_pipeline;

  // timeout
  guint m_timeout;
  sigc::connection m_connection_timeout;

  std::list<Glib::ustring> m_missing_plugins;
};
