#include "stdafx.h"

#include "xtr/view/OSSLayout.h"

#include "resource.h"

#include "xtr/Exteor.h"
#include "xtr/cclDescriptor.h"
#include "xtr/xtrDesign.h"

#include "xtr/ui/UIHelper.h"

#include "xtr/view/OSSView.h"

namespace xtr::view {

using ccl::oss::Pict;
using ccl::oss::PictID;
using ccl::oss::PictPtr;
using ccl::oss::GridPosition;

static constexpr auto AREA_BORDER_SIZE = 2;
static constexpr auto  ICON_SIZE = 16;

static constexpr auto CELL_WIDTH = 135;
static constexpr auto CELL_HEIGHT = 135;
static constexpr auto PICT_WIDTH = 90;
static constexpr auto PICT_HEIGHT = 45;

OSSLayout::OSSLayout(const ccl::oss::OSSchema& oss) 
  : oss(&oss) {
  zoom = static_cast<float>(XTROptions::OSS().zoom);

  const auto metrics = ui::GlobalTextMetrics();
  charHeight = metrics.tmHeight;
  charWidth = metrics.tmMaxCharWidth;

  cellWidth = CELL_WIDTH;
  cellHeight = CELL_HEIGHT;

  pictWidth = PICT_WIDTH;
  pictHeight = PICT_HEIGHT;
}

void OSSLayout::InitImageList() {
  if (!icons.Create(ICON_SIZE, ICON_SIZE, ILC_MASK, 2, 2)) {
    ::AfxMessageBox(IXTRS_OSS_CANT_CREATE_ICON_LIST, MB_ICONERROR);
  }

  icons.Add(::AfxGetApp()->LoadIconW(IXTRR_PICT_BASIS));
  icons.Add(::AfxGetApp()->LoadIconW(IXTRR_PICT_UNDEFINED));
  icons.Add(::AfxGetApp()->LoadIconW(IXTRR_PICT_DEFINED));
  icons.Add(::AfxGetApp()->LoadIconW(IXTRR_PICT_DONE));
  icons.Add(::AfxGetApp()->LoadIconW(IXTRR_PICT_REDO));
  icons.Add(::AfxGetApp()->LoadIconW(IXTRR_PICT_BROKEN));
  icons.Add(::AfxGetApp()->LoadIconW(IXTRR_PICT_HASFILE));

  icons.SetBkColor(design::clrWhite);
}

int32_t OSSLayout::GetClientWidth() const {
  static constexpr double AREA_X_MIN = 4;
  static constexpr auto BORDER_GAP = 1.5;
  const auto visibleWidth = std::accumulate(std::begin(*oss), std::end(*oss), AREA_X_MIN,
    [&grid = oss->Grid()](double maxEff, const Pict& pict) {
    if (grid(pict.uid).has_value()) {
      return std::max(grid(pict.uid)->EffectiveColumn(), maxEff);
    } else {
      return maxEff;
    }
  });
  return static_cast<int32_t>((visibleWidth + BORDER_GAP) * cellWidth / zoom) + AREA_BORDER_SIZE;
}

int32_t OSSLayout::GetClientHeight() const {
  static constexpr auto AREA_Y_MIN = 4;
  const auto maxYCell = std::max(oss->Grid().CountRows(), AREA_Y_MIN);
  return static_cast<int32_t>(maxYCell * cellHeight / zoom) + AREA_BORDER_SIZE;
}

CRect OSSLayout::PictRect(const PictID pid, const float scale) const {
  if (scale <= 0) {
    return CRect{};
  }
  auto pos = oss->Grid()(pid);

  CPoint leftTop{};
  leftTop.x = static_cast<long>(cellWidth * scale * pos->EffectiveColumn());
  leftTop.y = static_cast<long>(pos->row * cellHeight * scale);

  SIZE size{};
  size.cx = static_cast<long>(pictWidth * scale);
  size.cy = static_cast<long>(pictHeight * scale);
  CRect result{ leftTop, size };

  CPoint offset{};
  offset.x = static_cast<long>((cellWidth - pictWidth) / 2.0F * scale);
  offset.y = static_cast<long>((cellHeight - pictHeight) / 2.0F * scale);
  result.OffsetRect(offset);
  return result;
}

CRect OSSLayout::CellRect(const GridPosition pos, const float scale) const noexcept {
  CPoint leftTop{};
  leftTop.x = static_cast<long>(cellWidth * scale * pos.EffectiveColumn());
  leftTop.y = static_cast<long>(pos.row * cellHeight * scale);

  SIZE size{};
  size.cx = static_cast<long>(cellWidth * scale);
  size.cy = static_cast<long>(cellHeight * scale);
  return CRect{ leftTop, size };
}

PictPtr OSSLayout::PictFromPoint(CPoint point) const {
  if (const auto cell = PositionFromPoint(point);  !cell.has_value()) {
    return nullptr;
  } else if (const auto pict = oss->Grid()(cell.value()); !pict.has_value()) {
    return nullptr;
  } else if (!PictRect(pict.value(), 1 / zoom).PtInRect(point)) {
    return nullptr;
  } else {
    return (*oss)(pict.value());
  }
}

std::optional<GridPosition> OSSLayout::PositionFromPoint(CPoint point) const noexcept {
  const auto rowApprox = zoom * point.y / cellHeight;
  assert(rowApprox >= 0);
  const auto columnApprox = zoom * point.x / cellWidth - rowApprox / 2.0;
  if (columnApprox < 0) {
    return std::nullopt;
  } else {
    GridPosition result{};
    result.row = static_cast<ccl::oss::GridIndex>(zoom * point.y / cellHeight);
    result.column = static_cast<ccl::oss::GridIndex>(columnApprox);
    return result;
  }
}

void OSSLayout::Draw(CDC* pDC, const std::optional<ccl::oss::PictID> active) {
  static constexpr float IMAGE_MOD_ZOOM = 1;
  const auto drawZoom = saveImage ? IMAGE_MOD_ZOOM : zoom;
  drawScale = 1 / drawZoom;
  drawDC = pDC;

  const auto clientArea = DetermineDrawArea();
  PrepareBuffer(pDC, clientArea);

  DrawBackground(clientArea);
  DrawGrid();
  for (const auto& pict : *oss) {
    DrawPictConnections(pict);
  }
  for (const auto& pict : *oss) {
    DrawPict(pict, active.has_value() && pict.uid == active.value());
  }
  DrawLegend();

  if (buffer == nullptr) {
    throw std::runtime_error("Layout buffer unexpected error!");
  } else {
    LoadFromBuffer(pDC);
  }
}

CImage OSSLayout::GetImage() {
  CBitmap bitmap{};
  if (!bitmap.CreateBitmap(1, 1, 1, 32, nullptr)) { // NOLINT
    TRACE("Cannot create bitmap");
  }
  auto* memDC = ::CreateCompatibleDC(nullptr);
  auto* cdc = CDC::FromHandle(memDC);
  cdc->SelectObject(bitmap);

  saveImage = TRUE;
  Draw(cdc);
  saveImage = FALSE;
  ::DeleteDC(memDC);

  CImage image{};
  image.Attach(buffer->bmp);  
  return image;
}

void OSSLayout::LoadFromBuffer(CDC* pDC) {
  pDC->SetViewportOrg(0, 0);
  pDC->SetWindowOrg(0, 0);
  pDC->SetMapMode(MM_TEXT);

  drawDC->SetViewportOrg(0, 0);
  drawDC->SetWindowOrg(0, 0);
  drawDC->SetMapMode(MM_TEXT);

  pDC->BitBlt(buffer->rect.left, buffer->rect.top, buffer->rect.Width(), buffer->rect.Height(), drawDC, 0, 0, SRCCOPY);
  drawDC->SelectObject(buffer->oldBitmap);
}

void OSSLayout::PrepareBuffer(CDC* pDC, const CRect clientArea) {
  static constexpr auto BRUSH_RADIX = 8;
  buffer = std::make_unique<Buffer>();
  buffer->rect = clientArea;
  pDC->LPtoDP(buffer->rect);
  buffer->rect.NormalizeRect();
  if (!buffer->context.CreateCompatibleDC(pDC)) {
    return;
  } else if (!buffer->bmp.CreateCompatibleBitmap(pDC, buffer->rect.Width(), buffer->rect.Height())) {
    return;
  } else {
    if (auto* view = dynamic_cast<CView*>(pDC->GetWindow()); view != nullptr) {
      view->OnPrepareDC(&buffer->context, nullptr);
    }
    drawDC = &buffer->context;
    buffer->context.OffsetViewportOrg(-buffer->rect.left, -buffer->rect.top);
    buffer->oldBitmap = buffer->context.SelectObject(&buffer->bmp);
    buffer->context.SetBrushOrg(buffer->rect.left % BRUSH_RADIX, buffer->rect.top % BRUSH_RADIX);
    buffer->context.IntersectClipRect(clientArea);
  }
}

CRect OSSLayout::DetermineDrawArea() {
  CRect clientArea{};
  if (!saveImage) {
    drawDC->GetClipBox(clientArea);
  } else {
    clientArea = CRect{ CPoint{ 0, 0 },
                        CSize{ static_cast<int32_t>(GetClientWidth() * zoom * drawScale),
                             static_cast<int32_t>(GetClientHeight() * zoom * drawScale) } };
  }
  return clientArea;
}

void OSSLayout::DrawBackground(CRect bgRect) {
  if (CBrush brush{}; brush.CreateSolidBrush(design::clrWhite)) {
    brush.UnrealizeObject();
    drawDC->FillRect(bgRect, &brush);
  }
}

void OSSLayout::DrawGrid() {
  if (XTROptions::OSS().showGrid) {
    auto sizeX = 0;
    const auto sizeY = oss->Grid().CountRows();
    for (auto row = 0; row < sizeY; ++row) {
      sizeX = std::max(sizeX == 0 ? 0 : sizeX - 1, oss->Grid().MaxColumnIn(row));
      for (auto column = 0; column <= sizeX; ++column) {
        DrawCell({ row, column });
      }
    }
  }
}

void OSSLayout::DrawCell(GridPosition pos) {
  static constexpr auto GRID_THICKNESS = 1;
  static constexpr auto GRID_LINESTYLE = PS_DASH;
  if (CPen pen{}; pen.CreatePen(GRID_LINESTYLE, GRID_THICKNESS, design::clrGray200)) {
    auto* oldPen = drawDC->SelectObject(&pen);

    auto cellRect = CellRect(pos, drawScale);
    ++cellRect.right; // Note: grid rectangles overlap to form single width grid lines
    ++cellRect.bottom;
    drawDC->Rectangle(cellRect);

    drawDC->SelectObject(oldPen);
  }
}

void OSSLayout::DrawPictConnections(const Pict& pict) {
  const auto rect = PictRect(pict.uid, drawScale);
  const auto parents = oss->Graph().ParentsOf(pict.uid);
  for (const auto parent : parents) {
    const auto rectParent = PictRect(parent, drawScale);
    drawDC->MoveTo(static_cast<int32_t>(rectParent.CenterPoint().x),
                   static_cast<int32_t>(rectParent.bottom));
    drawDC->LineTo(rect.CenterPoint().x, rect.top);
  }
}

void OSSLayout::DrawPict(const Pict& pict, bool isActive) {
  const auto oldClrTxt = drawDC->GetTextColor();
  const auto oldClrTxtBg = drawDC->GetBkColor();

  const auto rect = PictRect(pict.uid, drawScale);
  if (isActive) {
    DrawActiveRect(rect);
  } else {
    DrawInactiveRect(rect);
  }
  DrawPictStatus(rect, pict.uid);
  DrawPictText(rect, pict);

  drawDC->SetTextColor(oldClrTxt);
  drawDC->SetBkColor(oldClrTxtBg);
}

void OSSLayout::DrawActiveRect(CRect rect) {
  static constexpr auto ACTIVE_PICT_THICKNESS = 2;

  if (CPen pen{}; pen.CreatePen(PS_SOLID, ACTIVE_PICT_THICKNESS, design::clrRed)) {
    auto* oldPen = drawDC->SelectObject(&pen);
    drawDC->Rectangle(rect);
    drawDC->SelectObject(oldPen);
  }
}

void OSSLayout::DrawInactiveRect(CRect rect) {
  static constexpr auto INACTIVE_PICT_THICKNESS = 1;
  if (CPen pen{}; pen.CreatePen(PS_SOLID, INACTIVE_PICT_THICKNESS, design::clrBlack)) {
    auto* oldPen = drawDC->SelectObject(&pen);
    drawDC->Rectangle(rect);
    drawDC->SelectObject(oldPen);
  }
}

void OSSLayout::DrawPictStatus(CRect rect, const PictID pid) {
  static constexpr auto ICON_OFFSET_X = 3;
  static constexpr auto ICON_OFFSET_Y = 5;
  if (drawScale >= 1) {
    CPoint iconShift{};
    iconShift.x = ICON_OFFSET_X;
    iconShift.y = charHeight + ICON_OFFSET_Y;
    DrawStatusIcon(rect.TopLeft() + iconShift, pid);
  }
}

void OSSLayout::DrawStatusIcon(CPoint topLeft, const PictID pid) const {
  static constexpr auto ICON_SHIFT = 50;
  const auto iconID = GetIconID(pid);
  icons.Draw(drawDC, static_cast<int>(iconID), topLeft, ILD_TRANSPARENT);
  if (!std::empty(*oss->Src()(pid)) && !saveImage) {
    topLeft.x = topLeft.x + ICON_SIZE + ICON_SHIFT;
    icons.Draw(drawDC, static_cast<int>(IconID::OSSI_PICT_HASFILE), topLeft, ILD_TRANSPARENT);
  }
}

void OSSLayout::DrawPictText(CRect rect, const Pict& pict) {
  static constexpr auto TEXT_OFFSET_X = 1;
  static constexpr auto TEXT_OFFSET_Y = 1;
  
  const CString text = [&alias = pict.alias] {
    if (!std::empty(alias)) {
      return mfc::ToMFC(alias);
    } else {
      return CString{ LR"(N/A)" };
    }
  }();
  
  drawDC->SetTextColor(design::clrBlack);
  if (saveImage) {
    drawDC->SetBkColor(design::clrWhite);
  } else {
    drawDC->SetBkColor(design::BackgroundColor(*oss, pict.uid));
  }
  drawDC->ExtTextOutW(rect.left + TEXT_OFFSET_X, rect.top + TEXT_OFFSET_Y, ETO_CLIPPED, rect, text, nullptr);
}

void OSSLayout::DrawLegend() {
  if (drawScale < 1 || !XTROptions::OSS().showLegend) {
    return;
  }
  const auto bottom = GetClientHeight() - AREA_BORDER_SIZE;
  const auto top = bottom - 2 * cellHeight;

  drawDC->Rectangle(0, bottom, cellWidth, top);
  legendYBase = top + 2;

  DrawLegendItem(IconID::OSSI_PICT_BASIS, mfc::LoadSID(IXTRS_DRAW_BASE));
  DrawLegendItem(IconID::OSSI_PICT_UNDEFINED, info::DrawableLabel(ccl::ops::Status::undefined));
  DrawLegendItem(IconID::OSSI_PICT_DEFINED, info::DrawableLabel(ccl::ops::Status::defined));
  DrawLegendItem(IconID::OSSI_PICT_DONE, info::DrawableLabel(ccl::ops::Status::done));
  DrawLegendItem(IconID::OSSI_PICT_REDO, info::DrawableLabel(ccl::ops::Status::outdated));
  DrawLegendItem(IconID::OSSI_PICT_BROKEN, info::DrawableLabel(ccl::ops::Status::broken));
  DrawLegendItem(IconID::OSSI_PICT_HASFILE, mfc::LoadSID(IXTRS_DRAW_HAS_FILE));
}

void OSSLayout::DrawLegendItem(const IconID icon, const CString text) {
  static constexpr auto LEGEND_SHIFT = 5;
  static constexpr auto TEXT_POS = ICON_SIZE + 2 * LEGEND_SHIFT;
  static constexpr auto TEXT_OFFSET = 20;

  const auto yPos = legendYBase +  static_cast<int>(icon) * (2 * charHeight + LEGEND_SHIFT);
  CRect textRect{ TEXT_POS, yPos, cellWidth - TEXT_POS + TEXT_OFFSET, yPos + 2 * charHeight };
  icons.Draw(drawDC, static_cast<int>(icon), { LEGEND_SHIFT, yPos + charHeight / 2 }, ILD_TRANSPARENT);
  drawDC->DrawTextW(text, textRect, 0);
}

OSSLayout::IconID OSSLayout::GetIconID(const PictID pid) const {
  if (!oss->Ops().HasOperation(pid)) {
    return IconID::OSSI_PICT_BASIS;
  } else {
    switch (oss->Ops().StatusOf(pid)) {
    default: return IconID::OSSI_PICT_BASIS;
    case ccl::ops::Status::undefined: return IconID::OSSI_PICT_UNDEFINED;
    case ccl::ops::Status::defined: return IconID::OSSI_PICT_DEFINED;
    case ccl::ops::Status::done: return IconID::OSSI_PICT_DONE;
    case ccl::ops::Status::outdated: return IconID::OSSI_PICT_REDO;
    case ccl::ops::Status::broken: return IconID::OSSI_PICT_BROKEN;
    }
  }
}

} // namespace xtr::view