/*
 * viewer.c: Viewer core
 *
 * PhotoCD Player plugin for VDR (the Video Disk Recorder)
 * Copyright (C) 2002  Thomas Heiligenmann  <thomas@heiligenmann.de>
 *
 * 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
 *
 */


#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "viewer.h"
#include "mpeg.h"
#include "setup.h"
#include "i18n.h"

#define PCD_IMAGE_SECTORS   384
#define PCD_IMAGE_SIZE      (PCD_IMAGE_SECTORS*CD_XA21_DATASIZE)

#define MAX_ZOOM            4
#define PAN_STEP_X          TMP_WIDTH/5
#define PAN_STEP_Y          TMP_HEIGHT/5


// --- cZoomImage ------------------------------------------------------------

class cZoomImage {
private:
  unsigned char *Y,*Cb,*Cr;
  bool pal;
  int width,height,size;
  int zoom;
  int brdWidth, brdHeight;
  int xpan, ypan;
public:
  cZoomImage(bool Pal);
  virtual ~cZoomImage();
  unsigned char* Data() { return Y; }
  bool Zoom(int Zoom);
  bool Pan(int X, int Y, int Off);
  void Load(unsigned char *Image, int Width, int Height);
};


cZoomImage::cZoomImage(bool Pal)
{
  pal = Pal;
  width = pal ? PAL_WIDTH : NTSC_WIDTH;
  height = pal ? PAL_HEIGHT : NTSC_HEIGHT;
  size = width * height;
  zoom = 0;
  brdWidth = TMP_WIDTH / 2;
  brdHeight = TMP_HEIGHT / 2;
  xpan = 0;
  ypan = 0;
  Y = (unsigned char*)malloc(size + size/4 + size/4);
  Cr = Y + size;
  Cb = Cr + size/4;
}

cZoomImage::~cZoomImage()
{
  free(Y);
}

bool cZoomImage::Zoom(int Zoom)
{
  if (zoom<MAX_ZOOM && Zoom>0) {
     zoom++;
     return true;
  } else if (zoom>-MAX_ZOOM && Zoom<0) {
     zoom--;
     return true;
  } else if (zoom!=0 && Zoom==0) {
     zoom = 0;
     return true;
  }
  return false;
}

bool cZoomImage::Pan(int X, int Y, int Off)
{
  if ((brdWidth>0 || brdHeight>0) && Off) {
     if (xpan!=0 || ypan!=0) {
        xpan = 0;
        ypan = 0;
        return true;
     }
  }
  if (brdWidth>0) {
     if (X>0 && xpan<brdWidth) {
        xpan += PAN_STEP_X>>zoom;
        if (xpan>brdWidth) xpan = brdWidth;
        return true;
     }
     if (X<0 && xpan>-brdWidth) {
        xpan -= PAN_STEP_X>>zoom;
        if (xpan<-brdWidth) xpan = -brdWidth;
        return true;
     }
  }
  if (brdHeight>0) {
     if (Y>0 && ypan<brdHeight) {
        ypan += PAN_STEP_Y>>zoom;
        if (ypan>brdHeight) ypan = brdHeight;
        return true;
     }
     if (Y<0 && ypan>-brdHeight) {
        ypan -= PAN_STEP_Y>>zoom;
        if (ypan<-brdHeight) ypan = -brdHeight;
        return true;
     }
  }
  return false;
}

void cZoomImage::Load(unsigned char *Image, int Width, int Height)
{
  int scale_n,scale_d;
  if (PcdSetupData.NoAdjust) {
     scale_n = 256;
     scale_d = 256;
  } else {
     int rotor = (3*Width) / (4*Height);    // 4:3 landscape>0 portrait=0
     scale_n = rotor ? TMP_WIDTH : TMP_HEIGHT;
     scale_d = rotor ? Width : Height;
  }
  scale_n = zoom>=0 ? scale_n<<zoom : scale_n>>-zoom;
  int imgWidth = Width * scale_n / scale_d;
  int imgHeight = Height * scale_n / scale_d;

  unsigned char *ImageCr = Image + Width*Height;
  unsigned char *ImageCb = ImageCr + Width*Height/4;
  memset(Y,    0, size);
  memset(Cr, 128, size/4);
  memset(Cb, 128, size/4);

  brdWidth = (imgWidth - TMP_WIDTH) / 2;
  brdHeight = (imgHeight - TMP_HEIGHT) / 2;
  if (brdWidth>0) {
     if (-brdWidth>(xpan*scale_n/scale_d)) xpan = -brdWidth*scale_d/scale_n;
     if ((xpan*scale_n/scale_d)>brdWidth) xpan = brdWidth*scale_d/scale_n;
  } else
     xpan = 0;
  if (brdHeight>0) {
     if (-brdHeight>(ypan*scale_n/scale_d)) ypan = -brdHeight*scale_d/scale_n;
     if ((ypan*scale_n/scale_d)>brdHeight) ypan = brdHeight*scale_d/scale_n;
  } else
     ypan = 0;

  const int filter[] = { 51, 86, 51, 86, 256, 86, 51, 86, 51 };
  for (int y=0; y<height; y++) {
     int iy = y*TMP_HEIGHT*scale_d/height/scale_n - ypan + brdHeight*scale_d/scale_n;
     if (iy>=0 && iy<Height) {
        for (int x=0; x<width; x++) {
           int ix = x*TMP_WIDTH*scale_d/width/scale_n - xpan + brdWidth*scale_d/scale_n;
           if (ix>=0 && ix<Width) {
              if (PcdSetupData.QuickScaling) {
                 Y[x+width*y] = Image[ix+Width*iy];
                 Cb[(x>>1)+(width>>1)*(y>>1)] = ImageCr[(ix>>1)+(Width>>1)*(iy>>1)];
                 Cr[(x>>1)+(width>>1)*(y>>1)] = ImageCb[(ix>>1)+(Width>>1)*(iy>>1)];
              } else {
                 int y_n = 0;
                 int cr_n = 0;
                 int cb_n = 0;
                 int y_d = 0;
                 for (int n=0; n<9; n++) {
                    int nx = n%3 - 1;
                    int ny = n/3 - 1;
                    y_d += filter[n];
                    if ((ix+nx)>=0 && (ix+nx)<Width && (iy+ny)>=0 && (iy+ny)<Height) {
                       y_n += filter[n] * Image[ix+nx+Width*(iy+ny)];
                       cr_n += filter[n] * ImageCr[(ix>>1)+(Width>>1)*(iy>>1)];
                       cb_n += filter[n] * ImageCb[(ix>>1)+(Width>>1)*(iy>>1)];
                    } else {
                       y_n += 128;
                       cr_n += 128;
                       cb_n += 128;
                    }
                 }
                 Y[x+width*y] = y_n / y_d;;
                 Cb[(x>>1)+(width>>1)*(y>>1)] = cr_n / y_d;
                 Cr[(x>>1)+(width>>1)*(y>>1)] = cb_n / y_d;
              }
           }
        }
     }
  }
}


// --- cPcdImage -------------------------------------------------------------

class cPcdImage {
private:
  cPCD *pcd;
  unsigned char *imageCd;
  unsigned char *Y, *Cb, *Cr;
  int width,height,size;
  unsigned char rotor;
public:
  cPcdImage(cPCD *Pcd);
  virtual ~cPcdImage();
  void Load(int Image);
  unsigned char* Data(void) { return Y; }
  int Width(void) { return rotor&1 ? height : width; }
  int Height(void) { return rotor&1 ? width : height; }
};

cPcdImage::cPcdImage(cPCD *Pcd)
{
  pcd = Pcd;
  width = WIDTH_BASE;
  height = HEIGHT_BASE;
  size = width * height;
  imageCd = (unsigned char*)malloc(CD_XA21_DATASIZE*DATA_SIZE_BASE);
  Y = (unsigned char*)malloc(size + size/4 + size/4);
  Cb = Y + size;
  Cr = Cb + size/4;
}

cPcdImage::~cPcdImage()
{
  free(imageCd);
  free(Y);
}

void cPcdImage::Load(int Image)
{
  int lba = pcd->Image[Image].lba;
  unsigned char buf[CD_XA21_DATASIZE];
  pcd->readSectorXA21(lba+INFO_OFFSET_BASE, buf);
  rotor = buf[0x48] & 0x03;
  for (int i=0; i<DATA_SIZE_BASE; i++, lba++) {
     pcd->readSectorXA21(lba+DATA_OFFSET_BASE, buf);
     memcpy(imageCd+i*CD_XA21_DATASIZE, buf, CD_XA21_DATASIZE);
  }
  memset(Y,    0, size);
  memset(Cb, 128, size/4);
  memset(Cr, 128, size/4);

  for (int y=0; y<height/2; y++)
     for (int x=0; x<width; x++) {
        int ys1 = x + 3*width*y;
        int ys2 = ys1 + width;
        int crs = (x>>1) + 3*width*y + 2*width;
        int cbs = crs + (width>>1);
        int yd1,yd2,crd,cbd;
        if (rotor&1) {       // portrait
           yd1 = width*height - 1 - (height-2) + 2*y - height*x;
           yd2 = yd1 - 1;
           crd = (width>>1)*(height>>1) - 1 - ((height-2)>>1) + y - (height>>1)*(x>>1);
           cbd = crd;
        } else {             // landscape
           yd1 = x + 2*width*y;
           yd2 = yd1 + width;
           crd = (x>>1) + (width>>1)*y;
           cbd = crd + (width>>1);
        }
        Y[yd1] = imageCd[ys1];
        Y[yd2] = imageCd[ys2];
        Cr[crd] = imageCd[crs];
        Cb[cbd] = imageCd[cbs];
     }
}


// --- cPcdViewer ------------------------------------------------------------

class cPcdViewer : public cPlayer, cThread {
private:
  cPCD *pcd;
  int image;
  bool active, running, done, put;
  int tick;
  int lba;
  cPcdImage *pcdImage;
  cZoomImage *zoomImage;
  cMpegFrame *mpegFrame;

  void Empty(void);
  bool Save(void);

protected:
  virtual void Activate(bool On);
  virtual void Action(void);

public:
  cPcdViewer(int Image, cPCD *Pcd);
  virtual ~cPcdViewer();
  bool Active(void) { return active; }
  bool SkipImages(int Images);
  int GetImage(void) { return image; }
  bool ZoomImage(int Zoom);
  bool PanImage(int X, int Y, int Off);
  void GotoImage(int Image);
  void ToggleSlideshow(void);
};

cPcdViewer::cPcdViewer(int Image, cPCD *Pcd)
{
  pcd = Pcd;

  active = true;
  running = false;
  done = false;
  put = false;
  tick = -1;

  pcdImage = new cPcdImage(pcd);
  zoomImage = new cZoomImage(PcdSetupData.Pal);
  mpegFrame = new cMpegFrame(PcdSetupData.Pal);

  GotoImage(Image);
}

cPcdViewer::~cPcdViewer()
{
  image = -1;
  Detach();
  Save();
}

bool cPcdViewer::Save(void)
{
  return false;
}

void cPcdViewer::Activate(bool On)
{
  if (On) {
     if (image >= 0)
        Start();
  } else if (active) {
     running = false;
     Cancel(3);
     active = false;
  }
}

void cPcdViewer::Empty(void)
{
  Lock();
  DeviceClear();
  Unlock();
}

bool cPcdViewer::ZoomImage(int Zoom)
{
  if (zoomImage->Zoom(Zoom)) {
     LOCK_THREAD;
     done = false;
     Empty();
     zoomImage->Load(pcdImage->Data(), pcdImage->Width(), pcdImage->Height());
     mpegFrame->Encode(zoomImage->Data());
     put = false;
     DevicePlay();
     return true;
  }
  return false;
}

bool cPcdViewer::PanImage(int X, int Y, int Off)
{
  if (zoomImage->Pan(X,Y,Off)) {
     LOCK_THREAD;
     done = false;
     zoomImage->Load(pcdImage->Data(), pcdImage->Width(), pcdImage->Height());
     mpegFrame->Encode(zoomImage->Data());
     put = false;
     DevicePlay();
     return true;
  }
  return false;
}

void cPcdViewer::GotoImage(int Image)
{
  dsyslog("PCD: Image %d", Image+1);
  LOCK_THREAD;
  done = false;
  Empty();
  image = Image;

  pcdImage->Load(image);

#ifdef DEBUG_PCD
  FILE *pgmfile;
  if ((pgmfile = fopen("/tmp/y.pgm", "w")) != NULL) {
     fprintf(pgmfile, "P5\n%d %d\n255\n", pcdImage->Width(), pcdImage->Height());
     fwrite(pcdImage->Data(),
        1, pcdImage->Width()*pcdImage->Height(), pgmfile);
     fclose(pgmfile);
  }
  if ((pgmfile = fopen("/tmp/cr.pgm", "w")) != NULL) {
     fprintf(pgmfile, "P5\n%d %d\n255\n", pcdImage->Width()/2, pcdImage->Height()/2);
     fwrite(pcdImage->Data() + pcdImage->Width()*pcdImage->Height(),
        1, pcdImage->Width()*pcdImage->Height()/4, pgmfile);
     fclose(pgmfile);
  }
  if ((pgmfile = fopen("/tmp/cb.pgm", "w")) != NULL) {
     fprintf(pgmfile, "P5\n%d %d\n255\n", pcdImage->Width()/2, pcdImage->Height()/2);
     fwrite(pcdImage->Data() + 5*pcdImage->Width()*pcdImage->Height()/4,
        1, pcdImage->Width()*pcdImage->Height()/4, pgmfile);
     fclose(pgmfile);
  }
#endif //DEBUG_PCD

  zoomImage->Load(pcdImage->Data(), pcdImage->Width(), pcdImage->Height());
  mpegFrame->Encode(zoomImage->Data());
  put = false;

  DevicePlay();
}

void cPcdViewer::ToggleSlideshow(void)
{
  if (tick>-1)
     tick = -1;
  else
     tick = PcdSetupData.SlideInterval;
  dsyslog("PCD: slideshow %s", tick>-1 ? "on" : "off");
  Skins.Message(mtInfo, tick>-1 ? tr("Slideshow on") : tr("Slideshow off"));
}

bool cPcdViewer::SkipImages(int Images)
{
  int newImage = image + Images;
  if (Images<0) {
     if (newImage>=0) {
        GotoImage(newImage);
        return true;
     } else if (Setup.MenuScrollWrap) {
        GotoImage(pcd->GetImageCount() - 1);
        return true;
     }
  }
  if (Images>0) {
     if (newImage<pcd->GetImageCount()) {
        GotoImage(newImage);
        return true;
     } else if (Setup.MenuScrollWrap) {
        GotoImage(0);
        return true;
     }
  }
  return false;
}

void cPcdViewer::Action(void)
{
  dsyslog("PCD: image viewer started (pid=%d)", getpid());
  time_t fuse = time(NULL);

  running = true;
  while (running) {
     cPoller Poller;

     if (tick>-1 && fuse != time(NULL)) {
        fuse = time(NULL);
        tick--;
        if (tick==0) {
           if (SkipImages(1))
              tick = PcdSetupData.SlideInterval;
           else
              tick = -1;
        }
     }

     if (DevicePoll(Poller, 100)) {

        LOCK_THREAD;

        if (mpegFrame->Size()) {
           if (!put) {

#ifdef DEBUG_PCD
              FILE *mpgfile;
              if ((mpgfile = fopen("/tmp/pcd.mpg", "w")) != NULL) {
                 fwrite(mpegFrame->Data(), sizeof(unsigned char), mpegFrame->Size(), mpgfile);
                 fclose(mpgfile);
              }
              dsyslog("PCD: debug (size=%d)", mpegFrame->Size());
#endif //DEBUG_PCD

              DeviceStillPicture(mpegFrame->Data(), mpegFrame->Size());
              put = true;
           }
        } else
           running = false;

     } else
        sleep(1);

  }
  active = running = false;

  dsyslog("PCD: image viewer ended (pid=%d)", getpid());
}


// --- cPcdViewerControl -----------------------------------------------------

cPcdViewerControl::cPcdViewerControl(int Image, cPCD *Pcd)
: cControl(viewer = new cPcdViewer(Image, Pcd))
{
}

cPcdViewerControl::~cPcdViewerControl()
{
  Stop();
}

bool cPcdViewerControl::Active(void)
{
  return viewer && viewer->Active();
}

int cPcdViewerControl::GetImage(void)
{
  if (viewer)
     return viewer->GetImage();
  return -1;
}

void cPcdViewerControl::Stop(void)
{
  delete viewer;
  viewer = NULL;
}

bool cPcdViewerControl::ZoomImage(int Zoom)
{
  if (viewer)
     return viewer->ZoomImage(Zoom);
  return false;
}

bool cPcdViewerControl::PanImage(int X, int Y, int Off)
{
  if (viewer)
     return viewer->PanImage(X,Y,Off);
  return false;
}

bool cPcdViewerControl::SkipImages(int Images)
{
  if (viewer)
     return viewer->SkipImages(Images);
  return false;
}

void cPcdViewerControl::GotoImage(int Image)
{
  if (viewer)
     viewer->GotoImage(Image);
}

void cPcdViewerControl::ToggleSlideshow(void)
{
  if (viewer)
     viewer->ToggleSlideshow();
}
