/*
 * osdbase.c: Basic interface to the On Screen Display
 *
 * See the main source file 'vdr.c' for copyright information and
 * how to reach the author.
 *
 * $Id: osdbase.c 5.10 2025/06/18 20:22:10 kls Exp $
 */

#include "osdbase.h"
#include <string.h>
#include "device.h"
#include "i18n.h"
#include "menuitems.h"
#include "remote.h"
#include "status.h"

// --- cOsdItem --------------------------------------------------------------

cOsdItem::cOsdItem(eOSState State)
{
  text = NULL;
  state = State;
  selectable = true;
  fresh = true;
}

cOsdItem::cOsdItem(const char *Text, eOSState State, bool Selectable)
{
  text = NULL;
  state = State;
  selectable = Selectable;
  fresh = true;
  SetText(Text);
}

cOsdItem::~cOsdItem()
{
  free(text);
}

void cOsdItem::SetText(const char *Text, bool Copy)
{
  free(text);
  text = Copy ? strdup(Text ? Text : "") : (char *)Text; // text assumes ownership!
}

void cOsdItem::SetSelectable(bool Selectable)
{
  selectable = Selectable;
}

void cOsdItem::SetFresh(bool Fresh)
{
  fresh = Fresh;
}

void cOsdItem::SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable)
{
  DisplayMenu->SetItem(Text(), Index, Current, Selectable);
}

eOSState cOsdItem::ProcessKey(eKeys Key)
{
  return Key == kOk ? state : osUnknown;
}

// --- cOsdObject ------------------------------------------------------------

void cOsdObject::Show(void)
{
  if (isMenu)
     ((cOsdMenu *)this)->Display();
}

// --- cOsdMenu --------------------------------------------------------------

cSkinDisplayMenu *cOsdMenu::displayMenu = NULL;
int cOsdMenu::displayMenuCount = 0;
int cOsdMenu::osdState = 0;
cOsdMenu *cOsdMenu::topMenu = NULL;

cOsdMenu::cOsdMenu(const char *Title, int c0, int c1, int c2, int c3, int c4)
{
  isMenu = true;
  digit = 0;
  hasHotkeys = false;
  if (!topMenu)
     topMenu = this;
  active = this == topMenu;
  displayMenuItems = 0;
  title = NULL;
  menuCategory = mcUnknown;
  menuSortMode = msmUnknown;
  menuOrientation = moVertical;
  SetTitle(Title);
  SetCols(c0, c1, c2, c3, c4);
  first = 0;
  lastOffset = -1;
  conveyStatus = true;
  current = marked = -1;
  subMenu = NULL;
  helpRed = helpGreen = helpYellow = helpBlue = NULL;
  helpDisplayed = false;
  status = NULL;
  if (!displayMenuCount++) {
     cOsdProvider::OsdSizeChanged(osdState); // to get the current state
     SetDisplayMenu();
     }
}

cOsdMenu::~cOsdMenu()
{
  free(title);
  delete subMenu;
  free(status);
  displayMenu->Clear();
  cStatus::MsgOsdClear();
  if (!--displayMenuCount)
     DELETENULL(displayMenu);
  if (this == topMenu)
     topMenu = NULL;
}

void cOsdMenu::SetMenuCategory(eMenuCategory MenuCategory)
{
  menuCategory = MenuCategory;
}

void cOsdMenu::SetMenuSortMode(eMenuSortMode MenuSortMode)
{
  menuSortMode = MenuSortMode;
}

void cOsdMenu::SetActive(bool Active)
{
  active = Active;
}

void cOsdMenu::SetDisplayMenu(void)
{
  if (displayMenu) {
     displayMenu->Clear();
     delete displayMenu;
     }
  displayMenu = Skins.Current()->DisplayMenu();
}

const char *cOsdMenu::hk(const char *s)
{
  static cString buffer;
  if (s && hasHotkeys) {
     if (digit == 0 && '1' <= *s && *s <= '9' && *(s + 1) == ' ')
        digit = -1; // prevents automatic hotkeys - input already has them
     if (digit >= 0) {
        digit++;
        buffer = cString::sprintf(" %c %s", (digit < 10) ? '0' + digit : ' ' , s);
        s = buffer;
        }
     }
  return s;
}

void cOsdMenu::SetCols(int c0, int c1, int c2, int c3, int c4)
{
  cols[0] = c0;
  cols[1] = c1;
  cols[2] = c2;
  cols[3] = c3;
  cols[4] = c4;
}

void cOsdMenu::SetHasHotkeys(bool HasHotkeys)
{
  hasHotkeys = HasHotkeys;
  digit = 0;
}

void cOsdMenu::SetStatus(const char *s)
{
  free(status);
  status = s ? strdup(s) : NULL;
  displayMenu->SetMessage(mtStatus, s);
  cStatus::MsgOsdStatusMessage(mtStatus, s);
}

void cOsdMenu::SetTitle(const char *Title)
{
  free(title);
  title = strdup(Title);
}

void cOsdMenu::DisplayHelp(bool Force)
{
  if (!helpDisplayed || Force) {
     displayMenu->SetButtons(helpRed, helpGreen, helpYellow, helpBlue);
     if (conveyStatus)
        cStatus::MsgOsdHelpKeys(helpRed, helpGreen, helpYellow, helpBlue);
     helpDisplayed = true;
     }
}

void cOsdMenu::SetHelp(const char *Red, const char *Green, const char *Yellow, const char *Blue)
{
  // strings are NOT copied - must be constants!!!
  helpRed    = Red;
  helpGreen  = Green;
  helpYellow = Yellow;
  helpBlue   = Blue;
  DisplayHelp(true);
}

void cOsdMenu::Del(int Index)
{
  cList<cOsdItem>::Del(Get(Index));
  int count = Count();
  while (current < count && !SelectableItem(current))
        current++;
  if (current == count) {
     while (current > 0 && !SelectableItem(current))
           current--;
     }
  if (Index == first && first > 0)
     first--;
}

void cOsdMenu::Add(cOsdItem *Item, bool Current, cOsdItem *After)
{
  cList<cOsdItem>::Add(Item, After);
  if (Current)
     current = Item->Index();
}

void cOsdMenu::Ins(cOsdItem *Item, bool Current, cOsdItem *Before)
{
  cList<cOsdItem>::Ins(Item, Before);
  if (Current)
     current = Item->Index();
}

void cOsdMenu::DisplayNoStatus(void)
{
  conveyStatus = false;
  Display();
  conveyStatus = true;
}

void cOsdMenu::Display(void)
{
  if (subMenu) {
     subMenu->Display();
     return;
     }
  if (!active)
     return;
  if (cOsdProvider::OsdSizeChanged(osdState))
     SetDisplayMenu();
  displayMenu->SetMessage(mtStatus, NULL);
  cStatus::MsgOsdStatusMessage(mtStatus, NULL);
  displayMenu->Clear();
  if (conveyStatus)
     cStatus::MsgOsdClear();
  if (menuCategory != displayMenu->MenuCategory())
     displayMenu->SetMenuCategory(menuCategory);
  displayMenu->SetMenuSortMode(menuSortMode);
  menuOrientation = displayMenu->MenuOrientation();
  displayMenuItems = displayMenu->MaxItems();
  displayMenu->SetTabs(cols[0], cols[1], cols[2], cols[3], cols[4]);//XXX
  displayMenu->SetTitle(title);
  if (conveyStatus)
     cStatus::MsgOsdTitle(title);
  DisplayHelp(true);
  int count = Count();
  if (count > 0) {
     int ni = 0;
     for (cOsdItem *item = First(); item; item = Next(item)) {
         if (conveyStatus)
            cStatus::MsgOsdItem(item->Text(), ni++, item->Selectable());
         if (current < 0 && item->Selectable())
            current = item->Index();
         }
     if (current < 0)
        current = 0; // just for safety - there HAS to be a current item!
     first = max(0, min(first, max(0, count - displayMenuItems))); // in case the menu size has changed
     if (current - first >= displayMenuItems || current < first) {
        first = current - displayMenuItems / 2;
        if (first + displayMenuItems > count)
           first = count - displayMenuItems;
        if (first < 0)
           first = 0;
        }
     int i = first;
     int n = 0;
     for (cOsdItem *item = Get(first); item; item = Next(item)) {
         bool CurrentSelectable = (i == current) && item->Selectable();
         item->SetMenuItem(displayMenu, i - first, CurrentSelectable, item->Selectable());
         if (CurrentSelectable) // not checking conveyStatus here!
            cStatus::MsgOsdCurrentItem(item->Text(), i);
         if (++n == displayMenuItems)
            break;
         i++;
         }
     }
  displayMenu->SetScrollbar(count, first);
  if (!isempty(status)) {
     displayMenu->SetMessage(mtStatus, status);
     cStatus::MsgOsdStatusMessage(mtStatus, status);
     }
}

void cOsdMenu::SetCurrent(cOsdItem *Item)
{
  current = Item ? Item->Index() : -1;
  if (current >= 0 && lastOffset >= 0)
     first = max(0, current - lastOffset);
  lastOffset = -1;
}

void cOsdMenu::RefreshCurrent(void)
{
  cOsdItem *item = Get(current);
  if (item)
     item->Set();
}

void cOsdMenu::DisplayCurrent(bool Current)
{
  cOsdItem *item = Get(current);
  if (item) {
     item->SetMenuItem(displayMenu, current - first, Current && item->Selectable(), item->Selectable());
     if (Current) {
        if (current - first >= displayMenuItems || current < first)
           DisplayNoStatus();
        else if (item->Selectable())
           cStatus::MsgOsdCurrentItem(item->Text(), current);
        }
     else
        item->SetFresh(true); // leaving the current item resets 'fresh'
     if (cMenuEditItem *MenuEditItem = dynamic_cast<cMenuEditItem *>(item)) {
        if (!MenuEditItem->DisplayHelp(Current))
           DisplayHelp();
        else
           helpDisplayed = false;
        }
     }
}

void cOsdMenu::DisplayItem(cOsdItem *Item)
{
  if (Item) {
     int Index = Item->Index();
     int Offset = Index - first;
     if (Offset >= 0 && Offset < first + displayMenuItems) {
        bool Current = Index == current;
        Item->SetMenuItem(displayMenu, Offset, Current && Item->Selectable(), Item->Selectable());
        if (Current && Item->Selectable())
           cStatus::MsgOsdCurrentItem(Item->Text(), Index);
        }
     }
}

void cOsdMenu::Clear(void)
{
  if (marked >= 0)
     SetStatus(NULL);
  if (current >= 0)
     lastOffset = (current > first) ? current - first : 0;
  first = 0;
  current = marked = -1;
  cList<cOsdItem>::Clear();
}

bool cOsdMenu::SelectableItem(int idx)
{
  cOsdItem *item = Get(idx);
  return item && item->Selectable();
}

void cOsdMenu::CursorUp(void)
{
  int tmpCurrent = current;
  int lastOnScreen = first + displayMenuItems - 1;
  int last = Count() - 1;
  if (last < 0)
     return;
  while (--tmpCurrent != current) {
        if (tmpCurrent < 0) {
           if (first > 0) {
              // make non-selectable items at the beginning visible:
              first = 0;
              DisplayNoStatus();
              return;
              }
           if (Setup.MenuScrollWrap)
              tmpCurrent = last + 1;
           else
              return;
           }
        else if (SelectableItem(tmpCurrent))
           break;
        }
  if (first <= tmpCurrent && tmpCurrent <= lastOnScreen)
     DisplayCurrent(false);
  current = tmpCurrent;
  if (current < first) {
     first = Setup.MenuScrollPage ? max(0, current - displayMenuItems + 1) : current;
     DisplayNoStatus();
     }
  else if (current > lastOnScreen) {
     first = max(0, current - displayMenuItems + 1);
     DisplayNoStatus();
     }
  else
     DisplayCurrent(true);
}

void cOsdMenu::CursorDown(void)
{
  int tmpCurrent = current;
  int lastOnScreen = first + displayMenuItems - 1;
  int last = Count() - 1;
  if (last < 0)
     return;
  while (++tmpCurrent != current) {
        if (tmpCurrent > last) {
           if (first < last - displayMenuItems) {
              // make non-selectable items at the end visible:
              first = last - displayMenuItems + 1;
              DisplayNoStatus();
              return;
              }
           if (Setup.MenuScrollWrap)
              tmpCurrent = -1;
           else
              return;
           }
        else if (SelectableItem(tmpCurrent))
           break;
        }
  if (first <= tmpCurrent && tmpCurrent <= lastOnScreen)
     DisplayCurrent(false);
  current = tmpCurrent;
  if (current > lastOnScreen) {
     first = Setup.MenuScrollPage ? current : max(0, current - displayMenuItems + 1);
     if (first + displayMenuItems > last)
        first = max(0, last - displayMenuItems + 1);
     DisplayNoStatus();
     }
  else if (current < first) {
     first = current;
     DisplayNoStatus();
     }
  else
     DisplayCurrent(true);
}

void cOsdMenu::PageUp(void)
{
  int oldCurrent = current;
  int oldFirst = first;
  current -= displayMenuItems;
  first -= displayMenuItems;
  int last = Count() - 1;
  if (current < 0)
     current = 0;
  if (first < 0)
     first = 0;
  int tmpCurrent = current;
  while (!SelectableItem(tmpCurrent) && --tmpCurrent >= 0)
        ;
  if (tmpCurrent < 0) {
     tmpCurrent = current;
     while (++tmpCurrent <= last && !SelectableItem(tmpCurrent))
           ;
     }
  current = tmpCurrent <= last ? tmpCurrent : -1;
  if (current >= 0) {
     if (current < first)
        first = current;
     else if (current - first >= displayMenuItems)
        first = current - displayMenuItems + 1;
     }
  if (current != oldCurrent || first != oldFirst)
     DisplayNoStatus();
  else if (Setup.MenuScrollWrap)
     CursorUp();
}

void cOsdMenu::PageDown(void)
{
  int oldCurrent = current;
  int oldFirst = first;
  current += displayMenuItems;
  first += displayMenuItems;
  int last = Count() - 1;
  if (current > last)
     current = last;
  if (first + displayMenuItems > last)
     first = max(0, last - displayMenuItems + 1);
  int tmpCurrent = current;
  while (!SelectableItem(tmpCurrent) && ++tmpCurrent <= last)
        ;
  if (tmpCurrent > last) {
     tmpCurrent = current;
     while (--tmpCurrent >= 0 && !SelectableItem(tmpCurrent))
           ;
     }
  current = tmpCurrent > 0 ? tmpCurrent : -1;
  if (current >= 0) {
     if (current < first)
        first = current;
     else if (current - first >= displayMenuItems)
        first = current - displayMenuItems + 1;
     }
  if (current != oldCurrent || first != oldFirst)
     DisplayNoStatus();
  else if (Setup.MenuScrollWrap)
     CursorDown();
}

void cOsdMenu::Mark(void)
{
  if (Count() && marked < 0) {
     marked = current;
     SetStatus(tr("Up/Dn for new location - OK to move"));
     }
}

eOSState cOsdMenu::HotKey(eKeys Key)
{
  for (cOsdItem *item = First(); item; item = Next(item)) {
      const char *s = item->Text();
      if (s && (s = skipspace(s)) != NULL) {
         if (*s == Key - k1 + '1') {
            DisplayCurrent(false);
            current = item->Index();
            RefreshCurrent();
            DisplayCurrent(true);
            cRemote::Put(kOk, true);
            break;
            }
         }
      }
  return osContinue;
}

eOSState cOsdMenu::AddSubMenu(cOsdMenu *SubMenu)
{
  SetActive(false);
  delete subMenu;
  subMenu = SubMenu;
  subMenu->SetActive(true);
  subMenu->Display();
  return osContinue; // convenience return value
}

eOSState cOsdMenu::CloseSubMenu(bool ReDisplay)
{
  delete subMenu;
  subMenu = NULL;
  SetActive(true);
  if (ReDisplay) {
     RefreshCurrent();
     Display();
     }
  return osContinue; // convenience return value
}

eOSState cOsdMenu::ProcessKey(eKeys Key)
{
  if (subMenu) {
     eOSState state = subMenu->ProcessKey(Key);
     if (state == osBack)
        return CloseSubMenu();
     return state;
     }

  cOsdItem *item = Get(current);
  if (marked < 0 && item) {
     eOSState state = item->ProcessKey(Key);
     if (state != osUnknown) {
        DisplayCurrent(true);
        return state;
        }
     }
  switch (int(Key)) {
    case k0:      return osUnknown;
    case k1...k9: return hasHotkeys ? HotKey(Key) : osUnknown;
    case kUp|k_Repeat:
    case kUp:     if (menuOrientation == moHorizontal) PageUp();     else CursorUp(); break;
    case kDown|k_Repeat:
    case kDown:   if (menuOrientation == moHorizontal) PageDown();   else CursorDown(); break;
    case kLeft|k_Repeat:
    case kLeft:   if (menuOrientation == moHorizontal) CursorUp();   else PageUp(); break;
    case kRight|k_Repeat:
    case kRight:  if (menuOrientation == moHorizontal) CursorDown(); else PageDown(); break;
    case kBack: return osBack;
    case kOk:   if (marked >= 0) {
                   SetStatus(NULL);
                   if (marked != current)
                      Move(marked, current);
                   marked = -1;
                   break;
                   }
                // else run into default
    default: if (marked < 0)
                return osUnknown;
    }
  return osContinue;
}
