#include "stdafx.h"

#include "xtr/ui/BuffRichEdit.h"

#include "xtr/core/Encoder.h"
#include "xtr/ExteorOptions.h"

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

#include "richole.h"

#include "resource.h"

namespace xtr::ui {

static constexpr BOOL REDIT_CAN_UNDO = TRUE;

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

BEGIN_MESSAGE_MAP(BuffRichEdit, CRichEditCtrl)
  ON_WM_KEYDOWN()
  ON_WM_KEYUP()
  ON_WM_SETCURSOR()
  ON_WM_SETFOCUS()
  ON_WM_KILLFOCUS()

  ON_CONTROL_REFLECT_EX(EN_CHANGE, &ThisClass::OnTextChange)
  ON_NOTIFY_REFLECT_EX(EN_SELCHANGE, &ThisClass::OnSelChange)
  ON_NOTIFY_REFLECT_EX(EN_LINK, &ThisClass::OnEnLink)
END_MESSAGE_MAP()

#pragma warning( pop )

CString BuffRichEdit::Text() const {
  auto text = WindowText(*this);
  text.Replace(L"\r\n", L"\n");
  return text;
}

void BuffRichEdit::InitRE(const Keyboard::Layout kl, const BOOL _enableFormat, const BOOL _enableTooltip) {
  enableFormat = _enableFormat;
  enableTooltip = _enableTooltip;
  defaultKL = kl;

  SetEventMask(GetEventMask() | ENM_CHANGE | ENM_SELCHANGE | ENM_LINK);
  InitToolTips();
  UpdateFormat();
}

CPoint BuffRichEdit::GetSubscriptDialogPosition() const {
  const auto rect = WindowRect(*this);

  const auto x = rect.left + GetCaretPos().x;
  auto y = rect.top + GetCaretPos().y;

  const auto metrics = TextMetricsOf(*this);
  y += metrics.tmHeight + metrics.tmExternalLeading;
  return { x, y };
}

CString BuffRichEdit::FixSelection() {
  if (auto selection = GetSelectionRange();  selection.length() == 0) {
    selection = ExpandRange(selection);
    SelectRange(selection);
  }
  return GetSelText();
}

StrRange BuffRichEdit::GetSelectionRange() const {
  long nSelStart{};
  long nSelEnd{};
  GetSel(nSelStart, nSelEnd);
  if (nSelEnd > GetTextLength()) {
    nSelEnd = std::max(nSelStart, GetTextLength());
  }
  return StrRange{ static_cast<StrPos>(nSelStart), static_cast<StrPos>(nSelEnd) };
}

void BuffRichEdit::SelectRange(const StrRange range) {
  SetSel(range.start, range.finish);
}

void BuffRichEdit::SelectChar(const StrPos position) {
  SetSel(position, position + 1);
}

void BuffRichEdit::SelectAll() {
  SetSel(0, mfc::noItems);
}

void BuffRichEdit::MoveCaretTo(const StrPos position) {
  SetSel(position, position);
}

void BuffRichEdit::SetReadOnly(const BOOL readOnlyFlag) {
  readOnly = readOnlyFlag;
  CRichEditCtrl::SetReadOnly(readOnlyFlag);
  UpdateBackground();
}

void BuffRichEdit::LockInput() {
  if (!readOnly) {
    CRichEditCtrl::SetReadOnly(TRUE);
    UpdateBackground();
  }
}

void BuffRichEdit::UnlockInput() {
  if (!readOnly) {
    CRichEditCtrl::SetReadOnly(FALSE);
    UpdateBackground();
  }
}

void BuffRichEdit::InitToolTips() {
  if (!tooltips.Create(this)) {
    TRACE(L"Unable to create the ToolTip!");
  } else {
    tooltips.SetMaxTipWidth(s_TooltipWidth);
    tooltips.AddTool(this, defaultToolTipText);
    tooltips.SetFont(&XTROptions::App().fontTooltip);

    tooltips.Activate(enableTooltip);
  }
}

void BuffRichEdit::UpdateFormat(BOOL ignoreSelection) {
  if (alreadyUpdating) {
    return;
  } else if (UpdateBackground(); !enableFormat) {
      return;
  } else if (const auto selection = GetSelectionRange();
             !ignoreSelection &&
             (selection.start != selection.finish ||
             (selection.start == cursorFormatPos && selection.start != 0))) {
    return;
  } else {
    alreadyUpdating = TRUE;
    cursorFormatPos = selection.start;

    FreezeUI();

    ResetFormatting();
    UpdateFormatItems(); // Note: overridable
    ApplyFormatTokens();

    MoveCaretTo(cursorFormatPos);
    AdditionalFormat(); // Note: overridable

    SelectAndFormat(StrRange{ cursorFormatPos, cursorFormatPos }, DefaultFormat());

    UnFreezeUI();

    alreadyUpdating = FALSE;
  }
}

void BuffRichEdit::ResetFormatting() {
  auto defaultFormat = DefaultFormat();
  SelectAll();
  SetSelectionCharFormat(defaultFormat);
}

void BuffRichEdit::UpdateBackground() {
  if ((GetStyle() & ES_READONLY) != 0) {
    SetBackgroundColor(FALSE, GetSysColor(COLOR_3DFACE));
  } else {
    SetBackgroundColor(FALSE, GetSysColor(COLOR_WINDOW));
  }
}

void BuffRichEdit::ApplyFormatTokens() {
  for (auto& item : formatItems) {
    SetSel(item.cr.start, item.cr.finish);
    SetSelectionCharFormat(item.format);
  }
}

BOOL BuffRichEdit::PreTranslateMessage(MSG* pMsg) {
  if (tooltips.m_hWnd != nullptr) {
    tooltips.RelayEvent(pMsg);
  }
  if (pMsg->message == WM_KEYDOWN) {
    if ((GetStyle() & ES_MULTILINE) != 0 &&
        pMsg->wParam == VK_RETURN &&
        !Keyboard::IsPressed(VK_CONTROL)) {
      InsertChar(VK_RETURN);
      return TRUE;
    }
  }
  return CRichEditCtrl::PreTranslateMessage(pMsg);
}

void BuffRichEdit::OnSetFocus(CWnd* pOldWnd) {
  CRichEditCtrl::OnSetFocus(pOldWnd);
  Keyboard::Instance().SetLayout(defaultKL);
}

void BuffRichEdit::OnKillFocus(CWnd* pOldWnd) {
  CRichEditCtrl::OnKillFocus(pOldWnd);
  UpdateFormat(TRUE);
  defaultKL = Keyboard::Instance().GetLayout();
}

BOOL BuffRichEdit::OnTextChange() {
  UpdateFormat();
  return FALSE;
}

BOOL BuffRichEdit::OnSelChange(NMHDR* /*pNotifyStruct*/, LRESULT* /*result*/) {
  UpdateFormat();
  return FALSE;
}

BOOL BuffRichEdit::OnSetCursor(CWnd* pWnd, uint32_t nHitTest, uint32_t message) {
  if (enableTooltip && GetSelText().GetLength() != 0) {
    tooltips.UpdateTipText(GenerateToolTipText(), this);
  }
  return CRichEditCtrl::OnSetCursor(pWnd, nHitTest, message);
}

void BuffRichEdit::OnKeyDown(uint32_t nChar, uint32_t nRepCnt, uint32_t nFlags) {
  switchLayoutOnCtrl = nChar == VK_CONTROL;

  if (nChar == 'C' && Keyboard::IsPressed(VK_CONTROL)) {
    DeepCopy();
    return;
  }

  CRichEditCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}

void BuffRichEdit::DeepCopy() {
  if (!OpenClipboard()) {
    ::AfxMessageBox(IXTRE_CLIPBOARD_OPEN, MB_ICONERROR);
  } else if (!EmptyClipboard()) {
    ::AfxMessageBox(IXTRE_CLIPBOARD_CLEAR, MB_ICONERROR);
  } else {
    if (auto cpyStr = GetSelText();  !cpyStr.IsEmpty()) {
      if (auto* hGlob = GlobalAlloc(GMEM_FIXED, (wcslen(cpyStr) + 1) * sizeof(wchar_t));
          hGlob != nullptr) {
        wcscpy_s(static_cast<wchar_t*>(hGlob), wcslen(cpyStr) + 1, cpyStr);
        if (::SetClipboardData(CF_UNICODETEXT, hGlob) == nullptr) {
          ::AfxMessageBox(IXTRE_CLIPBOARD_COPY, MB_ICONERROR);
          GlobalFree(hGlob);
        }
      }
    }
    ::CloseClipboard();
  }
}

void BuffRichEdit::OnKeyUp(uint32_t nChar, uint32_t nRepCnt, uint32_t nFlags) {
  if (nChar != VK_CONTROL || !switchLayoutOnCtrl) {
    CRichEditCtrl::OnKeyUp(nChar, nRepCnt, nFlags);
  } else {
    switchLayoutOnCtrl = FALSE;
    CRichEditCtrl::OnKeyUp(nChar, nRepCnt, nFlags);
    Keyboard::Instance().RotateLayout();
  }
}

BOOL BuffRichEdit::OnEnLink(NMHDR *pNMHDR, LRESULT *pResult) {
  const auto* pEnLink = reinterpret_cast<ENLINK *>(pNMHDR);
  *pResult = FALSE;
  if (pEnLink == nullptr || pEnLink->msg != WM_LBUTTONDOWN) {
    return FALSE;
  }

  CString strUrl{};
  GetTextRange(pEnLink->chrg.cpMin, pEnLink->chrg.cpMax, strUrl);
  ::ShellExecuteW(nullptr, nullptr, strUrl, nullptr, nullptr, SW_SHOW);
  return TRUE;
}

void BuffRichEdit::InsertChar(const uint32_t ch) {
  if ((GetStyle() & ES_READONLY) == 0) {
    CString str{};
    str.AppendChar(static_cast<wchar_t>(ch));
    ReplaceSel(str, REDIT_CAN_UNDO);
    UpdateFormat();
  }
}

void BuffRichEdit::InsertString(const CString str) {
  if ((GetStyle() & ES_READONLY) == 0) {
    ReplaceSel(str, REDIT_CAN_UNDO);
    UpdateFormat();
  }
}

CHARFORMAT2W BuffRichEdit::GetSelectionFormat() const {
  CHARFORMAT2W format{};
  GetSelectionCharFormat(format);
  return format;
}

CHARFORMAT2W BuffRichEdit::DefaultFormat() const {
  CHARFORMAT2W format{};
  GetDefaultCharFormat(format);
  return format;
}

void BuffRichEdit::SelectAndFormat(const StrRange range, CHARFORMAT2W format) {
  SelectRange(range);
  SetSelectionCharFormat(format);
}

StrRange BuffRichEdit::ExpandRange(StrRange cr) const {
  auto start = cr.start;
  auto end = cr.finish;
  const auto text = Text();
  while (start != end && encode::IsAlphaNumber(text[start])) {
    ++start;
  }
  while (start != end && encode::IsAlphaNumber(text[end])) {
    --end;
  }
  while (start > 0 && encode::IsAlphaNumber(text[start - 1])) {
    --start;
  }
  while (end < text.GetLength() && encode::IsAlphaNumber(text[end])) {
    ++end;
  }
  return StrRange{ start, end };
}

//TOM интерфейс ITextDocument
EXTERN_C const GUID CDECL IID_ITextDocument = { 
  0x8CC497C0, 0xA1DF, 0x11CE, { 0x80, 0x98, 0x00, 0xAA, 0x00, 0x47, 0xBE, 0x5D } 
};

ITextDocument* BuffRichEdit::GetFixedTextDocumentInterface() const {
  ITextDocument* tdi{};
  IUnknown *tr_code = GetIRichEditOle();
  tr_code->QueryInterface(IID_ITextDocument, reinterpret_cast<void**>(&tdi));
  return tdi;
}

void BuffRichEdit::FreezeUI() {
  static constexpr auto UI_FREEZE_TIME_MS = 1000;
  if (textInterface != nullptr) {
    ++freezeCount;
    return;
  } else {
    textInterface = GetFixedTextDocumentInterface();

#pragma warning( push )
#pragma warning( disable : 6387 )
    // 2nd parameter of Undo can be nullptr according to documentation
    textInterface->Undo(tomSuspend, nullptr);
#pragma warning( pop )

    freezeUID = std::make_unique<long>(UI_FREEZE_TIME_MS);
    textInterface->Freeze(freezeUID.get());

    eventOptions = GetEventMask();
    SetEventMask(0);

    HideCaret();
    HideSelection(TRUE, FALSE);

    storedSelection = GetSelectionRange();
    freezeCount = 1;
  }
}

void BuffRichEdit::UnFreezeUI() {
  if (freezeUID == nullptr ||  textInterface == nullptr) {
    return;
  } else if (--freezeCount; freezeCount > 0) {
    return;
  } else {
    SelectRange(storedSelection);

    ShowCaret();
    HideSelection(FALSE, FALSE);

    SetEventMask(eventOptions);

    textInterface->Unfreeze(freezeUID.get());

#pragma warning( push )
#pragma warning( disable : 6387 )
    // 2nd parameter of Undo can be nullptr according to documentation
    textInterface->Undo(tomResume, nullptr);
#pragma warning( pop )

    textInterface->Release();

    textInterface = nullptr;
    freezeUID = nullptr;
    storedSelection = {};
    freezeCount = 0;
  }
}

void BuffRichEdit::UnFreezeUI(StrRange newSelection) {
  if (freezeCount == 1) {
    storedSelection = newSelection;
  }
  UnFreezeUI();
}

} // namespace xtr::ui