//
// ZoneMinder RTSP Daemon
// Copyright (C) 2021 Isaac Connor
//
// 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 2
// 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, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//

/*

=head1 NAME

zm_rtsp_server - The ZoneMinder Server

=head1 SYNOPSIS

 zmc -m <monitor_id>
 zmc --monitor <monitor_id>
 zmc -h
 zmc --help
 zmc -v
 zmc --version

=head1 DESCRIPTION

This binary's job is to connect to fifo's provided by local zmc processes
and provide that stream over rtsp

=head1 OPTIONS

 -m, --monitor_id           - ID of a monitor to stream
 -h, --help                 - Display usage information
 -v, --version              - Print the installed version of ZoneMinder

=cut

*/

#include "zm.h"
#include "zm_db.h"
#include "zm_define.h"
#include "zm_monitor.h"
#include "zm_rtsp_server_authenticator.h"
#include "zm_rtsp_server_fifo_h264_source.h"
#include "zm_rtsp_server_fifo_adts_source.h"
#include "zm_signal.h"
#include "zm_time.h"
#include "zm_utils.h"

#include <getopt.h>
#include <iostream>
#include <vector>


#include "xop/RtspServer.h"

void Usage() {
  fprintf(stderr, "zm_rtsp_server -m <monitor_id>\n");

  fprintf(stderr, "Options:\n");
  fprintf(stderr, "  -m, --monitor <monitor_id> : We default to all monitors use this to specify just one\n");
  fprintf(stderr, "  -h, --help                 : This screen\n");
  fprintf(stderr, "  -v, --version              : Report the installed version of ZoneMinder\n");
  exit(0);
}

int main(int argc, char *argv[]) {
  self = argv[0];

  srand(getpid() * time(nullptr));

  int monitor_id = -1;

  static struct option long_options[] = {
    {"monitor", 1, nullptr, 'm'},
    {"help", 0, nullptr, 'h'},
    {"version", 0, nullptr, 'v'},
    {nullptr, 0, nullptr, 0}
  };

  while (1) {
    int option_index = 0;
    int c = getopt_long(argc, argv, "m:h:v", long_options, &option_index);
    if (c == -1)
      break;

    switch (c) {
    case 'm':
      monitor_id = atoi(optarg);
      break;
    case 'h':
    case '?':
      Usage();
      break;
    case 'v':
      std::cout << ZM_VERSION << "\n";
      exit(0);
    default:
      // fprintf(stderr, "?? getopt returned character code 0%o ??\n", c);
      break;
    }
  }

  if (optind < argc) {
    fprintf(stderr, "Extraneous options, ");
    while (optind < argc)
      printf("%s ", argv[optind++]);
    printf("\n");
    Usage();
  }

  const char *log_id_string = "zm_rtsp_server";
  ///std::string log_id_string = std::string("zm_rtsp_server");
  ///if ( monitor_id > 0 ) log_id_string += stringtf("_m%d", monitor_id);

  logInit(log_id_string);
  zmLoadStaticConfig();
  zmDbConnect();
  zmLoadDBConfig();
  logInit(log_id_string);
  if (!config.min_rtsp_port) {
    Debug(1, "Not starting RTSP server because min_rtsp_port not set");
    exit(-1);
  }

  HwCapsDetect();

  std::string where = "`Deleted` = 0 AND `Capturing` != 'None' AND `RTSPServer` != false";
  if (staticConfig.SERVER_ID)
    where += stringtf(" AND `ServerId`=%d", staticConfig.SERVER_ID);
  if (monitor_id > 0)
    where += stringtf(" AND `Id`=%d", monitor_id);

  Info("Starting RTSP Server version %s", ZM_VERSION);
  zmSetDefaultHupHandler();
  zmSetDefaultTermHandler();
  zmSetDefaultDieHandler();

  std::shared_ptr<xop::EventLoop> eventLoop(new xop::EventLoop());
  std::shared_ptr<xop::RtspServer> rtspServer = xop::RtspServer::Create(eventLoop.get());
  rtspServer->SetVersion("ZoneMinder RTSP Server");

  if (config.opt_use_auth) {
    std::shared_ptr<ZM_RtspServer_Authenticator> authenticator(new ZM_RtspServer_Authenticator());
    rtspServer->SetAuthenticator(authenticator);
  }

  if (!rtspServer->Start("0.0.0.0", config.min_rtsp_port)) {
    Debug(1, "Failed starting RTSP server on port %d", config.min_rtsp_port);
    exit(-1);
  }

  std::unordered_map<unsigned int, xop::MediaSession *> sessions;
  std::unordered_map<unsigned int, ZoneMinderFifoSource *> video_sources;
  std::unordered_map<unsigned int, ZoneMinderFifoSource *> audio_sources;
  std::unordered_map<unsigned int, std::shared_ptr<Monitor>> monitors;

  while (!zm_terminate) {
    std::unordered_map<unsigned int, std::shared_ptr<Monitor>> old_monitors = monitors;

    std::vector<std::shared_ptr<Monitor>> new_monitors = Monitor::LoadMonitors(where, Monitor::QUERY);
    for (const auto &monitor : new_monitors) {
      auto old_monitor_it = old_monitors.find(monitor->Id());
      if (old_monitor_it != old_monitors.end()
          and
          (old_monitor_it->second->GetRTSPStreamName() == monitor->GetRTSPStreamName())
         ) {
        Debug(1, "Found monitor in oldmonitors, clearing it");
        old_monitors.erase(old_monitor_it);
      } else {
        Debug(1, "Adding monitor %d to monitors", monitor->Id());
        monitors[monitor->Id()] = monitor;
      }
    }
    // Remove monitors that are no longer doing rtsp
    for (auto it = old_monitors.begin(); it != old_monitors.end(); ++it) {
      auto mid = it->first;
      auto &monitor = it->second;
      Debug(1, "Removing %d %s from monitors", monitor->Id(), monitor->Name());
      monitors.erase(mid);
      if (sessions.find(mid) != sessions.end()) {
        if (video_sources.find(monitor->Id()) != video_sources.end()) {
          delete video_sources[monitor->Id()];
          video_sources.erase(monitor->Id());
        }
        if (audio_sources.find(monitor->Id()) != audio_sources.end()) {
          delete audio_sources[monitor->Id()];
          audio_sources.erase(monitor->Id());
        }
        rtspServer->RemoveSession(sessions[mid]->GetMediaSessionId());
        sessions.erase(mid);
      }
    }

    for (auto it = monitors.begin(); it != monitors.end(); ++it) {
      auto &monitor = it->second;

      if (!monitor->ShmValid()) {
        Debug(1, "!ShmValid");
        monitor->disconnect();
        if (!monitor->connect()) {
          Warning("Couldn't connect to monitor %d", monitor->Id());
          if (sessions.find(monitor->Id()) != sessions.end()) {
            if (video_sources.find(monitor->Id()) != video_sources.end()) {
              video_sources.erase(monitor->Id());
            }
            if (audio_sources.find(monitor->Id()) != audio_sources.end()) {
              audio_sources.erase(monitor->Id());
            }
            rtspServer->RemoveSession(sessions[monitor->Id()]->GetMediaSessionId());
            sessions.erase(monitor->Id());
          }
          monitor->Reload();  // This is to pickup change of colours, width, height, etc
          continue;
        }  // end if failed to connect
      }  // end if !ShmValid

      if (sessions.end() == sessions.find(monitor->Id())) {
        Debug(1, "Monitor not found in sessions, opening it");
        std::string videoFifoPath = monitor->GetVideoFifoPath();
        if (videoFifoPath.empty()) {
          Debug(1, "video fifo is empty. Skipping.");
          continue;
        }

        std::string streamname = monitor->GetRTSPStreamName();
        xop::MediaSession *session = sessions[monitor->Id()] = xop::MediaSession::CreateNew(streamname);
        if (!session) {
          Error("Unable to create session for %s", streamname.c_str());
          continue;
        }
        session->AddNotifyConnectedCallback([] (xop::MediaSessionId sessionId, const std::string &peer_ip, uint16_t peer_port) {
          Debug(1, "RTSP client connect, ip=%s, port=%hu", peer_ip.c_str(), peer_port);
        });

        session->AddNotifyDisconnectedCallback([](xop::MediaSessionId sessionId, const std::string &peer_ip, uint16_t peer_port) {
          Debug(1, "RTSP client disconnect, ip=%s, port=%hu", peer_ip.c_str(), peer_port);
        });

        rtspServer->AddSession(session);
        //char *url = rtspServer->rtspURL(session);
        //Debug(1, "url is %s for stream %s", url, streamname.c_str());
        //delete[] url;
        monitors[monitor->Id()] = monitor;

        Debug(1, "Adding video fifo %s", videoFifoPath.c_str());
        ZoneMinderFifoVideoSource *videoSource = nullptr;

        if (std::string::npos != videoFifoPath.find("h264")) {
          session->AddSource(xop::channel_0, xop::H264Source::CreateNew());
          videoSource = new H264_ZoneMinderFifoSource(rtspServer, session->GetMediaSessionId(), xop::channel_0, videoFifoPath);
        } else if (
          std::string::npos != videoFifoPath.find("hevc")
          or
          std::string::npos != videoFifoPath.find("h265")) {
          session->AddSource(xop::channel_0, xop::H265Source::CreateNew());
          videoSource = new H265_ZoneMinderFifoSource(rtspServer, session->GetMediaSessionId(), xop::channel_0, videoFifoPath);
        } else {
          Warning("Unknown format in %s", videoFifoPath.c_str());
        }
        if (videoSource == nullptr) {
          Error("Unable to create source for %s", videoFifoPath.c_str());
          rtspServer->RemoveSession(sessions[monitor->Id()]->GetMediaSessionId());
          sessions.erase(monitor->Id());
          continue;
        }
        video_sources[monitor->Id()] = videoSource;
        videoSource->setWidth(monitor->Width());
        videoSource->setHeight(monitor->Height());

        std::string audioFifoPath = monitor->GetAudioFifoPath();
        if (audioFifoPath.empty()) {
          Debug(1, "audio fifo is empty. Skipping.");
          continue;
        }
        Debug(1, "Adding audio fifo %s", audioFifoPath.c_str());

        ZoneMinderFifoAudioSource *audioSource = nullptr;

        if (std::string::npos != audioFifoPath.find("aac")) {
          Debug(1, "Adding aac source at %dHz %d channels",
                monitor->GetAudioFrequency(), monitor->GetAudioChannels());
          session->AddSource(xop::channel_1, xop::AACSource::CreateNew(
                               monitor->GetAudioFrequency(),
                               monitor->GetAudioChannels(),
                               false /* has_adts */));
          audioSource = new ADTS_ZoneMinderFifoSource(rtspServer,
              session->GetMediaSessionId(), xop::channel_1, audioFifoPath);
          audioSource->setFrequency(monitor->GetAudioFrequency());
          audioSource->setChannels(monitor->GetAudioChannels());
        } else if (std::string::npos != audioFifoPath.find("pcm_alaw")) {
          Debug(1, "Adding G711A source at %dHz %d channels",
                monitor->GetAudioFrequency(), monitor->GetAudioChannels());
          session->AddSource(xop::channel_1, xop::G711ASource::CreateNew());
          audioSource = new ADTS_ZoneMinderFifoSource(rtspServer,
              session->GetMediaSessionId(), xop::channel_1, audioFifoPath);
          audioSource->setFrequency(monitor->GetAudioFrequency());
          audioSource->setChannels(monitor->GetAudioChannels());
        } else {
          Warning("Unknown format in %s", audioFifoPath.c_str());
        }
        if (audioSource == nullptr) {
          Error("Unable to create source");
        }
        audio_sources[monitor->Id()] = audioSource;
      }  // end if ! sessions[monitor->Id()]
      if (sessions[monitor->Id()]->GetNumClient() > 0) {
        SystemTimePoint now = std::chrono::system_clock::now();
        monitor->setLastViewed(now);
      }
    }  // end foreach monitor

    sleep(10);

    if (zm_reload) {
      logTerm();
      logInit(log_id_string);
      zm_reload = false;
    }  // end if zm_reload
  } // end while !zm_terminate

  Info("RTSP Server shutting down");

  for (const std::pair<const unsigned int, std::shared_ptr<Monitor>> &mon_pair : monitors) {
    unsigned int i = mon_pair.first;
    if (video_sources.find(i) != video_sources.end()) {
      delete video_sources[i];
    }
    if (audio_sources.find(i) != audio_sources.end()) {
      delete audio_sources[i];
    }
    if (sessions.find(i) != sessions.end()) {
      Debug(1, "Removing session for %s", mon_pair.second->Name());
      rtspServer->RemoveSession(sessions[i]->GetMediaSessionId());
      sessions.erase(i);
    }
  }  // end foreach monitor

  rtspServer->Stop();

  sessions.clear();

  Image::Deinitialise();
  logTerm();
  zmDbClose();

  return 0;
}
