/*
 *  Copyright 1994-2022 Olivier Girondel
 *  Copyright 2014-2022 Frantz Balinski
 *
 *  This file is part of lebiniou.
 *
 *  lebiniou 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.
 *
 *  lebiniou 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 lebiniou. If not, see <http://www.gnu.org/licenses/>.
 */

#include <glib.h>
#include <stdbool.h>
#include <time.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include "brandom.h"
#include "buffer_8bits.h"
#include "context.h"
#include "utils.h"
#include "pthread_utils.h"


// #define DEBUG_MONITOR

uint32_t version = 0;
uint32_t options = BO_LENS | BO_NOT_LENS | BO_WEBCAM | BO_SCHEMES;
enum LayerMode mode = LM_OVERLAY;
char desc[] = "Monitor-view effect";
char dname[] = "Monitor";

extern uint8_t max_fps;

#define bDISABLED ((_Bool) 0)
#define bENABLED  ((_Bool) 1)
// aliases
#define bOFF  (bDISABLED)
#define bON   (bENABLED)


/* recording sources */
enum eSource {
  sCAPTURE,  /* 0 = active_buffer(ctx)    */
  sCAMERA,   /* 1 = webcam (if available) */
  sCLEAR     /* 2 = noise                 */
};

/* don't use sCLEAR as random source: special source, only used to clear tape */
#define sFIRST    (sCAPTURE)
#define sLAST     (sCAMERA)
#define nSOURCES  (sLAST + 1 - sFIRST)


/* monitor positions */
enum ePosition {
  pTOPLEFT,     /* 0 %00 */
  pTOPRIGHT,    /* 1 %01 */
  pBOTTOMLEFT,  /* 2 %10 */
  pBOTTOMRIGHT  /* 3 %11 */
};

#define pFIRST      (pTOPLEFT)
#define pLAST       (pBOTTOMRIGHT)
#define nPOSITIONS  (pLAST + 1 - pFIRST)


static inline int
is_left_aligned(enum ePosition mp)
{
  return (((int) mp & 1) == 0);
}


static inline int
is_top_aligned(enum ePosition mp)
{
  return (((int) mp & 2) == 0);
}


static inline int16_t
clip_short(int16_t val, int16_t min, int16_t max)
{
  return (val < min) ? min : ((val > max) ? max : val);
}


/* 2d container */
typedef struct V2D_s {
  int16_t x, y;
} V2D_t;


/* rectangle */
typedef struct Rectangle_s {
  V2D_t position;
  V2D_t size;
} Rectangle_t;


// Scaling
static struct SwsContext *sws_context = NULL;

static uint8_t *source[4] = { NULL, NULL, NULL, NULL };
static int source_linesize[4] = { 0, 0, 0, 0 };

// Benchmark
#ifdef DEBUG_MONITOR
static Timer_t *timer = NULL;
static uint64_t total = 0;
static uint64_t runs = 0;
#endif

/* monitor */
static Rectangle_t     mView;     /* monitor view pos & size */
static enum ePosition  mPosition; /* see positions above (pXXX) */
/* relative positions and size -- in percents */
#define MONITOR_LEFTTOP_PERCENT      (4)
#define MONITOR_RIGHTBOTTOM_PERCENT  (100 - MONITOR_LEFTTOP_PERCENT)
#define MONITOR_SIZE_PERCENT         (100 / 3) /* monitor is 1/3 screen size */
/* make a rounded border */
#define MONITOR_CURVE_EXP  (6)
/* monitor screen border */
#define MONITOR_BORDER_START  (0.95f)
#define MONITOR_BORDER_END    (1)
#define MONITOR_BORDER_WIDTH  (MONITOR_BORDER_END - MONITOR_BORDER_START)

/* monitor source channel viewer */
#define MONITOR_SOURCE_LEFTTOP_PERCENT  (MONITOR_LEFTTOP_PERCENT + 6)
#define MONITOR_SOURCE_SIZE_PERCENT     (5)

/* macro to scale dimensions and sizes */
#define SCALE(x,percents)  (((long) ((x) * (percents) + 0.5f)) / 100)

/* uncomment this line to draw faster: no AA, ... */
/*#define FASTDRAW*/

/* draw monitor half-color lines ? (a bit faster too, if not) */
#define DRAWHALFCOLOR
#define HALFCOLOR_PERIOD  (3)


/* recorder */
#define rBUFFERS  (120)
static uint8_t       *rBuffer[rBUFFERS][4]; // recorder buffers
static int           rBuffer_linesize[4] = { 0, 0, 0, 0 }; // recorder buffer aligned line size
static enum eSource  rSource;           // see sources above (sXXX)
static int16_t       rPosition;         // buffer id (on read/write)
static _Bool         rRecording;        // flag


/* available sources */
static _Bool  sources[nSOURCES]; /* sources are checked before use */


static void init_params(void);

static void random_recording(void);
static void random_source(void);
static void random_position(void);

static void randomize_buffer(Pixel_t *);
static void randomize_sometimes(Pixel_t *, int16_t);


/* forward recorder position, looped */
static inline void
forward(void)
{
  rPosition = (rPosition + 1) % rBUFFERS;
}


int8_t
create(Context_t *ctx)
{
  /* compute monitor size */
  mView.size.x = SCALE(WIDTH, MONITOR_SIZE_PERCENT);
  assert(mView.size.x >= 1);
  mView.size.y = SCALE(HEIGHT, MONITOR_SIZE_PERCENT);
  assert(mView.size.y >= 1);

  /* check sources */
  for (enum eSource es = sFIRST; es <= sLAST; es++) {
    sources[es] = bENABLED;
  }

  if (ctx->webcams < 1) {
    sources[sCAMERA] = bDISABLED;
  }
  /* we have now, at least, 2 sources enabled: sCAPTURE & sCLEAR */

  /* init recording flag */
  rRecording = b_rand_boolean();

  int size;
  /* allocate monitor buffers */
  for (uint16_t i = 0; i < rBUFFERS; i++) {
    size = av_image_alloc(rBuffer[i], rBuffer_linesize, mView.size.x, mView.size.y, AV_PIX_FMT_GRAY8, 16);

    if (size < 0) {
      xerror("av_image_alloc() failed\n");
    }
    randomize_buffer(rBuffer[i][0]);
  }

  // Allocate source and destination buffer
  size = av_image_alloc(source, source_linesize, WIDTH, HEIGHT, AV_PIX_FMT_GRAY8, 16);
  if (size < 0) {
    xerror("av_image_alloc() failed\n");
  }

  // Scaler
  sws_context = sws_getContext(WIDTH, HEIGHT, AV_PIX_FMT_GRAY8, /* source */
                               mView.size.x, mView.size.y, AV_PIX_FMT_GRAY8, /* destination */
#ifdef __NetBSD__
                               SWS_BILINEAR, NULL, NULL, NULL
#else
                               SWS_FAST_BILINEAR, NULL, NULL, NULL
#endif
                               );
  if (NULL == sws_context) {
    xerror("sws_getContext\n");
  }
#ifdef DEBUG_MONITOR
  timer = Timer_new(__FILE__);
#endif

  return 1;
}


void
destroy(Context_t *ctx)
{
  for (uint16_t i = 0; i < rBUFFERS; i++) {
    av_freep(&rBuffer[i][0]);
  }
  av_freep(&source[0]);

  sws_freeContext(sws_context);
#ifdef DEBUG_MONITOR
  Timer_delete(timer);
#endif
}


void
on_switch_on(Context_t *ctx)
{
  init_params();
}


static void
init_params(void)
{
  random_source();
  random_position();
}


void
run(Context_t *ctx)
{
  Buffer8_t *dst;
  float r, rx, ry;
  int16_t cx, cy;
#ifndef FASTDRAW
  int16_t x1, x2, y1, y2;
#endif /* !FASTDRAW */
  Pixel_t col;

  if ((sources[rSource] != bDISABLED) && (rRecording != bOFF)) {
    int ret;

    switch (rSource) {
      case sCAMERA:
        if (!xpthread_mutex_lock(&ctx->cam_mtx[ctx->cam])) {
          for (uint16_t y = 0; y < HEIGHT; ++y) {
            memcpy(&source[0][y * source_linesize[0]], &ctx->cam_save[ctx->cam][0]->buffer[0] + y * WIDTH, WIDTH * sizeof(Pixel_t));
          }
          xpthread_mutex_unlock(&ctx->cam_mtx[ctx->cam]);
        }
#ifdef DEBUG_MONITOR
        Timer_start(timer);
#endif
        ret = sws_scale(sws_context, (const uint8_t * const *)source,
                        source_linesize, 0, HEIGHT, rBuffer[rPosition], rBuffer_linesize);
#ifdef DEBUG_MONITOR
        total += Timer_elapsed(timer) * 100000;
        xdebug("%s: %f\n", __func__, total / (float)++runs);
#endif
        if (ret < 0) {
          xerror("sws_scale\n");
        }

        randomize_sometimes(rBuffer[rPosition][0], b_rand_uint32_range(0, 11));
        break;

      case sCAPTURE:
        for (uint16_t y = 0; y < HEIGHT; ++y) {
          memcpy(source[0] + (y * source_linesize[0]), active_buffer(ctx)->buffer + y * WIDTH, WIDTH * sizeof(Pixel_t));
        }
#ifdef DEBUG_MONITOR
        Timer_start(timer);
#endif
        ret = sws_scale(sws_context, (const uint8_t * const *)source,
                        source_linesize, 0, HEIGHT, rBuffer[rPosition], rBuffer_linesize);
#ifdef DEBUG_MONITOR
        total += Timer_elapsed(timer) * 100000;
        xdebug("%s: %f\n", __func__, total / (float)++runs);
#endif
        if (ret < 0) {
          xerror("sws_scale\n");
        }

        randomize_sometimes(rBuffer[rPosition][0], b_rand_uint32_range(0, 6));
        break;

      case sCLEAR:
        randomize_buffer(rBuffer[rPosition][0]);
        break;

      default: /* do nothing */
        break;
    }
  }

  dst = passive_buffer(ctx);
  Buffer8_clear(dst);

  cx = mView.size.x / 2;
  cy = mView.size.y / 2;
  for (int16_t y = 0; y < mView.size.y; y++) {
    for (int16_t x = 0; x < mView.size.x; x++) {
      /* make a border look between circle and rectangle */
      /* rⁿ = |rx|ⁿ + |ry|ⁿ */
      rx = fabsf((float) (cx - x) / cx);
      ry = fabsf((float) (cy - y) / cy);
      r = powf(rx, MONITOR_CURVE_EXP) + powf(ry, MONITOR_CURVE_EXP);
      r = powf(r, 1 / (float) MONITOR_CURVE_EXP);

      /* inside the border ? */
      if ((r >= MONITOR_BORDER_START) && (r <= MONITOR_BORDER_END)) {
#ifdef FASTDRAW
        col = PIXEL_MAXVAL; /* make color highest */
#else /* !FASTDRAW */
        /* antialiased border:
         * so, we have our border (1 - 0.97) 3% wide: */
        /* compute distance pixel coord against the border middle */
        float d = (r - MONITOR_BORDER_START) / MONITOR_BORDER_WIDTH; /* 0.. 1 */
        d = fabsf(d - 0.5f) * 2; /* 0.. 1 */
        /* color from distance */
        d = rintf((1 - d) * PIXEL_MAXVAL); /* center bright, away dark */
        int16_t di = (int16_t) d;
        col = (Pixel_t) clip_short(di, PIXEL_MINVAL, PIXEL_MAXVAL);
#endif /* !FASTDRAW */
      }
      /* outside view ? */
      else if (r > 1) {
        col = PIXEL_TRANSPARENT;
      }
      /* inside view ? */
      else {
        col = rBuffer[rPosition][0][(mView.size.y - (y + 1)) * rBuffer_linesize[0] + x];
#ifdef DRAWHALFCOLOR
        if ((y % HALFCOLOR_PERIOD) == 1) {
          col >>= 1;  /* halfbright color, 1 line each HALFCOLOR_PERIOD */
        }
#endif /* DRAWHALFCOLOR */
        if (col < 1) {
          col = 1;  /* be sure monitor has no PIXEL_TRANSPARENT color */
        }
      }

      set_pixel_nc(dst,
                   mView.position.x + x, MAXY - (mView.position.y + y), col);
    }
  }

#ifndef FASTDRAW
  /* afficheur de canal (petites cases en haut à gauche). */
  /* ce qui est vu: cadre creux: source, cadre plein: enregistrement */
  cy = SCALE(mView.size.y, MONITOR_SOURCE_LEFTTOP_PERCENT);
  y1 = mView.position.y + cy;
  cy = SCALE(mView.size.y, MONITOR_SOURCE_SIZE_PERCENT);
  if (cy < 1) {
    cy = 1;  /* at least, use a 1x1 pixels area for channel colored-boxes */
  }
  y2 = y1 + cy - 1;

  for (uint16_t i = sFIRST; i <= sLAST; i++) {
    cx = SCALE(mView.size.x, MONITOR_SOURCE_LEFTTOP_PERCENT);
    cx += SCALE(mView.size.x, (i - sFIRST) * MONITOR_SOURCE_SIZE_PERCENT);
    x1 = mView.position.x + cx;
    cx = SCALE(mView.size.y, MONITOR_SOURCE_SIZE_PERCENT);
    if (cx < 1) {
      cx = 1;  /* at least, use a 1x1 pixels area for channel colored-boxes */
    }
    x2 = x1 + cx - 1;
    col = PIXEL_MAXVAL;
    if (rSource == (enum eSource) i) {
      if (rRecording == bON) {
        if ((cx <= 1) && (cy <= 1)) {
          set_pixel_nc(dst, x1, MAXY - y1, col);
        } else if (cx <= 1) {
          v_line_nc(dst, x1, MAXY - y1, MAXY - y2, col);
        } else if (cy <= 1) {
          h_line_nc(dst, MAXY - y1, x1, x2, col);
        } else {
          draw_filled_box_nc(dst, x1, MAXY - y1, x2, MAXY - y2, col);
        }
      } else {
        /* bg */
        if ((cx <= 1) && (cy <= 1)) {
          set_pixel_nc(dst, x1, MAXY - y1, col >> 2);
        } else if (cx <= 1) {
          v_line_nc(dst, x1, MAXY - y1, MAXY - y2, col >> 2);
        } else if (cy <= 1) {
          h_line_nc(dst, MAXY - y1, x1, x2, col >> 2);
        } else {
          draw_filled_box_nc(dst, x1, MAXY - y1, x2, MAXY - y2, col >> 2);
        }

        /* fg */
        if ((cx >= 3) && (cy >= 3)) {
          draw_box_nc(dst, x1, MAXY - y1, x2, MAXY - y2, col);
        }
      }
    } else {
      draw_filled_box_nc(dst, x1, MAXY - y1, x2, MAXY - y2, col >> 2);
    }
  }
#endif /* !FASTDRAW */

  /* point to next buffer */
  forward();

  /* change params */
  if (b_rand_uint32_range(0, max_fps * 2 + 1) == 1) {
    random_recording();
  }

  if (b_rand_uint32_range(0, max_fps * 5 + 1) == 1) {
    random_source();
  }

  if (b_rand_uint32_range(0, max_fps * 10 + 1) == 1) {
    random_position();
  }
}


static void
random_recording(void)
{
  int16_t rec;
  int16_t n = b_rand_uint32_range(0, 2 + 1);
  switch (n) {
    case 0:
      rec = bOFF;
      break;

    case 1:
      rec = bON;
      break;

    default: /* inverse */
      rec = (rRecording == bOFF ? bON : bOFF);
      break;
  }

  if (rec != rRecording) {
    int16_t len = b_rand_uint32_range(3, 5 + 1);

    if (rec == bOFF) {
      /* stop recording: fill a few buffers with noise, less and less */
      for (int16_t i = 0; i < len; i++) {
        randomize_sometimes(rBuffer[rPosition][0], ((len - i) * 256) / len);
        forward();
      }
    } else {
      /* start recording: fill a few buffers with noise, more and more */
      for (int16_t i = 0; i < len; ) {
        randomize_sometimes(rBuffer[rPosition][0], (++i * 256) / len);
        forward();
      }
    }

    rRecording = rec;
  }
}


static void
random_source(void)
{
  enum eSource es;

  while (1) {
    es = b_rand_uint32_range(sFIRST, sLAST + 1);
    if (sources[es] != bDISABLED) {
      break;
    }
  }

  rSource = es;
}


static void
random_position(void)
{
  mPosition = b_rand_uint32_range(pFIRST, pLAST + 1);

  if (is_left_aligned(mPosition)) { /* we are left aligned */
    mView.position.x = SCALE(WIDTH, MONITOR_LEFTTOP_PERCENT);
  } else { /* we are right aligned */
    mView.position.x = SCALE(WIDTH, MONITOR_RIGHTBOTTOM_PERCENT) - mView.size.x;
  }

  if (is_top_aligned(mPosition)) { /* we are top aligned */
    mView.position.y = SCALE(HEIGHT, MONITOR_LEFTTOP_PERCENT);
  } else { /* we are bottom aligned */
    mView.position.y = SCALE(HEIGHT, MONITOR_RIGHTBOTTOM_PERCENT) - mView.size.y;
  }
}


static void
randomize_buffer(Pixel_t *ptr)
{
  for (int16_t y = 0; y < mView.size.y; y++) {
    for (int16_t x = 0; x < mView.size.x; x++) {
      *ptr++ = b_rand_uint32_range(PIXEL_MINVAL, PIXEL_MAXVAL + 1);
    }
  }
}


static void
randomize_sometimes(Pixel_t *ptr, int16_t threshold)
{
  for (int16_t y = 0; y < mView.size.y; y++) {
    for (int16_t x = 0; x < mView.size.x; x++, ptr++) {
      if (b_rand_uint32_range(PIXEL_MINVAL, PIXEL_MAXVAL + 1) < (unsigned int) threshold) {
        int16_t a = *ptr;
        a += b_rand_uint32_range(PIXEL_MINVAL, (PIXEL_MAXVAL >> 1) + 1);
        *ptr = ((a << 1) / 3);
      }
    }
  }
}
