#include "stdafx.h"

#include "xtr/view/OSSView.h"

#include "resource.h"

#include "xtr/cclDescriptor.h"
#include "xtr/xtrProcedures.h"
#include "xtr/ExteorOptions.h"

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

namespace xtr::view {

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

IMPLEMENT_DYNCREATE(OSSView, CScrollView)

BEGIN_MESSAGE_MAP(OSSView, CScrollView)
  ON_WM_LBUTTONUP()
  ON_WM_RBUTTONUP()
  ON_WM_MOUSEMOVE()
  ON_WM_LBUTTONDBLCLK()
  ON_WM_MOUSEHOVER()
  ON_WM_CONTEXTMENU()
  ON_WM_MOUSEWHEEL()
  ON_WM_SETCURSOR()
  ON_WM_SETFOCUS()

  XTR_COMMAND(IXTRC_XTR_HELP)

  XTR_COMMAND_UPD(IXTRC_BACKUP)
  XTR_COMMAND_UPD(IXTRC_EXPORT_OSS)
  XTR_COMMAND_UPD(IXTRC_SAVE_IMAGE)
  XTR_COMMAND_UPD(IXTRC_OSS_ADD)
  XTR_COMMAND_UPD(IXTRC_OSS_DEL)
  XTR_COMMAND_UPD(IXTRC_OSS_ASSIGN_DOC)
  XTR_COMMAND_UPD(IXTRC_OSS_EDIT_DOC)
  XTR_COMMAND_UPD(IXTRC_OSS_DISCARD_DOC)
  XTR_COMMAND_UPD(IXTRC_EDIT_HLINK)
  XTR_COMMAND_UPD(IXTRC_FOLLOW_HLINK)
  XTR_COMMAND_UPD(IXTRC_OSS_OP_NEW)
  XTR_COMMAND_UPD(IXTRC_OSS_OP_EDIT)
  XTR_COMMAND_UPD(IXTRC_OSS_OP_RUN_SINGLE)
  XTR_COMMAND_UPD(IXTRC_OSS_OP_RUN_ALL)
  XTR_COMMAND_UPD(IXTRC_OSS_OP_CHECK)
  XTR_COMMAND_UPD(IXTRC_OSS_ENABLE_GRID)
  XTR_COMMAND_UPD(IXTRC_OSS_ENABLE_LEGEND)
  XTR_COMMAND_UPD(IXTRC_OSS_ZOOM_IN)
  XTR_COMMAND_UPD(IXTRC_OSS_ZOOM_OUT)
  XTR_COMMAND_UPD(IXTRC_OSS_SHIFT_LEFT)
  XTR_COMMAND_UPD(IXTRC_OSS_SHIFT_RIGHT)
 END_MESSAGE_MAP()

#pragma warning( pop )

void OSSView::OnInitialUpdate() {
  CScrollView::OnInitialUpdate();

  document = dynamic_cast<doc::OSSDoc*>(m_pDocument);
  oss = &document->OSS();

  SetFont(&XTROptions::App().fontText);

  layout = std::make_unique<OSSLayout>(*oss);
  layout->InitImageList();

  UpdateScrollSize();
  ShowScrollBar(SB_BOTH);

  InitCursor();
  InitTooltips();
}

void OSSView::InitCursor() {
  cursorMarkOperation = ::AfxGetApp()->LoadCursorW(IXTRR_CREATE_OPERATION);
  cursorHyperlink = ::AfxGetApp()->LoadCursorW(IXTRR_HYPERLINK);
  if (cursorMarkOperation == nullptr || cursorHyperlink == nullptr) {
    TRACE(L"Cannot load cursor");
  }
}

void OSSView::InitTooltips() {
  static constexpr auto TOOLTIP_DELAY_DEFAULT = 1000;

  if (!tooltips.Create(this, WS_POPUP)) {
    TRACE(L"Unable to create the ToolTip!");
  } else {
    tooltips.SetMaxTipWidth(ui::s_TooltipWidth);
    tooltips.AddTool(this, CString{});
    tooltips.SetFont(&XTROptions::App().fontTooltip);
    tooltips.SetDelayTime(TOOLTIP_DELAY_DEFAULT);
  }
}

void OSSView::DoDataExchange(CDataExchange* pDX){
  CScrollView::DoDataExchange(pDX);
  if (!pDX->m_bSaveAndValidate) {
    UpdateScrollSize();
    ScrollToSelected();
  }
}

BOOL OSSView::PreTranslateMessage(MSG* pMsg) {
  if (tooltips.m_hWnd != nullptr) {
    tooltips.RelayEvent(pMsg);
  }
  return CScrollView::PreTranslateMessage(pMsg);
}

BOOL OSSView::OnScrollBy(CSize sizeScroll, BOOL bDoScroll) {
  if (!CScrollView::OnScrollBy(sizeScroll, bDoScroll)) {
    return FALSE;
  } else {
    if (bDoScroll) {
      UpdateWindow();
    }
    return TRUE;
  }
}

BOOL OSSView::OnMouseWheel(uint32_t fFlags, short zDelta, CPoint /*point*/) {
  if (fFlags & MK_CONTROL)  {  // Если зажат контрол - зумим
    const auto doUpdate = 
      (zDelta > 0 && layout->zoom > ossOptions::zoomMin) || 
      (zDelta < 0 && layout->zoom < ossOptions::zoomMax);
    if (zDelta > 0 && layout->zoom > ossOptions::zoomMin) {
      --layout->zoom;
    }
    if (zDelta < 0 && layout->zoom < ossOptions::zoomMax) {
      ++layout->zoom;
    }
    if (doUpdate) {
      Invalidate();
      UpdateWindow();
    }
    return doUpdate;
  } else {
    BOOL bHasVertBar{};
    BOOL bHasHorzBar{};
    CheckScrollBars(bHasHorzBar, bHasVertBar);
    if (!bHasVertBar && !bHasHorzBar) {
      return FALSE;
    } else {
      auto bResult = FALSE;
      auto uWheelScrollLines = 3;
      ::SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &uWheelScrollLines, 0);

      const auto nToScroll = ::MulDiv(-zDelta, uWheelScrollLines, WHEEL_DELTA);

      auto nDisplacement = 0;
      if (bHasVertBar && !(fFlags & MK_SHIFT)) {
        if (uWheelScrollLines == WHEEL_PAGESCROLL) {
          nDisplacement = m_pageDev.cy;
          if (zDelta > 0) {
            nDisplacement = -nDisplacement;
          }
        } else {
          nDisplacement = nToScroll * m_lineDev.cy;
          nDisplacement = std::min(static_cast<long>(nDisplacement), m_pageDev.cy);
        }
        bResult = OnScrollBy(CSize(0, nDisplacement), TRUE);
      } else if (bHasHorzBar) {
        if (uWheelScrollLines == WHEEL_PAGESCROLL) {
          nDisplacement = m_pageDev.cx;
          if (zDelta > 0) {
            nDisplacement = -nDisplacement;
          }
        } else {
          nDisplacement = nToScroll * m_lineDev.cx;
          nDisplacement = std::min(static_cast<long>(nDisplacement), m_pageDev.cx);
        }
        bResult = OnScrollBy(CSize(nDisplacement, 0), TRUE);
      }
      if (bResult) {
        UpdateWindow();
      }
      return bResult;
    }
  }
}

BOOL OSSView::OnSetCursor(CWnd* /*pWnd*/, uint32_t /*nHitTest*/, uint32_t /*message*/) { // NOLINT(readability-convert-member-functions-to-static)
  return FALSE;
}

void OSSView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) {
  CScrollView::OnUpdate(pSender, lHint, pHint);
  if (oss != nullptr && pSender != this) {
    UpdateData(FALSE);
  }
}

void OSSView::OnLButtonUp(uint32_t nFlags, CPoint point) {
  CScrollView::OnLButtonUp(nFlags, point);

  ClientToDoc(point);
  const auto* currentSelection = (*oss)(document->selection);
  if (const auto* newSelection = layout->PictFromPoint(point); newSelection != nullptr) {
    if (newOperationMode) {
      if (currentSelection != newSelection) {
        newSelection = oss->InsertOperation(currentSelection->uid, newSelection->uid);
      } else {
        ::AfxMessageBox(IXTRE_UNARY_OP, MB_ICONEXCLAMATION);
      }
      assert(newSelection != nullptr);
      newOperationMode = FALSE;
      UpdateCursor(newSelection);
    }
    document->selection = newSelection->uid;
    document->UpdateAllViews(nullptr);
  }
}

void OSSView::OnRButtonUp(uint32_t nFlags, CPoint point) {
  if (newOperationMode) {
    newOperationMode = FALSE;
    UpdateCursor();
  } else {
    ClientToDoc(point);
    const auto* const pict = layout->PictFromPoint(point);
    if (pict != nullptr) {
      document->selection = pict->uid;
      document->UpdateAllViews(nullptr);
    }
    UpdateCursor(pict);
  }
  CScrollView::OnRButtonUp(nFlags, point);
}

void OSSView::OnMouseMove(uint32_t nFlags, CPoint point) {
  CScrollView::OnMouseMove(nFlags, point);

  ClientToDoc(point);
  const auto* const pict = layout->PictFromPoint(point);
  UpdateCursor(pict);
  if (pict == nullptr) {
    pictUnderCursor = std::nullopt;
    tooltips.ShowWindow(FALSE);
  } else if (pictUnderCursor.has_value()) {
    tooltips.Popup();
    tooltips.Update();
  } else {
    StartHoverTimer();
  }
}

void OSSView::StartHoverTimer() noexcept {
  static constexpr auto TOOLTIP_DEALY = 500;
  TRACKMOUSEEVENT csTME{};
  csTME.cbSize = sizeof(csTME);
  csTME.dwFlags = TME_HOVER;
  csTME.hwndTrack = m_hWnd;
  csTME.dwHoverTime = TOOLTIP_DEALY;
  ::TrackMouseEvent(&csTME);
}

void OSSView::OnMouseHover(uint32_t /*nFlags*/, CPoint point) {
  ClientToDoc(point);
  if (const auto* const pict = layout->PictFromPoint(point); pict != nullptr) {
    if (pictUnderCursor.has_value() && pictUnderCursor.value() == pict->uid) {
      tooltips.Popup();
    } else {
      pictUnderCursor = pict->uid;
      auto text = mfc::ToMFC(pict->title);
      if (text.IsEmpty()) {
        text = mfc::LoadSID(IXTRS_PICT_NONAME);
      }

      text += "\n\n";
      text += info::BasicDescriptionOf(pict->uid, document->OSS());

      tooltips.UpdateTipText(text, this);
      tooltips.Update();
      tooltips.ShowWindow(TRUE);
      tooltips.Popup();
    }
  }
}

void OSSView::OnSetFocus(CWnd* pOldWnd) {
  CScrollView::OnSetFocus(pOldWnd);
  RedrawWindow();
}

void OSSView::OnLButtonDblClk(uint32_t nFlags, CPoint point) {
  ClientToDoc(point);
  if (const auto* const pict = layout->PictFromPoint(point); pict != nullptr) {
    auto const pid = pict->uid;
    document->selection = pid;
    document->UpdateAllViews(nullptr);
    proc::DoAction(*document);
  }
  CScrollView::OnLButtonDblClk(nFlags, point);
}

void OSSView::OnContextMenu(CWnd* /*pWnd*/, CPoint point) {
  auto clinePt = point;
  ScreenToClient(&clinePt);
  ClientToDoc(clinePt);

  if (const auto* pict = layout->PictFromPoint(clinePt); pict != nullptr) {
    CMenu popup{};
    popup.LoadMenuW(IXTRAD_OSS_POPUP);
    auto *item0 = popup.GetSubMenu(0);
    for (auto i = 0; i < item0->GetMenuItemCount(); ++i) {
      if (const auto command = item0->GetMenuItemID(i); command != 0) {
        if (command == IXTRC_FOLLOW_HLINK) {
          auto msg = mfc::LoadSID(IXTRS_FOLLOW_HYPERLINK);
          if (!std::empty(pict->lnk)) {
            msg += LR"(: )";
            msg += mfc::ToMFC(pict->lnk.address);
          }
          item0->ModifyMenuW(command, MF_BYCOMMAND | MF_STRING | MF_POPUP, command, msg);
        }

        if (!IsCommandActive(command)) {
          item0->EnableMenuItem(command, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
        } else {
          item0->EnableMenuItem(command, MF_BYCOMMAND | MF_ENABLED);
        }
      }
    }
    item0->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
  }
}

void OSSView::OnUpdateCommandUI(CCmdUI* pCmdUI) {
  pCmdUI->Enable(IsCommandActive(pCmdUI->m_nID));

  if (pCmdUI->m_nID == IXTRC_OSS_OP_NEW) {
    pCmdUI->SetCheck(newOperationMode);
  } else if (pCmdUI->m_nID == IXTRC_OSS_ENABLE_GRID) {
    pCmdUI->SetCheck(XTROptions::OSS().showGrid);
  } else if (pCmdUI->m_nID == IXTRC_OSS_ENABLE_LEGEND) {
    pCmdUI->SetCheck(XTROptions::OSS().showLegend);
  }
}

BOOL OSSView::IsCommandActive(const uint32_t command) const {
  switch (command) {
  default: break;
    case IXTRC_BACKUP:
    case IXTRC_SAVE_IMAGE:
    case IXTRC_EXPORT_OSS:
    case IXTRC_OSS_OP_NEW:
    case IXTRC_OSS_OP_RUN_ALL:
    case IXTRC_EDIT_HLINK:
    case IXTRC_OSS_ENABLE_GRID:
    case IXTRC_OSS_ENABLE_LEGEND:
    case IXTRC_OSS_SHIFT_RIGHT:
      return TRUE;
  }

  const auto* operation = oss->Ops()(document->selection);
  switch (command) {
  default: return FALSE;
  case IXTRC_OSS_ADD:
  case IXTRC_OSS_DEL: return !newOperationMode;

  case IXTRC_OSS_ASSIGN_DOC: return operation == nullptr;
  case IXTRC_OSS_DISCARD_DOC:
    return !std::empty(*oss->Src()(document->selection));
  case IXTRC_OSS_EDIT_DOC:
    return operation == nullptr || !std::empty(*oss->Src()(document->selection));

  case IXTRC_OSS_OP_EDIT:
  case IXTRC_OSS_OP_CHECK: return operation != nullptr;

  case IXTRC_OSS_OP_RUN_SINGLE:
    return operation != nullptr && 
      operation->type != ccl::ops::Type::tba && 
      !operation->broken;

  case IXTRC_FOLLOW_HLINK: return !std::empty((*oss)(document->selection)->lnk);

  case IXTRC_OSS_ZOOM_IN: return layout->zoom > ossOptions::zoomMin;
  case IXTRC_OSS_ZOOM_OUT: return layout->zoom < ossOptions::zoomMax;
  case IXTRC_OSS_SHIFT_LEFT: return  oss->Grid()(document->selection)->column > 0;
  }
}

BOOL OSSView::OnCommandRun(const uint32_t command) {
  if (!IsCommandActive(command)) {
    return TRUE;
  } else {
    if (command != IXTRC_OSS_OP_NEW) {
      newOperationMode = FALSE;
    }
    auto needUpdate = TRUE;
    const auto pid = document->selection;
    switch (command) {
    case IXTRC_XTR_HELP: ui::HelpHandler::RunHelp(ui::HelpID::viewRSOSS); return TRUE;

    case IXTRC_BACKUP: proc::BackupOSS(*document); return TRUE;
    case IXTRC_SAVE_IMAGE: proc::ExportOSSGraph(*document, *layout); return TRUE;
    case IXTRC_EXPORT_OSS: proc::ExportOSS(*document); return TRUE;

    case IXTRC_OSS_ADD: document->selection = oss->InsertBase()->uid;  break;
    case IXTRC_OSS_DEL: proc::EraseSelected(*document); return TRUE;

    case IXTRC_OSS_ASSIGN_DOC: proc::AttachSourceToSelected(*document); return TRUE;
    case IXTRC_OSS_DISCARD_DOC: proc::DiscardSelectedSource(*document); return TRUE;
    case IXTRC_OSS_EDIT_DOC: proc::OpenSelectedSource(*document); return TRUE;

    case IXTRC_OSS_OP_NEW: newOperationMode = !newOperationMode; break;
    case IXTRC_OSS_OP_EDIT: proc::EditSelectedOperation(*document); return TRUE;
    case IXTRC_OSS_OP_CHECK: {
      ::AfxMessageBox(info::DescriptionOf(oss->Ops(), pid), MB_ICONINFORMATION);
      return TRUE;
    }
    case IXTRC_OSS_OP_RUN_SINGLE: proc::RunSelectedOperation(*document); return TRUE;
    case IXTRC_OSS_OP_RUN_ALL: proc::RerunAllOperations(*document); return TRUE;

    case IXTRC_EDIT_HLINK:  proc::EditSelectedHyperlink(*document); return TRUE;
    case IXTRC_FOLLOW_HLINK: {
      const auto& link = (*oss)(document->selection)->lnk;
      proc::FollowHyperlink(mfc::ToMFC(link.address), mfc::ToMFC(link.subAddr));
      return TRUE;
    }

    case IXTRC_OSS_ENABLE_GRID:  XTROptions::OSS().showGrid = !XTROptions::OSS().showGrid;  break;
    case IXTRC_OSS_ENABLE_LEGEND:  XTROptions::OSS().showLegend = !XTROptions::OSS().showLegend;  break;
    case IXTRC_OSS_ZOOM_IN: --layout->zoom; break;
    case IXTRC_OSS_ZOOM_OUT: ++layout->zoom; break;
    case IXTRC_OSS_SHIFT_LEFT: needUpdate = oss->Grid().ShiftPict(pid, -1);  break;
    case IXTRC_OSS_SHIFT_RIGHT: needUpdate = oss->Grid().ShiftPict(pid, 1);  break;
    }
    if (needUpdate) {
      document->UpdateAllViews(nullptr);
    }
    return TRUE;
  }
}

void OSSView::UpdateCursor(ccl::oss::PictPtr pict) const {
  if (newOperationMode) {
    ::SetCursor(cursorMarkOperation);
  } else if (pict != nullptr && !std::empty(pict->lnk)) {
    ::SetCursor(cursorHyperlink);
  } else {
    ::SetCursor(::AfxGetApp()->LoadStandardCursor(IDC_ARROW));
  }
}

void OSSView::ScrollToSelected() {
  const auto pos = oss->Grid()(document->selection);
  assert(pos.has_value());

  // Check if scrolling is needed
  auto rect = layout->PictRect(document->selection, 1 / layout->zoom);
  DocToClient(rect);
  auto visibleRect = ui::WindowRect(*this);
  ScreenToClient(visibleRect);

  if (!visibleRect.PtInRect(rect.TopLeft())
      || !visibleRect.PtInRect(rect.BottomRight())) {

    auto row = pos->row;
    if (row > 1) {
      --row;
    }
    const auto visionWidth = visibleRect.Width();
    const auto visionHeight = visibleRect.Height();
    const auto clientSize = GetTotalSize();
    const auto cellRect = layout->CellRect(pos.value(), 1 / layout->zoom);

    // Prevent cursor from leaving bounds
    POINT pt{};
    pt.x = clientSize.cx >= cellRect.left + visionWidth ? cellRect.left : clientSize.cx - visionWidth;
    pt.y = clientSize.cy >= cellRect.top + visionHeight ? cellRect.top : clientSize.cy - visionHeight;
    if (pt.x < 0) {
      pt.x = 0;
    }
    if (pt.y < 0) {
      pt.y = 0;
    }
    ScrollToPosition(pt);
  }
}

void OSSView::UpdateScrollSize() {
  CSize sizeTotal{};
  sizeTotal.cx = layout->GetClientWidth();
  sizeTotal.cy = layout->GetClientHeight();
  SetScrollSizes(MM_TEXT, sizeTotal);
}

void OSSView::ClientToDoc(CPoint& point) {
  CClientDC dc{ this };
  OnPrepareDC(&dc, nullptr);
  dc.DPtoLP(&point);
}

void OSSView::ClientToDoc(CRect& rect) {
  CClientDC dc{ this };
  OnPrepareDC(&dc, nullptr);
  dc.DPtoLP(rect);
  assert(rect.left <= rect.right);
  assert(rect.bottom <= rect.top);
}

void OSSView::DocToClient(CPoint& point) {
  CClientDC dc{ this };
  OnPrepareDC(&dc, nullptr);
  dc.LPtoDP(&point);
}

void OSSView::DocToClient(CRect& rect) {
  CClientDC dc{ this };
  OnPrepareDC(&dc, nullptr);
  dc.LPtoDP(rect);
  rect.NormalizeRect();
}

void OSSView::OnDraw(CDC* pDC) {
  if (layout != nullptr) {
    layout->Draw(pDC, document->selection);
  }
}

} // namespace xtr::view