#include "stdafx.h"

#include "xtr/ui/DataGridCtrl.h"

#include "resource.h"

#include "xtr/ExteorOptions.h"
#include "xtr/doc/RSModelDoc.h"

#include "xtr/ui/KeyboardHandler.h"
#include "xtr/ui/UIHelper.h"

namespace xtr::ui {

using ccl::semantic::TextInterpretation;
using ccl::object::SDCompact;

static constexpr auto DATA_TABLE_WIDTH_BOOL = 30;
static constexpr auto DATA_TABLE_WIDTH_BSET = 60;
static constexpr auto DATA_TABLE_WIDTH_TEXT = 140;

static constexpr auto  NO_SELECTION = -2;

static constexpr auto ENSURE_ARROW = 2;
static constexpr auto ENSURE_NUMBER = 1;
static constexpr auto ENSURE_NONE = 0;

#pragma warning( push )
#pragma warning( disable : 26440 26436 26454 )

BEGIN_MESSAGE_MAP(DataGridCtrl, CListCtrl)
  ON_WM_MOUSEMOVE()
  ON_WM_SETFOCUS()
  ON_WM_PAINT()

  ON_NOTIFY_REFLECT(NM_CLICK, &ThisClass::OnClick)
  ON_NOTIFY_REFLECT(NM_DBLCLK, &ThisClass::OnDbClick)
  ON_NOTIFY_REFLECT(LVN_KEYDOWN, &ThisClass::OnKeydown)
  ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, &ThisClass::OnCustomDraw)
END_MESSAGE_MAP()

#pragma warning( pop )

DataGridCtrl::DataGridCtrl()
  : ensureVisible{ ENSURE_NONE }, selectedRow{ NO_SELECTION }, selectedColumn{ NO_SELECTION }  {}

void DataGridCtrl::Init(CComboBox* box, BOOL textFlag, doc::RSModelDoc& _doc) {
  m_Doc = &_doc;
  model = &m_Doc->Model();
  editCtrl = box;
  showText = textFlag;

  SetFont(&XTROptions::App().fontData);
  SetExtendedStyle(GetExtendedStyle() | LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT);

  InitToolTips();
}

void DataGridCtrl::InitToolTips() {
  static constexpr auto widthTooltip = 350;

  if (tooltips.Create(this)) {
    tooltips.SetMaxTipWidth(widthTooltip);
    tooltips.AddTool(this, mfc::LoadSID(IXTRS_DEFAULT_TOOLTIP_DATA));
    tooltips.SetFont(&XTROptions::App().fontData);
    tooltips.Activate(TRUE);
  } else {
    TRACE(L"Unable to create the ToolTip!");
  }
}

void DataGridCtrl::SetTextFlag(BOOL flag) {
  if (flag != showText) {
    showText = flag;
    Invalidate();
  }
}

void DataGridCtrl::SetActive(const EntityUID activeUID) {
  const auto cstType = model->GetRS(activeUID).type;
  activeCst = activeUID;
  isEditable = ccl::semantic::IsBaseNotion(cstType);
  isBasic = ccl::semantic::IsBaseSet(cstType);
  typification = model->Core().GetParse(activeCst).Typification();
  isModified = FALSE;

  ReloadData();
  UpdateData(FALSE);
  isModified = FALSE;
}

void DataGridCtrl::ReloadData() {
  SetRedraw(FALSE);
  Clear();
  if (typification != nullptr) {
    if (isBasic) {
      LoadBasicData();
    } else {
      LoadSData();
    }
  }
  SetRedraw(TRUE);
}

void DataGridCtrl::Clear() {
  DeleteAllItems();
  while (DeleteColumn(0)) { ; }
  headerIDs.clear();
  columnCount = 0;
  selectedRow = selectedColumn = NO_SELECTION;
  Invalidate();
}

void DataGridCtrl::LoadBasicData() {
  CreateBasicHeader(mfc::ToMFC(model->GetRS(activeCst).alias));
  if (const auto* interp = model->Values().TextFor(activeCst); interp != nullptr) {
    InputTextData(*interp);
  }
}

void DataGridCtrl::LoadSData() {
  const auto data = model->Values().SDataFor(activeCst);
  CreateHeader();
  if (data.has_value()) {
    const auto compact = SDCompact::FromSData(data.value(), *typification);
    InputStructuredtData(compact.data);
  }
}

void DataGridCtrl::UpdateSelectedCellText() {
  Invalidate();

  editCtrl->ShowWindow(SW_HIDE);

  auto ctxt = WindowText(*editCtrl);
  auto oldTxt = mfc::ToSTL(ctxt);
  if (!isBasic && showText && headerIDs.at(selectedColumn).has_value() && !std::empty(oldTxt)) {
    const auto headID = *headerIDs.at(selectedColumn);
    const auto* interp = model->Values().TextFor(headID);
    auto id = interp->GetInterpreteeOf(oldTxt);
    if (!id.has_value()) {
      if (::AfxMessageBox(mfc::FormatSID(IXTRS_DATA_ADD_BASE_ELEMENT, 
                                       ctxt.GetString(),
                                       mfc::ToMFC(model->GetRS(headID).alias).GetString()),
                        MB_YESNO | MB_DEFBUTTON1) == IDYES) {
        id = model->Values().AddBasicElement(headID, oldTxt);
      } else {
        return;
      }
    }
    ctxt.Empty();
    ctxt.AppendFormat(LR"(%ld)", *id);
  }
  SetTableItem(selectedRow, selectedColumn, ctxt);
}

void DataGridCtrl::SaveData() {
  if (!isEditable || !isModified || typification == nullptr) {
    return;
  } 
  
  if (isBasic) {
    TextInterpretation newInterp{};
    for (auto i = 0; i < GetItemCount(); ++i) {
      if (auto text = mfc::ToSTL(GetItemText(i, 1)); !std::empty(text)) {
        newInterp.PushBack(text);
      }
    }
    model->Values().SetBasicText(activeCst, newInterp);
  } else {
    const auto idTable = GetIDTable();
    auto data = SDCompact::Unpack(idTable, *typification);
    if (!data.has_value()) {
      if (typification->IsCollection()) {
        model->Values().ResetDataFor(activeCst);
      }
    } else {
      model->Values().SetStructureData(activeCst, data.value());
    }
    isModified = FALSE;
  }
}

SDCompact::Data  DataGridCtrl::GetIDTable() const {
  SDCompact::Data result{};
  const auto itemCount = GetItemCount();
  for (auto i = 0; i < itemCount; ++i) {
    if (!GetItemText(i, 0).IsEmpty()) {
      SDCompact::Data::value_type row{};
      if (typification->IsCollection()) {
        row.emplace_back(SDCompact::unknownCount);
      }
      for (auto j = 0; j < columnCount; ++j) {
        if (const auto str = GetItemText(i, j); !str.IsEmpty()) {
          row.emplace_back(_wtoi(str));
        } else {
          break;
        }
      }
      result.emplace_back(row);
    }
  }
  return result;
}

void DataGridCtrl::InputTextData(const TextInterpretation& input) {
  auto nItem = 0;
  for (const auto& it : input) {
    auto index = it.first;
    auto inrepretantName = mfc::ToMFC(input.GetInterpretantFor(index));
    CString inrepretantIndex{};
    inrepretantIndex.AppendFormat(LR"(%ld)", index);

    InsertItem(nItem, CString{});
    SetItemText(nItem, 0, inrepretantIndex);
    SetItemText(nItem, 1, inrepretantName);
    ++nItem;
  }
}

void DataGridCtrl::InputStructuredtData(const SDCompact::Data& data) {
  CString str{};
  const auto indexStart = typification->IsCollection() ? 1 : 0;
  for (auto i = 0; i < ssize(data); ++i) {
    InsertItem(i, CString{});
    for (auto j = indexStart; j < ssize(data.at(i)); ++j) {
      str.Empty();
      str.AppendFormat(LR"(%ld)", data.at(i).at(j));
      SetItemText(i, j - indexStart, str);
    }
  }
}

void DataGridCtrl::CreateBasicHeader(const CString basicName) {
  InsertColumn(columnCount++, basicName, LVCFMT_CENTER, DATA_TABLE_WIDTH_BSET);
  InsertColumn(columnCount++, mfc::LoadSID(IXTRS_HEADER_INTERPRETATION), LVCFMT_CENTER, DATA_TABLE_WIDTH_TEXT);
}

void DataGridCtrl::CreateHeader() {
  const auto dataHeader = SDCompact::CreateHeader(*typification);
  auto header = begin(dataHeader);
  if (typification->IsCollection()) {
    ++header;
  }
  for ( ; header != end(dataHeader); ++header) {
    const auto width = *header == "B" ? DATA_TABLE_WIDTH_BOOL : DATA_TABLE_WIDTH_BSET;
    headerIDs.emplace_back(model->Core().FindAlias(*header));
    InsertColumn(columnCount++, mfc::ToMFC(*header), LVCFMT_CENTER, width);
  }
}

BOOL DataGridCtrl::PreTranslateMessage(MSG* pMsg) {
  if (tooltips.m_hWnd != nullptr) {
    tooltips.RelayEvent(pMsg);
  }
  if (pMsg->message == WM_KEYDOWN) {
    if (pMsg->wParam == VK_RETURN) {
      ShowEditWindowAtCell(selectedRow, selectedColumn);
      return TRUE;
    } else if (pMsg->wParam == VK_F5 && !editCtrl->IsWindowVisible()) {
      SortData();
      return TRUE;
    }
  }
  return CListCtrl::PreTranslateMessage(pMsg);
}

void DataGridCtrl::ShowEditWindowAtCell(int32_t row, int32_t col) {
  static constexpr auto CELL_EDIT_XOFFSET = 5;
  static constexpr auto CELL_EDIT_YOFFSET = 5;
  static constexpr auto CELL_EDIT_WIDTHOFFSET = 3;
  if (row < 0 || col < 0) {
    return;
  } else if (!isEditable || editCtrl == nullptr) {
    return;
  } else if (isBasic && col == 0) {
    return;
  }

  editCtrl->ResetContent();
  auto str = GetItemText(row, col);
  if (!isBasic && showText  && headerIDs.at(col).has_value()) {
    auto headID = *headerIDs.at(col);
    const auto id = _wtoi(str);
    const auto& interp = *model->Values().TextFor(headID);
    if (interp.HasInterpretantFor(id)) {
      str = mfc::ToMFC(interp.GetInterpretantFor(id));
    }
    for (const auto& it : interp) {
      auto dataName = mfc::ToMFC(it.second);
      editCtrl->AddString(dataName);
    }
  }

  CRect cell{};
  GetSubItemRect(row, col, LVIR_BOUNDS, cell);

  const auto dataRect = WindowRect(*this);
  const auto dialogRect = WindowRect(*GetParent());
  const auto x = cell.left + dataRect.left - dialogRect.left;
  const auto y = cell.top + dataRect.top - dialogRect.top;

  editCtrl->SetWindowPos(&wndTop, x + CELL_EDIT_XOFFSET, y + CELL_EDIT_YOFFSET,
                            cell.Width() + CELL_EDIT_WIDTHOFFSET, cell.Height(), SWP_SHOWWINDOW);
  editCtrl->SetFocus();
  editCtrl->SetWindowTextW(str);
  editCtrl->RedrawWindow();
}

void DataGridCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) {
  auto& drawer = reinterpret_cast< NMLVCUSTOMDRAW&>(*pNMHDR);
  const auto stage = drawer.nmcd.dwDrawStage;
  *pResult = [&] {
    switch (stage) {
    default: return CDRF_DODEFAULT;
    case CDDS_PREPAINT: return CDRF_NOTIFYITEMDRAW;
    case CDDS_ITEMPREPAINT: return CDRF_NOTIFYSUBITEMDRAW;
    case CDDS_SUBITEM | CDDS_ITEMPREPAINT: return CDRF_SKIPDEFAULT;
    }
  }();

  if (stage == CDDS_PREPAINT) {
    OnFirstDraw();
  } else if (stage == (CDDS_SUBITEM | CDDS_ITEMPREPAINT)) {
    OnDrawItem(drawer);
  }
}

void DataGridCtrl::OnFirstDraw() {
  if (isBasic) {
    if (isEditable && (GetItemCount() == 0 || !GetItemText(GetItemCount() - 1, 1).IsEmpty())) {
      AddBaseRow();
    }
  } else if (GetItemCount() == 0 || (isEditable && !GetItemText(GetItemCount() - 1, 0).IsEmpty())) {
    InsertItem(GetItemCount(), CString{});
  }
}

void DataGridCtrl::OnDrawItem(NMLVCUSTOMDRAW& drawer) {
  static constexpr auto CELL_TEXT_OFFSET = 0U;
  static constexpr auto SELECTION_COLOR_FOCUS = RGB(150, 180, 255);
  static constexpr auto SELECTION_COLOR_NOFOCUS = RGB(180, 180, 180);

  const auto row = static_cast<int32_t>(drawer.nmcd.dwItemSpec);
  const auto col = drawer.iSubItem;

  auto str = GetItemText(row, col);
  const auto id = _wtoi(str);

  if (!isBasic && showText && id > 0 
      && ssize(headerIDs) > col 
      && headerIDs.at(col).has_value()) {
    if (const auto* const interp = model->Values().TextFor(*headerIDs.at(col)); 
        interp != nullptr && interp->HasInterpretantFor(id)) {
      str = mfc::ToMFC(interp->GetInterpretantFor(id));
    }
  }
  CRect rect{};
  if (col > 0) {
    GetSubItemRect(row, col, LVIR_BOUNDS, rect);
  } else {
    GetItemRect(row, &rect, LVIR_LABEL);
  }

  auto *pDC = CDC::FromHandle(drawer.nmcd.hdc);
  if (row == selectedRow && col == selectedColumn) {
    const auto color = GetFocus() == this ? SELECTION_COLOR_FOCUS : SELECTION_COLOR_NOFOCUS;
    CBrush brush{ color };
    pDC->FillRect(&rect, &brush);
  } else {
    CBrush brush{ RGB(255, 255, 255) };
    pDC->FillRect(&rect, &brush);
  }

  rect.OffsetRect(CELL_TEXT_OFFSET, 0);
  pDC->DrawTextW(str, &rect, DT_END_ELLIPSIS | DT_CENTER);
}

int32_t DataGridCtrl::AddBaseRow() {
  if (isBasic) {
    CString msg{};
    msg.AppendFormat(LR"(%d)", GetMaxIndex() + 1);
    return InsertItem(GetItemCount(), msg);
  } else {
    return -1;
  }
}

int32_t DataGridCtrl::GetMaxIndex() const {
  int32_t result{ 0 };
  for (auto nRow = 0; nRow < GetItemCount(); ++nRow) {
    auto index = _wtoi(GetItemText(nRow, 0));
    result = std::max(result, index);
  }
  return result;
}

void DataGridCtrl::OnSetFocus(CWnd* pOldWnd) {
  Keyboard::Instance().SetLayout(Keyboard::Layout::RUS);
  CListCtrl::OnSetFocus(pOldWnd);
}

void DataGridCtrl::OnClick(NMHDR* pNMHDR, LRESULT* pResult) {
  const auto* activeItem = reinterpret_cast<const NMITEMACTIVATE*>(pNMHDR);
  InvalidateCell(selectedRow, selectedColumn);
  selectedRow = activeItem->iItem;
  selectedColumn = activeItem->iSubItem;
  InvalidateCell(selectedRow, selectedColumn);
  *pResult = 0;
}

void DataGridCtrl::OnKeydown(NMHDR* pNMHDR, LRESULT* pResult) {
  static constexpr auto CODE_PLUS_SIGN = 0xBB;
  static constexpr auto CODE_MINUS_SIGN = 0x8D;
  const auto* keyDown = reinterpret_cast<const NMLVKEYDOWN*>(pNMHDR);
  switch(keyDown->wVKey) {
  case VK_LEFT: {
    selectedColumn = selectedColumn == 0 ? 0 : selectedColumn - 1;
    InvalidateCell(selectedRow, selectedColumn + 1);
    InvalidateCell(selectedRow, selectedColumn);
    ensureVisible = ENSURE_ARROW;
    topIndex = GetTopIndex();
    break;
  }
  case VK_RIGHT: {
    selectedColumn = selectedColumn + 1 >=  columnCount ? selectedColumn : selectedColumn + 1;
    InvalidateCell(selectedRow, selectedColumn - 1);
    InvalidateCell(selectedRow, selectedColumn);
    ensureVisible = ENSURE_ARROW;
    topIndex = GetTopIndex();
    break;
  }
  case VK_UP: {
    selectedRow = selectedRow == 0 ? 0 : selectedRow - 1;
    InvalidateCell(selectedRow + 1, selectedColumn);
    InvalidateCell(selectedRow, selectedColumn);
    ensureVisible = ENSURE_ARROW;
    topIndex = GetTopIndex();
    break;
  }
  case VK_DOWN: {
    selectedRow = selectedRow + 1 >= GetItemCount() ? selectedRow : selectedRow + 1;
    InvalidateCell(selectedRow - 1, selectedColumn);
    InvalidateCell(selectedRow, selectedColumn);
    ensureVisible = ENSURE_ARROW;
    topIndex = GetTopIndex() + 1;
    break;
  }
  case VK_DELETE: {
    SetTableItem(selectedRow, selectedColumn, CString{});
    break;
  }
  case '0': case '1': case '2': case '3': case '4':
  case '5': case '6': case '7': case '8': case '9': {
    auto str = GetItemText(selectedRow, selectedColumn);
    str.AppendChar(keyDown->wVKey);
    SetTableItem(selectedRow, selectedColumn, str);
    ensureVisible = ENSURE_NUMBER;
    topIndex = GetTopIndex();
    break;
  }
  case VK_NUMPAD0: case VK_NUMPAD1: case VK_NUMPAD2:
  case VK_NUMPAD3: case VK_NUMPAD4: case VK_NUMPAD5:
  case VK_NUMPAD6: case VK_NUMPAD7: case VK_NUMPAD8:
  case VK_NUMPAD9: {
    auto str = GetItemText(selectedRow, selectedColumn);
    str.AppendChar(keyDown->wVKey + '0' - VK_NUMPAD0);
    SetTableItem(selectedRow, selectedColumn, str);
    ensureVisible = ENSURE_NUMBER;
    topIndex = GetTopIndex();
    break;
  }
  case VK_ADD:
  case CODE_PLUS_SIGN: { // Shift + =
    CopyCurrentRow();
    break;
  }
  case VK_SUBTRACT:
  case CODE_MINUS_SIGN: { // дефис
    DeleteCurrentRow();
    break;
  }
  }
  *pResult = 0;
}

void DataGridCtrl::CopyCurrentRow() {
  if (selectedRow >= 0 && isEditable) {
    SetRedraw(FALSE);
    Invalidate();
    if (!isBasic) {
      InsertItem(selectedRow, CString{});
      for (auto i = 0; i < columnCount; ++i) {
        SetItemText(selectedRow, i, GetItemText(selectedRow + 1, i));
      }
      ++selectedRow;
    } else {
      SetItemText(GetItemCount() - 1, 1, GetItemText(selectedRow, 1));
      selectedRow = GetItemCount() - 1;
    }

    isModified = TRUE;
    SetRedraw(TRUE);
  }
}

void DataGridCtrl::DeleteCurrentRow() {
  if (selectedRow >= 0 && isEditable) {
    if (DeleteItem(selectedRow)) {
      isModified = TRUE;
    }
    if (selectedRow == GetItemCount() - 1) {
      --selectedRow;
    }
  }
}

void DataGridCtrl::OnDbClick(NMHDR* pNMHDR, LRESULT* pResult) {
  const auto* activeItem = reinterpret_cast<const NMITEMACTIVATE*>(pNMHDR);
  *pResult = 0;
  if (activeItem != nullptr) {
    ShowEditWindowAtCell(activeItem->iItem, activeItem->iSubItem);
  }
}

void DataGridCtrl::OnPaint() {
  switch (ensureVisible) {
  case ENSURE_ARROW: {
    ensureVisible = ENSURE_NONE;
    CListCtrl::OnPaint();
    EnsureVisible(selectedRow, FALSE);
    EnsureVisible(topIndex, FALSE);
    break;
  }
  case ENSURE_NUMBER: {
    ensureVisible = ENSURE_NONE;
    CListCtrl::OnPaint();
    EnsureVisible(GetItemCount() -1, FALSE);
    EnsureVisible(topIndex, FALSE);
    break;
  }
  case ENSURE_NONE: {
    CListCtrl::OnPaint();
    break;
  }
  }
}

void DataGridCtrl::OnMouseMove(const uint32_t nFlags, const CPoint point) {
  if (tooltips.m_hWnd != nullptr)  {
    LVHITTESTINFO lvhti{};
    lvhti.pt = point;
    SubItemHitTest(&lvhti);
    UpdateToolTipText(lvhti.iItem, lvhti.iSubItem);
  }
  CListCtrl::OnMouseMove(nFlags, point);
}

void DataGridCtrl::UpdateToolTipText(const int32_t row, const int32_t col) {
  CString old{};
  tooltips.GetText(old, this);

  CString msg{};
  if (row < 0 || col < 0 || ssize(headerIDs) <= col) {
    msg = mfc::LoadSID(IXTRS_DEFAULT_TOOLTIP_DATA);
  } else {
    msg = GetItemText(row, col);
    if (!isBasic && headerIDs.at(col).has_value()) {
      const auto headID = *headerIDs.at(col);
      const auto id = _wtoi(msg);
      if (const auto* interp = model->Values().TextFor(headID); 
          interp != nullptr && interp->HasInterpretantFor(id)) {
        msg = mfc::ToMFC(interp->GetInterpretantFor(id));
      }
    }
  }

  if (msg != old) {
    tooltips.UpdateTipText(msg, this);
    tooltips.Update();
  }
}

void DataGridCtrl::InvalidateCell(const int32_t row, const int32_t col) {
  if (col != NO_SELECTION && row != NO_SELECTION) {
    CRect rect{};
    if (col == 0) {
      GetItemRect(row, &rect, LVIR_LABEL);
    } else {
      GetSubItemRect(row, col, LVIR_BOUNDS, rect);
    }
    InvalidateRect(&rect);
  }
}

void DataGridCtrl::SetTableItem(const int32_t row, const int32_t col, const CString text) {
  if (row >= 0 && col >= 0 && GetItemText(row, col) != text && isEditable && (!isBasic || col != 0)) {
    SetItemText(selectedRow, selectedColumn, text);
    isModified = TRUE;
  }
}

enum CompareSign {
  COMPARE_LESS,
  COMPARE_EQUAL,
  COMPARE_GREATER,
};

void DataGridCtrl::SortData() {
  auto flag = TRUE;
  const auto count = GetItemCount();
  for (auto k = 0; k < count - 1; ++k) {
    flag = FALSE;
    for (auto i = 0; i < count - 1 - k; ++i) {
      if (CompareItems(i, i + 1) == COMPARE_GREATER) {
        flag = TRUE;
        SwapItems(i, i + 1);
      }
    }
    if (!flag) {
      break;
    }
  }
}

int32_t DataGridCtrl::CompareItems(const int32_t item1, const int32_t item2) const {
  if (item1 < 0 || item2 < 0 || item1 >= GetItemCount() || item2 >= GetItemCount()) {
    return 0;
  }
  for (auto j = 0; j < columnCount; ++j)  {
    auto str1 = GetItemText(item1, j);
    auto str2 = GetItemText(item2, j);
    if (j == 0 && (str1.IsEmpty() || str2.IsEmpty())) {
      return COMPARE_EQUAL;
    } else {
      const auto n1 = _wtoi(str1);
      const auto n2 = _wtoi(str2);

      if (n1 != 0 && n2 != 0) {
        if (n1 > n2) {
          return COMPARE_GREATER;
        } else if (n2 > n1) {
          return COMPARE_LESS;
        }
      } else if (const auto comp = str1.Compare(str2); comp > 0) {
        return COMPARE_GREATER;
      } else if (comp < 0) {
        return COMPARE_LESS;
      }
    }
  }
  return COMPARE_EQUAL;
}

void DataGridCtrl::SwapItems(const int32_t item1, const int32_t item2) {
  for (auto j = 0; j < columnCount; ++j) {
    const auto tmp = GetItemText(item1, j);
    SetItemText(item1, j, GetItemText(item2, j));
    SetItemText(item2, j, tmp);
  }
}

} // namespace xtr::ui