#include "stdafx.h"

#include "xtr/ui/RSTableCtrl.h"

#include "resource.h"

#include "xtr/Exteor.h"
#include "xtr/cclDescriptor.h"
#include "xtr/doc/RSModelDoc.h"
#include "xtr/xtrDesign.h"
#include "xtr/xtrProcedures.h"

#include "xtr/ui/UIHelper.h"

namespace xtr::ui {

using ccl::SetOfEntities;

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

BEGIN_MESSAGE_MAP(RSTableCtrl, RSListCtrl)
  ON_WM_DESTROY()
  ON_WM_MOUSEMOVE()
  ON_WM_LBUTTONUP()

  ON_NOTIFY_REFLECT_EX(LVN_BEGINDRAG, &ThisClass::OnBegindragConstituenta)

  ON_NOTIFY_REFLECT_EX(NM_CUSTOMDRAW, &ThisClass::OnNMCustomdraw)
  ON_NOTIFY_REFLECT_EX(LVN_ITEMACTIVATE, &ThisClass::OnDblclkConstituenta)
  ON_NOTIFY_REFLECT_EX(LVN_ITEMCHANGED, &ThisClass::OnSelectChange)

  ON_COMMAND_EX(ID_EDIT_COPY, &ThisClass::RunCommand)
  ON_COMMAND_EX(ID_EDIT_PASTE, &ThisClass::RunCommand)
END_MESSAGE_MAP()

#pragma warning( pop )

struct RSTableCtrl::SchemaHeader : xtr::ui::Header {
  enum Columns : uint8_t {
    POS_NAME = 0,
    POS_TYPE,
    POS_RSDEF,
    POS_TERM,
    POS_TEXTDEF,
    POS_COMMENT,
    count
  };

  SchemaHeader() {
    AddColumn({ IXTRS_HEADER_NAME, Alignment::left, XTROptions::RS().nameWidth });
    AddColumn({ IXTRS_HEADER_TYPE, Alignment::left, XTROptions::RS().typeWidth });
    AddColumn({ IXTRS_HEADER_RSDEF, Alignment::left, XTROptions::RS().expWidth });
    AddColumn({ IXTRS_HEADER_TERM, Alignment::left, XTROptions::RS().termWidth });
    AddColumn({ IXTRS_HEADER_TEXTDEF, Alignment::left, XTROptions::RS().interWidth });
    AddColumn({ IXTRS_HEADER_CONVENTION, Alignment::left, XTROptions::RS().conventionWidth });
  }
};

struct RSTableCtrl::ModelHeader : xtr::ui::Header {
  enum Columns : uint8_t {
    POS_NAME = 0,
    POS_TYPE,
    POS_RSDEF,
    POS_VAL,
    POS_TERM,
    POS_TEXTDEF,
    POS_COMMENT,
    count
  };

  ModelHeader() {
    AddColumn({ IXTRS_HEADER_NAME, Alignment::left, XTROptions::Model().nameWidth });
    AddColumn({ IXTRS_HEADER_TYPE, Alignment::left, XTROptions::Model().typeWidth });
    AddColumn({ IXTRS_HEADER_RSDEF, Alignment::left, XTROptions::Model().expWidth });
    AddColumn({ IXTRS_HEADER_CARD, Alignment::left, XTROptions::Model().valWidth });
    AddColumn({ IXTRS_HEADER_TERM, Alignment::left, XTROptions::Model().termWidth });
    AddColumn({ IXTRS_HEADER_TEXTDEF, Alignment::left, XTROptions::Model().interWidth });
    AddColumn({ IXTRS_HEADER_CONVENTION, Alignment::left, XTROptions::Model().conventionWidth });
  }
};


void RSTableCtrl::Init(doc::RSFacade& doc) {
  editor = &doc;
  InitHeaders();
  InitToolTips();
  Update(TRUE);
}

void RSTableCtrl::InitHeaders() {
  if (editor->IsModel()) {
    InitHeaderFor(*this, ModelHeader{});
  } else {
    InitHeaderFor(*this, SchemaHeader{});
  }
}

void RSTableCtrl::InitToolTips() {
  if (tooltips.Create(this, TTS_ALWAYSTIP)) {
    tooltips.SetMaxTipWidth(s_TooltipWidth);
    tooltips.AddTool(this, mfc::LoadSID(IXTRS_TRSV_TOOLTIP_DEFAULT));
    tooltips.SetFont(&XTROptions::App().fontTooltip);
    tooltips.Activate(TRUE);
  } else {
    TRACE(L"Unable to create the ToolTip!");
  }
}

void RSTableCtrl::Update(const BOOL updateSelection) {
  const auto& selList = updateSelection ? editor->Document().Selection() : selection;
  const auto topIndex = GetTopIndex();

  SetRedraw(FALSE);
  DeleteAllItems();
  if (!std::empty(editor->Core())) {
    for (const auto uid : editor->List()) {
      InsertCst(uid);
    }
    EnsureVisible(GetItemCount() - 1, FALSE);
    EnsureVisible(topIndex, TRUE);
    SelectMultiple(selList);
  }
  SetRedraw(TRUE);
}

std::optional<EntityUID> RSTableCtrl::UIDForIndex(const int nItem) const {
  if (nItem < GetItemCount() && nItem >= 0) {
    const auto name = mfc::ToSTL(GetItemText(nItem, SchemaHeader::Columns::POS_NAME));
    return editor->Core().FindAlias(name);
  }
  return std::nullopt;
}

std::optional<int32_t> RSTableCtrl::IndexForUID(const EntityUID uid) const {
  const auto name = mfc::ToMFC(editor->GetRS(uid).alias);
  for (auto i = 0; i < GetItemCount(); ++i) {
    if (name == GetItemText(i, SchemaHeader::Columns::POS_NAME)) {
      return i;
    }
  }
  return std::nullopt;
}

void RSTableCtrl::EnsureVisibleCst(const EntityUID uid) {
  const auto nIndex = IndexForUID(uid);
  assert(nIndex.has_value());

  SCROLLINFO info{};
  GetScrollInfo(SB_VERT, &info);

  CPoint pos;
  GetItemPosition(nIndex.value(), &pos);

  CPoint scrol;
  GetItemPosition(info.nPos, &scrol);

  const auto rect = WindowRect(*this);
  if (pos.y > scrol.y + rect.bottom - rect.top || static_cast<int32_t>(nIndex.value()) < info.nPos) {
    EnsureVisible(nIndex.value(), TRUE);
  }
}

void RSTableCtrl::ResetColumns() {
  if (editor->IsModel()) {
    XTROptions::Model().ResetTableWidth();
    SetColumnWidth(ModelHeader::Columns::POS_NAME, XTROptions::Model().nameWidth);
    SetColumnWidth(ModelHeader::Columns::POS_TYPE, XTROptions::Model().typeWidth);
    SetColumnWidth(ModelHeader::Columns::POS_RSDEF, XTROptions::Model().expWidth);
    SetColumnWidth(ModelHeader::Columns::POS_VAL, XTROptions::Model().valWidth);
    SetColumnWidth(ModelHeader::Columns::POS_TERM, XTROptions::Model().termWidth);
    SetColumnWidth(ModelHeader::Columns::POS_TEXTDEF, XTROptions::Model().interWidth);
    SetColumnWidth(ModelHeader::Columns::POS_COMMENT, XTROptions::Model().conventionWidth);    
  } else {
    XTROptions::RS().ResetTableWidth();
    SetColumnWidth(SchemaHeader::Columns::POS_NAME, XTROptions::RS().nameWidth);
    SetColumnWidth(SchemaHeader::Columns::POS_TYPE, XTROptions::RS().typeWidth);
    SetColumnWidth(SchemaHeader::Columns::POS_RSDEF, XTROptions::RS().expWidth);
    SetColumnWidth(SchemaHeader::Columns::POS_TERM, XTROptions::RS().termWidth);
    SetColumnWidth(SchemaHeader::Columns::POS_TEXTDEF, XTROptions::RS().interWidth);
    SetColumnWidth(SchemaHeader::Columns::POS_COMMENT, XTROptions::RS().conventionWidth);
  }
  RedrawWindow();
}

void RSTableCtrl::Select(const EntityUID uid, const BOOL bClearSelection) {
  if (bClearSelection) {
    ClearSelection();
  }
  const auto nIndex = IndexForUID(uid);
  if (nIndex.has_value()) {
    SetSelection(nIndex.value());
  }
}

void RSTableCtrl::SelectMultiple(const SetOfEntities& selList) {
  disableReentrance = TRUE;

  ClearSelection();
  for (const auto uid : selList) {
    Select(uid, FALSE);
  }
  if (!empty(selList)) {
    EnsureVisibleCst(*begin(selList));
  }

  disableReentrance = FALSE;
  UpdateSelList();
}

BOOL RSTableCtrl::PreTranslateMessage(MSG* pMsg) {
  if (tooltips.m_hWnd != nullptr) {
    tooltips.RelayEvent(pMsg);
  }
  if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) {
    ActivateCst();
    return TRUE;
  }
  return RSListCtrl::PreTranslateMessage(pMsg);
}

void RSTableCtrl::OnDestroy() {
  if (editor->IsModel()) {
    XTROptions::Model().nameWidth = GetColumnWidth(ModelHeader::Columns::POS_NAME);
    XTROptions::Model().typeWidth = GetColumnWidth(ModelHeader::Columns::POS_TYPE);
    XTROptions::Model().expWidth = GetColumnWidth(ModelHeader::Columns::POS_RSDEF);
    XTROptions::Model().valWidth = GetColumnWidth(ModelHeader::Columns::POS_VAL);
    XTROptions::Model().termWidth = GetColumnWidth(ModelHeader::Columns::POS_TERM);
    XTROptions::Model().interWidth = GetColumnWidth(ModelHeader::Columns::POS_TEXTDEF);
    XTROptions::Model().conventionWidth = GetColumnWidth(ModelHeader::Columns::POS_COMMENT);
  } else {
    XTROptions::RS().nameWidth = GetColumnWidth(SchemaHeader::Columns::POS_NAME);
    XTROptions::RS().typeWidth = GetColumnWidth(SchemaHeader::Columns::POS_TYPE);
    XTROptions::RS().expWidth = GetColumnWidth(SchemaHeader::Columns::POS_RSDEF);
    XTROptions::RS().termWidth = GetColumnWidth(SchemaHeader::Columns::POS_TERM);
    XTROptions::RS().interWidth = GetColumnWidth(SchemaHeader::Columns::POS_TEXTDEF);
    XTROptions::RS().conventionWidth = GetColumnWidth(SchemaHeader::Columns::POS_COMMENT);
  }
  RSListCtrl::OnDestroy();
}

BOOL RSTableCtrl::IsCommandActive(const uint32_t command) const noexcept {
  switch (command) {
  case IXTRC_MOVE_UP:
  case IXTRC_MOVE_DOWN:
    return size(selection) == 1;
  case IXTRC_SELECT_BASIS:
  case IXTRC_SELECT_DEPENDENT:
    return !empty(selection);
  default:
    return TRUE;
  }
}

BOOL RSTableCtrl::RunCommand(const uint32_t command) {
  if (IsCommandActive(command)) {
    UpdateData();
    const auto& core = editor->Core();
    switch (command) {
    default: break;
    case IXTRC_SELECT_BASIS: {
      SelectMultiple(core.RSLang().Graph().ExpandInputs(selection));
      break;
    }
    case IXTRC_SELECT_DEPENDENT: {
      SelectMultiple(core.RSLang().Graph().ExpandOutputs(selection));
      break;
    }
    case ID_EDIT_COPY: {
      g_Exteor.cstClipboard.clear();
      const auto sorted = core.List().SortSubset(selection);
      for (const auto uid : sorted) {
        g_Exteor.cstClipboard.emplace_back(core.AsRecord(uid));
      }
      break;
    }
    case ID_EDIT_PASTE: PasteFromClipboard();  break;
    case IXTRC_MOVE_UP: MoveUp();  break;
    case IXTRC_MOVE_DOWN: MoveDown(); break;
    }
  }
  return TRUE;
}

BOOL RSTableCtrl::OnDblclkConstituenta(NMHDR* /*pNMHDR*/, LRESULT* result) {
  ActivateCst();
  *result = FALSE;
  return TRUE;
}

BOOL RSTableCtrl::OnSelectChange(NMHDR* pNMHDR, LRESULT* result) {
  *result = FALSE;
  if (!disableReentrance) {
    const auto* listView = reinterpret_cast<const NM_LISTVIEW*>(pNMHDR);
    const auto selected = (listView->uOldState & LVNI_SELECTED) && !(listView->uNewState & LVNI_SELECTED);
    const auto deselected = listView->uNewState & LVNI_SELECTED;
    const auto changed = listView->uChanged & LVIF_STATE;
    if (changed && (selected || deselected)) {
      UpdateSelList();
    }
  }
  return TRUE;
}

BOOL RSTableCtrl::OnNMCustomdraw(NMHDR* pNMHDR, LRESULT* result) {
  *result = CDRF_DODEFAULT;
  auto* device = reinterpret_cast<NMLVCUSTOMDRAW*>(pNMHDR);
  if (ProcessDrawStages(device, result)) {
    return TRUE;
  } else {
    DrawSingleCell(device);
    return FALSE;
  }
}

BOOL RSTableCtrl::ProcessDrawStages(const NMLVCUSTOMDRAW* device, LRESULT* result) noexcept {
  if (device->nmcd.dwDrawStage == CDDS_PREPAINT) {
    *result = CDRF_NOTIFYITEMDRAW;
    return TRUE;
  } else if (device->nmcd.dwDrawStage == CDDS_ITEMPREPAINT) {
    *result = CDRF_NOTIFYSUBITEMDRAW;
    return TRUE;
  } else if (device->nmcd.dwDrawStage == (CDDS_ITEMPREPAINT | CDDS_SUBITEM)) {
    *result = CDRF_DODEFAULT;
    return FALSE;
  } else {
    return TRUE;
  }
}

void RSTableCtrl::DrawSingleCell(NMLVCUSTOMDRAW* device) {
  if (const auto uid = UIDForIndex(static_cast<int>(device->nmcd.dwItemSpec)); uid.has_value()) {
    if (editor->IsModel()) {
      device->clrText = design::TextColor(editor->Model(), uid.value());
      device->clrTextBk = design::BackgroundColor(editor->Model(), uid.value());
    } else {
      device->clrText = design::TextColor(editor->Schema(), uid.value());
      device->clrTextBk = design::BackgroundColor(editor->Schema(), uid.value());
    }
  }
}

BOOL RSTableCtrl::OnBegindragConstituenta(NMHDR *pNMHDR, LRESULT* result) {
  const auto* listView = reinterpret_cast<const tagNMLISTVIEW*>(pNMHDR);
  dnd.dragFrom = listView->iItem;
  dnd.active = TRUE;
  dnd.dragTo = std::nullopt;
  dnd.source = this;
  dnd.destination = this;

  SetCapture(); // Capture all mouse messages

  *result = FALSE;
  return TRUE;
}

void RSTableCtrl::OnMouseMove(const uint32_t nFlags, CPoint point) {
  if (!dnd.active || m_hWnd == nullptr) {
    RSListCtrl::OnMouseMove(nFlags, point);
    return;
  } 
  ClientToScreen(&point);

  auto* dropWnd = WindowFromPoint(point);
  if (dropWnd != dnd.destination && dnd.destination == this) {
    ResetHighlightScroll();
  }
  dnd.destination = dropWnd;
  if (dropWnd == this) {
    UpdateHighlightScroll(point);
  }
}

void RSTableCtrl::ResetHighlightScroll() {
  if (!dnd.dragTo.has_value()) {
    return;
  }
  auto& dest = dynamic_cast<CListCtrl&>(*dnd.destination);
  dest.SetItemState(dnd.dragTo.value(), 0, LVIS_DROPHILITED);
  dest.RedrawItems(dnd.dragTo.value(), dnd.dragTo.value());
  dest.UpdateWindow();
  dnd.dragTo = std::nullopt;
}

void RSTableCtrl::UpdateHighlightScroll(CPoint cursorPos) {
  static constexpr auto scrollDelay = 3;
  static auto delay = 0;

  auto& dest = dynamic_cast<CListCtrl&>(*dnd.destination);
  if (dnd.dragTo.has_value()) {
    dest.SetItemState(dnd.dragTo.value(), 0, LVIS_DROPHILITED);
    dest.RedrawItems(dnd.dragTo.value(), dnd.dragTo.value());
  }

  dnd.destination->ScreenToClient(&cursorPos);

  auto uFlags = 0U;
  dnd.dragTo = dest.HitTest(cursorPos, &uFlags);
  if (dnd.dragTo.value() == mfc::noItems) {
    dnd.dragTo = std::nullopt;
  }
  if (dnd.dragTo.has_value()) {
    dest.SetItemState(dnd.dragTo.value(), LVIS_DROPHILITED, LVIS_DROPHILITED);

    if (delay <= 0) {
      delay = scrollDelay;
      if (dnd.dragTo.value() + 1 < dest.GetItemCount()) {
        dest.EnsureVisible(dnd.dragTo.value() + 1, TRUE);
      }
      if (dnd.dragTo.value() - 1 >= 0) {
        dest.EnsureVisible(dnd.dragTo.value() - 1, TRUE);
      }
    } else {
      --delay;
    }

    dest.RedrawItems(dnd.dragTo.value(), dnd.dragTo.value());
    dest.UpdateWindow();
  }
}

void RSTableCtrl::OnLButtonUp(const uint32_t nFlags, CPoint point) {
  if (!dnd.active || m_hWnd == nullptr) {
    RSListCtrl::OnLButtonUp(nFlags, point);
    return;
  }
  ReleaseCapture();

  dnd.active = FALSE;

  ClientToScreen(&point);
  auto* dropWnd = WindowFromPoint(point);
  if (dropWnd == this) {
    if (dnd.dragTo.has_value()) {
      auto& dropCtrl = dynamic_cast<CListCtrl&>(*dropWnd);
      dropCtrl.SetItemState(dnd.dragTo.value(), 0, LVIS_DROPHILITED);
    }
    dnd.destination = dropWnd;
    if (dnd.dragTo.has_value()) {
      MoveSelectedBefore(dnd.dragTo.value());
    }
  }
}

void RSTableCtrl::UpdateSelList() {
  selection.clear();
  auto nItem = mfc::noItems;
  while ((nItem = GetNextItem(nItem, LVNI_SELECTED)) != mfc::noItems) {
    selection.emplace(UIDForIndex(nItem).value());
  }
}

void RSTableCtrl::PasteFromClipboard() {
  const auto inserted = InsertClipboard();
  if (empty(inserted)) {
    return;
  }

  if (dynamic_cast<CView*>(GetParent()) != nullptr) {
    dynamic_cast<CView*>(GetParent())->UpdateData(FALSE);
  } else {
    Update();
  }
  SelectMultiple({ begin(inserted),  end(inserted) });
}

ccl::VectorOfEntities RSTableCtrl::InsertClipboard() {
  if (empty(g_Exteor.cstClipboard)) {
    return {};
  } else if (empty(selection)) {
    return editor->InsertCopy(g_Exteor.cstClipboard);
  } else {
    const auto selected = editor->List().SortSubset(selection);
    const auto position = std::next(editor->List().Find(selected.back()));
    bool isLast = position == editor->List().end();
    auto inserted = editor->InsertCopy(g_Exteor.cstClipboard);
    for (const auto uid : inserted) {
      editor->MoveBefore(uid, isLast ? editor->List().end() : position);
    }
    return inserted;
  }
}

void RSTableCtrl::MoveUp() {
  if (const auto nItem = GetNextItem(mfc::noItems, LVNI_SELECTED); nItem > 0) {
    MoveSelectedBefore(nItem - 1);
  }
}

void RSTableCtrl::MoveDown() {
  if (const auto nItem = GetNextItem(mfc::noItems, LVNI_SELECTED); nItem != mfc::noItems) {
    MoveSelectedBefore(nItem + 2);
  }
}

void RSTableCtrl::MoveSelectedBefore(const int32_t index) {
  auto posWhere = std::end(editor->List());
  if (index < GetItemCount()) {
    posWhere = editor->List().Find(UIDForIndex(index).value());
  }
  proc::MoveCstBefore(*editor, selection, posWhere);
}

void RSTableCtrl::ActivateCst() {
  GetParent()->UpdateData(TRUE);
  if (const auto nItem = GetFocusIndex(); nItem != mfc::noItems) {
    proc::EditConstituenta(*editor, UIDForIndex(nItem).value());
  }
}

void RSTableCtrl::InsertCst(const EntityUID target) {
  const auto nItem = InsertItem(LVIF_TEXT | LVIF_STATE, GetItemCount(), mfc::ToMFC(editor->GetRS(target).alias), 0, 0, 0, 0);
  if (nItem != mfc::noItems) {
    InputCstData(nItem, target);
  } else {
    ::AfxMessageBox(IXTRE_CST_INSERT, MB_ICONERROR);
  }
}

void RSTableCtrl::InputCstData(const int32_t nItem, const EntityUID target) {
  const auto& rsCst = editor->GetRS(target);
  const auto& textCst = editor->GetText(target);
  auto name = mfc::ToMFC(rsCst.alias);
  auto expr = mfc::ToMFC(rsCst.definition);
  auto comment = mfc::ToMFC(rsCst.convention);
  auto inter = mfc::ToMFC(textCst.definition.Str());
  auto term = mfc::ToMFC(textCst.term.Nominal());
  auto type = info::Label(editor->Core().GetParse(target).TypePtr());

  expr.Replace(L"\n", L" ");
  inter.Replace(L"\n", L" ");
  term.Replace(L"\n", L" ");
  comment.Replace(L"\n", L" ");

  if (editor->IsModel())  {
    const auto val = info::ValueStatus(target, editor->Model());
    SetItemText(nItem, ModelHeader::Columns::POS_NAME, name);
    SetItemText(nItem, ModelHeader::Columns::POS_TYPE, type);
    SetItemText(nItem, ModelHeader::Columns::POS_RSDEF, expr);
    SetItemText(nItem, ModelHeader::Columns::POS_VAL, val);
    SetItemText(nItem, ModelHeader::Columns::POS_TEXTDEF, inter);
    SetItemText(nItem, ModelHeader::Columns::POS_TERM, term);
    SetItemText(nItem, ModelHeader::Columns::POS_COMMENT, comment);
  } else {
    SetItemText(nItem, SchemaHeader::Columns::POS_NAME, name);
    SetItemText(nItem, SchemaHeader::Columns::POS_TYPE, type);
    SetItemText(nItem, SchemaHeader::Columns::POS_RSDEF, expr);
    SetItemText(nItem, SchemaHeader::Columns::POS_TEXTDEF, inter);
    SetItemText(nItem, SchemaHeader::Columns::POS_TERM, term);
    SetItemText(nItem, SchemaHeader::Columns::POS_COMMENT, comment);
  }
}

} // namespace xtr::ui