/*
 * CINELERRA
 * Copyright (C) 2016-2020 William Morrow
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <math.h>
#include <pthread.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/XShm.h>

extern "C" {
#include "libavfilter/buffersrc.h"
#include "libavfilter/buffersink.h"
#include "libavformat/avformat.h"
#include "libavformat/avio.h"
#include "libavcodec/avcodec.h"
#include "libavfilter/avfilter.h"
#include "libavutil/avutil.h"
#include "libavutil/imgutils.h"
#include "libavutil/opt.h"
#include "libavutil/pixdesc.h"
#include "libswresample/swresample.h"
#include "libswscale/swscale.h"
}

#ifdef FFMPEG3
#define url filename
#else
#define av_register_all(s)
#define avfilter_register_all(s)
#endif

int done = 0;
int64_t tm = 0, tn = 0;

void sigint(int n)
{
  done = 1;
}

class gg_window
{
public:
  gg_window(Display *display, int x, int y, int w, int h);
  ~gg_window();
  Display *display;
  Window win;
  GC gc;
  int x, y, w, h;
  void show();
  void hide();
};

gg_window::gg_window(Display *display, int x, int y, int w, int h)
{
  this->display = display;
  this->x = x;  this->y = y;
  this->w = w;  this->h = h;

  Window root = DefaultRootWindow(display);
  Screen *screen = DefaultScreenOfDisplay(display);
  Visual *visual = DefaultVisualOfScreen(screen);
  int depth = DefaultDepthOfScreen(screen);
  int border = 0;
  unsigned long gcmask = GCGraphicsExposures;
  XGCValues gcvalues;
  gcvalues.graphics_exposures = 0;
  gc = XCreateGC(display, root, gcmask, &gcvalues);

  XSetWindowAttributes attributes;
  attributes.background_pixel = BlackPixel(display, DefaultScreen(display));
  attributes.border_pixel = WhitePixel(display, DefaultScreen(display));
  attributes.event_mask =
    EnterWindowMask | LeaveWindowMask |
    ButtonPressMask | ButtonReleaseMask |
    PointerMotionMask | FocusChangeMask;
  int valueMask = CWBackPixel | CWBorderPixel | CWEventMask;
  this->win = XCreateWindow(display, root, x, y, w, h, border, depth,
      InputOutput, visual, valueMask, &attributes);
}

gg_window::~gg_window()
{
  XFreeGC(display, gc);
  XDestroyWindow(display, win);
}

void gg_window::show()
{
  XMapWindow(display,win);
  XFlush(display);
}
void gg_window::hide()
{
  XUnmapWindow(display,win);
  XFlush(display);
}

class gg_ximage
{
public:
  Display *display;
  XShmSegmentInfo shm_info;
  XImage *ximage;
  int w, h;
  unsigned char *data;
  int shm, sz;
  uint32_t lsb[3];
  gg_ximage(Display *display, int w, int h, int shm);
  ~gg_ximage();

  void put_image(gg_window &gw);
};

gg_ximage::gg_ximage(Display *display, int w, int h, int shm)
{
  this->display = display;
  this->w = w;  this->h = h;
  this->shm = shm;

  ximage = 0;  sz = 0;  data = 0;
  Screen *screen = DefaultScreenOfDisplay(display);
  Visual *visual = DefaultVisualOfScreen(screen);
  int depth = DefaultDepthOfScreen(screen);

  if( shm ) {
    ximage = XShmCreateImage(display, visual, depth, ZPixmap, (char*)NULL, &shm_info, w, h);
// Create shared memory
    sz = h * ximage->bytes_per_line;
    shm_info.shmid = shmget(IPC_PRIVATE, sz + 8, IPC_CREAT | 0777);
    if(shm_info.shmid < 0) perror("shmget");
    data = (unsigned char *)shmat(shm_info.shmid, NULL, 0);
// This causes it to automatically delete when the program exits.
    shmctl(shm_info.shmid, IPC_RMID, 0);
    ximage->data = shm_info.shmaddr = (char*)data;
    shm_info.readOnly = 0;
// Get the real parameters
    if(!XShmAttach(display, &shm_info)) perror("XShmAttach");
  }
  else {
      ximage = XCreateImage(display, visual, depth, ZPixmap, 0, (char*)data, w, h, 8, 0);
      sz = h * ximage->bytes_per_line;
      data = new unsigned char[sz+8];
  }
  memset(data, 0, sz);
  ximage->data = (char*) data;
  lsb[0] = ximage->red_mask & ~(ximage->red_mask<<1);
  lsb[1] = ximage->green_mask & ~(ximage->green_mask<<1);
  lsb[2] = ximage->blue_mask & ~(ximage->blue_mask<<1);
}

gg_ximage::~gg_ximage()
{
  if( shm ) {
    data = 0;
    ximage->data = 0;
    XDestroyImage(ximage);
    XShmDetach(display, &shm_info);
    XFlush(display);
    shmdt(shm_info.shmaddr);
  }
  else {
    delete [] data;
    data = 0;
    ximage->data = 0;
    XDestroyImage(ximage);
  }
}

void gg_ximage::put_image(gg_window &gw)
{
  Display *display = gw.display;
  Window win = gw.win;
  GC gc = gw.gc;
  if( shm )
    XShmPutImage(display, win, gc, ximage, 0,0, 0,0,w,h, 0);
  else
    XPutImage(display, win, gc, ximage, 0,0, 0,0,w,h); 
  XFlush(display);
}

class gg_thread
{
public:
  pthread_t tid;
  gg_window &gw;
  gg_ximage *imgs[2], *img;
  int active, done;
  gg_thread(gg_window &gw, int shm) ;
  ~gg_thread();

  static void *entry(void *t);
  void start();
  void *run();
  void join();
  void post(gg_ximage *ip);
  gg_ximage *next_img();

  pthread_mutex_t draw;
  void draw_lock() { pthread_mutex_lock(&draw); }
  void draw_unlock() { pthread_mutex_unlock(&draw); }
};

gg_thread::gg_thread(gg_window &gw, int shm)
 : gw(gw)
{
  imgs[0] = new gg_ximage(gw.display, gw.w, gw.h, shm);
  imgs[1] = new gg_ximage(gw.display, gw.w, gw.h, shm);
  done = -1;
  img = 0;  active = 0;
  pthread_mutex_init(&draw, 0);
}

gg_thread::~gg_thread()
{
  delete imgs[0];
  delete imgs[1];
  pthread_mutex_destroy(&draw);
}

void *gg_thread::entry(void *t)
{
  return ((gg_thread*)t)->run();
}

void gg_thread::start()
{
  pthread_attr_t attr;
  pthread_attr_init(&attr);
  pthread_attr_setinheritsched(&attr, PTHREAD_INHERIT_SCHED);
  done = 0;
  pthread_create(&tid, &attr, &entry, this);
  pthread_attr_destroy(&attr);
}
void gg_thread::join()
{
  done = 1;
  pthread_join(tid, 0);
}

void *gg_thread::run()
{
  while( !done ) {
    if( XPending(gw.display) ) {
      XEvent xev;
      XNextEvent(gw.display, &xev);
      switch( xev.type ) {
      case KeyPress:
      case KeyRelease:
      case ButtonPress:
      case Expose:
        break;
      }
      continue;
    }

    if( !img ) { usleep(10000);  continue; }
    img->put_image(gw);
    img = 0;
    draw_unlock();
  }
  return (void*)0;
}

gg_ximage *gg_thread::next_img()
{
  gg_ximage *ip = imgs[active];
  active ^= 1;
  return ip;
}

void gg_thread::post(gg_ximage *ip)
{
  this->img = ip;
}


class ffcmpr {
public:
  ffcmpr();
  ~ffcmpr();
  AVPacket ipkt;
  AVFormatContext *fmt_ctx;
  AVFrame *ipic;
  AVStream *st;
  AVCodecContext *ctx;
  AVPixelFormat pix_fmt;
  double frame_rate;
  int width, height;
  int need_packet, eof;
  int open_decoder(const char *filename, int vid_no);
  void close_decoder();
  AVFrame *read_frame();
};

ffcmpr::ffcmpr()
{
  av_init_packet(&this->ipkt);
  this->fmt_ctx = 0;
  this->ipic = 0;
  this->st = 0;
  this->ctx = 0 ;
  this->frame_rate = 0;
  this->need_packet = 0;
  this->eof = 0;
  this->pix_fmt = AV_PIX_FMT_NONE;
  width = height = 0;
}

void ffcmpr::close_decoder()
{
  av_packet_unref(&ipkt);
  if( !fmt_ctx ) return;
  if( ctx ) avcodec_free_context(&ctx);
  avformat_close_input(&fmt_ctx);
  av_frame_free(&ipic);
}

ffcmpr::~ffcmpr()
{
  close_decoder();
}

int ffcmpr::open_decoder(const char *filename, int vid_no)
{
  struct stat fst;
  if( stat(filename, &fst) ) return 1;
  fmt_ctx = 0;
  AVDictionary *fopts = 0;
  av_register_all();
  av_dict_set(&fopts, "formatprobesize", "5000000", 0);
  av_dict_set(&fopts, "scan_all_pmts", "1", 0);
  av_dict_set(&fopts, "threads", "auto", 0);
  int ret = avformat_open_input(&fmt_ctx, filename, NULL, &fopts);
  av_dict_free(&fopts);
  if( ret < 0 ) {
    fprintf(stderr,"file open failed: %s\n", filename);
    return ret;
  }
  ret = avformat_find_stream_info(fmt_ctx, NULL);
  if( ret < 0 ) {
    fprintf(stderr,"file probe failed: %s\n", filename);
    return ret;
  }

  this->st = 0;
  for( int i=0; !this->st && ret>=0 && i<(int)fmt_ctx->nb_streams; ++i ) {
    AVStream *fst = fmt_ctx->streams[i];
    AVMediaType type = fst->codecpar->codec_type;
    if( type != AVMEDIA_TYPE_VIDEO ) continue;
    if( --vid_no < 0 ) this->st = fst;
  }

  AVCodecID codec_id = st->codecpar->codec_id;
  AVDictionary *copts = 0;
  //av_dict_copy(&copts, opts, 0);
  AVCodec *decoder = avcodec_find_decoder(codec_id);
  ctx = avcodec_alloc_context3(decoder);
  avcodec_parameters_to_context(ctx, st->codecpar);
  if( avcodec_open2(ctx, decoder, &copts) < 0 ) {
    fprintf(stderr,"codec open failed: %s\n", filename);
    return -1;
  }
  av_dict_free(&copts);
  ipic = av_frame_alloc();
  eof = 0;
  need_packet = 1;

  AVRational framerate = av_guess_frame_rate(fmt_ctx, st, 0);
  this->frame_rate = !framerate.den ? 0 : (double)framerate.num / framerate.den;
  this->pix_fmt = (AVPixelFormat)st->codecpar->format;
  this->width  = st->codecpar->width;
  this->height = st->codecpar->height;
  return 0;
}

AVFrame *ffcmpr::read_frame()
{
  av_frame_unref(ipic);

  for( int retrys=1000; --retrys>=0; ) {
    if( need_packet ) {
      if( eof ) return 0;
      AVPacket *pkt = &ipkt;
      av_packet_unref(pkt);
      int ret = av_read_frame(fmt_ctx, pkt);
      if( ret < 0 ) {
        if( ret != AVERROR_EOF ) return 0;
        ret = 0;  eof = 1;  pkt = 0;
      }
      if( pkt ) {
        if( pkt->stream_index != st->index ) continue;
        if( !pkt->data || !pkt->size ) continue;
      }
      avcodec_send_packet(ctx, pkt);
      need_packet = 0;
    }
    int ret = avcodec_receive_frame(ctx, ipic);
    if( ret >= 0 ) return ipic;
    if( ret != AVERROR(EAGAIN) ) {
      eof = 1; need_packet = 0;
      break;
    }
    need_packet = 1;
  }
  return 0;
}

static inline int get_depth(AVPixelFormat pix_fmt)
{
  int depth = 0;
  const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt);
  if( desc ) {
    for( int i=desc->nb_components; --i>=0; ) {
      int bits = desc->comp[i].depth;
      if( depth < bits ) depth = bits;
    }
  }
  return depth;
}

static int diff_frame(AVFrame *afrm, AVFrame *bfrm,
    gg_ximage *ximg, int w, int h, int s, int a1)
{
  int n = 0, m = 0;
  uint8_t *arow = afrm->data[0];
  uint8_t *brow = bfrm->data[0];
  uint8_t *frow = ximg->data;
  int asz = afrm->linesize[0], bsz = afrm->linesize[0];
  XImage *ximage = ximg->ximage;
  int fsz = ximage->bytes_per_line;
  int rsz = w, bpp = (ximage->bits_per_pixel+7)/8;
  uint32_t *lsb = ximg->lsb;

  for( int y=h; --y>=0; arow+=asz, brow+=bsz, frow+=fsz ) {
    uint16_t *ap = (uint16_t*)arow + a1;
    uint16_t *bp = (uint16_t*)brow + a1;
    uint8_t *fp = frow;
    for( int x=rsz; --x>=0; ) {
      uint32_t rgb = 0;  uint8_t *rp = fp;
      for( int i=0; i<3; ++i ) {
        int d = *ap++ - *bp++;
        if( s > 0 ) d >>= s;
        else if( s < 0 ) d <<= -s;
        int v = d + 128;
        if( v > 255 ) v = 255;
        else if( v < 0 ) v = 0;
        rgb |= v * lsb[i];
        m += d;
        if( d < 0 ) d = -d;
        n += d;
      }
      if( ximage->byte_order == MSBFirst )
        for( int i=3; --i>=0; ) *rp++ = rgb>>(8*i);
      else
        for( int i=0; i<3; ++i ) *rp++ = rgb>>(8*i);
      ++ap;  ++bp;  fp += bpp;
    }
  }
  int sz = h*rsz;
  printf("%d %d %d %f", sz, m, n, (double)n/sz);
  tm += m;  tn += n;
  return n;
}

int main(int ac, char **av)
{
  setbuf(stdout,NULL);
  XInitThreads();
  Display *display = XOpenDisplay(getenv("DISPLAY"));
  if( !display ) {
    fprintf(stderr,"Unable to open display\n");
    exit(1);
  }
  if( ac < 3 ) {
    printf("usage: %s a.fmt b.fmt <frm0> <s>\n"
        "  a = src media, b = src media, frm0 = a/b skew\n"
        "  s = shift <0:lt, =0:none(dft), >0:rt\n"
        "  env var GG_LOG_LEVEL=q/f/e/v/d/<nbr>\n", av[0]);
    exit(1);
  }
  const char *cp = getenv("GG_LOG_LEVEL");
  if( cp ) {
    int lvl = -1;
    switch( *cp ) {
    case 'q':  lvl = AV_LOG_QUIET;    break;
    case 'f':  lvl = AV_LOG_FATAL;    break;
    case 'e':  lvl = AV_LOG_ERROR;    break;
    case 'v':  lvl = AV_LOG_VERBOSE;  break;
    case 'd':  lvl = AV_LOG_DEBUG;    break;
    case '0'...'9': lvl = atoi(cp);   break;
    }
    if( lvl >= 0 )
      av_log_set_level(lvl);
  }

  ffcmpr a, b;
  if( a.open_decoder(av[1],0) ) return 1;
  if( b.open_decoder(av[2],0) ) return 1;

  int64_t err = 0;
  int frm_no = 0;
  int frm0 = ac>3 ? atoi(av[3]) : 0;
  int s = ac>4 ? atoi(av[4]) : 0;

  printf("file a:%s\n", av[1]);
  printf("  id 0x%06x:", a.ctx->codec_id);
  const AVCodecDescriptor *adesc = avcodec_descriptor_get(a.ctx->codec_id);
  printf("  video %s\n", adesc ? adesc->name : " (unkn)");
  printf(" %dx%d %5.2f", a.width, a.height, a.frame_rate);
  const char *apix = av_get_pix_fmt_name(a.pix_fmt);
  int ad = get_depth(a.pix_fmt);
  printf(" pix %s, depth=%d\n", apix ? apix : "(unkn)", ad);

  printf("file b:%s\n", av[2]);
  printf("  id 0x%06x:", b.ctx->codec_id);
  const AVCodecDescriptor *bdesc = avcodec_descriptor_get(b.ctx->codec_id);
  printf("  video %s\n", bdesc ? bdesc->name : " (unkn)");
  printf(" %dx%d %5.2f", b.width, b.height, b.frame_rate);
  const char *bpix = av_get_pix_fmt_name(b.pix_fmt);
  int bd = get_depth(b.pix_fmt);
  printf(" pix %s, depth=%d\n", bpix ? bpix : "(unkn)", bd);
  int d = ad>bd ? ad : bd;
  s = 16-d + s;
  int lsb = s, msb = lsb + 7;
  if( lsb < 0 ) lsb = 0;
  if( msb > 15 ) msb = 15;
  printf("shift: %d, msb..lsb: %d..%d of uint16\n", s, msb, lsb);

//  if( a.ctx->codec_id != b.ctx->codec_id ) { printf("codec mismatch\n"); return 1;}
  if( a.width != b.width ) { printf("width mismatch\n"); return 1;}
  if( a.height != b.height ) { printf("height mismatch\n"); return 1;}
//  if( a.frame_rate != b.frame_rate ) { printf("framerate mismatch\n"); return 1;}
//  if( a.pix_fmt != b.pix_fmt ) { printf("format mismatch\n"); return 1;}

  signal(SIGINT,sigint);
  const AVPixFmtDescriptor *afmt = av_pix_fmt_desc_get(a.pix_fmt);
  AVPixelFormat a_pix_fmt = afmt->flags & AV_PIX_FMT_FLAG_RGB ?
      AV_PIX_FMT_RGBA64LE : AV_PIX_FMT_AYUV64LE ;
  const AVPixFmtDescriptor *bfmt = av_pix_fmt_desc_get(b.pix_fmt);
  AVPixelFormat b_pix_fmt = bfmt->flags & AV_PIX_FMT_FLAG_RGB ?
      AV_PIX_FMT_RGBA64LE : AV_PIX_FMT_AYUV64LE ;
  if( a_pix_fmt != b_pix_fmt ) {
    printf(" a/b yuv/rgb mismatched, using a = %s\n", apix);
    b_pix_fmt = a_pix_fmt;
  }
  int a1 = a_pix_fmt == AV_PIX_FMT_AYUV64LE ? 1 : 0; // alpha 1st chan

  struct SwsContext *a_cvt = sws_getCachedContext(0, a.width, a.height, a.pix_fmt,
                a.width, a.height, a_pix_fmt, SWS_POINT, 0, 0, 0);
  struct SwsContext *b_cvt = sws_getCachedContext(0, b.width, b.height, b.pix_fmt,
                b.width, b.height, b_pix_fmt, SWS_POINT, 0, 0, 0);
  if( !a_cvt || !b_cvt ) {
    printf("sws_getCachedContext() failed\n");
    exit(1);
  }

  AVFrame *afrm = av_frame_alloc();
  av_image_alloc(afrm->data, afrm->linesize,
     a.width, a.height, a_pix_fmt, 1);

  AVFrame *bfrm = av_frame_alloc();
  av_image_alloc(bfrm->data, bfrm->linesize,
     b.width, b.height, b_pix_fmt, 1);
{ gg_window gw(display, 10,10, a.width,a.height);
  gw.show();
  gg_thread thr(gw, 1);
  thr.start();

  while( frm0 > 0 ) { a.read_frame(); --frm0; }
  while( frm0 < 0 ) { b.read_frame(); ++frm0; }

  while( !done ) {
    AVFrame *ap = a.read_frame();
    if( !ap ) break;
    AVFrame *bp = b.read_frame();
    if( !bp ) break;
    sws_scale(a_cvt, ap->data, ap->linesize, 0, ap->height,
       afrm->data, afrm->linesize);
    sws_scale(b_cvt, bp->data, bp->linesize, 0, bp->height,
       bfrm->data, bfrm->linesize);
    thr.draw_lock();
    gg_ximage *fimg = thr.next_img();
    int ret = diff_frame(afrm, bfrm, fimg, ap->width, ap->height, s, a1);
    thr.post(fimg);
    err += ret;  ++frm_no;
    printf("  %d\n",frm_no);
  }

  av_freep(&afrm->data);
  av_frame_free(&afrm);
  av_freep(&bfrm->data);
  av_frame_free(&bfrm);
  
  b.close_decoder();
  a.close_decoder();

  thr.join();
  gw.hide(); }
  XCloseDisplay(display);
  printf("\n%jd %jd\n", tm, tn);
  return 0;
}

