/*
 * dvbosd.c: Interface to the DVB On Screen Display
 *
 * See the main source file 'vdr.c' for copyright information and
 * how to reach the author.
 *
 * $Id: dvbosd.c 1.12 2002/01/13 16:25:18 kls Exp $
 */

#include "dvbosd.h"
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/unistd.h>
#include "tools.h"

// --- cPalette --------------------------------------------------------------

cPalette::cPalette(int Bpp)
{
  maxColors = 1 << Bpp;
  numColors = 0;
  full = false;
}

int cPalette::Index(eDvbColor Color)
{
  for (int i = 0; i < numColors; i++) {
      if (color[i] == Color) {
         used[i] = true;
         return i;
         }
      }
  if (!full) {
     if (numColors < maxColors) {
        color[numColors++] = Color;
        used[numColors - 1] = true;
        fetched[numColors - 1] = false;
        return numColors - 1;
        }
     for (int i = maxColors; --i >= 0; ) {
         if (!used[i]) {
            color[i] = Color;
            used[i] = true;
            fetched[i] = false;
            return i;
            }
         }
     esyslog(LOG_ERR, "ERROR: too many different colors used in palette");
     full = true;
     }
  return 0;
}

void cPalette::Reset(void)
{
  for (int i = 0; i < numColors; i++)
      used[i] = false;
  full = false;
}

const eDvbColor *cPalette::Colors(int &FirstColor, int &LastColor)
{
  for (FirstColor = 0; FirstColor < numColors; FirstColor++) {
      if (!fetched[FirstColor]) {
         for (LastColor = FirstColor; LastColor < numColors && !fetched[LastColor]; LastColor++)
             fetched[LastColor] = true;
         LastColor--; // the loop ended one past the last one!
         return &color[FirstColor];
         }
      }
  return NULL;
}

void cPalette::Take(const cPalette &Palette, tIndexes *Indexes)
{
  for (int i = 0; i < Palette.numColors; i++) {
      if (Palette.used[i]) {
         int n = Index(Palette.color[i]);
         if (Indexes)
            (*Indexes)[i] = n;
         }
      }
}

// --- cBitmap ---------------------------------------------------------------

cBitmap::cBitmap(int Width, int Height, int Bpp, bool ClearWithBackground)
:cPalette(Bpp)
{
  width = Width;
  height = Height;
  clearWithBackground = ClearWithBackground;
  bitmap = NULL;
  fontType = fontOsd;
  font = NULL;
  if (width > 0 && height > 0) {
     bitmap = new char[width * height];
     if (bitmap) {
        Clean();
        memset(bitmap, 0x00, width * height);
        SetFont(fontOsd);
        }
     else
        esyslog(LOG_ERR, "ERROR: can't allocate bitmap!");
     }
  else
     esyslog(LOG_ERR, "ERROR: illegal bitmap parameters (%d, %d)!", width, height);
}

cBitmap::~cBitmap()
{
  delete font;
  delete bitmap;
}

eDvbFont cBitmap::SetFont(eDvbFont Font)
{
  eDvbFont oldFont = fontType;
  if (fontType != Font || !font) {
     delete font;
     font = new cFont(Font);
     fontType = Font;
     }
  return oldFont;
}

bool cBitmap::Dirty(int &x1, int &y1, int &x2, int &y2)
{
  if (dirtyX2 >= 0) {
     //XXX Workaround: apparently the bitmap sent to the driver always has to be a multiple
     //XXX of 8 bits wide, and (dx * dy) also has to be a multiple of 8.
     //TODO Fix driver (should be able to handle any size bitmaps!)
     while ((dirtyX1 > 0 || dirtyX2 < width - 1) && ((dirtyX2 - dirtyX1) & 7) != 7) {
           if (dirtyX2 < width - 1)
              dirtyX2++;
           else if (dirtyX1 > 0)
              dirtyX1--;
           }
     //XXX "... / 2" <==> Bpp???
     while ((dirtyY1 > 0 || dirtyY2 < height - 1) && (((dirtyX2 - dirtyX1 + 1) * (dirtyY2 - dirtyY1 + 1) / 2) & 7) != 0) {
           if (dirtyY2 < height - 1)
              dirtyY2++;
           else if (dirtyY1 > 0)
              dirtyY1--;
           }
     while ((dirtyX1 > 0 || dirtyX2 < width - 1) && (((dirtyX2 - dirtyX1 + 1) * (dirtyY2 - dirtyY1 + 1) / 2) & 7) != 0) {
           if (dirtyX2 < width - 1)
              dirtyX2++;
           else if (dirtyX1 > 0)
              dirtyX1--;
           }
     x1 = dirtyX1;
     y1 = dirtyY1;
     x2 = dirtyX2;
     y2 = dirtyY2;
     return true;
     }
  return false;
}

void cBitmap::Clean(void)
{
  dirtyX1 = width;
  dirtyY1 = height;
  dirtyX2 = -1;
  dirtyY2 = -1;
}

void cBitmap::SetIndex(int x, int y, char Index)
{
  if (bitmap) {
     if (0 <= x && x < width && 0 <= y && y < height) {
        if (bitmap[width * y + x] != Index) {
           bitmap[width * y + x] = Index;
           if (dirtyX1 > x)  dirtyX1 = x;
           if (dirtyY1 > y)  dirtyY1 = y;
           if (dirtyX2 < x)  dirtyX2 = x;
           if (dirtyY2 < y)  dirtyY2 = y;
           }
        }
     }
}

void cBitmap::SetPixel(int x, int y, eDvbColor Color)
{
  SetIndex(x, y, Index(Color));
}

void cBitmap::SetBitmap(int x, int y, const cBitmap &Bitmap)
{
  if (bitmap && Bitmap.bitmap) {
     tIndexes Indexes;
     Take(Bitmap, &Indexes);
     for (int ix = 0; ix < Bitmap.width; ix++) {
         for (int iy = 0; iy < Bitmap.height; iy++)
             SetIndex(x + ix, y + iy, Indexes[Bitmap.bitmap[Bitmap.width * iy + ix]]);
         }
     }
}

int cBitmap::Width(unsigned char c)
{
  return font ? font->Width(c) : -1;
}

int cBitmap::Width(const char *s)
{
  return font ? font->Width(s) : -1;
}

void cBitmap::Text(int x, int y, const char *s, eDvbColor ColorFg, eDvbColor ColorBg)
{
  if (bitmap) {
     char fg = Index(ColorFg);
     char bg = Index(ColorBg);
     int h = font->Height(s);
     while (s && *s) {
           const cFont::tCharData *CharData = font->CharData(*s++);
           if (int(x + CharData->width) > width)
              break;
           for (int row = 0; row < h; row++) {
               cFont::tPixelData PixelData = CharData->lines[row];
               for (int col = CharData->width; col-- > 0; ) {
                   SetIndex(x + col, y + row, (PixelData & 1) ? fg : bg);
                   PixelData >>= 1;
                   }
               }
           x += CharData->width;
           }
     }
}

void cBitmap::Fill(int x1, int y1, int x2, int y2, eDvbColor Color)
{
  if (bitmap) {
     char c = Index(Color);
     for (int y = y1; y <= y2; y++)
         for (int x = x1; x <= x2; x++)
             SetIndex(x, y, c);
     }
}

void cBitmap::Clear(void)
{
  Reset();
  if (clearWithBackground)
     Fill(0, 0, width - 1, height - 1, clrBackground);
}

const char *cBitmap::Data(int x, int y)
{
  return &bitmap[y * width + x];
}

// --- cWindow ---------------------------------------------------------------

class cWindow : public cBitmap {
private:
  int handle;  // the index within the OSD's window array (0...MAXNUMWINDOWS - 1)
  int x0, y0;
  int bpp;
  bool tiled;
  bool shown;
public:
  cWindow(int Handle, int x, int y, int w, int h, int Bpp, bool ClearWithBackground, bool Tiled);
  int X0(void) { return x0; }
  int Y0(void) { return y0; }
  int Bpp(void) { return bpp; }
  bool Tiled(void) { return tiled; }
  bool Shown(void) { bool s = shown; shown = true; return s; }
  int Handle(void) { return handle; }
  bool Contains(int x, int y);
  void Relocate(int x, int y);
  void Fill(int x1, int y1, int x2, int y2, eDvbColor Color);
  void SetBitmap(int x, int y, const cBitmap &Bitmap);
  void Text(int x, int y, const char *s, eDvbColor ColorFg = clrWhite, eDvbColor ColorBg = clrBackground);
  const char *Data(int x, int y);
  };

cWindow::cWindow(int Handle, int x, int y, int w, int h, int Bpp, bool ClearWithBackground, bool Tiled)
:cBitmap(w, h, Bpp, ClearWithBackground)
{
  handle = Handle;
  x0 = x;
  y0 = y;
  bpp = Bpp;
  tiled = Tiled;
  shown = false;
}

bool cWindow::Contains(int x, int y)
{
  x -= x0;
  y -= y0;
  return x >= 0 && y >= 0 && x < width && y < height;
}

void cWindow::Relocate(int x, int y)
{
  x0 = x;
  y0 = y;
}

void cWindow::Fill(int x1, int y1, int x2, int y2, eDvbColor Color)
{
  if (tiled) {
     x1 -= x0;
     y1 -= y0;
     x2 -= x0;
     y2 -= y0;
     }
  cBitmap::Fill(x1, y1, x2, y2, Color);
}

void cWindow::SetBitmap(int x, int y, const cBitmap &Bitmap)
{
  if (tiled) {
     x -= x0;
     y -= y0;
     }
  cBitmap::SetBitmap(x, y, Bitmap);
}

void cWindow::Text(int x, int y, const char *s, eDvbColor ColorFg, eDvbColor ColorBg)
{
  if (tiled) {
     x -= x0;
     y -= y0;
     }
  cBitmap::Text(x, y, s, ColorFg, ColorBg);
}

const char *cWindow::Data(int x, int y)
{
  return cBitmap::Data(x, y);
}

// --- cDvbOsd ---------------------------------------------------------------

cDvbOsd::cDvbOsd(int VideoDev, int x, int y, int w, int h, int Bpp)
{
  videoDev = VideoDev;
  numWindows = 0;
  x0 = x;
  y0 = y;
  if (videoDev >= 0) {
     if (w > 0 && h > 0)
        Create(0, 0, w, h, Bpp);
     }
  else
     esyslog(LOG_ERR, "ERROR: illegal video device handle (%d)!", videoDev);
}

cDvbOsd::~cDvbOsd()
{
  if (videoDev >= 0) {
     for (int i = 0; i < numWindows; i++) {
         SetWindow(window[i]);
         Cmd(OSD_Close);
         delete window[i];
         }
     numWindows = 0;
     }
}

bool cDvbOsd::SetWindow(cWindow *Window)
{
  // Window handles are counted 0...(MAXNUMWINDOWS - 1), but the actual window
  // numbers in the driver are used from 1...MAXNUMWINDOWS.
  int Handle = Window->Handle();
  if (0 <= Handle && Handle < MAXNUMWINDOWS) {
     Cmd(OSD_SetWindow, 0, Handle + 1);
     return true;
     }
  esyslog(LOG_ERR, "ERROR: illegal window handle: %d", Handle);
  return -1;
}

void cDvbOsd::Cmd(OSD_Command cmd, int color, int x0, int y0, int x1, int y1, const void *data)
{
  if (videoDev >= 0) {
     osd_cmd_t dc;
     dc.cmd   = cmd;
     dc.color = color;
     dc.x0    = x0;
     dc.y0    = y0;
     dc.x1    = x1;
     dc.y1    = y1;
     dc.data  = (void *)data;
     // must block all signals, otherwise the command might not be fully executed
     sigset_t set, oldset;
     sigfillset(&set);
     sigdelset(&set, SIGALRM);
     sigprocmask(SIG_BLOCK, &set, &oldset);
     ioctl(videoDev, OSD_SEND_CMD, &dc);
     if (cmd == OSD_SetBlock) // XXX this is the only command that takes longer
        usleep(5000); // XXX Workaround for a driver bug (cInterface::DisplayChannel() displayed texts at wrong places
                      // XXX and sometimes the OSD was no longer displayed).
                      // XXX Increase the value if the problem still persists on your particular system.
                      // TODO Check if this is still necessary with driver versions after 0.7.
     sigprocmask(SIG_SETMASK, &oldset, NULL);
     }
}

tWindowHandle cDvbOsd::Create(int x, int y, int w, int h, int Bpp, bool ClearWithBackground, bool Tiled)
{
  if (numWindows < MAXNUMWINDOWS) {
     if (x >= 0 && y >= 0 && w > 0 && h > 0 && (Bpp == 1 || Bpp == 2 || Bpp == 4 || Bpp == 8)) {
        if ((w & 0x03) != 0) {
           w += 4 - (w & 0x03);
           esyslog(LOG_ERR, "ERROR: OSD window width must be a multiple of 4 - increasing to %d", w);
           }
        cWindow *win = new cWindow(numWindows, x, y, w, h, Bpp, ClearWithBackground, Tiled);
        if (SetWindow(win)) {
           window[win->Handle()] = win;
           Cmd(OSD_Open, Bpp, x0 + x, y0 + y, x0 + x + w - 1, y0 + y + h - 1, (void *)1); // initially hidden!
           numWindows++;
           return win->Handle();
           }
        else
           delete win;
        }
     else
        esyslog(LOG_ERR, "ERROR: illegal OSD parameters");
     }
  else
     esyslog(LOG_ERR, "ERROR: too many OSD windows");
  return -1;
}

void cDvbOsd::AddColor(eDvbColor Color, tWindowHandle Window)
{
  cWindow *w = GetWindow(Window);
  if (w) {
     w->Index(Color);
     w->Reset();
     }
}

cWindow *cDvbOsd::GetWindow(int x, int y)
{
  for (int i = 0; i < numWindows; i++) {
      if (window[i]->Tiled() && window[i]->Contains(x, y))
         return window[i];
      }
  return NULL;
}

cWindow *cDvbOsd::GetWindow(tWindowHandle Window)
{
  if (0 <= Window && Window < numWindows)
     return window[Window];
  if (Window == LAST_CREATED_WINDOW && numWindows > 0)
     return window[numWindows - 1];
  return NULL;
}

void cDvbOsd::Flush(void)
{
  for (int i = 0; i < numWindows; i++) {
      int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
      if (window[i]->Dirty(x1, y1, x2, y2)) {
         SetWindow(window[i]);
         int FirstColor = 0, LastColor = 0;
         const eDvbColor *pal;
         while ((pal = window[i]->Colors(FirstColor, LastColor)) != NULL)
               Cmd(OSD_SetPalette, FirstColor, LastColor, 0, 0, 0, pal);
         Cmd(OSD_SetBlock, window[i]->Width(), x1, y1, x2, y2, window[i]->Data(x1, y1));
         window[i]->Clean();
         }
      }
  // Showing the windows in a separate loop to avoid seeing them come up one after another
  for (int i = 0; i < numWindows; i++) {
      if (!window[i]->Shown()) {
         SetWindow(window[i]);
         Cmd(OSD_MoveWindow, 0, x0 + window[i]->X0(), y0 + window[i]->Y0());
         }
      }
}

void cDvbOsd::Clear(tWindowHandle Window)
{
  if (Window == ALL_TILED_WINDOWS || Window == ALL_WINDOWS) {
     for (int i = 0; i < numWindows; i++)
         if (Window == ALL_WINDOWS || window[i]->Tiled())
            window[i]->Clear();
     }
  else {
     cWindow *w = GetWindow(Window);
     if (w)
        w->Clear();
     }
}

void cDvbOsd::Fill(int x1, int y1, int x2, int y2, eDvbColor Color, tWindowHandle Window)
{
  cWindow *w = (Window == ALL_TILED_WINDOWS) ? GetWindow(x1, y1) : GetWindow(Window);
  if (w)
     w->Fill(x1, y1, x2, y2, Color);
}

void cDvbOsd::SetBitmap(int x, int y, const cBitmap &Bitmap, tWindowHandle Window)
{
  cWindow *w = (Window == ALL_TILED_WINDOWS) ? GetWindow(x, y) : GetWindow(Window);
  if (w)
     w->SetBitmap(x, y, Bitmap);
}

int cDvbOsd::Width(unsigned char c)
{
  return numWindows ? window[0]->Width(c) : 0;
}

int cDvbOsd::Width(const char *s)
{
  return numWindows ? window[0]->Width(s) : 0;
}

eDvbFont cDvbOsd::SetFont(eDvbFont Font)
{
  eDvbFont oldFont = Font;
  for (int i = 0; i < numWindows; i++)
      oldFont = window[i]->SetFont(Font);
  return oldFont;
}

void cDvbOsd::Text(int x, int y, const char *s, eDvbColor ColorFg = clrWhite, eDvbColor ColorBg = clrBackground, tWindowHandle Window)
{
  cWindow *w = (Window == ALL_TILED_WINDOWS) ? GetWindow(x, y) : GetWindow(Window);
  if (w)
     w->Text(x, y, s, ColorFg, ColorBg);
}

void cDvbOsd::Relocate(tWindowHandle Window, int x, int y, int NewWidth, int NewHeight)
{
  cWindow *w = GetWindow(Window);
  if (w) {
     SetWindow(w);
     if (NewWidth > 0 && NewHeight > 0) {
        if ((NewWidth & 0x03) != 0) {
           NewWidth += 4 - (NewWidth & 0x03);
           esyslog(LOG_ERR, "ERROR: OSD window width must be a multiple of 4 - increasing to %d", NewWidth);
           }
        Cmd(OSD_Close);
        window[w->Handle()] = new cWindow(w->Handle(), x, y, NewWidth, NewHeight, w->Bpp(), w->ClearWithBackground(), w->Tiled());
        delete w;
        Cmd(OSD_Open, 2, x0 + x, y0 + y, x0 + x + NewWidth - 1, y0 + y + NewHeight - 1, (void *)1); // initially hidden!
        }
     else {
        w->Relocate(x, y);
        Cmd(OSD_MoveWindow, 0, x0 + x, y0 + y);
        }
     }
}

void cDvbOsd::Hide(tWindowHandle Window)
{
  if (SetWindow(GetWindow(Window)))
     Cmd(OSD_Hide, 0);
}

void cDvbOsd::Show(tWindowHandle Window)
{
  if (SetWindow(GetWindow(Window)))
     Cmd(OSD_Show, 0);
}