/*
htop - Panel.c
(C) 2004-2011 Hisham H. Muhammad
Released under the GNU GPLv2+, see the COPYING file
in the source distribution for its full text.
*/

#include "config.h" // IWYU pragma: keep

#include "Panel.h"

#include <assert.h>
#include <ctype.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

#include "CRT.h"
#include "ListItem.h"
#include "Macros.h"
#include "ProvideCurses.h"
#include "RichString.h"
#include "XUtils.h"


const PanelClass Panel_class = {
   .super = {
      .extends = Class(Object),
      .delete = Panel_delete
   },
   .eventHandler = Panel_selectByTyping,
};

Panel* Panel_new(int x, int y, int w, int h, const ObjectClass* type, bool owner, FunctionBar* fuBar) {
   Panel* this = xMalloc(sizeof(Panel));
   Object_setClass(this, Class(Panel));
   Panel_init(this, x, y, w, h, type, owner, fuBar);
   return this;
}

void Panel_delete(Object* cast) {
   Panel* this = (Panel*)cast;
   Panel_done(this);
   free(this);
}

void Panel_init(Panel* this, int x, int y, int w, int h, const ObjectClass* type, bool owner, FunctionBar* fuBar) {
   this->x = x;
   this->y = y;
   this->w = w;
   this->h = h;
   this->cursorX = 0;
   this->cursorY = 0;
   this->eventHandlerState = NULL;
   this->items = Vector_new(type, owner, VECTOR_DEFAULT_SIZE);
   this->scrollV = 0;
   this->scrollH = 0;
   this->selected = 0;
   this->oldSelected = 0;
   this->prevSelected = -1;
   this->selectedLen = 0;
   this->needsRedraw = true;
   this->cursorOn = false;
   this->wasFocus = false;
   this->allowExcessScrollV = false;
   RichString_beginAllocated(this->header);
   this->defaultBar = fuBar;
   this->currentBar = fuBar;
   this->selectionColorId = PANEL_SELECTION_FOCUS;
}

void Panel_done(Panel* this) {
   assert(this != NULL);

   free(this->eventHandlerState);
   Vector_delete(this->items);
   FunctionBar_delete(this->defaultBar);
   RichString_delete(&this->header);
}

void Panel_setCursorToSelection(Panel* this) {
   this->cursorY = this->y + this->selected - this->scrollV + 1;
   this->cursorX = this->x + (int)this->selectedLen - this->scrollH;
}

void Panel_setSelectionColor(Panel* this, ColorElements colorId) {
   this->selectionColorId = colorId;
}

inline void Panel_setHeader(Panel* this, const char* header) {
   RichString_writeWide(&(this->header), CRT_colors[PANEL_HEADER_FOCUS], header);
   this->needsRedraw = true;
}

void Panel_move(Panel* this, int x, int y) {
   assert(this != NULL);

   this->x = x;
   this->y = y;
   this->needsRedraw = true;
}

void Panel_resize(Panel* this, int w, int h) {
   assert(this != NULL);

   this->w = w;
   this->h = h;
   this->needsRedraw = true;
}

void Panel_prune(Panel* this) {
   assert(this != NULL);

   Vector_prune(this->items);
   this->prevSelected = -1;
   this->scrollV = 0;
   this->selected = 0;
   this->oldSelected = 0;
   this->needsRedraw = true;
   this->allowExcessScrollV = false;
}

void Panel_add(Panel* this, Object* o) {
   assert(this != NULL);

   Vector_add(this->items, o);
   this->prevSelected = -1;
   this->needsRedraw = true;
}

void Panel_insert(Panel* this, int i, Object* o) {
   assert(this != NULL);

   Vector_insert(this->items, i, o);
   this->prevSelected = -1;
   this->needsRedraw = true;
}

void Panel_set(Panel* this, int i, Object* o) {
   assert(this != NULL);

   Vector_set(this->items, i, o);
}

Object* Panel_get(Panel* this, int i) {
   assert(this != NULL);

   return Vector_get(this->items, i);
}

Object* Panel_remove(Panel* this, int i) {
   assert(this != NULL);

   this->needsRedraw = true;
   Object* removed = Vector_remove(this->items, i);
   this->prevSelected = -1;
   if (this->selected > 0 && this->selected >= Vector_size(this->items)) {
      this->selected--;
   }

   return removed;
}

Object* Panel_getSelected(Panel* this) {
   assert(this != NULL);

   if (Vector_size(this->items) > 0) {
      return Vector_get(this->items, this->selected);
   } else {
      return NULL;
   }
}

void Panel_moveSelectedUp(Panel* this) {
   assert(this != NULL);

   Vector_moveUp(this->items, this->selected);
   this->prevSelected = -1;
   if (this->selected > 0) {
      this->selected--;
   }
}

void Panel_moveSelectedDown(Panel* this) {
   assert(this != NULL);

   Vector_moveDown(this->items, this->selected);
   this->prevSelected = -1;
   if (this->selected + 1 < Vector_size(this->items)) {
      this->selected++;
   }
}

int Panel_getSelectedIndex(const Panel* this) {
   assert(this != NULL);

   return this->selected;
}

int Panel_size(const Panel* this) {
   assert(this != NULL);

   return Vector_size(this->items);
}

void Panel_setSelected(Panel* this, int selected) {
   assert(this != NULL);

   int size = Vector_size(this->items);
   if (selected >= size) {
      selected = size - 1;
   }
   if (selected < 0) {
      selected = 0;
   }
   this->selected = selected;
   if (Panel_eventHandlerFn(this)) {
      Panel_eventHandler(this, EVENT_SET_SELECTED);
   }
}

void Panel_splice(Panel* this, Vector* from) {
   assert(this != NULL);
   assert(from != NULL);

   Vector_splice(this->items, from);
   this->prevSelected = -1;
   this->needsRedraw = true;
}

void Panel_draw(Panel* this, bool force_redraw, bool focus, bool highlightSelected, bool hideFunctionBar) {
   assert(this != NULL);

   int size = Vector_size(this->items);
   int scrollH = this->scrollH;
   int y = this->y;
   int x = this->x;
   int h = this->h;

   if (hideFunctionBar)
      h++;

   const int header_attr = focus
                         ? CRT_colors[PANEL_HEADER_FOCUS]
                         : CRT_colors[PANEL_HEADER_UNFOCUS];
   if (force_redraw) {
      if (Panel_printHeaderFn(this))
         Panel_printHeader(this);
      else
         RichString_setAttr(&this->header, header_attr);
   }
   int headerLen = RichString_sizeVal(this->header);
   if (headerLen > 0) {
      attrset(header_attr);
      mvhline(y, x, ' ', this->w);
      if (scrollH < headerLen) {
         RichString_printoffnVal(this->header, y, x, scrollH,
            MINIMUM(headerLen - scrollH, this->w));
      }
      attrset(CRT_colors[RESET_COLOR]);
      y++;
      h--;
   }

   /* ensure scroll area is on screen
      Note: when allowExcessScrollV is set, negative scrollV is allowed to display
      empty lines above the first row, and scrollV > size-h is allowed to display
      empty lines below the last row. Both are used by stable tree view hard mode
      to keep the selected row at a fixed screen position. */
   if (!this->allowExcessScrollV) {
      if (this->scrollV < 0) {
         this->scrollV = 0;
         this->needsRedraw = true;
      } else if (this->scrollV > size - h) {
         this->scrollV = MAXIMUM(size - h, 0);
         this->needsRedraw = true;
      }
   }   if (!this->allowExcessScrollV && this->scrollV > size - h) {
      this->scrollV = MAXIMUM(size - h, 0);
      this->needsRedraw = true;
   }
   // ensure selection is on screen
   if (this->selected < this->scrollV) {
      this->scrollV = this->selected;
      this->needsRedraw = true;
   } else if (this->selected >= this->scrollV + h) {
      this->scrollV = this->selected - h + 1;
      this->needsRedraw = true;
   }

   // topPad: empty screen lines above the first row (non-zero when scrollV is negative)
   int topPad = (this->scrollV < 0) ? -this->scrollV : 0;
   int first = this->scrollV + topPad;
   int upTo = MINIMUM(first + h - topPad, size);

   int selectionColor = focus
      ? CRT_colors[this->selectionColorId]
      : CRT_colors[PANEL_SELECTION_UNFOCUS];

   RichString_begin(item);
   if (this->needsRedraw || force_redraw) {
      int line = 0;
      while (line < topPad) {
         mvhline(y + line, x, ' ', this->w);
         line++;
      }
      for (int i = first; line < h && i < upTo; i++) {
         const Object* itemObj = Vector_get(this->items, i);
         RichString_rewind(&item, RichString_size(&item));
         item.highlightAttr = 0;
         Object_display(itemObj, &item);
         int itemLen = RichString_sizeVal(item);
         int amt = MINIMUM(itemLen - scrollH, this->w);
         if (highlightSelected && i == this->selected) {
            item.highlightAttr = selectionColor;
         }
         if (item.highlightAttr) {
            attrset(item.highlightAttr);
            RichString_setAttr(&item, item.highlightAttr);
            this->selectedLen = itemLen;
         }
         mvhline(y + line, x, ' ', this->w);
         if (amt > 0)
            RichString_printoffnVal(item, y + line, x, scrollH, amt);
         if (item.highlightAttr)
            attrset(CRT_colors[RESET_COLOR]);
         line++;
      }
      while (line < h) {
         mvhline(y + line, x, ' ', this->w);
         line++;
      }

   } else {
      const Object* oldObj = Vector_get(this->items, this->oldSelected);
      RichString_rewind(&item, RichString_size(&item));
      Object_display(oldObj, &item);
      int oldLen = RichString_sizeVal(item);
      mvhline(y + this->oldSelected - this->scrollV, x + 0, ' ', this->w);
      if (scrollH < oldLen)
         RichString_printoffnVal(item, y + this->oldSelected - this->scrollV, x,
            scrollH, MINIMUM(oldLen - scrollH, this->w));

      const Object* newObj = Vector_get(this->items, this->selected);
      RichString_rewind(&item, RichString_size(&item));
      item.highlightAttr = 0;
      Object_display(newObj, &item);
      int newLen = RichString_sizeVal(item);
      this->selectedLen = newLen;
      attrset(selectionColor);
      mvhline(y + this->selected - this->scrollV, x + 0, ' ', this->w);
      RichString_setAttr(&item, selectionColor);
      if (scrollH < newLen)
         RichString_printoffnVal(item, y + this->selected - this->scrollV, x,
            scrollH, MINIMUM(newLen - scrollH, this->w));
      attrset(CRT_colors[RESET_COLOR]);
   }
   RichString_delete(&item);

   if (focus && (this->needsRedraw || force_redraw || !this->wasFocus)) {
      if (Panel_drawFunctionBarFn(this))
         Panel_drawFunctionBar(this, hideFunctionBar);
      else if (!hideFunctionBar)
         FunctionBar_draw(this->currentBar);
   }

   this->oldSelected = this->selected;
   this->wasFocus = focus;
   this->needsRedraw = false;
}

static int Panel_headerHeight(const Panel* this) {
   assert(this != NULL);

   return RichString_sizeVal(this->header) > 0 ? 1 : 0;
}

bool Panel_onKey(Panel* this, int key) {
   assert(this != NULL);

   const int size = Vector_size(this->items);
   const int availableHeight = this->h - Panel_headerHeight(this);
   const int maxScroll = MAXIMUM(0, size - availableHeight);

   #define PANEL_SCROLL(amount)                                                                                     \
   do {                                                                                                             \
      this->selected += (amount);                                                                                   \
      this->scrollV = CLAMP(this->scrollV + (amount), 0, MAXIMUM(0, (size - this->h - Panel_headerHeight(this))));  \
      this->needsRedraw = true;                                                                                     \
   } while (0)

   switch (key) {
      case KEY_DOWN:
      case KEY_CTRL('N'):
      #ifdef KEY_C_DOWN
      case KEY_C_DOWN:
      #endif
         this->selected++;
         break;

      case KEY_UP:
      case KEY_CTRL('P'):
      #ifdef KEY_C_UP
      case KEY_C_UP:
      #endif
         this->selected--;
         break;

      case KEY_LEFT:
      case KEY_CTRL('B'):
         if (this->scrollH > 0) {
            this->scrollH -= MAXIMUM(CRT_scrollHAmount, 0);
            this->needsRedraw = true;
         }
         break;

      case KEY_RIGHT:
      case KEY_CTRL('F'):
         this->scrollH += CRT_scrollHAmount;
         this->needsRedraw = true;
         break;

      case KEY_PPAGE:
         PANEL_SCROLL(-(this->h - Panel_headerHeight(this)));
         break;

      case KEY_NPAGE:
         PANEL_SCROLL(+(this->h - Panel_headerHeight(this)));
         break;

      case KEY_WHEELUP:
         PANEL_SCROLL(-CRT_scrollWheelVAmount);
         break;

      case KEY_WHEELDOWN:
         PANEL_SCROLL(+CRT_scrollWheelVAmount);
         break;

      case KEY_SR:
         if (this->scrollV > 0) {
            // keep selection within the now-visible area
            if (this->selected < this->scrollV + availableHeight) {
               this->scrollV--;
               this->needsRedraw = true;
            }
         }
         break;

      case KEY_SF:
         if (this->scrollV < maxScroll) {
            // keep selection within the now-visible area
            if (this->selected >= this->scrollV) {
               this->scrollV++;
               this->needsRedraw = true;
            }
         }
         break;

      case KEY_HOME:
         this->selected = 0;
         break;

      case KEY_END:
         this->selected = size - 1;
         break;

      case KEY_CTRL('A'):
      case '^':
         this->scrollH = 0;
         this->needsRedraw = true;
         break;

      case KEY_CTRL('E'):
      case '$':
         assert(this->w > 0);
         if (this->selectedLen < (size_t)this->w) {
            this->scrollH = 0;
         } else if (this->selectedLen - (size_t)this->w > (size_t)INT_MAX) {
            this->scrollH = INT_MAX;
         } else {
            this->scrollH = (int)(this->selectedLen - (size_t)this->w);
         }
         this->needsRedraw = true;
         break;

      default:
         return false;
   }

   #undef PANEL_SCROLL

   // ensure selection within bounds
   if (this->selected < 0 || size == 0) {
      this->selected = 0;
      this->needsRedraw = true;
   } else if (this->selected >= size) {
      this->selected = size - 1;
      this->needsRedraw = true;
   }

   return true;
}


HandlerResult Panel_selectByTyping(Panel* this, int ch) {
   int size = Panel_size(this);

   if (ch == '#')
      return IGNORED;

   if (!this->eventHandlerState)
      this->eventHandlerState = xCalloc(100, sizeof(char));

   char* buffer = this->eventHandlerState;

   if (0 < ch && ch < 255 && isgraph((unsigned char)ch)) {
      size_t len = strlen(buffer);
      if (!len) {
         if ('/' == ch) {
            ch = '\001';
         } else if ('q' == ch) {
            return BREAK_LOOP;
         }
      } else if (1 == len && '\001' == buffer[0]) {
         len--;
      }

      if (len < 99) {
         buffer[len] = (char) ch;
         buffer[len + 1] = '\0';
      }

      for (int try = 0; try < 2; try++) {
         len = strlen(buffer);
         for (int i = 0; i < size; i++) {
            const char* cur = ((ListItem*) Panel_get(this, i))->value;
            while (*cur == ' ')
               cur++;
            if (strncasecmp(cur, buffer, len) == 0) {
               Panel_setSelected(this, i);
               return HANDLED;
            }
         }

         // if current word did not match,
         // retry considering the character the start of a new word.
         buffer[0] = (char) ch;
         buffer[1] = '\0';
      }

      return HANDLED;
   } else if (ch != ERR) {
      buffer[0] = '\0';
   }

   if (ch == 13) {
      return BREAK_LOOP;
   }

   return IGNORED;
}

int Panel_getCh(Panel* this) {
   assert(this != NULL);

   if (this->cursorOn) {
      move(this->cursorY, this->cursorX);
      curs_set(1);
   } else {
      curs_set(0);
   }

#ifdef HAVE_SET_ESCDELAY
   set_escdelay(25);
#endif

   return getch();
}
