/*
 * detector.c: frame size detection and signaling
 *
 * Copyright (C) 2006 - 2007 Jens Vogel
 * Copyright (C) 2007 - 2008 Christoph Haubrich
 *
 * 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
 * Or, point your browser to http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 *
 * See the README file how to reach the author.
 *
 * $Id$
 */

#include <stdlib.h>
#include <math.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/dvb/video.h>
#include <asm/types.h>
#include <linux/videodev2.h>
#include <linux/dvb/frontend.h>

#include <vdr/tools.h>
#include <vdr/osdbase.h>
#include <vdr/skins.h>

#include "detector.h"
#include "avards.h"

struct SAspectInfo
{
   float       fMinAspect;
   const char* pszName;
   unsigned    iWssData;
   int         Lines;
};

SAspectInfo aAspectInfo[] =
{
   { 0.00,    "unknown",0x08, 576 },
   { 1.46,    "4:3",    0x08, 576 },
   { 1.66,    "L14:9",  0x01, 504 },
   { 1.90,    "L16:9",  0x0b, 432 },
   { FLT_MAX, "L>16:9", 0x0d, 340 },
   { 1.77,    "16:9",   0x07, 576 }
};

cDetector::cDetector(void)
:cThread("avards-detector")
{
   snprintf(szDevDvbVideo, MAXSTRING, "%s%s", config.pszDevDvbAdapter, "/video0");
   snprintf(szDevDvbFrontend, MAXSTRING, "%s%s", config.pszDevDvbAdapter, "/frontend0");

   lastAspectInfo = 0;
   lastWidth = 0;
   lastHeight = 0;
   top    = Setup.OSDTop;
   height = Setup.OSDHeight;
}

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

#if APIVERSNUM < 10504
bool cDetector::GetMaxOSDSize(avards_MaxOSDsize_v1_0 *OSDsize)
{
   //isyslog("avards: cDetector::GetOSDSize()");
   OSDsize->left   = Setup.OSDLeft;
   OSDsize->width  = Setup.OSDWidth;

   if (Running()) {
      Lock();
      OSDsize->top    = top;
      OSDsize->height = height;
      Unlock();
   } else {
      OSDsize->top    = Setup.OSDTop;
      OSDsize->height = Setup.OSDHeight;
   }
   return true;
}
#endif

const char *cDetector::GetWSSModeString()
{
   Lock();
   int AspectIndex = lastAspectInfo;
   Unlock();
   return aAspectInfo[AspectIndex].pszName;
}

void cDetector::GetVideoFormat(avards_CurrentVideoFormat_v1_0 *format)
{
   Lock();
   format->ModeString = aAspectInfo[lastAspectInfo].pszName;
   format->Mode       = lastAspectInfo;
   format->Width      = lastWidth;
   format->Height     = lastHeight;
   Unlock();
   return;
}

bool cDetector::StartUp()
{
   if (!Running())
      Start();

   return true;
}

void cDetector::Stop()
{
   if (Running()) {
      Cancel(-1);
      Wait.Signal();
   }
}

void cDetector::UninitDevices(void)
{
   munmap(pbVideo, mbufVideo.size);

   close(fdDevDvbVideo);
   close(fdDevVideo);
}


bool cDetector::InitDevices(const char* DevDvbVideo, const char* DevVideo)
{
   fdDevDvbVideo = open(DevDvbVideo,O_RDONLY|O_NONBLOCK); /// /dev/dvb/adapter0/video0
   if (fdDevDvbVideo < 0) {
      esyslog("avards: Error: Can't open dvb video device '%s' (%s)\n", DevDvbVideo, strerror(errno));
      return false;
   }

   fdDevVideo = open(DevVideo, O_RDWR); /// /dev/video0
   if (-1 == fdDevVideo) {
      esyslog("avards: Error: Can't open video device '%s' (%s)\n", DevVideo, strerror(errno));
      return false;
   }


   //struct video_mbuf mbufVideo;
   if (ioctl(fdDevVideo, VIDIOCGMBUF, &mbufVideo) != 0) {
      esyslog("avards: Error: Can't open video buffer on device '%s' (%s)\n", config.pszDevVideo, strerror(errno));
      return false;
   }

   pbVideo = (unsigned char *)mmap(0, mbufVideo.size, PROT_READ | PROT_WRITE, MAP_SHARED, fdDevVideo, 0);
   if (!pbVideo || (pbVideo == (unsigned char *)-1)) {
      esyslog("avards: Error: Can't map video buffer of video device '%s' (%s)\n", DevVideo, strerror(errno));
      return false;
   }

   struct video_capability capabilityVideo;
   if (ioctl(fdDevVideo, VIDIOCGCAP, &capabilityVideo)) {
      esyslog("avards: Error: Can't query caps of video device '%s' (%s)\n", DevVideo, strerror(errno));
      return false;
   }

   mmapVideo.frame = 0;
   mmapVideo.width = capabilityVideo.maxwidth;
   mmapVideo.height = capabilityVideo.maxheight;
   mmapVideo.format = VIDEO_PALETTE_GREY;

   return true;
}


int cDetector::AnalyzeFrame(void)
{
   int iAspectInfo = 0;
   bool bFormatChanged = false;

   const int nMaxLogoWidth = int(mmapVideo.width*config.MaxLogoWidth_pct/100);
   const int nYoverscan = int (mmapVideo.height*config.Overscan_pct/100);
   const int nXoverscan = int (mmapVideo.width*config.Overscan_pct/100);

   unsigned char iPanToneMin;
   unsigned char iPanToneMax;

   int nActiveLines = 0;
   int iBlackLines = 999;
   int iYtop = nYoverscan;

   if (config.PanToneBlackMax) {
      iPanToneMin = 0;
      iPanToneMax = config.PanToneBlackMax;
   }
   else {
      int aHistogram[256];
      memset(aHistogram, 0, sizeof(aHistogram));
      unsigned char *pbVideoLine = pbVideo+iYtop*mmapVideo.width;
      for (int iX = nXoverscan; iX < mmapVideo.width-nXoverscan; iX++)
         aHistogram[pbVideoLine[iX]]++;
      int iXmax = 0;
      for (int i = 0; i < 256; i++)
         if (aHistogram[i] > aHistogram[iXmax])
            iXmax = i;
         if (iXmax - config.PanToneTolerance < 0)
            iPanToneMin = 0;
         else
            iPanToneMin = iXmax - config.PanToneTolerance;
         if (iXmax + config.PanToneTolerance > 255)
            iPanToneMax = 255;
         else
            iPanToneMax = iXmax + config.PanToneTolerance;
   }

   while (iYtop < mmapVideo.height/2) {
      unsigned char *pbVideoLine = pbVideo+iYtop*mmapVideo.width;
      int iXleft = nXoverscan;
      while ( (iXleft < (mmapVideo.width-nXoverscan)) &&
               (pbVideoLine[iXleft] >= iPanToneMin)    &&
               (pbVideoLine[iXleft] <= iPanToneMax)  )
         iXleft++;

      int iXright = mmapVideo.width-nXoverscan-1;
      while ( (iXright > iXleft) &&
               (pbVideoLine[iXright] >= iPanToneMin) &&
               (pbVideoLine[iXright] <= iPanToneMax) )
         iXright--;

      if (iXright - iXleft > nMaxLogoWidth)
         break;
      iYtop++;
   }

   if (iYtop < mmapVideo.height/2) {
      int iYbottom = nYoverscan;
      while (iYbottom < mmapVideo.height/2)
      {
         unsigned char *pbVideoLine = pbVideo+(mmapVideo.height-1-iYbottom)*mmapVideo.width;
         int iXleft = nXoverscan;
         while ( (iXleft < (mmapVideo.width-nXoverscan)) &&
                  (pbVideoLine[iXleft] >= iPanToneMin) &&
                  (pbVideoLine[iXleft] <= iPanToneMax)  )
            iXleft++;

         int iXright = mmapVideo.width-nXoverscan-1;
         while ( (iXright > iXleft) &&
                  (pbVideoLine[iXright] >= iPanToneMin) &&
                  (pbVideoLine[iXright] <= iPanToneMax) )
            iXright--;

         if (iXright - iXleft > nMaxLogoWidth)
            break;
         iYbottom++;
      }

      if (iYbottom < mmapVideo.height/2) {
         iBlackLines = 2*min(iYtop, iYbottom);
         nActiveLines = mmapVideo.height - iBlackLines;
      }
   }

   if (nActiveLines > 0) {
      const float fLinearWidth = 4./3.*mmapVideo.height;
      if (iAspectInfo == 0)
         bFormatChanged = true;
      else {
         const int nAspectHyst = 2;
         int nMinActiveLines = (int)(fLinearWidth/aAspectInfo[iAspectInfo].fMinAspect) - nAspectHyst;
         int nMaxActiveLines =   (iAspectInfo > 1)
            ? (int)(fLinearWidth/aAspectInfo[iAspectInfo-1].fMinAspect) + nAspectHyst
            : mmapVideo.height;
         bFormatChanged = (nActiveLines < nMinActiveLines) || (nActiveLines > nMaxActiveLines);
      }
      float fAspect = fLinearWidth/nActiveLines;
      if (bFormatChanged) {
         if (config.ShowAllLines) {
            iAspectInfo = config.Overzoom ? 4 : 3;
            while (nActiveLines > aAspectInfo[iAspectInfo].Lines && iAspectInfo > 1)
               iAspectInfo--;
         }
         else {
            iAspectInfo = 1;
            while (fAspect > aAspectInfo[iAspectInfo].fMinAspect && iAspectInfo < (config.Overzoom?4:3))
               iAspectInfo++;
         }
      }
   }

   return iAspectInfo;
}


void cDetector::Action(void)
{
   naAspectInfo = sizeof(aAspectInfo) / sizeof(SAspectInfo);
   int fdDevDvbFrontend = 0;

   if (InitDevices(szDevDvbVideo, config.pszDevVideo)) {

      if (config.FrontendHasLockTest) {
         fdDevDvbFrontend = open(szDevDvbFrontend,O_RDONLY|O_NONBLOCK); /// /dev/dvb/adapter0/frontend0
         if (fdDevDvbFrontend < 0) {
            esyslog("avards: Error: Can't open dvb frontend device '%s' (%s)\n", szDevDvbFrontend, strerror(errno));
            fdDevDvbFrontend = 0;
         }
      }

      wssoverdrive = new cWSSoverdrive(config.pszDevVbi);
      int iAspectInfo = 0;
      Lock();
      lastAspectInfo = 0;
      Unlock();
      char ModeBuffer[MAXDELAY];
      memset (&ModeBuffer, 0, sizeof(ModeBuffer));

      bool bError = false;
      bool isPAL = config.VDROSDisPAL;
      video_size_t videoSize;

      while (!bError && Running()) {
         Wait.Wait(max(80, config.PollRate));

         fe_status_t FrontendStatus;
         if (fdDevDvbFrontend && (bError = (ioctl(fdDevDvbFrontend, FE_READ_STATUS, &FrontendStatus) < 0))) {
            esyslog("avards: Error: Can't get status of device '%s' (%s)\n", szDevDvbFrontend, strerror(errno));
            continue;
         }

         if (!fdDevDvbFrontend || (FrontendStatus & FE_HAS_LOCK)) {
            if (config.Mode > MODE_AUTO)
               iAspectInfo = config.Mode;
            else {
               iAspectInfo = 0;

               bError = ioctl(fdDevDvbVideo, VIDEO_GET_SIZE, &videoSize) < 0;
               if (bError) {
                  esyslog("avards: Error: Can't get video size on device '%s' (%s)\n", szDevDvbVideo, strerror(errno));
                  continue;
               }
               isPAL = videoSize.h == 576 || videoSize.h == 288;

               switch (videoSize.aspect_ratio) {
               case VIDEO_FORMAT_221_1:
                  //isyslog("avards: aspect ratio 2.21:1 sent - using 16:9");
                  // fall through
               case VIDEO_FORMAT_16_9:
                  iAspectInfo = 5;
                  break;

               case  VIDEO_FORMAT_4_3:
               default:
                  if (isPAL) {
                     bError = (ioctl(fdDevVideo, VIDIOCMCAPTURE, &mmapVideo) | ioctl(fdDevVideo, VIDIOCSYNC, &mmapVideo.frame));
                     if (bError) {
                        esyslog("avards: Error: Can't capture on video device '%s' (%s)\n", config.pszDevVideo, strerror(errno));
                        continue;
                     }

                     iAspectInfo = AnalyzeFrame();
                  }
                  break;
               }
            }
         }
         //isyslog("avards1: iAspectInfo=%d", iAspectInfo);

         if (!bError) {
            int iDelay = 0;
            if (iAspectInfo == 5 || !isPAL)
               iDelay = config.Delay;
            else
               if (iAspectInfo > 0) {
                  while ((iDelay < config.Delay) && ModeBuffer[iDelay] == iAspectInfo)
                  iDelay++;
               }

            if (iDelay == config.Delay && (lastAspectInfo != iAspectInfo || wasPAL != isPAL)) {
               int newtop, newheight;
               cString msg;

               if (isPAL) { // PAL: setup WSS
                  isyslog("avards: switching WSS to %s", aAspectInfo[iAspectInfo].pszName);
                  int BlackLines = mmapVideo.height - aAspectInfo[iAspectInfo].Lines;
                  wssoverdrive->SetWSS(aAspectInfo[iAspectInfo].iWssData);
                  newtop    = iAspectInfo?Setup.OSDTop + BlackLines/2 : Setup.OSDTop;
                  newheight = iAspectInfo?Setup.OSDHeight - BlackLines : Setup.OSDHeight;
                  if (config.ShowMsg)
                     msg = cString::sprintf("%s %s", tr("switching WSS mode to"), aAspectInfo[iAspectInfo].pszName);
               }
               else {  // non-PAL: stop WSS
                  isyslog("avards: switching off WSS");
                  wssoverdrive->SetWSS(0);
                  double factor = config.VDROSDisPAL ? videoSize.h/576.0 : videoSize.h/480.0;
                  newtop    = int(factor * Setup.OSDTop);
                  newheight = int(factor * Setup.OSDHeight);
                  //isyslog("avards:non-PAL: %d/%d f=%.2f %d/%d %d/%d", mmapVideo.width, mmapVideo.height, factor, Setup.OSDTop, newtop, Setup.OSDHeight, newheight);
                  if (newheight < MINOSDHEIGHT) {
                     newheight = MINOSDHEIGHT;
                     newtop = (mmapVideo.height - newheight) / 2;
                  }
                  if (newheight > MAXOSDHEIGHT) {
                     newheight = MAXOSDHEIGHT;
                     newtop = (mmapVideo.height - newheight) / 2;
                  }
                  if (config.ShowMsg)
                     msg = cString::sprintf("%s", tr("switching off WSS"));
               }

               Lock();
               top    = newtop;
               height = newheight;
               lastAspectInfo = iAspectInfo;
               lastWidth = videoSize.w;
               lastHeight = videoSize.h;
               wasPAL = isPAL;
               Unlock();
#if APIVERSNUM >= 10504
               cOsd::SetOsdPosition(Setup.OSDLeft, top, Setup.OSDWidth, height);
#endif

               if (config.ShowMsg) {
                  Skins.QueueMessage(mtInfo, msg, 0, -1);
               }
            }

            if (config.Delay) {
               memmove(&ModeBuffer[1], &ModeBuffer[0], config.Delay-1);
               ModeBuffer[0] = iAspectInfo;
            }
         }
      }

      lastAspectInfo = 0;
      top    = Setup.OSDTop;
      height = Setup.OSDHeight;
#if APIVERSNUM >= 10504
      cOsd::SetOsdPosition(Setup.OSDLeft, Setup.OSDTop, Setup.OSDWidth, Setup.OSDHeight);
#endif

      wssoverdrive->SetWSS(0);
      delete wssoverdrive;
      wssoverdrive = NULL;

      if (fdDevDvbFrontend)
         close(fdDevDvbFrontend);
   }
   UninitDevices();
}


cWSSoverdrive::cWSSoverdrive(const char* szDevice)
{
   fdDevVbi = 0;
   lastWssData = -1;
   szVbiDevice = strdup(szDevice);
}

cWSSoverdrive::~cWSSoverdrive(void)
{
   if (fdDevVbi)
      CloseVbiDevice();
   free(szVbiDevice);
}

bool cWSSoverdrive::OpenVbiDevice(void)
{
   fdDevVbi = open(szVbiDevice, O_WRONLY);
   if (fdDevVbi < 0) 
   {
      esyslog("avards: Error: Can't open vbi device '%s' (%s)\n", szVbiDevice, strerror(errno));
      return false;
   }

   struct v4l2_format fmt;
   memset(&fmt, 0, sizeof fmt);
   fmt.type = V4L2_BUF_TYPE_SLICED_VBI_OUTPUT;
   fmt.fmt.sliced.service_set = V4L2_SLICED_WSS_625;
   if (ioctl(fdDevVbi, VIDIOC_S_FMT, &fmt) < 0) {
      esyslog("avards Error: Can't init vbi device '%s' (%s)\n", szVbiDevice, strerror(errno));
      CloseVbiDevice();
      return false;
   }

   return true;
}

void cWSSoverdrive::CloseVbiDevice(void)
{
   isyslog("avards: closing vbi device\n");
   if (fdDevVbi) {
      close(fdDevVbi);
      fdDevVbi = 0;
   }
}

bool cWSSoverdrive::SetWSS(int iWssData)
{
   bool result = true;

   if ( 0 == iWssData) {
      if (fdDevVbi)
         CloseVbiDevice();
   }
   else {
      if (!fdDevVbi && !OpenVbiDevice())
         return false;

      if (iWssData != lastWssData) {
         //isyslog("avards: setting WSS data to %02x", iWssData);
         struct v4l2_sliced_vbi_data vbidata;
         memset(&vbidata, 0, sizeof(vbidata));
         vbidata.id = (iWssData) ? V4L2_SLICED_WSS_625 : 0;
         vbidata.line = 23;
         vbidata.data[0] = iWssData & 0xff;
         vbidata.data[1] = (iWssData >> 8) & 0x3f;

         if (write(fdDevVbi, &vbidata, sizeof(vbidata)) != sizeof(vbidata)) {
            esyslog("avards: Error: write to vbi device failed (%s)\n", strerror(errno));
            CloseVbiDevice();
            result = false;
         }
         lastWssData = iWssData;
      }
   }
   return result;
}
