

#include "meshifypopup.h"

// Toonz includes
#include "tapp.h"
#include "cellselection.h"
#include "columnselection.h"
#include "selectionutils.h"

// TnzQt includes
#include "toonzqt/tselectionhandle.h"
#include "toonzqt/menubarcommand.h"
#include "toonzqt/doublefield.h"
#include "toonzqt/planeviewer.h"
#include "toonzqt/icongenerator.h"
#include "toonzqt/dvdialog.h"

// TnzLib includes
#include "toonz/toonzscene.h"
#include "toonz/txsheet.h"
#include "toonz/txshcell.h"
#include "toonz/levelproperties.h"
#include "toonz/txshleveltypes.h"
#include "toonz/txshsimplelevel.h"
#include "toonz/txshchildlevel.h"
#include "toonz/txshlevelcolumn.h"
#include "toonz/txshmeshcolumn.h"
#include "toonz/txshlevelhandle.h"
#include "toonz/tframehandle.h"
#include "toonz/tcolumnhandle.h"
#include "toonz/txsheethandle.h"
#include "toonz/tscenehandle.h"
#include "toonz/stage.h"
#include "toonz/namebuilder.h"
#include "toonz/levelset.h"
#include "toonz/tstageobject.h"
#include "toonz/tstageobjecttree.h"
#include "toonz/stagevisitor.h"

// TnzExt includes
#include "ext/meshbuilder.h"
#include "ext/meshutils.h"

// TnzCore includes
#include "trasterimage.h"
#include "ttoonzimage.h"
#include "tvectorimage.h"
#include "tofflinegl.h"
#include "tvectorrenderdata.h"
#include "tmsgcore.h"
#include "tsystem.h"
#include "tundo.h"

// Qt includes
#include <QStringList>
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
#include <QScrollArea>
#include <QSplitter>
#include <QShowEvent>
#include <QHideEvent>
#include <QMainWindow>

// Tunable parameters

#define RENDERED_IMAGES_MAX_LATERAL_RESOLUTION 2048
#define MESH_TARGET_MAX_VERTICES_COUNT 1000

//********************************************************************************************
//    Local classes
//********************************************************************************************

namespace {

struct MeshifyOptions  //!  Available options for the meshification process.
{
  double m_edgesLength;  //!< The desired mesh edges length, in standard world
                         //! coordinates
  double m_rasterizationDpi;  //!< Dpi value used to render PLIs and sub-xsheets
  int m_margin;               //!< Margin to the original shape (in pixels)
};

}  // namespace

//********************************************************************************************
//    Local functions
//********************************************************************************************

namespace {

TRaster32P render(const TVectorImageP &vi, double &rasDpi, int margin,
                  TPointD &worldOriginPos) {
  // Extract vi's bbox
  TRectD bboxD(vi->getBBox());
  if (bboxD.x0 >= bboxD.x1 || bboxD.y0 >= bboxD.y1) return TRaster32P();

  // Build the scale factor from scene's to specified dpi
  double scale = rasDpi / Stage::inch;

  // Ensure that the maximum lateral resolution is respected
  if (scale * bboxD.getLx() > RENDERED_IMAGES_MAX_LATERAL_RESOLUTION ||
      scale * bboxD.getLy() > RENDERED_IMAGES_MAX_LATERAL_RESOLUTION) {
    scale = std::min(RENDERED_IMAGES_MAX_LATERAL_RESOLUTION / bboxD.getLx(),
                     RENDERED_IMAGES_MAX_LATERAL_RESOLUTION / bboxD.getLy());
  }

  bboxD = TScale(scale) * bboxD;

  // Enlarge by margin+1 pix. This is necessary since:
  //  1. We'd better be sure about drawing. This accounts for 1-pix enlargement
  //  2. A margin enlargement is enforced by the MeshBuilder.
  bboxD = bboxD.enlarge(margin + 1);

  TRect bbox(tfloor(bboxD.x0), tfloor(bboxD.y0), tceil(bboxD.x1) - 1,
             tceil(bboxD.y1) - 1);

  worldOriginPos = -convert(bbox.getP00());
  rasDpi         = scale * Stage::inch;

  // Initialize a corresponding OpenGL context
  std::unique_ptr<TOfflineGL> offlineGlContext(new TOfflineGL(bbox.getSize()));
  offlineGlContext->makeCurrent();

  // Draw the image
  TVectorRenderData rd(TTranslation(worldOriginPos) * TScale(scale),
                       TRect(bbox.getSize()), vi->getPalette(), 0, true, true);
  offlineGlContext->draw(vi, rd, false);

  return offlineGlContext->getRaster();
}

//-----------------------------------------------------------------------

TRaster32P render(const TXsheet *xsh, int row, double &rasDpi, int margin,
                  TPointD &worldOriginPos) {
  // Extract xsh's bbox
  TRectD bbox(xsh->getBBox(row));
  if (bbox.x0 >= bbox.x1 || bbox.y0 >= bbox.y1) return TRaster32P();

  // Since xsh represents a sub-xsheet, its camera affine must be applied
  const TAffine &cameraAff =
      xsh->getPlacement(xsh->getStageObjectTree()->getCurrentCameraId(), row);
  bbox = cameraAff.inv() * bbox;

  // Build the scale factor from scene's to specified dpi
  double scale = rasDpi / Stage::inch;

  // Ensure that the maximum lateral resolution is respected
  if (scale * bbox.getLx() > RENDERED_IMAGES_MAX_LATERAL_RESOLUTION ||
      scale * bbox.getLy() > RENDERED_IMAGES_MAX_LATERAL_RESOLUTION) {
    scale = std::min(RENDERED_IMAGES_MAX_LATERAL_RESOLUTION / bbox.getLx(),
                     RENDERED_IMAGES_MAX_LATERAL_RESOLUTION / bbox.getLy());
  }

  bbox = TScale(scale) * bbox;

  // Enlarge by margin+1 pix. This is necessary since:
  //  1. We'd better be sure about drawing. This accounts for 1-pix enlargement
  //  2. A margin enlargement is enforced by the MeshBuilder.
  bbox = bbox.enlarge(margin + 1);

  bbox =
      TRectD(tfloor(bbox.x0), tfloor(bbox.y0), tceil(bbox.x1), tceil(bbox.y1));

  worldOriginPos = -bbox.getP00();
  rasDpi         = scale * Stage::inch;

  // Draw the xsheet
  TRaster32P ras(tround(bbox.getLx()), tround(bbox.getLy()));

  ToonzScene *scene = TApp::instance()->getCurrentScene()->getScene();
  scene->renderFrame(ras, row, xsh, bbox, TScale(scale));

  return ras;
}

//-----------------------------------------------------------------------

inline void xshPos(int &r, int &c) {
  TApp *app = TApp::instance();

  r = app->getCurrentFrame()->getFrame();
  c = app->getCurrentColumn()->getColumnIndex();
}

//-----------------------------------------------------------------------

inline const TXshCell &xshCell() {
  TApp *app = TApp::instance();
  return app->getCurrentXsheet()->getXsheet()->getCell(
      app->getCurrentFrame()->getFrame(),
      app->getCurrentColumn()->getColumnIndex());
}

//-----------------------------------------------------------------------

// Creates a suitable mesh level from a given one. Observe that the output
// mesh level has "levelName..mesh" format.
TXshSimpleLevel *createMeshLevel(TXshLevel *texturesLevel) {
  struct locals {
    static void copyLevelProperties(TXshLevel *src, TXshSimpleLevel *dstSl) {
      LevelProperties *dstProp = dstSl->getProperties();
      dstProp->setDpiPolicy(LevelProperties::DP_ImageDpi);
      dstProp->setDpi(TPointD(Stage::inch, Stage::inch));

      TXshSimpleLevel *srcSl = dynamic_cast<TXshSimpleLevel *>(src);
      if (srcSl && srcSl->getType() != PLI_XSHLEVEL) {
        LevelProperties *srcProp = srcSl->getProperties();

        // Assign the original dpi ONLY if it proves to be not null
        const TPointD &srcDpi = srcProp->getDpi();
        if (srcDpi.x != 0.0 && srcDpi.y != 0.0) {
          dstProp->setDpiPolicy(srcProp->getDpiPolicy());
          dstProp->setDpi(srcProp->getDpi());
        }
      }
    }
  };  // locals

  TXshSimpleLevel *result = 0;

  ToonzScene *scene = TApp::instance()->getCurrentScene()->getScene();

  // Build a suitable name (avoid duplicate names from the xsheet)
  std::wstring levelName = texturesLevel->getName() + L"_mesh";
  {
    NameBuilder *nameBuilder = NameBuilder::getBuilder(levelName);

    while (scene->getLevelSet()->getLevel(levelName))
      levelName = nameBuilder->getNext();

    delete nameBuilder;
  }

  // Build a suitable path (avoid duplicate names from the file system AND
  // scene's level set)
  TFilePath origPath, dstPath, codedOrigPath, codedDstPath;
  TXshSimpleLevel *origSl = 0;

  {
    std::wstring pathName = texturesLevel->getName();

    codedOrigPath = texturesLevel->getPath();
    if (dynamic_cast<TXshChildLevel *>(
            texturesLevel))  // Sub-xsheets do not have a suitable
      codedOrigPath = TFilePath(
          "+drawings/a");  // parent directory. Store them in "+drawings".

    // temporarily disable underscore in order to get proper file path when the
    // level name includes underscore like "sub_1"
    TFilePath::setUnderscoreFormatAllowed(false);
    codedOrigPath = codedDstPath =
        codedOrigPath.withName(pathName).withType("mesh").withFrame(
            TFrameId::EMPTY_FRAME);
    TFilePath::setUnderscoreFormatAllowed(true);

    origPath = dstPath = scene->decodeFilePath(codedOrigPath);

    {
      NameBuilder *nameBuilder = NameBuilder::getBuilder(pathName);

      while (TSystem::doesExistFileOrLevel(dstPath) ||
             scene->getLevelSet()->hasLevel(*scene, codedDstPath)) {
        pathName = nameBuilder->getNext();
        TFilePath::setUnderscoreFormatAllowed(false);
        codedDstPath = origPath.withName(pathName).withType("mesh").withFrame(
            TFrameId::EMPTY_FRAME);
        TFilePath::setUnderscoreFormatAllowed(true);
        dstPath = scene->decodeFilePath(codedDstPath);
      }

      delete nameBuilder;
    }

    origSl = dynamic_cast<TXshSimpleLevel *>(
        scene->getLevelSet()->getLevel(*scene, codedOrigPath));
  }

  // In case the preferred mesh path is already occupied, ask the user what to
  // do
  if (codedOrigPath != codedDstPath) {
    // Prompt for an answer
    enum { CANCEL = 0, DELETE_OLD, OVERWRITE_OLD, KEEP_OLD };

    int answer = -1;
    {
      QString question(
          MeshifyPopup::tr("A level with the preferred path \"%1\" already "
                           "exists.\nWhat do you want to do?")
              .arg(QString::fromStdWString(codedOrigPath.getWideString())));

      QStringList optionsList;
      optionsList << MeshifyPopup::tr("Delete the old level entirely")
                  << MeshifyPopup::tr(
                         "Keep the old level and overwrite processed frames")
                  << MeshifyPopup::tr("Choose a different path (%1)")
                         .arg(QString::fromStdWString(
                             codedDstPath.getWideString()));

      answer = DVGui::RadioButtonMsgBox(DVGui::QUESTION, question, optionsList,
                                        TApp::instance()->getMainWindow());
    }

    // Apply decision
    switch (answer) {
    case CANCEL:
      return result;

    case DELETE_OLD:
      // Remove the level on disk
      TSystem::removeFileOrLevel(origPath);
      if (origSl) {
        removeIcons(origSl, origSl->getFids());
        origSl->clearFrames();
        origSl->setDirtyFlag(true);

        locals::copyLevelProperties(texturesLevel, origSl);
      }

      codedDstPath = codedOrigPath;
      dstPath      = origPath;
      break;

    case OVERWRITE_OLD:
      if (origSl) {
        removeIcons(origSl, origSl->getFids());  // Invalidate the levels' icons
        origSl->setDirtyFlag(true);
      }

      codedDstPath = codedOrigPath;
      dstPath      = origPath;
      break;
    }
  }

  // Build the TXshSimpleLevel instance
  if (codedDstPath == codedOrigPath && origSl) {
    // Return an already existing level
    result = origSl;
    assert(result);
  } else {
    // Create a new level
    result = dynamic_cast<TXshSimpleLevel *>(
        scene->createNewLevel(MESH_XSHLEVEL, levelName));
    assert(result);

    result->setPath(codedDstPath);

    locals::copyLevelProperties(texturesLevel, result);
  }

  return result;
}

//-----------------------------------------------------------------------

void makeMeshColumn(TXsheet &xsh, int texColId, int meshColIdx) {
  TXshMeshColumn *meshColumn = new TXshMeshColumn;
  xsh.insertColumn(meshColIdx, meshColumn);

  TStageObject *colObj = xsh.getStageObject(TStageObjectId::ColumnId(texColId));
  TStageObject *meshColObj =
      xsh.getStageObject(TStageObjectId::ColumnId(meshColIdx));

  const std::string &meshColName = colObj->getName() + "_mesh";

  meshColObj->setParent(colObj->getParent());
  meshColObj->setParentHandle(colObj->getParentHandle());
  meshColObj->setName(meshColName);

  colObj->setParent(TStageObjectId::ColumnId(meshColIdx));
}

//-----------------------------------------------------------------------

TStageObjectId firstChildLevelColumn(const TXsheet &xsh,
                                     const TStageObject &obj) {
  TStageObjectId result;

  const std::list<TStageObject *> &children = obj.getChildren();

  std::list<TStageObject *>::const_iterator ct, cEnd(children.end());
  for (ct = children.begin(); ct != cEnd; ++ct) {
    const TStageObjectId &id = (*ct)->getId();
    if (!id.isColumn() ||
        xsh.getColumn(id.getIndex())->getColumnType() != TXshColumn::eLevelType)
      continue;

    if (!result.isColumn() || id < result) result = id;
  }

  return result;
}

//-----------------------------------------------------------------------

TXshLevel *levelToMeshify() {
  TXshLevel *result = 0;

  int r, c;
  ::xshPos(r, c);

  TXsheet *xsh = TApp::instance()->getCurrentXsheet()->getXsheet();

  TXshColumn *col = xsh->getColumn(c);
  if (col) {
    TXshMeshColumn *meshCol = col->getMeshColumn();
    if (meshCol) {
      TStageObject *meshObj = xsh->getStageObject(TStageObjectId::ColumnId(c));

      // Retrieve the mesh cell
      const TXshCell &cell = xsh->getCell(r, c);

      TXshSimpleLevel *ml = cell.getSimpleLevel();
      if (ml && ml->getType() == MESH_XSHLEVEL) {
        // Retrieve the associated texture
        const TStageObjectId &childId = firstChildLevelColumn(*xsh, *meshObj);

        const TXshCell &texCell = xsh->getCell(r, childId.getIndex());

        TXshLevel *xl = texCell.m_level.getPointer();
        if (xl &&
            (xl->getType() & LEVELCOLUMN_XSHLEVEL))  // Includes sub-xsheet type
          result = xl;
      }
    } else {
      const TXshCell &cell = xsh->getCell(r, c);

      TXshLevel *xl = cell.m_level.getPointer();
      if (xl && (xl->getType() & LEVELCOLUMN_XSHLEVEL)) result = xl;
    }
  }

  return result;
}

//-----------------------------------------------------------------------

TMeshImageP meshify(const TXshCell &cell, const MeshifyOptions &options);
TMeshImageP meshify(const TRasterP &ras, const TPointD &rasDpi,
                    const TPointD &worldOriginPos, const TPointD &meshDpi,
                    const MeshBuilderOptions &options);

bool meshifySelection(const MeshifyOptions &options);

}  // namespace

//********************************************************************************************
//    Cell to image locals
//********************************************************************************************

namespace {

void getRaster(const TImageP &img, TPointD &imgDpi, TRasterP &ras,
               TPointD &rasDpi, TPointD &worldOriginPos, int margin) {
  TRasterImageP ri(img);
  if (ri) {
    ras = ri->getRaster();

    worldOriginPos = ras->getCenterD();

    ri->getDpi(imgDpi.x, imgDpi.y);
    rasDpi = imgDpi;
  }

  TToonzImageP ti(img);
  if (ti) {
    ras = ti->getRaster();

    worldOriginPos = ras->getCenterD();

    ti->getDpi(imgDpi.x, imgDpi.y);
    rasDpi = imgDpi;
  }

  TVectorImageP vi(img);
  if (vi) {
    ras      = render(vi, rasDpi.x, margin, worldOriginPos);
    rasDpi.y = rasDpi.x;
    imgDpi   = TPointD(Stage::inch, Stage::inch);
  }
}

//-----------------------------------------------------------------------

void getRaster(const TXsheet *xsh, int row, TRasterP &ras, TPointD &rasDpi,
               TPointD &worldOriginPos, int margin) {
  ras      = render(xsh, row, rasDpi.x, margin, worldOriginPos);
  rasDpi.y = rasDpi.x;
}

}  // namespace

//********************************************************************************************
//    MeshifyPopup::Swatch  definition
//********************************************************************************************

class MeshifyPopup::Swatch final : public PlaneViewer {
public:
  TImageP m_img;  //!< The eventual image to be meshified

  TXsheetP m_xsh;  //!< The eventual sub-xsheet to be meshified...
  int m_row;       //!< ... at this row (frame)

  TMeshImageP m_meshImg;  //!< The mesh image preview

public:
  Swatch(QWidget *parent = 0) : PlaneViewer(parent), m_row(-1) {}

  void clear() {
    m_img = TImageP(), m_xsh = TXsheetP(), m_row = -1,
    m_meshImg = TMeshImageP();
  }

  void paintGL() override {
    drawBackground();

    // Draw original
    if (m_img) {
      pushGLWorldCoordinates();

      glEnable(GL_BLEND);
      glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

      // Note GL_ONE instead of GL_SRC_ALPHA: it's needed since the input
      // image is supposedly premultiplied - and it works because the
      // viewer's background is opaque.
      // See tpixelutils.h's overPixT function for comparison.

      draw(m_img);

      glDisable(GL_BLEND);

      popGLCoordinates();
    } else if (m_xsh) {
      // Build reference change affines

      // EXPLANATION: RasterPainter receives an affine specifying the reference
      // change
      // from world coordinates to the OpenGL viewport, where (0,0) corresponds
      // to the
      // viewport center.

      const TAffine &cameraAff = m_xsh->getPlacement(
          m_xsh->getStageObjectTree()->getCurrentCameraId(), m_row);

      TTranslation centeredWidgetToWidgetAff(0.5 * width(), 0.5 * height());
      const TAffine &worldToCenteredWigetAff =
          centeredWidgetToWidgetAff.inv() * viewAff() * cameraAff.inv();

      glPushMatrix();
      tglMultMatrix(centeredWidgetToWidgetAff);

      assert(m_row >= 0);

      ImagePainter::VisualSettings vs;

      TDimension viewerSize(width(), height());
      Stage::RasterPainter painter(viewerSize, worldToCenteredWigetAff,
                                   TRect(viewerSize), vs, false);

      ToonzScene *scene = TApp::instance()->getCurrentScene()->getScene();
      Stage::visit(painter, scene, m_xsh.getPointer(), m_row);

      painter.flushRasterImages();
      glFlush();

      glPopMatrix();
    }

    // Draw mesh preview
    if (m_meshImg) {
      glEnable(GL_BLEND);
      glEnable(GL_LINE_SMOOTH);

      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

      // Retrieve mesh dpi
      TPointD meshDpi;
      m_meshImg->getDpi(meshDpi.x, meshDpi.y);

      // Push mesh-to-world coordinates change
      pushGLWorldCoordinates();
      glScaled(Stage::inch / meshDpi.x, Stage::inch / meshDpi.y, 1.0);

      glColor4f(0.0, 1.0, 0.0, 0.7);  // Translucent green
      tglDrawEdges(*m_meshImg);

      popGLCoordinates();

      glDisable(GL_LINE_SMOOTH);
      glDisable(GL_BLEND);
    }
  }
};

//********************************************************************************************
//    Meshify Popup  implementation
//********************************************************************************************

MeshifyPopup::MeshifyPopup()
    : DVGui::Dialog(TApp::instance()->getMainWindow(), true, false,
                    "MeshifyPopup")
    , m_r(-1)
    , m_c(-1) {
  setWindowTitle(tr("Create Mesh"));
  setLabelWidth(0);
  setModal(false);

  setTopMargin(0);
  setTopSpacing(0);

  beginVLayout();

  QSplitter *splitter = new QSplitter(Qt::Vertical);
  splitter->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding,
                                      QSizePolicy::MinimumExpanding));
  addWidget(splitter);

  endVLayout();

  //------------------------- Top Layout --------------------------

  QScrollArea *scrollArea = new QScrollArea(splitter);
  scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  scrollArea->setWidgetResizable(true);
  scrollArea->setMinimumWidth(450);
  splitter->addWidget(scrollArea);
  splitter->setStretchFactor(0, 1);

  QFrame *topWidget = new QFrame(scrollArea);
  scrollArea->setWidget(topWidget);

  QGridLayout *topLayout =
      new QGridLayout(topWidget);  // Needed to justify at top
  topWidget->setLayout(topLayout);

  //------------------------- Parameters --------------------------

  int row = 0;

  QLabel *edgesLengthLabel = new QLabel(tr("Mesh Edges Length:"));
  topLayout->addWidget(edgesLengthLabel, row, 0,
                       Qt::AlignRight | Qt::AlignVCenter);

  m_edgesLength = new DVGui::MeasuredDoubleField;
  m_edgesLength->setMeasure("length.x");
  m_edgesLength->setRange(0.0, 1.0);  // In inches (standard unit)
  m_edgesLength->setValue(0.2);

  topLayout->addWidget(m_edgesLength, row++, 1);

  QLabel *rasterizationDpiLabel = new QLabel(tr("Rasterization DPI:"));
  topLayout->addWidget(rasterizationDpiLabel, row, 0,
                       Qt::AlignRight | Qt::AlignVCenter);

  m_rasterizationDpi = new DVGui::DoubleLineEdit;
  m_rasterizationDpi->setRange(1.0, (std::numeric_limits<double>::max)());
  m_rasterizationDpi->setValue(300.0);

  topLayout->addWidget(m_rasterizationDpi, row++, 1);

  QLabel *shapeMarginLabel = new QLabel(tr("Mesh Margin (pixels):"));
  topLayout->addWidget(shapeMarginLabel, row, 0,
                       Qt::AlignRight | Qt::AlignVCenter);

  m_margin = new DVGui::IntLineEdit;
  m_margin->setRange(2, (std::numeric_limits<int>::max)());
  m_margin->setValue(5);

  topLayout->addWidget(m_margin, row++, 1);

  connect(m_edgesLength, SIGNAL(valueChanged(bool)), this,
          SLOT(onParamsChanged(bool)));
  connect(m_rasterizationDpi, SIGNAL(editingFinished()), this,
          SLOT(onParamsChanged()));
  connect(m_margin, SIGNAL(editingFinished()), this, SLOT(onParamsChanged()));

  topLayout->setRowStretch(row, 1);

  //------------------------- View Widget -------------------------

  // NOTE: It's IMPORTANT that parent widget is supplied. It's somewhat
  // used by QSplitter to decide the initial widget sizes...

  m_viewer = new Swatch(splitter);
  m_viewer->setMinimumHeight(150);
  m_viewer->setFocusPolicy(Qt::WheelFocus);

  splitter->addWidget(m_viewer);

  //--------------------------- Buttons ---------------------------

  m_okBtn = new QPushButton(tr("Apply"));
  addButtonBarWidget(m_okBtn);

  connect(m_okBtn, SIGNAL(clicked()), this, SLOT(apply()));

  // Finally, acquire current selection
  onCellSwitched();

  m_viewer->resize(600, 350);
  resize(600, 700);
}

//------------------------------------------------------------------------------

void MeshifyPopup::showEvent(QShowEvent *se) {
  TFrameHandle *frameHandle   = TApp::instance()->getCurrentFrame();
  TColumnHandle *columnHandle = TApp::instance()->getCurrentColumn();

  connect(frameHandle, SIGNAL(frameSwitched()), this, SLOT(onCellSwitched()));
  connect(columnHandle, SIGNAL(columnIndexSwitched()), this,
          SLOT(onCellSwitched()));

  onCellSwitched();
}

//------------------------------------------------------------------------------

void MeshifyPopup::hideEvent(QHideEvent *he) {
  Dialog::hideEvent(he);

  TFrameHandle *frameHandle   = TApp::instance()->getCurrentFrame();
  TColumnHandle *columnHandle = TApp::instance()->getCurrentColumn();

  disconnect(frameHandle, SIGNAL(frameSwitched()), this,
             SLOT(onCellSwitched()));
  disconnect(columnHandle, SIGNAL(columnIndexSwitched()), this,
             SLOT(onCellSwitched()));

  m_viewer->clear();
  m_r = -1, m_c = -1, m_cell = TXshCell();
}

//------------------------------------------------------------------------------

void MeshifyPopup::acquirePreview() {
  m_viewer->clear();

  // Assign preview input to the viewer
  bool enabled = false;

  ::xshPos(m_r, m_c);
  m_cell = TApp::instance()->getCurrentXsheet()->getXsheet()->getCell(m_r, m_c);

  // Redirect mesh case to texture
  TXshSimpleLevel *sl = m_cell.getSimpleLevel();
  if (sl && sl->getType() == MESH_XSHLEVEL) {
    // Mesh image case
    TXsheet *xsh          = TApp::instance()->getCurrentXsheet()->getXsheet();
    TStageObject *meshObj = xsh->getStageObject(TStageObjectId::ColumnId(m_c));

    const TStageObjectId &childId = ::firstChildLevelColumn(*xsh, *meshObj);
    if (childId.isColumn())
      // Retrieved the associated texture cell - redirect acquisition there
      m_cell = xsh->getCell(m_r, childId.getIndex());
  }

  if ((sl = m_cell.getSimpleLevel())) {
    // Standard image case
    m_viewer->m_img = sl->getFullsampledFrame(m_cell.getFrameId(),
                                              ImageManager::dontPutInCache);

    enabled = true;
  } else if (TXshChildLevel *cl = m_cell.getChildLevel()) {
    // Sub-xsheet case
    TXsheet *xsh = cl->getXsheet();
    int row      = m_cell.getFrameId().getNumber() - 1;

    m_viewer->m_xsh = xsh, m_viewer->m_row = row;

    enabled = true;
  }

  m_okBtn->setEnabled(enabled);

  // Update the corresponding processed image in the viewer
  updateMeshPreview();
}

//------------------------------------------------------------------------------

void MeshifyPopup::updateMeshPreview() {
  // Meshify
  MeshifyOptions options = {m_edgesLength->getValue(),
                            m_rasterizationDpi->getValue(),
                            m_margin->getValue()};
  TMeshImageP meshImg(::meshify(m_cell, options));

  // Update the swatch
  m_viewer->m_meshImg = meshImg;
  m_viewer->update();
}

//------------------------------------------------------------------------------

void MeshifyPopup::onCellSwitched() {
  // In case current cell level is not of the suitable type, disable the
  // rasterization parameter
  {
    TXshLevel *level = ::levelToMeshify();

    int type = level ? level->getType() : UNKNOWN_XSHLEVEL;
    m_rasterizationDpi->setEnabled(type == PLI_XSHLEVEL ||
                                   type == CHILD_XSHLEVEL);
  }

  acquirePreview();
}

//------------------------------------------------------------------------------

void MeshifyPopup::onParamsChanged(bool dragging) {
  if (dragging) return;

  updateMeshPreview();
}

//------------------------------------------------------------------------------

void MeshifyPopup::apply() {
  MeshifyOptions options = {m_edgesLength->getValue(),
                            m_rasterizationDpi->getValue(),
                            m_margin->getValue()};

  if (meshifySelection(options))  // Meshify invocation
  {
    TUndoManager::manager()
        ->reset();  // Since this operation CURRENTLY HAS NO UNDO, and yet
    close();        // it modifies the xsheet, it CLEARS THE UNDOS stack.
  }
}

//********************************************************************************************
//    Meshifying  functions
//********************************************************************************************

namespace {

TMeshImageP meshify(const TXshCell &cell, const MeshifyOptions &options) {
  struct locals {
    static inline void checkEmptyDpi(TPointD &dpi) {
      if (dpi.x == 0.0 || dpi.y == 0.0) dpi.x = dpi.y = Stage::inch;
    }
  };  // locals

  TRasterP ras;
  TPointD worldOriginPos;
  TPointD imageDpi, rasDpi, slDpi, shownDpi;

  MeshBuilderOptions opts;
  opts.m_margin = options.m_margin;

  rasDpi = TPointD(options.m_rasterizationDpi, options.m_rasterizationDpi);

  if (TXshSimpleLevel *sl = cell.getSimpleLevel()) {
    TImageP img(sl->getFullsampledFrame(cell.getFrameId(),
                                        ImageManager::dontPutInCache));

    ::getRaster(img, imageDpi, ras, rasDpi, worldOriginPos, opts.m_margin);
    locals::checkEmptyDpi(imageDpi), locals::checkEmptyDpi(rasDpi);

    // Get the level dpi
    slDpi = sl->getDpi();
    locals::checkEmptyDpi(slDpi);

    // Due to a Toonz bug when loading a PLI, slDpi may actually acquire the
    // camera dpi -
    // but it's always shown to be at the STANDARD world DPI, Stage::inch -
    // plus, the
    // image to be meshed is rendered at imageDpi anyway.
    shownDpi = (sl->getType() == PLI_XSHLEVEL) ? rasDpi : slDpi;

    opts.m_transparentColor = sl->getProperties()->whiteTransp()
                                  ? TPixel64::White
                                  : TPixel64::Transparent;
  } else if (TXshChildLevel *cl = cell.getChildLevel()) {
    TXsheet *xsh = cl->getXsheet();
    int row      = cell.getFrameId().getNumber() - 1;

    ::getRaster(xsh, row, ras, rasDpi, worldOriginPos, opts.m_margin);

    slDpi.x = slDpi.y = Stage::inch;
    imageDpi = slDpi, shownDpi = rasDpi;

    opts.m_transparentColor = TPixel64::Transparent;
  }

  if (!ras) return TMeshImageP();

  // Build Mesh Builder Options and Meshify
  opts.m_targetEdgeLength       = options.m_edgesLength * shownDpi.x;
  opts.m_targetMaxVerticesCount = MESH_TARGET_MAX_VERTICES_COUNT;

  return meshify(ras, rasDpi, worldOriginPos, imageDpi, opts);
}

//------------------------------------------------------------------------------

TMeshImageP meshify(const TRasterP &ras, const TPointD &rasDpi,
                    const TPointD &worldOriginPos, const TPointD &meshDpi,
                    const MeshBuilderOptions &opts) {
  // Meshify the image
  TMeshImageP meshImg = buildMesh(ras, opts);

  // Transform it to have the correct image reference (origin at center, dpi
  // scale)
  transform(meshImg, TScale(meshDpi.x / rasDpi.x, meshDpi.y / rasDpi.y) *
                         TTranslation(-worldOriginPos));

  meshImg->setDpi(meshDpi.x, meshDpi.y);

  return meshImg;
}

//------------------------------------------------------------------------------

void createMeshifiedLevels(std::map<TXshLevel *, TXshSimpleLevel *> &meshLevels,
                           const MeshifyOptions &options) {
  // Preliminaries
  typedef std::map<TXshLevel *, std::set<TFrameId>> LevelsMap;
  typedef std::map<TXshLevel *, TXshSimpleLevel *> MeshLevelsMap;

  // Build the images set to meshify
  LevelsMap levels;
  getSelectedFrames(levels);

  if (levels.empty()) {
    // Perhaps the selection was empty, or there was no selection at all. Try to
    // access cell at current frame and column
    const TXshCell &cell = ::xshCell();

    TXshLevel *xl       = cell.m_level.getPointer();
    const TFrameId &fid = cell.getFrameId();

    if (xl) levels[xl].insert(fid);
  }

  // Prepare a progress dialog
  std::unique_ptr<DVGui::ProgressDialog> progressDialog(
      new DVGui::ProgressDialog(
          MeshifyPopup::tr("Mesh Creation in progress..."), QString(), 0, 0));
  {
    progressDialog->setWindowTitle("Create Mesh");
    progressDialog->setModal(true);

    // Build the number of objects to be meshified
    int count = 0;

    LevelsMap::const_iterator lt, lEnd(levels.end());
    for (lt = levels.begin(); lt != lEnd; ++lt) count += lt->second.size();

    progressDialog->setMaximum(count);
    progressDialog->show();

    progressDialog->setValue(0);  // Processes the show()
  }

  // Process each level independently
  LevelsMap::const_iterator lt, lEnd(levels.end());
  for (lt = levels.begin(); lt != lEnd; ++lt) {
    TXshLevel *xl = lt->first;

    // Create the associated mesh level
    TXshSimpleLevel *ml = ::createMeshLevel(xl);

    if (!ml) {
      progressDialog->setValue(progressDialog->value() + lt->second.size());
      continue;
    }

    meshLevels[xl] = ml;

    // Meshify the associated set of frames, and assign the results
    // to the mesh level
    const std::set<TFrameId> &frames = lt->second;

    // The path format of single image levels must be reproduced. It was not
    // possible to enforce this in ::createMeshLevels(), so we're doing it now.
    if (frames.size() == 1 && *frames.begin() == TFrameId(TFrameId::NO_FRAME))
      ml->setPath(ml->getPath().withFrame(TFrameId::NO_FRAME));

    std::set<TFrameId>::const_iterator ft, fEnd(frames.end());
    for (ft = frames.begin(); ft != fEnd; ++ft) {
      TMeshImageP meshImage = meshify(TXshCell(xl, *ft), options);
      if (meshImage) ml->setFrame(*ft, meshImage);

      progressDialog->setValue(progressDialog->value() + 1);
    }

    ml->setDirtyFlag(true);
  }
}

//------------------------------------------------------------------------------

void createMeshifiedColumns(int r0, int c0, int r1, int c1,
                            const MeshifyOptions &options) {
  struct locals {
    static inline bool isEmpty(const TXsheet &xsh, int r0, int r1, int c) {
      for (int r = r0; r != r1; ++r) {
        if (!xsh.getCell(r, c).isEmpty()) return false;
      }

      return true;
    }

    static inline int meshColumnIdx(const TXsheet &xsh, int r0, int r1, int c) {
      TStageObject *texObj = xsh.getStageObject(TStageObjectId::ColumnId(c));
      assert(texObj);

      const TStageObjectId &parentId = texObj->getParent();
      if (!parentId.isColumn()) return -1;

      int meshColIdx = parentId.getIndex();

      const TXshMeshColumn *meshCol =
          xsh.getColumn(meshColIdx)->getMeshColumn();
      if (!meshCol) return -1;

      return isEmpty(xsh, r0, r1, meshColIdx) ? meshColIdx : -1;
    }

  };  // locals

  // Retrieve selection data
  TXsheet *xsh = TApp::instance()->getCurrentXsheet()->getXsheet();

  // Meshify levels
  std::map<TXshLevel *, TXshSimpleLevel *> meshLevels;
  createMeshifiedLevels(meshLevels, options);
  if (meshLevels.empty()) return;

  // Create corresponding columns and assign them the meshified levels
  int meshColIdx;

  int r, c;
  for (c = c1; c >= c0;
       --c)  // Reverse iteration since we're interleaving the results
  {
    TXshLevelColumn *column =
        dynamic_cast<TXshLevelColumn *>(xsh->getColumn(c));
    if (!column || column->isEmpty())  // Skip empty/innocent columns
      continue;

    meshColIdx = -1;

    // Deal with cells contents
    for (r = r0; r <= r1; ++r) {
      // Set meshCol's cells accordingly
      const TXshCell &srcCell = xsh->getCell(r, c);
      if (srcCell.isEmpty()) continue;

      std::map<TXshLevel *, TXshSimpleLevel *>::iterator lt(
          meshLevels.find(srcCell.m_level.getPointer()));

      if (lt != meshLevels.end()) {
        if (meshColIdx < 0) {
          meshColIdx = locals::meshColumnIdx(
              *xsh, r0, r1, c);  // Attempt retrieval of an existing mesh column
          if (meshColIdx < 0)    // first - if not found, then make a new one
            ::makeMeshColumn(*xsh, c,
                             meshColIdx = c + 1);  // right after current one.
        }

        TXshCell dstCell(lt->second, srcCell.m_frameId);
        xsh->setCell(r, meshColIdx, dstCell);
      }
    }
  }

  // Set current column to meshColIdx - this allows the users to skip selecting
  // it
  // in case they want to start editing meshes immediately
  assert(meshColIdx >= 0);
  TApp::instance()->getCurrentColumn()->setColumnIndex(meshColIdx);
}

//------------------------------------------------------------------------------

void updateMeshifiedColumns(int r0, int c0, int r1, int c1,
                            const MeshifyOptions &options) {
  typedef std::map<std::pair<TXshLevel *, TFrameId>, TXshCell> MeshesMap;

  // Build a map of the mesh images to be updated
  TXsheet *xsh = TApp::instance()->getCurrentXsheet()->getXsheet();
  assert(xsh);

  MeshesMap meshToUpdate;

  for (int c = c0; c <= c1; ++c) {
    TXshColumn *col = xsh->getColumn(c);
    if (!col) continue;

    TXshMeshColumn *meshCol = col->getMeshColumn();
    if (!meshCol) continue;

    TStageObject *meshObj = xsh->getStageObject(TStageObjectId::ColumnId(c));
    assert(meshObj);

    for (int r = r0; r <= r1; ++r) {
      // Retrieve the mesh cell
      const TXshCell &cell = xsh->getCell(r, c);

      TXshSimpleLevel *ml = cell.getSimpleLevel();
      if (!ml || ml->getType() != MESH_XSHLEVEL) continue;

      // Retrieve the associated texture
      const TStageObjectId &childId = firstChildLevelColumn(*xsh, *meshObj);

      const TXshCell &texCell = xsh->getCell(r, childId.getIndex());

      TXshLevel *xl = texCell.m_level.getPointer();
      if (!(xl && (xl->getType() & LEVELCOLUMN_XSHLEVEL))) continue;

      // Found a match - insert it in the map.
      // NOTE: The same mesh cell could be found multiple times. In this case,
      // subsequent ones
      // will be ignored.
      MeshesMap::key_type meshPair(cell.m_level.getPointer(), cell.m_frameId);

      if (meshToUpdate.find(meshPair) == meshToUpdate.end())
        meshToUpdate.insert(std::make_pair(meshPair, texCell));
    }
  }

  // Now, we have to re-meshify the map's former mesh references against the
  // latter textures

  MeshesMap::iterator ct, cEnd(meshToUpdate.end());
  for (ct = meshToUpdate.begin(); ct != cEnd; ++ct) {
    TXshSimpleLevel *ml  = ct->first.first->getSimpleLevel();
    const TFrameId &mFid = ct->first.second;

    TMeshImageP meshImg = meshify(ct->second, options);

    ml->setFrame(mFid, meshImg);
    ml->setDirtyFlag(true);
  }

  // Update icons. Okay, I'm doing this in a separate cycle, because I suspect
  // it's
  // not safe to re-render icons as we're manipulating the level frames map. It
  // *could* be
  // that the TThread::Executor waits until the control loop returns before
  // starting the
  // icon threads... well - better safe than sorry :)
  for (ct = meshToUpdate.begin(); ct != cEnd; ++ct)
    IconGenerator::instance()->invalidate(
        ct->first.first, ct->first.second);  // Yep, this is still manual...
}

//------------------------------------------------------------------------------

template <typename Func>
void meshifySelection(Func func, TSelection *selection,
                      const MeshifyOptions &options) {
  bool emptySelection = selection ? selection->isEmpty() : true;

  if (TCellSelection *cellSelection =
          emptySelection ? (TCellSelection *)0
                         : dynamic_cast<TCellSelection *>(selection)) {
    int r0, c0, r1, c1;
    cellSelection->getSelectedCells(r0, c0, r1, c1);

    (*func)(r0, c0, r1, c1, options);
    return;
  }

  if (TColumnSelection *colSelection =
          emptySelection ? (TColumnSelection *)0
                         : dynamic_cast<TColumnSelection *>(selection)) {
    TXsheet *xsh = TApp::instance()->getCurrentXsheet()->getXsheet();
    const std::set<int> &indices = colSelection->getIndices();

    int c0 = *indices.begin(), c1 = *indices.rbegin();
    int r0 = (std::numeric_limits<int>::max)(), r1 = -r0;

    std::set<int>::const_iterator it, iEnd(indices.end());
    for (it = indices.begin(); it != iEnd; ++it) {
      TXshCellColumn *column =
          dynamic_cast<TXshCellColumn *>(xsh->getColumn(*it));
      if (!column || column->isEmpty()) continue;

      r0 = std::min(r0, column->getFirstRow());
      r1 = std::max(r1, column->getMaxFrame());
    }

    (*func)(r0, c0, r1, c1, options);
    return;
  }

  // Meshify the sole cell at current frame and column positions
  int r = TApp::instance()->getCurrentFrame()->getFrame();
  int c = TApp::instance()->getCurrentColumn()->getColumnIndex();

  (*func)(r, c, r, c, options);
}

//------------------------------------------------------------------------------

enum ColumnEnum { HAS_LEVEL_COLUMNS = 0x1, HAS_MESH_COLUMNS = 0x2 };

//------------------------------------------------------------------------------

// Ensure that the specified range has homogeneous columns with respect to
// meshification
int columnTypes(TXsheet *xsh, int c0, int c1) {
  int columnBits = 0x0;

  for (int c = c0; c <= c1; ++c) {
    TXshColumn *xc = xsh->getColumn(c);
    if (!xc) continue;

    if (xc->getLevelColumn()) columnBits |= HAS_LEVEL_COLUMNS;

    if (xc->getMeshColumn()) columnBits |= HAS_MESH_COLUMNS;
  }

  return columnBits;
}

//------------------------------------------------------------------------------

// Ensure that the specified range has homogeneous columns with respect to
// meshification
int columnTypes(TXsheet *xsh, const std::set<int> &indices) {
  int columnBits = 0x0;

  std::set<int>::const_iterator it, iEnd(indices.end());
  for (it = indices.begin(); it != iEnd; ++it) {
    TXshColumn *xc = xsh->getColumn(*it);

    if (xc->getLevelColumn()) columnBits |= HAS_LEVEL_COLUMNS;

    if (xc->getMeshColumn()) columnBits |= HAS_MESH_COLUMNS;
  }

  return columnBits;
}

//------------------------------------------------------------------------------

bool meshifySelection(const MeshifyOptions &options) {
  TApp *app = TApp::instance();

  // Build selection data
  TSelection *selection = TSelection::getCurrent();
  bool emptySelection   = selection ? selection->isEmpty() : true;

  TCellSelection *cellSelection =
      emptySelection ? (TCellSelection *)0
                     : dynamic_cast<TCellSelection *>(selection);
  TColumnSelection *colSelection =
      emptySelection ? (TColumnSelection *)0
                     : dynamic_cast<TColumnSelection *>(selection);

  int r0, c0, r1, c1;
  if (cellSelection)
    cellSelection->getSelectedCells(r0, c0, r1, c1);
  else {
    // Meshify a single frame (the one at current frame and current column)
    r0 = r1 = app->getCurrentFrame()->getFrame();
    c0 = c1 = app->getCurrentColumn()->getColumnIndex();
  }

  // Decide action. Dismiss mixed selections containing both mesh AND simple
  // levels.
  TXsheet *xsh = app->getCurrentXsheet()->getXsheet();

  int cTypes = colSelection ? columnTypes(xsh, colSelection->getIndices())
                            : columnTypes(xsh, c0, c1);

  switch (cTypes) {
  case HAS_LEVEL_COLUMNS:
    // Create new mesh columns corresponding to specified selection
    meshifySelection(&createMeshifiedColumns, selection, options);
    break;
  case HAS_MESH_COLUMNS:
    // Check parental relationship - if specified columns have level column
    // children,
    // update related meshes
    meshifySelection(&updateMeshifiedColumns, selection, options);
    break;
  case HAS_LEVEL_COLUMNS | HAS_MESH_COLUMNS:
    // Error message
    DVGui::error(MeshifyPopup::tr(
        "Current selection contains mixed image and mesh level types"));
    return false;
  default:
    // Error message
    DVGui::error(MeshifyPopup::tr(
        "Current selection contains no image or mesh level types"));
    return false;
  }

  // Notify xsheet change
  app->getCurrentXsheet()->notifyXsheetChanged();
  app->getCurrentScene()->setDirtyFlag(true);
  app->getCurrentScene()->notifyCastChange();

  return true;
}

}  // namespace

//********************************************************************************************
//    Meshify Command  definition
//********************************************************************************************

class MeshifyCommand final : public MenuItemHandler {
public:
  MeshifyCommand() : MenuItemHandler("A_ToolOption_Meshify") {}

  void execute() override {
    static MeshifyPopup *thePopup = 0;
    if (!thePopup) thePopup = new MeshifyPopup;

    thePopup->raise();
    thePopup->show();
  }

} meshifyCommand;
