/**
 * \file test_play.cpp
 * \brief simple client-sdk testing tool with additional options of login and getting media URL
 *
 * Ubuntu 20.04 requirements:
 *    sudo apt install -y g++ libz-dev libxml2-dev libva2 libva-drm2 libva-X11-2
 *
 * Compile with:
 *    g++ -o test_play test_play.cpp -D_GLIBCXX_USE_CXX11_ABI=0 -DVN_SDK_STATIC -Wall -O0 -ggdb -I<path to vn-client-sdk/prebuilt/include/core/include> -I<path to vn-client-sdk/prebuilt/include/core/impl> <path to vn-client-sdk/prebuilt/lib/x86_64-linux/libVNMediaClient.a -lpthread -ldl -lrt -lz -lxml2
*/

#ifdef WIN32
#include <conio.h>
#include <winsock2.h>
#include <windows.h>
#else
#include <termios.h>
#include <unistd.h>
#include <pthread.h>
#endif

#include <sys/time.h>
#include <getopt.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <string>
#include <stdlib.h>
#include <iostream>

#include "vn_player.h"
#include "MediaClient.h"
#include "MediaStreamHandler.h"

using namespace std;

#ifdef WIN32
#define sleep(seconds) Sleep(seconds * 1000)
string getpass(const char *prompt, bool show_asterisk=true)
{
  const char BACKSPACE = 8;
  const char RETURN = 13;

  string password;
  unsigned char ch = 0;

  cout << prompt;

  while((ch = getch()) != RETURN)
  {
      if(ch == BACKSPACE) {
          if(password.length() != 0) {
              if(show_asterisk)
                  cout << "\b \b";
              password.resize(password.length() - 1);
          }
      } else if(ch==0 || ch==224) { // handle escape sequences
          getch(); // ignore non printable chars
          continue;
      } else {
          password += ch;
          if(show_asterisk)
              cout << '*';
      }
  }

  cout <<endl;
  return password;
}
#else
unsigned int (*_sleep_)(unsigned int) = &sleep;
#define sleep(seconds) _sleep_(seconds)

int getch() {
    int ch;
    struct termios t_old, t_new;

    tcgetattr(STDIN_FILENO, &t_old);
    t_new = t_old;
    t_new.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &t_new);

    ch = getchar();

    tcsetattr(STDIN_FILENO, TCSANOW, &t_old);
    return ch;
}

string getpass(const char *prompt, bool show_asterisk = true)
{
  const char BACKSPACE = 127;
  const char RETURN = 10;

  string password;
  unsigned char ch = 0;

  cout << prompt;

  while((ch = getch()) != RETURN)
  {
      if(ch == BACKSPACE) {
          if(password.length() != 0) {
              if(show_asterisk)
                  cout << "\b \b";
              password.resize(password.length() - 1);
          }
      } else {
          password+=ch;
          if(show_asterisk)
              cout << '*';
      }
  }

  cout << endl;
  return password;
}
#endif

static bool verbose;
static float speed = 1.f;
static unsigned buffer_len = 0; // in ms
static bool disable_decoder = false;
static bool local_streaming = false;
static bool use_hw_decoder = false;
static bool use_rtp_over_tcp = false;

class Media_Player
{
public:
    Media_Player(unsigned player_num);
    ~Media_Player();

    void open(const char *url, float speed);
    void play();
    void pause();
    void resume();
    void step_mode(bool value);
    void start_recording();
    void end_recording();

    VN_PLAYER_STREAM_STATE state;
    std::string filename;
    unsigned record_time;
    unsigned record_loop;

private:
    static void new_frame(const vn_player_frame_t*, const vn_player_cache_boundaries_t*, int buf_num, void *client_data);

    static void buffer_changed(const vn_player_cache_boundaries_t*, void *client_data);
    
    static void state_changed(VN_PLAYER_STREAM_STATE, const vn_player_result_status_t*, void *client_data);

    static void new_stream(const vn_player_stream_info_t*, void *client_data);

    static void stream_removed(const vn_player_stream_info_t*, void *client_data);

    static void recording_status_changed(const vn_player_recording_status_t* status, void* client_data);

    static void msg_log(const char *msg, void *client_data);

private:
#ifdef WIN32
    static DWORD WINAPI stop_record_func(LPVOID arg);
#else
    static void* stop_record_func(void* arg);
#endif
    void stop_recording();

    struct vn_player_config_t conf_;
    struct vn_player_context_t *ctx_;
    unsigned frame_num_;
    unsigned player_num_;
    bool recording_thread_active_;
    bool recording_started_;
    bool destroying_;
#ifdef WIN32
    HANDLE recording_thread_handle_;
#else
    pthread_t recording_thread_;
#endif
};

Media_Player::Media_Player(unsigned player_num)
    : state(IDLE), record_time(5), record_loop(0), frame_num_(0), player_num_(player_num)
    , recording_thread_active_(false), recording_started_(false), destroying_(false)
    #ifdef WIN32
    , recording_thread_handle_(NULL)
    #endif
{
    memset(&conf_, 0, sizeof(conf_));
    conf_.cache_size = 0;

    if (disable_decoder)
        conf_.decoder_type = DECODER_NONE;
    else
        conf_.decoder_type = use_hw_decoder ? DECODER_HW : DECODER_SW;
    conf_.pixel_format = PIX_NONE; // other possible values: PIX_RGB24, PIX_YUV420P...
            conf_.rtp_transport = use_rtp_over_tcp? RTP_TRANSPORT_TCP : RTP_TRANSPORT_UDP;
    conf_.client_data = this;
    conf_.buffer_len  = buffer_len;

    struct vn_player_callbacks_t callbacks = {};
    callbacks.buffer_changed = buffer_changed;
    callbacks.new_frame      = new_frame;
    callbacks.new_stream     = new_stream;
    callbacks.state_changed  = state_changed;
    callbacks.stream_removed = stream_removed;
    callbacks.recording_status_changed = recording_status_changed;
    callbacks.msg_log        = msg_log;

    ctx_ = vn_player_create(&conf_);
    vn_player_set_callbacks(ctx_, &callbacks);
}

Media_Player::~Media_Player()
{
    destroying_ = true;
    if (recording_thread_active_)
    {
#ifdef WIN32
        WaitForSingleObject(recording_thread_handle_, INFINITE);
#else
        pthread_join(recording_thread_, NULL);
#endif
    }
    vn_player_destroy(ctx_);
}

void Media_Player::open(const char *url, float speed)
{
    vn_player_open(ctx_, url, speed);
}

void Media_Player::play()
{
    vn_player_play(ctx_);
}

void Media_Player::pause()
{
    vn_player_pause(ctx_);
}

void Media_Player::resume()
{
    vn_player_resume(ctx_, 1);
}

void Media_Player::step_mode(bool value)
{
    vn_player_set_step_mode(ctx_, value);
}

#ifdef WIN32
DWORD WINAPI Media_Player::stop_record_func(LPVOID arg)
#else
void* Media_Player::stop_record_func(void* arg)
#endif
{
    Media_Player* player = (Media_Player*)arg;
    player->stop_recording();
    return 0;
}

void Media_Player::stop_recording()
{
    recording_thread_active_ = true;
    sleep(record_time);
    end_recording();
    bool startAgain = false;
    if (record_loop && !destroying_) {
        sleep(record_loop);
        if (state == PLAY_FROM_SERVER || state == PLAY_FROM_BUFFER || state == BUFFERING)
            startAgain = true;
        else
            recording_started_ = false; // Will be started on state change
    }
    recording_thread_active_ = false;
#ifdef WIN32	
    CloseHandle(recording_thread_handle_);
    recording_thread_handle_ = NULL;
#endif
    if (startAgain)
        start_recording();
}

void Media_Player::start_recording()
{
    if (recording_thread_active_)
        return;

    vn_player_start_recording(ctx_, filename.c_str(), NULL);

#ifdef WIN32
    recording_thread_handle_ = CreateThread(NULL, 0, &stop_record_func, this, 0, NULL);
    if (recording_thread_handle_ == NULL)
        printf("Could not create stop thread\n");
#else
    if (pthread_create(&recording_thread_, 0, stop_record_func, this))
        printf("Could not create stop thread\n");
#endif
}

void Media_Player::end_recording()
{
    vn_player_end_recording(ctx_);
}

void Media_Player::new_frame(const vn_player_frame_t *frame, const vn_player_cache_boundaries_t *bounds, int buf_num, void *client_data)
{
    Media_Player *p = static_cast<Media_Player*>(client_data);

    if (++p->frame_num_ % 100 == 0)
        printf("[%d] Got %d frames (%dx%d)\n", p->player_num_, p->frame_num_, frame->width, frame->height);

    if (verbose) {
        struct timeval tv;
        gettimeofday(&tv, 0);
        printf("%ld.%06d Got frame (%s). Time: %ld.%06d\n", tv.tv_sec, (int)tv.tv_usec, frame->stream_info->type == AUDIO ? "audio" : "video", frame->captured_time->tv_sec, (int)frame->captured_time->tv_usec);
        if (frame->objects_data)
            printf("Metadata: %s\n", frame->objects_data);
    }
}

void Media_Player::buffer_changed(const vn_player_cache_boundaries_t *bounds, void *client_data)
{
    //    printf("buffer_changed\n");
}

void Media_Player::state_changed(VN_PLAYER_STREAM_STATE state, const vn_player_result_status_t *result_status, void *client_data)
{
    Media_Player *p = static_cast<Media_Player*>(client_data);

    struct timeval tv;
    gettimeofday(&tv, 0);
    //if (result_status->error_code)
    printf("[%d] %ld.%06d State_changed: %s (%d). Result: %s(%d)\n", p->player_num_, tv.tv_sec, tv.tv_usec,
           vn_player_state_to_str(state), state, result_status->error_str, result_status->error_code);


    p->state = state;

    if (state == OPENED)
        p->play();

    if (state == PLAY_FROM_SERVER && !p->recording_started_ && !p->filename.empty()) {
        p->recording_started_ = true;
        p->start_recording();
    }

    /*if (result_status->error_code && result_status->error_code < 10000)
    {
        char err_buf[512];
        strerror_r(result_status->error_code, err_buf, sizeof(err_buf));
        printf("ERROR: %s\n", err_buf);
    }*/
}

void Media_Player::new_stream(const vn_player_stream_info_t *stream_info, void *client_data)
{
    printf("New stream: %s. Duration: %d ms\n", stream_info->type == AUDIO ? "audio" : "video", stream_info->duration);
    if (verbose && stream_info->metadata) {
        printf("Metadata:\n");
        for (int i = 0; stream_info->metadata[i] != NULL; i++) {
            if (stream_info->metadata[i]->key && stream_info->metadata[i]->value)
                printf("%s: %s\n", (char*)stream_info->metadata[i]->key, (char*)stream_info->metadata[i]->value);
        }
    }
}

void Media_Player::stream_removed(const vn_player_stream_info_t *stream_info, void *client_data)
{
    //	printf("stream_removed\n");
}

void Media_Player::recording_status_changed(const vn_player_recording_status_t* status, void* client_data)
{
    const char* statusText = NULL;
    switch (status->status)
    {
    case RECORDING_IDLE: statusText = "IDLE"; break;
    case RECORDING_STARTED: statusText = "STARTED"; break;
    case RECORDING_ENDING: statusText = "ENDING"; break;
    case RECORDING_ENDED: statusText = "ENDED"; break;
    case RECORDING_ERROR: statusText = "ERROR"; break;
    case RECORDING_SKIPPED: statusText = "SKIPPED"; break;
    }
    if(status->filename)
        printf("Recording status: %s (%s)", statusText, status->filename);
    else
        printf("Recording status: %s.", statusText);
    if (status->progress != 0)
        printf(" Progress: %d.", status->progress);
    if (status->error_str)
        printf(status->status == RECORDING_ERROR ? " Error: %s" : " Message: %s", status->error_str);
    printf("\n");
}

void Media_Player::msg_log(const char *msg, void *client_data)
{
    Media_Player *p = static_cast<Media_Player*>(client_data);

    if (verbose)
        printf("[%d] %s", p->player_num_, msg);
}

void print_usage(char* argv[], int exit_code)
{
    fprintf(stderr, "Usage: %s options\nOptions:\n", argv[0]);
    fprintf(stderr,
            "-u, --url              direct URL (if specified next 6 parameters are ignored)\n"
            "-c, --overcast-host    Overcast host to connect to\n"
            "-g, --realm_login      Realm login\n"
            "-w, --realm_passwd     Realm password (optional. Will be requested separately and masked with asterisks if missing)\n"
            "-a, --auth-token       Authorization token (could be used instead of login/passwd for realm)\n"
            "-o, --objid            Camera OBJID\n"
            "-s, --s-time           Archive start time\n"
            "-e, --e-time           Archive end time\n"
            "-m, --local-streaming  Enable local streaming (stream from cloud by default).\n"
            "-n, --players-num      Number of concurrent players (default 1)\n"
            "-b, --buffer-len       Playout buffer length (in ms)\n"
            "-t, --playback-time    Time of seconds for each iteration (default 10secs)\n"
            "-i, --iterations-num   Number of iterations (default infinity)\n"
            "-x, --use-hw-decoder   Use hardware decoder\n"
            "-p, --use-rtp-over-tcp Force RTP-over-TCP\n"
            "-d, --disable-decoding Disable decoding at all. -x does no matter\n"
            "-f, --filename         File name to record video to\n"
            "-r, --record-time      Time to record to file (in seconds) (default 5secs)\n"
            "-l, --record-loop      Recording loop interval (in seconds) (default 0secs - no loop)\n"
            "-y, --speed            Playback speed (float)\n"
            "-v, --verbose          Be verbose\n"
            "-h, --help             Show this help\n"
            "Examples:\n"
            "   ./test_play -c go.cavu.me -g kiev_titansys -o 4f27db5e-2f24-11eb-977d-42010a8e0004 -t 30\n"
            "   ./test_play -c go.cavu.me -a c4e061cec37aee8c58888f440a7e10995e268ad28b187a681663e23e6490ea09 -o ad466710-a4a1-11e8-98e9-42010a8e0005 -t 30\n"
            );

    exit(exit_code);
}

int main(int argc, char* argv[])
{
    if (argc < 2)
        print_usage(argv, EXIT_SUCCESS);

    int next_option;
    const char* const short_options = "u:c:g:w:a:o:s:e:mn:b:t:i:xpdf:r:l:y:vh";
    const struct option long_options[] = {
    {"help",             0, NULL, 'h'},
    {"verbose",          0, NULL, 'v'},
    {"url",              1, NULL, 'u'},
    {"overcast-host",    1, NULL, 'c'},
    {"realm_login",      1, NULL, 'g'},
    {"realm_passwd",     1, NULL, 'w'},
    {"auth-token",       1, NULL, 'a'},
    {"objid",            1, NULL, 'o'},
    {"s-time",           1, NULL, 's'},
    {"e-time",           1, NULL, 'e'},
    {"local-streaming",  0, NULL, 'm'},
    {"players-num",      1, NULL, 'n'},
    {"playback-time",    1, NULL, 't'},
    {"buffer-len",       1, NULL, 'b'},
    {"iterations-num",   1, NULL, 'i'},
    {"use-hw-decoder",   0, NULL, 'x'},
    {"use-rtp-over-tcp", 0, NULL, 'p'},
    {"disable-decoding", 0, NULL, 'd'},
    {"filename",         1, NULL, 'f'},
    {"record-time",      1, NULL, 'r'},
    {"record-loop",      1, NULL, 'l'},
    {"speed",            1, NULL, 'y'},
    {NULL,               0, NULL, 0} };

    std::string host, login, passwd, token, objid, def_url, filename;
    unsigned players_num = 1;
    unsigned playback_time = 10;
    unsigned iterations_num = ~0;
    unsigned record_time = 5;
    unsigned record_loop = 0;
    time_t s_time = 0, e_time = 0;

    do {
        next_option = getopt_long(argc, argv, short_options, long_options, NULL);

        switch (next_option)
        {
        case 'h': /* -h or --help */
            print_usage(argv, EXIT_SUCCESS);

        case 'p':
            use_rtp_over_tcp = true;
            break;
        case 'u':
            if (optarg)
                def_url = optarg;
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'c':
            if (optarg)
                host = optarg;
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'g':
            if (optarg)
                login = optarg;
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'w':
            if (optarg)
                passwd = optarg;
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'a':
            if (optarg)
                token = optarg;
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'o':
            if (optarg)
                objid = optarg;
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 's':
            if (optarg)
                s_time = (time_t)atoll(optarg);
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'e':
            if (optarg)
                e_time = (time_t)atoll(optarg);
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'm':
            local_streaming = true;
            break;

        case 'n':
            if (optarg)
                players_num = atol(optarg);
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'i':
            if (optarg)
                iterations_num = atol(optarg);
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 't':
            if (optarg)
                playback_time = atol(optarg);
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'v':
            verbose = true;
            break;

        case 'x':
            use_hw_decoder = true;
            break;

        case 'b':
            if (optarg)
                buffer_len = atol(optarg);
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'd':
            disable_decoder = true;
            break;

        case 'f':
            if (optarg)
                filename = optarg;
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'r':
            if (optarg)
                record_time = atol(optarg);
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'l':
            if (optarg)
                record_loop = atol(optarg);
            else print_usage(argv, EXIT_FAILURE);
            break;

        case 'y':
            if (optarg)
                speed = atof(optarg);
            else print_usage(argv, EXIT_FAILURE);
            break;

        case '?': /* The user specified an invalid option*/
            print_usage(argv, EXIT_FAILURE);

        case -1: /* Done with options */
            break;
        }
    } while (next_option != -1);

    // FIXME:
#if 0
    if((host.empty() && def_url.empty())
        || (token.empty() && (login.empty() || objid.empty())))
        print_usage(argv, EXIT_FAILURE);
#endif

    if(!host.empty() && !login.empty() && passwd.empty())
        passwd = getpass("Please enter realm password: ", true);

    videonext::media::MediaClient* mc = videonext::media::MediaClient::instance();
    videonext::media::ResultStatus result;

    if(!host.empty()) {
        mc->login(&result, host, 443, login, passwd, true, 60);
        if(result.errorCode != 0 && token.empty()) {
            fprintf(stderr, "login() result: %d (%s)\n",
                    result.errorCode, result.errorString.c_str());
            exit(EXIT_FAILURE);
        }
    }
    mc->setAuth(token, token);

    for (unsigned i = 1; i <= iterations_num; i++)
    {
        printf("Iteration: %d\n", i);
        Media_Player *player[256];
        string url(def_url);

        if(url.empty()) {
            if(s_time && e_time)
                mc->getMediaURL(&result, &url, objid, local_streaming, "", s_time, e_time);
            else
                mc->getMediaURL(&result, &url, objid, local_streaming);
            if(result.errorCode != 0 || result.errorString != "No error") {
                fprintf(stderr, "Fail to get media url for objid \"%s\": %d (%s)\n",
                        objid.c_str(), result.errorCode, result.errorString.c_str());
                exit(EXIT_FAILURE);
            }
        }

        for (unsigned p = 0; p < players_num; p++)
        {
            player[p] = new Media_Player(p);
            if (!filename.empty())
            {
                std::string fname = filename;
                if (players_num > 1)
                {
                    size_t pos = filename.find_last_of('.');
                    std::string name = pos != std::string::npos ? filename.substr(0, pos) : filename;
                    std::string ext = pos != std::string::npos ? filename.substr(pos) : "";
                    char num[64];
                    snprintf(num, sizeof(num), "%d", p);
                    fname = name + num + ext;
                }
                player[p]->filename = fname;
                player[p]->record_time = record_time;
                player[p]->record_loop = record_loop;
            }

            player[p]->open(url.c_str(), speed);
        }

        sleep(playback_time);

        for (unsigned p = 0; p < players_num; p++)
            delete player[p];
    }

    sleep(3);
    return 0;
}
