//
// ZoneMinder Capture Daemon, $Date$, $Revision$
// Copyright (C) 2001-2008 Philip Coombes
//
// 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

zmc - The ZoneMinder Capture daemon

=head1 SYNOPSIS

 zmc -d <device_path>
 zmc --device <device_path>
 zmc -f <file_path>
 zmc --file <file_path>
 zmc -m <monitor_id>
 zmc --monitor <monitor_id>
 zmc -h
 zmc --help
 zmc -v
 zmc --version

=head1 DESCRIPTION

This binary's job is to sit on a video device and suck frames off it as fast as
possible, this should run at more or less constant speed.

=head1 OPTIONS

 -d, --device <device_path>         - For local cameras, device to access. e.g /dev/video0 etc
 -f, --file <file_path>           - For local images, jpg file to access.
 -m, --monitor_id             - ID of the monitor to analyse
 -h, --help                 - Display usage information
 -v, --version              - Print the installed version of ZoneMinder

=cut

*/

#include "zm.h"
#include "zm_camera.h"
#include "zm_db.h"
#include "zm_define.h"
#include "zm_fifo.h"
#include "zm_monitor.h"
#include "zm_signal.h"
#include "zm_time.h"
#include "zm_utils.h"

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

void Usage() {
  fprintf(stderr, "zmc -d <device_path> or -r <proto> -H <host> -P <port> -p <path> or -f <file_path> or -m <monitor_id>\n");

  fprintf(stderr, "Options:\n");
#if defined(BSD)
  fprintf(stderr, "  -d, --device <device_path> : For local cameras, device to access. E.g /dev/bktr0 etc\n");
#else
  fprintf(stderr, "  -d, --device <device_path> : For local cameras, device to access. E.g /dev/video0 etc\n");
#endif
  fprintf(stderr, "  -f, --file <file_path>     : For local images, jpg file to access.\n");
  fprintf(stderr, "  -m, --monitor <monitor_id> : For sources associated with a single monitor\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));

  const char *device = "";
  const char *protocol = "";
  const char *host = "";
  const char *port = "";
  const char *path = "";
  const char *file = "";
  int monitor_id = -1;

  static struct option long_options[] = {
    {"device", 1, nullptr, 'd'},
    {"protocol", 1, nullptr, 'r'},
    {"host", 1, nullptr, 'H'},
    {"port", 1, nullptr, 'P'},
    {"path", 1, nullptr, 'p'},
    {"file", 1, nullptr, 'f'},
    {"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, "d:H:P:p:f:m:h:v", long_options, &option_index);
    if ( c == -1 ) {
      break;
    }

    switch (c) {
    case 'd':
      device = optarg;
      break;
    case 'H':
      host = optarg;
      break;
    case 'P':
      port = optarg;
      break;
    case 'p':
      path = optarg;
      break;
    case 'f':
      file = optarg;
      break;
    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();
  }

  int modes = ( (device[0]?1:0) + (host[0]?1:0) + (file[0]?1:0) + (monitor_id > 0 ? 1 : 0) );
  if ( modes > 1 ) {
    fprintf(stderr, "Only one of device, host/port/path, file or monitor id allowed\n");
    Usage();
    exit(0);
  }

  if ( modes < 1 ) {
    fprintf(stderr, "One of device, host/port/path, file or monitor id must be specified\n");
    Usage();
    exit(0);
  }

  char log_id_string[32] = "";
  if ( device[0] ) {
    const char *slash_ptr = strrchr(device, '/');
    snprintf(log_id_string, sizeof(log_id_string), "zmc_d%s", slash_ptr?slash_ptr+1:device);
  } else if ( host[0] ) {
    snprintf(log_id_string, sizeof(log_id_string), "zmc_h%s", host);
  } else if ( file[0] ) {
    const char *slash_ptr = strrchr(file, '/');
    snprintf(log_id_string, sizeof(log_id_string), "zmc_f%s", slash_ptr?slash_ptr+1:file);
  } else {
    snprintf(log_id_string, sizeof(log_id_string), "zmc_m%d", monitor_id);
  }

  logInit(log_id_string);
  zmLoadStaticConfig();
  zmDbConnect();
  zmLoadDBConfig();
  logInit(log_id_string);

  HwCapsDetect();
#if HAVE_LIBCURL
  curl_global_init(CURL_GLOBAL_DEFAULT);
#endif  // HAVE_LIBCURL

  std::vector<std::shared_ptr<Monitor>> monitors;
#if ZM_HAS_V4L2
  if ( device[0] ) {
    monitors = Monitor::LoadLocalMonitors(device, Monitor::CAPTURE);
  } else
#endif  // ZM_HAS_V4L2
    if ( host[0] ) {
      if ( !port )
        port = "80";
      monitors = Monitor::LoadRemoteMonitors(protocol, host, port, path, Monitor::CAPTURE);
    } else if ( file[0] ) {
      monitors = Monitor::LoadFileMonitors(file, Monitor::CAPTURE);
    } else {
      std::shared_ptr<Monitor> monitor = Monitor::Load(monitor_id, true, Monitor::CAPTURE);
      if ( monitor ) {
        monitors.push_back(monitor);
      }
    }

  if (monitors.empty()) {
    Error("No monitors found");
    exit(-1);
  } else {
    Debug(2, "%zu monitors loaded", monitors.size());
  }

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

  struct sigaction sa;
  sa.sa_handler = SIG_IGN; //handle signal by ignoring
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = 0;
  if (sigaction(SIGCHLD, &sa, 0) == -1) {
    Error("Unable to set SIGCHLD to ignore. There may be zombies.");
  }

  int result = 0;
  int prime_capture_log_count = 0;

  while (!zm_terminate) {
    result = 0;

    for (const std::shared_ptr<Monitor> &monitor : monitors) {
      std::string sql = stringtf(
                          "INSERT INTO Monitor_Status (MonitorId,Status,CaptureFPS,AnalysisFPS,CaptureBandwidth)"
                          " VALUES (%u, 'Running',0,0,0) ON DUPLICATE KEY UPDATE Status='Running',CaptureFPS=0,AnalysisFPS=0,CaptureBandwidth=0",
                          monitor->Id());
      zmDbDo(sql);

      monitor->LoadCamera();

      while (!monitor->connect() and !zm_terminate) {
        Warning("Couldn't connect to monitor %d", monitor->Id());
        monitor->SetHeartbeatTime(std::chrono::system_clock::now());
        sleep(1);
      }
      if (zm_terminate) break;

      SystemTimePoint now = std::chrono::system_clock::now();
      monitor->SetStartupTime(now);

      if (monitor->StartupDelay() > 0) {
        Debug(1, "Doing startup sleep for %ds", monitor->StartupDelay());
        std::this_thread::sleep_for(Seconds(monitor->StartupDelay()));
      }

      Seconds sleep_time = Seconds(0);
      while ((monitor->PrimeCapture() <= 0) and !zm_terminate) {
        if (prime_capture_log_count % 60) {
          logPrintf(Logger::ERROR + monitor->Importance(), "Failed to prime capture of initial monitor");
        } else {
          Debug(1, "Failed to prime capture of initial monitor");
        }

        prime_capture_log_count++;
        if (sleep_time < Seconds(ZM_WATCH_MAX_DELAY)) {
          sleep_time++;
        }

        std::this_thread::sleep_for(sleep_time);
        monitor->SetHeartbeatTime(std::chrono::system_clock::now());
      }
      if (zm_terminate) break;

      sql = stringtf(
              "INSERT INTO Monitor_Status (MonitorId,Status) VALUES (%u, 'Connected') ON DUPLICATE KEY UPDATE Status='Connected'",
              monitor->Id());
      zmDbDo(sql);
    }  // end foreach monitor

    if (zm_terminate) break;

    std::vector<SystemTimePoint> last_capture_times = std::vector<SystemTimePoint>(monitors.size());
    Microseconds sleep_time = Microseconds(0);

    while (!zm_terminate) {
      for (size_t i = 0; i < monitors.size(); i++) {
        monitors[i]->CheckAction();

        if (monitors[i]->Capturing() == Monitor::CAPTURING_ONDEMAND) {
          SystemTimePoint now = std::chrono::system_clock::now();
          monitors[i]->SetHeartbeatTime(now);

          time_t last_viewed = monitors[i]->getLastViewed();
          int64 since_last_view = static_cast<int64>(std::chrono::duration_cast<Seconds>(now.time_since_epoch()).count()) - last_viewed;
          Debug(1, "Last view %jd= %" PRId64 " seconds since last view", last_viewed, since_last_view);
          if (((!last_viewed) or (since_last_view > 10)) and (monitors[i]->GetLastWriteIndex() != -1)) {
            if (monitors[i]->getCamera()->isPrimed()) {
              monitors[i]->Pause();
            }
            std::this_thread::sleep_for(Microseconds(100000));
            result = 0;
            continue;
          } else if (!monitors[i]->getCamera()->isPrimed()) {
            if (1 > (result = monitors[i]->Play())) {
              Debug(1, "Failed to play");
              break;
            }
          }
        } // end if ONDEMAND

        if (monitors[i]->PreCapture() < 0) {
          Error("Failed to pre-capture monitor %d %s (%zu/%zu)",
                monitors[i]->Id(), monitors[i]->Name(), i + 1, monitors.size());
          result = -1;
          break;
        }
        if (monitors[i]->Capture() < 0) {
          if (!zm_terminate)
            logPrintf(Logger::ERROR + monitors[i]->Importance(), "Failed to capture image from monitor %d %s (%zu/%zu)",
                monitors[i]->Id(), monitors[i]->Name(), i + 1, monitors.size());
          result = -1;
          break;
        }
        if (monitors[i]->PostCapture() < 0) {
          Error("Failed to post-capture monitor %d %s (%zu/%zu)",
                monitors[i]->Id(), monitors[i]->Name(), i + 1, monitors.size());
          result = -1;
          break;
        }

        if (!result) monitors[i]->UpdateFPS();

        SystemTimePoint now = std::chrono::system_clock::now();
        monitors[i]->SetHeartbeatTime(now);

        // capture_delay is the amount of time we should sleep in useconds to achieve the desired framerate.
        Microseconds delay = (monitors[i]->GetState() == Monitor::ALARM) ? monitors[i]->GetAlarmCaptureDelay()
                             : monitors[i]->GetCaptureDelay();
        if (delay != Microseconds(0)) {
          if (last_capture_times[i].time_since_epoch() != Seconds(0)) {
            Microseconds delta_time = std::chrono::duration_cast<Microseconds>(now - last_capture_times[i]);

            // You have to add back in the previous sleep time
            sleep_time = delay - (delta_time - sleep_time);

            // Limit negative since feedback - if an error occurred can loose seconds and then run too fast for a while so rather accept the frame loss
            // Or if the camera exposure is longer than intended FPS (eg at night) don't build up a huge negative that will disable the FPS limit later and OOM (eg at dawn)
            if (sleep_time < (delay * -1)) {
              sleep_time = delay * -1;
            }

            Debug(4,
                  "Sleep time is %" PRIi64 " us from now: %.3f s last: %.3f s delta % " PRIi64 " us delay: %" PRIi64 " us",
                  static_cast<int64>(Microseconds(sleep_time).count()),
                  FPSeconds(now.time_since_epoch()).count(),
                  FPSeconds(last_capture_times[i].time_since_epoch()).count(),
                  static_cast<int64>(delta_time.count()),
                  static_cast<int64>(Microseconds(delay).count()));

            // When negative the capture took more than the configured FPS time and no need to sleep
            if (sleep_time > Microseconds(0)) {
              std::this_thread::sleep_for(sleep_time);
            }
          }  // end if has a last_capture time

          last_capture_times[i] = now;
        }  // end if delay
      }  // end foreach n_monitors

      if ((result < 0) or zm_reload) {
        // Failure, try reconnecting
        break;
      }
    }  // end while ! zm_terminate and connected

    for (std::shared_ptr<Monitor> & monitor : monitors) {
      monitor->SetHeartbeatTime(std::chrono::system_clock::now());
      monitor->Close();
      monitor->SetHeartbeatTime(std::chrono::system_clock::now());
      monitor->disconnect();
    }

    if (zm_reload) {
      zmLoadStaticConfig();
      zmLoadDBConfig();
      for (std::shared_ptr<Monitor> &monitor : monitors) {
        monitor->Reload();
      }
      logTerm();
      logInit(log_id_string);

      zm_reload = false;
    }  // end if zm_reload
  }  // end while ! zm_terminate outer connection loop

  for (std::shared_ptr<Monitor> &monitor : monitors) {
    std::string sql = stringtf(
                        "INSERT INTO Monitor_Status (MonitorId,Status) VALUES (%u, 'NotRunning') ON DUPLICATE KEY UPDATE Status='NotRunning',CaptureFPS=0,AnalysisFPS=0,CaptureBandwidth=0",
                        monitor->Id());
    zmDbDo(sql);
  }
  monitors.clear();
  Debug(1, "Cleared monitors");

  Image::Deinitialise();
#if HAVE_LIBCURL
  curl_global_cleanup();
#endif  // HAVE_LIBCURL
  Debug(1, "terminating");
  dbQueue.stop();
  zmDbClose();
  logTerm();

  return zm_terminate ? 0 : result;
}
