#include "RtspPlayer.h"
#include "VideoItem.h"

#include <QImage>
#include <QMetaObject>
#include <QUrl>

#include <gst/rtsp/gstrtsptransport.h>
#include <gst/video/video.h>

namespace
{
QString gstErrorToQString(const GError *error)
{
   if (!error || !error->message) {
      return QStringLiteral("Unknown GStreamer error");
   }

   return QString::fromUtf8(error->message);
}
}

RtspPlayer::RtspPlayer(QObject *parent)
   : QObject(parent)
{
   m_busTimer.setInterval(100);
   connect(&m_busTimer, &QTimer::timeout, this, &RtspPlayer::pollBus);
}

RtspPlayer::~RtspPlayer()
{
   destroyPipeline();
}

QString RtspPlayer::status() const
{
   return m_status;
}

QString RtspPlayer::errorString() const
{
   return m_errorString;
}

bool RtspPlayer::playing() const
{
   return m_playing;
}

int RtspPlayer::latency() const
{
   return m_latency;
}

void RtspPlayer::setLatency(int latency)
{
   const int normalized = qBound(0, latency, 5000);

   if (m_latency == normalized) {
      return;
   }

   m_latency = normalized;
   emit latencyChanged();
}

void RtspPlayer::attachVideoItem(QObject *item)
{
   VideoItem *videoItem = qobject_cast<VideoItem *>(item);

   if (!videoItem) {
      setErrorString(QStringLiteral("Video surface could not be attached."));
      setStatus(QStringLiteral("Video surface initialization failed."));
      return;
   }

   m_videoItem = videoItem;
}

QString RtspPlayer::buildLocation(
   const QString &url,
   const QString &username,
   const QString &password
) const
{
   QString value = url.trimmed();

   if (value.isEmpty()) {
      return QString();
   }

   if (!value.startsWith(QStringLiteral("rtsp://"), Qt::CaseInsensitive)) {
      value.prepend(QStringLiteral("rtsp://"));
   }

   QUrl parsed(value);

   if (!parsed.isValid()) {
      return QString();
   }

   if (!username.isEmpty()) {
      parsed.setUserName(username);
      parsed.setPassword(password);
   }

   return parsed.toString(QUrl::FullyEncoded);
}

void RtspPlayer::play(
   const QString &url,
   const QString &username,
   const QString &password,
   const QString &displayName
)
{
   m_lastUrl = url;
   m_lastUsername = username;
   m_lastPassword = password;
   m_lastDisplayName = displayName;

   setErrorString(QString());

   if (!m_videoItem) {
      setErrorString(QStringLiteral("The video surface is not ready."));
      setStatus(QStringLiteral("Video surface is not ready."));
      return;
   }

   const QString location = buildLocation(url, username, password);

   if (location.isEmpty()) {
      setErrorString(QStringLiteral("Invalid RTSP URL."));
      setStatus(QStringLiteral("Invalid RTSP URL."));
      return;
   }

   destroyPipeline();

   setStatus(
      displayName.isEmpty()
         ? QStringLiteral("Connecting...")
         : QStringLiteral("Connecting to %1...").arg(displayName)
   );

   if (!createPipeline(location)) {
      return;
   }

   const GstStateChangeReturn result =
      gst_element_set_state(m_pipeline, GST_STATE_PLAYING);

   if (result == GST_STATE_CHANGE_FAILURE) {
      setErrorString(QStringLiteral("GStreamer refused to start the pipeline."));
      setStatus(QStringLiteral("Could not start playback."));
   }
}

void RtspPlayer::restart()
{
   if (m_lastUrl.isEmpty()) {
      setStatus(QStringLiteral("No camera has been selected."));
      return;
   }

   play(
      m_lastUrl,
      m_lastUsername,
      m_lastPassword,
      m_lastDisplayName
   );
}

void RtspPlayer::stop()
{
   destroyPipeline();

   setStatus(
      m_lastDisplayName.isEmpty()
         ? QStringLiteral("Stopped")
         : QStringLiteral("Stopped %1").arg(m_lastDisplayName)
   );
}

bool RtspPlayer::createPipeline(const QString &location)
{
   m_pipeline = gst_pipeline_new("rtsp-camera-pipeline");
   m_source = gst_element_factory_make("rtspsrc", "source");
   m_depay = gst_element_factory_make("rtph264depay", "depay");
   m_parser = gst_element_factory_make("h264parse", "parser");
   m_decoder = gst_element_factory_make("decodebin", "decoder");
   m_convert = gst_element_factory_make("videoconvert", "convert");
   m_capsFilter = gst_element_factory_make("capsfilter", "video_caps");
   m_sink = gst_element_factory_make("appsink", "video_sink");

   if (
      !m_pipeline ||
      !m_source ||
      !m_depay ||
      !m_parser ||
      !m_decoder ||
      !m_convert ||
      !m_capsFilter ||
      !m_sink
   ) {
      setErrorString(
         QStringLiteral("Required GStreamer elements are missing.")
      );
      setStatus(QStringLiteral("GStreamer setup is incomplete."));
      destroyPipeline();
      return false;
   }

   const QByteArray encodedLocation = location.toUtf8();

   g_object_set(
      m_source,
      "location",
      encodedLocation.constData(),
      "protocols",
      static_cast<guint>(GST_RTSP_LOWER_TRANS_TCP),
      "latency",
      static_cast<guint>(m_latency),
      nullptr
   );

   GstCaps *videoCaps = gst_caps_new_simple(
      "video/x-raw",
      "format",
      G_TYPE_STRING,
      "BGRA",
      nullptr
   );

   g_object_set(
      m_capsFilter,
      "caps",
      videoCaps,
      nullptr
   );

   gst_caps_unref(videoCaps);

   g_object_set(
      m_sink,
      "emit-signals",
      TRUE,
      "sync",
      FALSE,
      "max-buffers",
      static_cast<guint>(1),
      "drop",
      TRUE,
      nullptr
   );

   g_signal_connect(
      m_sink,
      "new-sample",
      G_CALLBACK(RtspPlayer::onNewSample),
      this
   );

   gst_bin_add_many(
      GST_BIN(m_pipeline),
      m_source,
      m_depay,
      m_parser,
      m_decoder,
      m_convert,
      m_capsFilter,
      m_sink,
      nullptr
   );

   if (!gst_element_link_many(m_depay, m_parser, m_decoder, nullptr)) {
      setErrorString(QStringLiteral("Could not link the H.264 RTP pipeline."));
      setStatus(QStringLiteral("GStreamer pipeline link failed."));
      destroyPipeline();
      return false;
   }

   if (!gst_element_link_many(m_convert, m_capsFilter, m_sink, nullptr)) {
      setErrorString(QStringLiteral("Could not link the video output pipeline."));
      setStatus(QStringLiteral("GStreamer output link failed."));
      destroyPipeline();
      return false;
   }

   g_signal_connect(
      m_source,
      "pad-added",
      G_CALLBACK(RtspPlayer::onRtspPadAdded),
      this
   );

   g_signal_connect(
      m_decoder,
      "pad-added",
      G_CALLBACK(RtspPlayer::onDecoderPadAdded),
      this
   );

   m_bus = gst_element_get_bus(m_pipeline);
   m_busTimer.start();

   return true;
}

void RtspPlayer::destroyPipeline()
{
   m_busTimer.stop();
   setPlaying(false);

   if (m_pipeline) {
      gst_element_set_state(m_pipeline, GST_STATE_NULL);
   }

   if (m_bus) {
      gst_object_unref(m_bus);
      m_bus = nullptr;
   }

   if (m_pipeline) {
      gst_object_unref(m_pipeline);
   }

   m_pipeline = nullptr;
   m_source = nullptr;
   m_depay = nullptr;
   m_parser = nullptr;
   m_decoder = nullptr;
   m_convert = nullptr;
   m_capsFilter = nullptr;
   m_sink = nullptr;

   if (m_videoItem) {
      m_videoItem->clearFrame();
   }
}

void RtspPlayer::setStatus(const QString &status)
{
   if (m_status == status) {
      return;
   }

   m_status = status;
   emit statusChanged();
}

void RtspPlayer::setErrorString(const QString &error)
{
   if (m_errorString == error) {
      return;
   }

   m_errorString = error;
   emit errorStringChanged();
}

void RtspPlayer::setPlaying(bool playing)
{
   if (m_playing == playing) {
      return;
   }

   m_playing = playing;
   emit playingChanged();
}

void RtspPlayer::pollBus()
{
   if (!m_bus) {
      return;
   }

   GstMessage *message = nullptr;

   while ((message = gst_bus_pop(m_bus)) != nullptr) {
      switch (GST_MESSAGE_TYPE(message)) {
      case GST_MESSAGE_ERROR: {
         GError *error = nullptr;
         gchar *debugInfo = nullptr;

         gst_message_parse_error(message, &error, &debugInfo);

         const QString errorMessage = gstErrorToQString(error);
         setErrorString(errorMessage);
         setStatus(QStringLiteral("Playback error: %1").arg(errorMessage));
         setPlaying(false);

         if (error) {
            g_error_free(error);
         }

         if (debugInfo) {
            g_free(debugInfo);
         }

         break;
      }

      case GST_MESSAGE_EOS:
         setStatus(QStringLiteral("Stream ended."));
         setPlaying(false);
         break;

      case GST_MESSAGE_STATE_CHANGED:
         if (GST_MESSAGE_SRC(message) == GST_OBJECT(m_pipeline)) {
            GstState oldState;
            GstState newState;
            GstState pendingState;

            gst_message_parse_state_changed(
               message,
               &oldState,
               &newState,
               &pendingState
            );

            Q_UNUSED(oldState);
            Q_UNUSED(pendingState);

            if (newState == GST_STATE_PLAYING) {
               setPlaying(true);
               setStatus(
                  m_lastDisplayName.isEmpty()
                     ? QStringLiteral("Playing")
                     : QStringLiteral("Playing %1").arg(m_lastDisplayName)
               );
            } else if (newState == GST_STATE_PAUSED) {
               setPlaying(false);
               setStatus(
                  m_lastDisplayName.isEmpty()
                     ? QStringLiteral("Buffering...")
                     : QStringLiteral("Buffering %1...").arg(m_lastDisplayName)
               );
            }
         }
         break;

      default:
         break;
      }

      gst_message_unref(message);
   }
}

void RtspPlayer::onRtspPadAdded(
   GstElement *source,
   GstPad *newPad,
   gpointer userData
)
{
   Q_UNUSED(source);

   RtspPlayer *self = static_cast<RtspPlayer *>(userData);

   if (!self || !self->m_depay) {
      return;
   }

   GstPad *sinkPad = gst_element_get_static_pad(self->m_depay, "sink");

   if (!sinkPad) {
      return;
   }

   if (gst_pad_is_linked(sinkPad)) {
      gst_object_unref(sinkPad);
      return;
   }

   GstCaps *caps = gst_pad_get_current_caps(newPad);

   if (!caps) {
      caps = gst_pad_query_caps(newPad, nullptr);
   }

   bool isH264Video = false;

   if (caps && gst_caps_get_size(caps) > 0) {
      const GstStructure *structure = gst_caps_get_structure(caps, 0);
      const gchar *capsName = gst_structure_get_name(structure);
      const gchar *media = gst_structure_get_string(structure, "media");
      const gchar *encoding = gst_structure_get_string(
         structure,
         "encoding-name"
      );

      isH264Video =
         g_strcmp0(capsName, "application/x-rtp") == 0 &&
         g_strcmp0(media, "video") == 0 &&
         g_ascii_strcasecmp(encoding ? encoding : "", "H264") == 0;
   }

   if (isH264Video) {
      gst_pad_link(newPad, sinkPad);
   }

   if (caps) {
      gst_caps_unref(caps);
   }

   gst_object_unref(sinkPad);
}

void RtspPlayer::onDecoderPadAdded(
   GstElement *decoder,
   GstPad *newPad,
   gpointer userData
)
{
   Q_UNUSED(decoder);

   RtspPlayer *self = static_cast<RtspPlayer *>(userData);

   if (!self || !self->m_convert) {
      return;
   }

   GstPad *sinkPad = gst_element_get_static_pad(self->m_convert, "sink");

   if (!sinkPad) {
      return;
   }

   if (gst_pad_is_linked(sinkPad)) {
      gst_object_unref(sinkPad);
      return;
   }

   GstCaps *caps = gst_pad_get_current_caps(newPad);

   if (!caps) {
      caps = gst_pad_query_caps(newPad, nullptr);
   }

   bool isRawVideo = false;

   if (caps && gst_caps_get_size(caps) > 0) {
      const GstStructure *structure = gst_caps_get_structure(caps, 0);
      const gchar *capsName = gst_structure_get_name(structure);

      isRawVideo =
         capsName &&
         g_str_has_prefix(capsName, "video/x-raw");
   }

   if (isRawVideo) {
      gst_pad_link(newPad, sinkPad);
   }

   if (caps) {
      gst_caps_unref(caps);
   }

   gst_object_unref(sinkPad);
}

GstFlowReturn RtspPlayer::onNewSample(
   GstAppSink *sink,
   gpointer userData
)
{
   RtspPlayer *self = static_cast<RtspPlayer *>(userData);

   if (!self) {
      return GST_FLOW_ERROR;
   }

   GstSample *sample = gst_app_sink_pull_sample(sink);

   if (!sample) {
      return GST_FLOW_EOS;
   }

   GstCaps *caps = gst_sample_get_caps(sample);
   GstBuffer *buffer = gst_sample_get_buffer(sample);

   GstVideoInfo videoInfo;
   gst_video_info_init(&videoInfo);

   if (
      !caps ||
      !buffer ||
      !gst_video_info_from_caps(&videoInfo, caps)
   ) {
      gst_sample_unref(sample);
      return GST_FLOW_ERROR;
   }

   GstVideoFrame videoFrame;

   if (
      !gst_video_frame_map(
         &videoFrame,
         &videoInfo,
         buffer,
         GST_MAP_READ
      )
   ) {
      gst_sample_unref(sample);
      return GST_FLOW_ERROR;
   }

   const int width = GST_VIDEO_FRAME_WIDTH(&videoFrame);
   const int height = GST_VIDEO_FRAME_HEIGHT(&videoFrame);
   const int stride = GST_VIDEO_FRAME_PLANE_STRIDE(&videoFrame, 0);
   const uchar *data = static_cast<const uchar *>(
      GST_VIDEO_FRAME_PLANE_DATA(&videoFrame, 0)
   );

   const QImage wrappedFrame(
      data,
      width,
      height,
      stride,
      QImage::Format_ARGB32
   );

   const QImage copiedFrame = wrappedFrame.copy();

   gst_video_frame_unmap(&videoFrame);
   gst_sample_unref(sample);

   QPointer<VideoItem> videoItem = self->m_videoItem;

   QMetaObject::invokeMethod(
      self,
      [videoItem, copiedFrame]() {
         if (videoItem) {
            videoItem->setFrame(copiedFrame);
         }
      },
      Qt::QueuedConnection
   );

   return GST_FLOW_OK;
}
