// Aseprite UI Library
// Copyright (C) 2018-2023  Igara Studio S.A.
// Copyright (C) 2001-2017  David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "ui/combobox.h"

#include "gfx/size.h"
#include "os/font.h"
#include "ui/button.h"
#include "ui/entry.h"
#include "ui/fit_bounds.h"
#include "ui/listbox.h"
#include "ui/listitem.h"
#include "ui/manager.h"
#include "ui/message.h"
#include "ui/resize_event.h"
#include "ui/scale.h"
#include "ui/size_hint_event.h"
#include "ui/system.h"
#include "ui/theme.h"
#include "ui/view.h"
#include "ui/window.h"

#include <algorithm>

namespace ui {

using namespace gfx;

class ComboBoxButton : public Button {
public:
  ComboBoxButton() : Button("") {
    setFocusStop(false);
  }
};

class ComboBoxEntry : public Entry {
public:
  ComboBoxEntry(ComboBox* comboBox)
    : Entry(256, ""),
      m_comboBox(comboBox) {
  }

protected:
  bool onProcessMessage(Message* msg) override;
  void onPaint(PaintEvent& ev) override;
  void onChange() override;

private:
  ComboBox* m_comboBox;
};

class ComboBoxListBox : public ListBox {
public:
  ComboBoxListBox(ComboBox* comboBox)
    : m_comboBox(comboBox) {
    for (auto item : *comboBox) {
      if (item->parent())
        item->parent()->removeChild(item);
      addChild(item);
    }
  }

  void clean() {
    // Remove all added items so ~Widget() don't delete them.
    removeAllChildren();
    selectChild(nullptr);
  }

protected:
  bool onProcessMessage(Message* msg) override;
  void onChange() override;

private:
  bool isValidItem(int index) const {
    return (index >= 0 && index < m_comboBox->getItemCount());
  }

  ComboBox* m_comboBox;
};

ComboBox::ComboBox()
  : Widget(kComboBoxWidget)
  , m_entry(new ComboBoxEntry(this))
  , m_button(new ComboBoxButton())
  , m_window(nullptr)
  , m_listbox(nullptr)
  , m_selected(-1)
  , m_editable(false)
  , m_clickopen(true)
  , m_casesensitive(true)
  , m_filtering(false)
  , m_useCustomWidget(false)
{
  m_entry->setExpansive(true);

  // When the "m_button" is clicked ("Click" signal) call onButtonClick() method
  m_button->Click.connect(&ComboBox::onButtonClick, this);

  addChild(m_entry);
  addChild(m_button);

  setFocusStop(true);
  setEditable(m_editable);

  initTheme();
}

ComboBox::~ComboBox()
{
  removeMessageFilters();
  deleteAllItems();
}

void ComboBox::setEditable(bool state)
{
  m_editable = state;

  if (state) {
    m_entry->setReadOnly(false);
    m_entry->showCaret();
  }
  else {
    m_entry->setReadOnly(true);
    m_entry->hideCaret();
  }
}

void ComboBox::setClickOpen(bool state)
{
  m_clickopen = state;
}

void ComboBox::setCaseSensitive(bool state)
{
  m_casesensitive = state;
}

void ComboBox::setUseCustomWidget(bool state)
{
  m_useCustomWidget = state;
}

int ComboBox::addItem(Widget* item)
{
  bool sel_first = m_items.empty();

  m_items.push_back(item);

  if (sel_first && !isEditable())
    setSelectedItemIndex(0);

  return m_items.size()-1;
}

int ComboBox::addItem(const std::string& text)
{
  return addItem(new ListItem(text));
}

void ComboBox::insertItem(int itemIndex, Widget* item)
{
  bool sel_first = m_items.empty();

  m_items.insert(m_items.begin() + itemIndex, item);

  if (sel_first)
    setSelectedItemIndex(0);
}

void ComboBox::insertItem(int itemIndex, const std::string& text)
{
  insertItem(itemIndex, new ListItem(text));
}

void ComboBox::removeItem(Widget* item)
{
  auto it = std::find(m_items.begin(), m_items.end(), item);
  ASSERT(it != m_items.end());
  if (it != m_items.end())
    m_items.erase(it);

  // Do not delete the given "item"
}

void ComboBox::deleteItem(int itemIndex)
{
  ASSERT(itemIndex >= 0 && (std::size_t)itemIndex < m_items.size());

  Widget* item = m_items[itemIndex];

  m_items.erase(m_items.begin() + itemIndex);
  delete item;
}

void ComboBox::deleteAllItems()
{
  // Delete all items back to front, in this way Widget::removeChild()
  // doesn't have to use linear search to update m_parentIndex of all
  // other children.
  auto end = m_items.rend();
  for (auto it=m_items.rbegin(); it != end; ++it)
    delete *it;                // widget

  m_items.clear();
  m_selected = -1;
}

int ComboBox::getItemCount() const
{
  return m_items.size();
}

Widget* ComboBox::getItem(const int itemIndex) const
{
  if (itemIndex >= 0 && (std::size_t)itemIndex < m_items.size()) {
    return m_items[itemIndex];
  }
  else
    return nullptr;
}

const std::string& ComboBox::getItemText(int itemIndex) const
{
  if (itemIndex >= 0 && (std::size_t)itemIndex < m_items.size()) {
    Widget* item = m_items[itemIndex];
    return item->text();
  }
  else {
    // Returns the text of the combo-box (it should be empty).
    ASSERT(text().empty());
    return text();
  }
}

void ComboBox::setItemText(int itemIndex, const std::string& text)
{
  ASSERT(itemIndex >= 0 && (std::size_t)itemIndex < m_items.size());

  Widget* item = m_items[itemIndex];
  item->setText(text);
}

int ComboBox::findItemIndex(const std::string& text) const
{
  int i = 0;
  for (const Widget* item : m_items) {
    if ((m_casesensitive && item->text() == text) ||
        (!m_casesensitive && item->text() == text)) {
      return i;
    }
    i++;
  }
  return -1;
}

int ComboBox::findItemIndexByValue(const std::string& value) const
{
  int i = 0;
  for (const Widget* item : m_items) {
    if (auto listItem = dynamic_cast<const ListItem*>(item)) {
      if (listItem->getValue() == value)
        return i;
    }
    ++i;
  }
  return -1;
}

Widget* ComboBox::getSelectedItem() const
{
  return getItem(m_selected);
}

void ComboBox::setSelectedItem(Widget* item)
{
  auto it = std::find(m_items.begin(), m_items.end(), item);
  if (it != m_items.end())
    setSelectedItemIndex(std::distance(m_items.begin(), it));
  else if (m_selected >= 0) {
    m_selected = -1;
    onChange();
  }
}

int ComboBox::getSelectedItemIndex() const
{
  return (!m_items.empty() ? m_selected: -1);
}

void ComboBox::setSelectedItemIndex(int itemIndex)
{
  if (itemIndex >= 0 &&
      (std::size_t)itemIndex < m_items.size() &&
      m_selected != itemIndex) {
    m_selected = itemIndex;

    auto it = m_items.begin() + itemIndex;
    Widget* item = *it;
    m_entry->setText(item->text());
    if (isEditable())
      m_entry->setCaretToEnd();

    onChange();
  }
}

std::string ComboBox::getValue() const
{
  if (isEditable())
    return m_entry->text();
  int index = getSelectedItemIndex();
  if (index >= 0) {
    if (auto listItem = dynamic_cast<ListItem*>(m_items[index]))
      return listItem->getValue();
  }
  return std::string();
}

void ComboBox::setValue(const std::string& value)
{
  if (isEditable()) {
    m_entry->setText(value);
    if (hasFocus())
      m_entry->selectAllText();
  }
  else {
    int index = findItemIndexByValue(value);
    if (index >= 0)
      setSelectedItemIndex(index);
  }
}

Entry* ComboBox::getEntryWidget()
{
  return m_entry;
}

Button* ComboBox::getButtonWidget()
{
  return m_button;
}

bool ComboBox::onProcessMessage(Message* msg)
{
  switch (msg->type()) {

    case kCloseMessage:
      closeListBox();
      break;

    case kWinMoveMessage:
      // If we mouse the parent window, we close the list box popup.
      closeListBox();
      break;

    case kKeyDownMessage:
      if (m_window) {
        KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
        KeyScancode scancode = keymsg->scancode();

        // If the popup is opened
        if (scancode == kKeyEsc) {
          closeListBox();
          return true;
        }
      }
      break;

    case kMouseDownMessage:
      if (m_window) {
        if (View::getView(m_listbox)->hasMouse()) {
          // As we are filtering the kMouseDownMessage, and the
          // ListBox has the mouse, we "return false" here to say "we
          // are not interested in this mouse message, it will be
          // processed by the ListBox itself". In other case, if we
          // "break" and call Widget::onProcessMessage(), the message
          // will be propagated to the parent window and could be used
          // to move the parent window (instead of clicking a listbox
          // item of the popup m_window).
          return false;
        }
        else {
          MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);

          // Use the nativeWindow() from the mouseMsg before we close
          // the listbox because the mouseMsg->display() could be from
          // the same popup.
          const gfx::Point screenPos =
            mouseMsg->display()->nativeWindow()->pointToScreen(mouseMsg->position());

          closeListBox();

          Widget* pick = manager()->pickFromScreenPos(screenPos);
          if (pick && pick->hasAncestor(this))
            return true;
        }
      }
      break;

    case kFocusEnterMessage:
      // Here we focus the entry field only if the combobox is
      // editable and receives the focus in a direct way (e.g. when
      // the window was just opened and the combobox is the first
      // child or has the "focus magnet" flag enabled.)
      if ((isEditable()) &&
          (manager()->getFocus() == this)) {
        m_entry->requestFocus();
      }
      break;

  }

  return Widget::onProcessMessage(msg);
}

void ComboBox::onInitTheme(InitThemeEvent& ev)
{
  Widget::onInitTheme(ev);
  if (m_window) {
    m_window->initTheme();
    m_window->noBorderNoChildSpacing();
  }
}

void ComboBox::onResize(ResizeEvent& ev)
{
  gfx::Rect bounds = ev.bounds();
  setBoundsQuietly(bounds);

  // Button
  Size buttonSize = m_button->sizeHint();
  m_button->setBounds(Rect(bounds.x2() - buttonSize.w, bounds.y,
                           buttonSize.w, bounds.h));

  // Entry
  m_entry->setBounds(Rect(bounds.x, bounds.y,
                          bounds.w - buttonSize.w, bounds.h));

  putSelectedItemAsCustomWidget();
}

void ComboBox::onSizeHint(SizeHintEvent& ev)
{
  Size reqSize(0, 0);

  // Calculate the max required width depending on the text-length of
  // each item.
  for (const auto& item : m_items)
    reqSize |= Entry::sizeHintWithText(m_entry, item->text());

  Size buttonSize = m_button->sizeHint();
  reqSize.w += buttonSize.w;
  reqSize.h = std::max(reqSize.h, buttonSize.h);

  ev.setSizeHint(reqSize);
}

bool ComboBoxEntry::onProcessMessage(Message* msg)
{
  switch (msg->type()) {

    case kKeyDownMessage:
      if (hasFocus()) {
        KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
        KeyScancode scancode = keymsg->scancode();

        // In a non-editable ComboBox
        if (!m_comboBox->isEditable()) {
          if (scancode == kKeySpace ||
              scancode == kKeyEnter ||
              scancode == kKeyEnterPad) {
            m_comboBox->switchListBox();
            return true;
          }
        }
        // In a editable ComboBox
        else {
          if (scancode == kKeyUp ||
              scancode == kKeyDown ||
              scancode == kKeyPageUp ||
              scancode == kKeyPageDown) {
            if (m_comboBox->m_listbox &&
                m_comboBox->m_listbox->isVisible()) {
              m_comboBox->m_listbox->requestFocus();
              m_comboBox->m_listbox->sendMessage(msg);
              return true;
            }
          }
          else if (scancode == kKeyEnter ||
                   scancode == kKeyEnterPad) {
            m_comboBox->onEnterOnEditableEntry();
          }
        }
      }
      break;

    case kMouseDownMessage:
      if (m_comboBox->isClickOpen() &&
          (!m_comboBox->isEditable() ||
           !m_comboBox->m_items.empty())) {
        m_comboBox->switchListBox();
      }

      if (m_comboBox->isEditable()) {
        requestFocus();
      }
      else {
        captureMouse();
        return true;
      }
      break;

    case kMouseUpMessage:
      if (hasCapture())
        releaseMouse();
      break;

    case kMouseMoveMessage:
      if (hasCapture()) {
        MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
        gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen(mouseMsg->position());
        Widget* pick = manager()->pickFromScreenPos(screenPos);
        Widget* listbox = m_comboBox->m_listbox;

        if (pick != nullptr &&
            (pick == listbox || pick->hasAncestor(listbox))) {
          releaseMouse();

          MouseMessage mouseMsg2(
            kMouseDownMessage,
            *mouseMsg,
            mouseMsg->positionForDisplay(pick->display()));
          mouseMsg2.setRecipient(pick);
          mouseMsg2.setDisplay(pick->display());
          pick->sendMessage(&mouseMsg2);
          return true;
        }
      }
      break;

    case kFocusEnterMessage: {
      bool result = Entry::onProcessMessage(msg);
      if (m_comboBox &&
          m_comboBox->isEditable() &&
          m_comboBox->m_listbox &&
          m_comboBox->m_listbox->isVisible()) {
        // In case that the ListBox is visible and the focus is
        // obtained by the Entry field, we set the carret at the end
        // of the text. We don't select the whole text so the user can
        // delete the last caracters using backspace and complete the
        // item name.
        setCaretToEnd();
      }
      return result;
    }

    case kFocusLeaveMessage:
      if (m_comboBox->isEditable() &&
          m_comboBox->m_window &&
          !View::getView(m_comboBox->m_listbox)->hasMouse()) {
        m_comboBox->closeListBox();
      }
      break;

  }

  return Entry::onProcessMessage(msg);
}

void ComboBoxEntry::onPaint(PaintEvent& ev)
{
  theme()->paintComboBoxEntry(ev);
}

void ComboBoxEntry::onChange()
{
  Entry::onChange();
  if (m_comboBox &&
      m_comboBox->isEditable()) {
    m_comboBox->onEntryChange();
  }
}

bool ComboBoxListBox::onProcessMessage(Message* msg)
{
  switch (msg->type()) {

    case kMouseUpMessage:
      m_comboBox->closeListBox();
      return true;

    case kKeyDownMessage:
      if (hasFocus()) {
        KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
        KeyScancode scancode = keymsg->scancode();

        if (scancode == kKeySpace ||
            scancode == kKeyEnter ||
            scancode == kKeyEnterPad) {
          m_comboBox->closeListBox();
          return true;
        }
      }
      break;

    case kFocusEnterMessage:
      // If the ComboBox is editable, we prefer the focus in the Entry
      // field (so the user can continue editing it).
      if (m_comboBox->isEditable())
        m_comboBox->getEntryWidget()->requestFocus();
      break;
  }

  return ListBox::onProcessMessage(msg);
}

void ComboBoxListBox::onChange()
{
  ListBox::onChange();

  int index = getSelectedIndex();
  if (isValidItem(index))
    m_comboBox->setSelectedItemIndex(index);
}

// When the mouse is clicked we switch the visibility-status of the list-box
void ComboBox::onButtonClick()
{
  switchListBox();
}

void ComboBox::openListBox()
{
  if (!isEnabled() || m_window)
    return;

  onBeforeOpenListBox();

  m_window = new Window(Window::WithoutTitleBar);
  View* view = new View();
  m_listbox = new ComboBoxListBox(this);
  m_window->setAutoRemap(false);
  m_window->setOnTop(true);
  m_window->setWantFocus(false);
  m_window->setSizeable(false);
  m_window->setMoveable(false);

  Widget* viewport = view->viewport();
  {
    gfx::Rect entryBounds = m_entry->bounds();
    gfx::Size size;
    size.w = m_button->bounds().x2() - entryBounds.x - view->border().width();
    size.h = viewport->border().height();
    for (Widget* item : m_items)
      if (!item->hasFlags(HIDDEN))
        size.h += item->sizeHint().h;

    if (!get_multiple_displays()) {
      const int maxVal =
        std::max(entryBounds.y, display()->size().h - entryBounds.y2())
        - 8*guiscale();
      size.h = std::clamp(size.h, textHeight(), maxVal);
    }

    viewport->setMinSize(size);
  }

  m_window->addChild(view);
  view->attachToView(m_listbox);

  m_listbox->selectIndex(m_selected);

  initTheme();
  m_window->remapWindow();

  updateListBoxPos();
  m_window->openWindow();

  filterMessages();

  if (isEditable())
    m_entry->requestFocus();
  else
    m_listbox->requestFocus();

  onOpenListBox();
}

void ComboBox::closeListBox()
{
  if (m_window) {
    removeMessageFilters();

    m_listbox->clean();

    m_window->closeWindow(this);
    delete m_window;            // window, frame

    m_window = nullptr;
    m_listbox = nullptr;

    putSelectedItemAsCustomWidget();
    m_entry->requestFocus();

    onCloseListBox();
  }
}

void ComboBox::switchListBox()
{
  if (!m_window)
    openListBox();
  else
    closeListBox();
}

void ComboBox::updateListBoxPos()
{
  gfx::Rect entryBounds = m_entry->bounds();
  gfx::Rect rc(gfx::Point(entryBounds.x,
                          entryBounds.y2()),
               gfx::Point(m_button->bounds().x2(),
                          entryBounds.y2() + m_window->bounds().h));

  fit_bounds(
    display(),
    m_window,
    rc,
    [this](const gfx::Rect& workarea,
           gfx::Rect& bounds,
           std::function<gfx::Rect(Widget*)> getWidgetBounds) {
      if (bounds.y2() > workarea.y2())
        bounds.offset(0, -(bounds.h + getWidgetBounds(m_entry).h));
    });
}

void ComboBox::onChange()
{
  Change();
}

void ComboBox::onEntryChange()
{
  // Do nothing
}

void ComboBox::onBeforeOpenListBox()
{
  // Do nothing
}

void ComboBox::onOpenListBox()
{
  OpenListBox();
}

void ComboBox::onCloseListBox()
{
  CloseListBox();
}

void ComboBox::onEnterOnEditableEntry()
{
  // Do nothing
}

void ComboBox::filterMessages()
{
  if (!m_filtering) {
    manager()->addMessageFilter(kMouseDownMessage, this);
    manager()->addMessageFilter(kKeyDownMessage, this);
    m_filtering = true;
  }
}

void ComboBox::removeMessageFilters()
{
  if (m_filtering) {
    manager()->removeMessageFilter(kMouseDownMessage, this);
    manager()->removeMessageFilter(kKeyDownMessage, this);
    m_filtering = false;
  }
}

void ComboBox::putSelectedItemAsCustomWidget()
{
  if (!useCustomWidget())
    return;

  Widget* item = getSelectedItem();
  if (item && item->parent() == nullptr) {
    if (!m_listbox) {
      item->setBounds(m_entry->childrenBounds());
      m_entry->addChild(item);
    }
    else {
      m_entry->removeChild(item);
    }
  }
}

} // namespace ui
