#include "searchlist.h"
#include "fontsys.h"
#include <windowsx.h>
#include <algorithm>

////////////////////////////////////////

LRESULT Scrollable::onMessage(uint32 message, WPARAM wParam, LPARAM lParam) {
  SCROLLINFO si;
  int step;
  switch (message) {
  case WM_SIZE:
    memset(&si, 0, sizeof si);
    si.cbSize = sizeof si;
    si.fMask = SIF_PAGE | SIF_DISABLENOSCROLL;
    si.nPage = HIWORD(lParam);
    SetScrollInfo(hWnd, SB_VERT, &si, TRUE);
    scroll(scrollPos);
    return 0;
  case WM_VSCROLL:
    memset(&si, 0, sizeof si);
    si.cbSize = sizeof si;
    si.fMask = SIF_ALL;
    GetScrollInfo(hWnd, SB_VERT, &si);
    switch (LOWORD(wParam)) {
    case SB_TOP:
      si.nPos = si.nMin;
      break;
    case SB_BOTTOM:
      si.nPos = si.nMax;
      break;
    case SB_LINEUP:
      si.nPos -= 16;
      break;
    case SB_LINEDOWN:
      si.nPos += 16;
      break;
    case SB_PAGEUP:
      si.nPos -= si.nPage;
      break;
    case SB_PAGEDOWN:
      si.nPos += si.nPage;
      break;
    case SB_THUMBTRACK:
      si.nPos = si.nTrackPos;
      break;
    }
    scroll(si.nPos);
    SetFocus(hWnd);
    return 0;
  case WM_MOUSEWHEEL:
    SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &step, 0);
    if (step < 0) step = 3;
    scrollAccum += GET_WHEEL_DELTA_WPARAM(wParam) * step * 16;
    scroll(scrollPos - scrollAccum / WHEEL_DELTA);
    scrollAccum %= WHEEL_DELTA;
    return 0;
  }
  return M_UNHANDLED;
}

void Scrollable::scroll(int pos) {
  SCROLLINFO si;
  memset(&si, 0, sizeof si);
  si.cbSize = sizeof si;
  si.fMask = SIF_RANGE | SIF_PAGE;
  GetScrollInfo(hWnd, SB_VERT, &si);
  if (pos < si.nMin) pos = si.nMin;
  if (pos > si.nMax - si.nPage + 1) pos = si.nMax - si.nPage + 1;
  si.fMask = SIF_POS;
  if (pos != scrollPos) {
    si.nPos = pos;
    SetScrollInfo(hWnd, SB_VERT, &si, TRUE);

    int deltaY = scrollPos - pos;
    scrollPos = pos;
    RECT rc;
    GetClientRect(hWnd, &rc);
    ScrollWindowEx(hWnd, 0, deltaY, &rc, &rc, NULL, NULL, SW_ERASE | SW_INVALIDATE);
  }
}

////////////////////////////////////////

SearchList::SearchList(Frame* parent, int id)
  : Scrollable(parent)
  , cursel(-1)
{
  itemHeight = FontSys::getTextSize(FontSys::getSysFont(), "123").cy + 3;

  if (WNDCLASSEX* wcx = createclass("SearchListClass")) {
    wcx->hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wcx->hCursor = LoadCursor(NULL, IDC_ARROW);
    RegisterClassEx(wcx);
  }
  create("", WS_CHILD | WS_VSCROLL, WS_EX_CLIENTEDGE);
  setId(id);
}

void SearchList::clear() {
  items.clear();
  display.clear();
  resize();
}
void SearchList::insert(uint32 id, std::string const& text) {
  items.emplace_back(id, text);
}
void SearchList::insertEx(uint32 id, std::string const& text) {
  itemsEx.emplace_back(id, text);
}

static bool contains(std::vector<size_t> const& kmp, std::string const& query, std::string const& text) {
  if (kmp.empty()) return true;
  size_t cur = 0;
  for (size_t i = 0; i < text.length(); ++i) {
    while (cur && query[cur] != tolower((uint8) text[i])) {
      cur = kmp[cur - 1];
    }
    cur += (query[cur] == tolower((uint8)text[i]));
    if (cur == query.length()) return true;
  }
  return false;
}

void SearchList::search(std::string const& query_) {
  query = strlower(query_);
  std::vector<size_t> kmp(query.length(), 0);
  for (size_t i = 1; i < query.length(); ++i) {
    kmp[i] = kmp[i - 1];
    while (kmp[i] && query[kmp[i]] != query[i]) {
      kmp[i] = kmp[kmp[i] - 1];
    }
    kmp[i] += (query[kmp[i]] == query[i]);
  }
  Item* sel = nullptr;
  if (cursel >= 0) sel = display[cursel];
  display.clear();
  cursel = -1;
  scrollPos = 0;
  scrollAccum = 0;
  auto* list = &items;
  if (query.length() && itemsEx.size()) {
    list = &itemsEx;
  }
  for (auto& item : *list) {
    if (contains(kmp, query, item.text)) {
      if (&item == sel) cursel = display.size();
      display.push_back(&item);
    }
  }
  resize();
}

void SearchList::sort() {
  std::sort(items.begin(), items.end());
}
void SearchList::sortEx() {
  std::sort(itemsEx.begin(), itemsEx.end());
}
void SearchList::update() {
  search(query);
}
void SearchList::resize() {
  RECT rc;
  GetClientRect(hWnd, &rc);
  SCROLLINFO si;
  memset(&si, 0, sizeof si);
  si.cbSize = sizeof si;
  si.fMask = SIF_RANGE | SIF_PAGE | SIF_DISABLENOSCROLL;
  si.nMin = 0;
  si.nMax = display.size() * itemHeight;
  si.nPage = rc.bottom;
  SetScrollInfo(hWnd, SB_VERT, &si, TRUE);
  scroll(scrollPos);

  InvalidateRect(hWnd, NULL, TRUE);
}

void SearchList::render(HDC hDC) {
  RECT rc;
  GetClientRect(hWnd, &rc);
  SelectObject(hDC, FontSys::getSysFont());
  int top = (rc.top + scrollPos) / itemHeight;
  int bottom = (rc.bottom + scrollPos + itemHeight) / itemHeight;
  if (top < 0) top = 0;
  if (bottom > display.size()) bottom = display.size();

  uint32 fgHighlight = GetSysColor(COLOR_HIGHLIGHTTEXT);
  uint32 bgHighlight = GetSysColor(COLOR_HIGHLIGHT);
  uint32 fgNormal = 0x000000;
  uint32 bgNormal = 0xFFFFFF;

  for (int index = top; index < bottom; ++index) {
    Item* item = display[index];
    if (index == cursel) {
      SetTextColor(hDC, fgHighlight);
      SetBkColor(hDC, bgHighlight);
    } else {
      SetTextColor(hDC, fgNormal);
      SetBkColor(hDC, bgNormal);
    }
    RECT irc = rc;
    irc.top = index * itemHeight - scrollPos;
    irc.bottom = irc.top + itemHeight;
    ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &irc, NULL, 0, NULL);
    DrawText(hDC, item->text.c_str(), item->text.length(), &irc, DT_SINGLELINE | DT_NOPREFIX | DT_LEFT | DT_VCENTER);
  }
  rc.top = bottom * itemHeight - scrollPos;
  if (rc.top < rc.bottom) {
    SetBkColor(hDC, bgNormal);
    ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &rc, NULL, 0, NULL);
  }
}

LRESULT SearchList::onMessage(uint32 message, WPARAM wParam, LPARAM lParam) {
  int x, y, index;
  PAINTSTRUCT ps;
  HDC hDC;
  switch (message) {
  case WM_PAINT:
    hDC = BeginPaint(hWnd, &ps);
    render(hDC);
    EndPaint(hWnd, &ps);
    return 0;
  case WM_LBUTTONDOWN:
    SetFocus(hWnd);
    x = GET_X_LPARAM(lParam);
    y = GET_Y_LPARAM(lParam);
    index = (y + scrollPos) / itemHeight;
    if (index < 0 || index >= display.size()) {
      index = -1;
    }
    if (index != cursel) {
      cursel = index;
      notify(WM_COMMAND, MAKELONG(id(), BN_CLICKED), (LPARAM)hWnd);
      InvalidateRect(hWnd, NULL, TRUE);
    }
    return 0;
  }
  return Scrollable::onMessage(message, wParam, lParam);
}

///////////////////////////////////////////

class OptionList::Editor : public ComboFrame {
public:
  Editor(OptionList* parent, int id = 0)
    : ComboFrame(parent, id)
  {
  }

  LRESULT onMessage(uint32 message, WPARAM wParam, LPARAM lParam) override {
    if (message == WM_KILLFOCUS || (message == WM_COMMAND && HIWORD(wParam) == CBN_CLOSEUP)) {
      hide();
      SetFocus(getOwner());
    }
    return M_UNHANDLED;
  }
};

OptionList::OptionList(Frame* parent, int id)
  : Scrollable(parent)
  , editor(nullptr)
  , cursel(-1)
{
  itemHeight = FontSys::getTextSize(FontSys::getSysFont(), "123").cy + 5;

  if (WNDCLASSEX* wcx = createclass("OptionListClass")) {
    wcx->hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wcx->hCursor = LoadCursor(NULL, IDC_ARROW);
    RegisterClassEx(wcx);
  }
  create("", WS_CHILD | WS_VSCROLL | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, WS_EX_CLIENTEDGE);
  setId(id);

  editor = new Editor(this);
  editor->addString("", -1);
}

void OptionList::addOption(std::string const& text) {
  if (editor) editor->addString(text, options.size());
  options.push_back(text);
}

void OptionList::update() {
  RECT rc;
  GetClientRect(hWnd, &rc);
  SCROLLINFO si;
  memset(&si, 0, sizeof si);
  si.cbSize = sizeof si;
  si.fMask = SIF_RANGE | SIF_PAGE | SIF_DISABLENOSCROLL;
  si.nMin = 0;
  si.nMax = items.size() * itemHeight;
  si.nPage = rc.bottom;
  SetScrollInfo(hWnd, SB_VERT, &si, TRUE);
  scroll(scrollPos);

  InvalidateRect(hWnd, NULL, TRUE);
}

void OptionList::render(HDC hDC) {
  RECT rc;
  GetClientRect(hWnd, &rc);
  SelectObject(hDC, FontSys::getSysFont());
  int top = (rc.top + scrollPos) / itemHeight;
  int bottom = (rc.bottom + scrollPos + itemHeight) / itemHeight;
  if (top < 0) top = 0;
  if (bottom > items.size()) bottom = items.size();

  uint32 fgNormal = 0x000000;
  uint32 bgNormal = 0xFFFFFF;
  uint32 bgLine = 0x808080;

  for (int index = top; index < bottom; ++index) {
    std::string item = items[index];
    std::string value;
    if (choices[index] >= 0) value = options[choices[index]];

    SetTextColor(hDC, fgNormal);
    SetBkColor(hDC, bgNormal);
    RECT irc = rc;
    irc.top = index * itemHeight - scrollPos;
    irc.bottom = irc.top + itemHeight;
    if (cursel == index && editor->visible()) {
      irc.right = rc.right / 2;
    }
    ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &irc, NULL, 0, NULL);
    irc.top += 1;
    irc.bottom -= 1;
    irc.left += 1;
    irc.right = rc.right / 2 - 2;
    DrawText(hDC, item.c_str(), item.length(), &irc, DT_SINGLELINE | DT_NOPREFIX | DT_LEFT | DT_VCENTER);
    irc.left = irc.right + 3;
    irc.right = rc.right - 1;
    if (cursel != index || !editor->visible()) {
      DrawText(hDC, value.c_str(), value.length(), &irc, DT_SINGLELINE | DT_NOPREFIX | DT_LEFT | DT_VCENTER);
    }
    SetBkColor(hDC, bgLine);
    irc.right = rc.right / 2;
    irc.left = irc.right - 1;
    irc.top -= 1;
    irc.bottom += 1;
    ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &irc, NULL, 0, NULL);
    irc.left = rc.left;
    irc.right = rc.right;
    irc.top = irc.bottom - 1;
    ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &irc, NULL, 0, NULL);
  }
  rc.top = bottom * itemHeight - scrollPos;
  if (rc.top < rc.bottom) {
    SetBkColor(hDC, bgNormal);
    ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &rc, NULL, 0, NULL);
  }
}

LRESULT OptionList::onMessage(uint32 message, WPARAM wParam, LPARAM lParam) {
  int x, y, index;
  PAINTSTRUCT ps;
  HDC hDC;
  RECT rc;
  switch (message) {
  case WM_ERASEBKGND:
    return TRUE;
  case WM_PAINT:
    hDC = BeginPaint(hWnd, &ps);
    render(hDC);
    EndPaint(hWnd, &ps);
    return 0;
  case WM_SIZE:
    InvalidateRect(hWnd, NULL, TRUE);
    break;
  case WM_COMMAND:
    if (editor && cursel >= 0 && HIWORD(wParam) == CBN_SELCHANGE && (HWND) lParam == editor->getHandle()) {
      int sel = editor->getCurSel();
      choices[cursel] = (sel >= 0 ? editor->getItemData(sel) : -1);
      notify(WM_OPTIONCHANGE, id(), cursel);
    }
    return 0;
  case WM_LBUTTONDOWN:
    SetFocus(hWnd);
    x = GET_X_LPARAM(lParam);
    y = GET_Y_LPARAM(lParam);
    cursel = (y + scrollPos) / itemHeight;
    GetClientRect(hWnd, &rc);
    if (x < rc.right / 2) cursel = -1;
    if (cursel >= 0 && cursel < items.size() && editor) {
      editor->setPoint(PT_TOPLEFT, rc.right / 2 - 1, cursel * itemHeight - scrollPos);
      editor->setWidth(rc.right / 2 + 3);
      editor->show();
      SetFocus(editor->getHandle());
      ComboBox_SetCurSel(editor->getHandle(), choices[cursel] + 1);
      ComboBox_ShowDropdown(editor->getHandle(), TRUE);
    } else {
      cursel = -1;
    }
    return 0;
  }
  return Scrollable::onMessage(message, wParam, lParam);
}

///////////////////////////////////////////
//
//TestWindow::TestWindow() {
//  if (WNDCLASSEX* wcx = createclass("MainWndClass")) {
//    wcx->hbrBackground = HBRUSH(COLOR_BTNFACE + 1);
//    wcx->hCursor = LoadCursor(NULL, IDC_ARROW);
//    RegisterClassEx(wcx);
//  }
//  create(CW_USEDEFAULT, 0, 400, 700, "Test Window",
//    WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, 0);
//
//  query = new EditFrame(this, 100);
//  query->setPoint(PT_TOPLEFT, 0, 0);
//  query->setPoint(PT_TOPRIGHT, 0, 0);
//  query->setHeight(21);
//
//  list = new SearchList(this, 101);
//  list->setPoint(PT_TOPLEFT, query, PT_BOTTOMLEFT, 0, 4);
//  list->setPoint(PT_TOPRIGHT, query, PT_BOTTOMRIGHT, 0, 4);
//  list->setHeight(300);
//
//  list->insert(0, "fasdfasdf");
//  list->insert(1, "gasdfasdg");
//  list->insert(2, "sadfasdgq");
//  list->insert(3, "gqfeqwe");
//  list->insert(4, "gasdfawe");
//  list->insert(5, "asdfawe");
//  list->insert(6, "gasdfawe");
//  list->insert(7, "asdgawe");
//  list->insert(8, "gasdfawe");
//  list->insert(9, "badsfawe");
//  list->update();
//
//  opts = new OptionList(this, 102);
//  opts->setPoint(PT_TOPLEFT, list, PT_BOTTOMLEFT, 0, 4);
//  opts->setPoint(PT_BOTTOMRIGHT, 0, 0);
//
//  opts->addItem("Item 1", -1);
//  opts->addItem("Item 2", 0);
//  opts->addItem("Item 3", 1);
//  opts->addItem("Item 4", 2);
//  opts->addItem("Item 5", 3);
//  opts->addOption("Value 1");
//  opts->addOption("Value 2");
//  opts->addOption("Value 3");
//  opts->addOption("Value 4");
//  opts->update();
//}
//
//LRESULT TestWindow::onMessage(uint32 message, WPARAM wParam, LPARAM lParam) {
//  if (message == WM_COMMAND) {
//    int id = LOWORD(wParam);
//    if (id == 100) {
//      if (HIWORD(wParam) == EN_CHANGE) list->search(query->getText());
//    } else if (id == 101) {
//    }
//    return 0;
//  }
//  return M_UNHANDLED;
//}
//
//void TestWindow::loop() {
//  ShowWindow(hWnd, SW_SHOW);
//  while (hWnd) {
//    MSG msg;
//    if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
//      TranslateMessage(&msg);
//      DispatchMessage(&msg);
//      if (msg.message == WM_QUIT) {
//        DestroyWindow(hWnd);
//      }
//    }
//  }
//}
