

// TnzCore includes
#include "tsystem.h"
#include "tundo.h"
#include "tpalette.h"

// TnzLib includes
#include "toonz/txsheet.h"
#include "toonz/toonzscene.h"
#include "toonz/txshcell.h"
#include "toonz/txshsimplelevel.h"
#include "toonz/levelset.h"
#include "toonz/hook.h"
#include "toonz/levelproperties.h"
#include "toonz/txshlevelhandle.h"
#include "toonz/txsheethandle.h"
#include "toonz/tscenehandle.h"
#include "toonz/txshleveltypes.h"

// TnzQt includes
#include "toonzqt/menubarcommand.h"

// Tnz6 includes
#include "tapp.h"
#include "cellselection.h"
#include "columnselection.h"
#include "keyframeselection.h"
#include "filmstripselection.h"
#include "menubarcommandids.h"
#include "columncommand.h"

// Qt includes
#include <QCoreApplication>
#include <QPushButton>
#include <QMainWindow>

#include "matchline.h"

//*****************************************************************************
//    MergeCmappedDialog  implementation
//*****************************************************************************

void MergeCmappedDialog::accept() {
  m_levelPath = TFilePath(QString(m_saveInFileFld->getPath() + "\\" +
                                  m_fileNameFld->text() + ".tlv")
                              .toStdString());
  TFilePath fp =
      TApp::instance()->getCurrentScene()->getScene()->decodeFilePath(
          m_levelPath);

  if (TSystem::doesExistFileOrLevel(fp)) {
    if (DVGui::MsgBox(
            tr("Level %1 already exists! Are you sure you want to overwrite "
               "it?")
                .arg(QString::fromStdWString(m_levelPath.getWideString())),
            tr("Ok"), tr("Cancel")) != 1)
      return;
    else {
      TSystem::removeFileOrLevel(fp);
      TSystem::removeFileOrLevel(fp.withType("tpl"));
    }
  }

  Dialog::accept();
}

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

MergeCmappedDialog::MergeCmappedDialog(TFilePath &levelPath)
    : Dialog(TApp::instance()->getMainWindow(), true, true, "Merge Tlv")
    , m_levelPath(levelPath) {
  bool ret = true;

  QString path =
      QString::fromStdWString(m_levelPath.getParentDir().getWideString());
  QString name = QString::fromStdString(m_levelPath.getName());

  setWindowTitle(tr(" Merge Tlv Levels"));
  m_saveInFileFld = new DVGui::FileField(0, path);
  ret             = ret && connect(m_saveInFileFld, SIGNAL(pathChanged()), this,
                       SLOT(onPathChanged()));
  addWidget(tr("Save in:"), m_saveInFileFld);

  m_fileNameFld = new DVGui::LineEdit(name + "_merged");
  m_fileNameFld->setMaximumHeight(DVGui::WidgetHeight);
  ret = ret && connect(m_fileNameFld, SIGNAL(editingFinished()),
                       SLOT(onNameChanged()));
  addWidget(tr("File Name:"), m_fileNameFld);

  QPushButton *okBtn = new QPushButton(tr("Apply"), this);
  okBtn->setDefault(true);
  QPushButton *cancelBtn = new QPushButton(tr("Cancel"), this);
  connect(okBtn, SIGNAL(clicked()), this, SLOT(accept()));
  connect(cancelBtn, SIGNAL(clicked()), this, SLOT(reject()));

  addButtonBarWidget(okBtn, cancelBtn);
}

//*****************************************************************************
//    MergeColumns  command
//*****************************************************************************

static bool isVectorColumn(const std::set<int> &columns) {
  TXsheet *xsh = TApp::instance()->getCurrentXsheet()->getXsheet();
  std::set<int>::const_iterator column = columns.begin();
  int start, end;
  xsh->getCellRange(*column, start, end);

  if (start > end) return false;
  // a cell at "start" must be occupied
  TXshCell cell = xsh->getCell(start, *column);
  return cell.m_level->getType() == PLI_XSHLEVEL;
}

class MergeColumnsCommand final : public MenuItemHandler {
public:
  MergeColumnsCommand() : MenuItemHandler(MI_MergeColumns) {}

  void execute() override {
    TColumnSelection *selection =
        dynamic_cast<TColumnSelection *>(TSelection::getCurrent());

    std::set<int> indices =
        selection ? selection->getIndices() : std::set<int>();

    if (indices.empty()) {
      DVGui::warning(QObject::tr(
          "It is not possible to execute the merge column command because "
          "no column was selected."));
      return;
    }

    if (indices.size() == 1) {
      DVGui::warning(QObject::tr(
          "It is not possible to execute the merge column command because "
          "only one columns is selected."));
      return;
    }

    bool groupLevels = true;
    if (isVectorColumn(indices)) {
      int opt = DVGui::MsgBox(QObject::tr("Group strokes by vector levels?"),
                              QObject::tr("Yes"), QObject::tr("No"),
                              QObject::tr("Cancel"));
      if (opt == 0 || opt == 3)
        return;
      else {
        groupLevels = (opt == 1);
      };
    }

    mergeColumns(indices, groupLevels);
    TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
  }

} MergeColumnsCommand;

//*****************************************************************************
//    ApplyMatchlines  command
//*****************************************************************************

class ApplyMatchlinesCommand final : public MenuItemHandler {
public:
  ApplyMatchlinesCommand() : MenuItemHandler(MI_ApplyMatchLines) {}

  void execute() override {
    TColumnSelection *selection =
        dynamic_cast<TColumnSelection *>(TSelection::getCurrent());
    if (!selection) {
      DVGui::warning(QObject::tr(
          "It is not possible to apply the match lines because no column "
          "was selected."));
      return;
    }

    std::set<int> indices = selection->getIndices();

    if (indices.size() != 2) {
      DVGui::warning(QObject::tr(
          "It is not possible to apply the match lines because two columns "
          "have to be selected."));
      return;
    }

    std::set<int>::iterator it = indices.begin();
    int i = *it++, j = *it;

    doMatchlines(i, j, -1, -1);
    TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
  }

} ApplyMatchlinesCommand;

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

namespace {

bool checkColumnValidity(int column) {
  TXsheet *xsh = TApp::instance()->getCurrentXsheet()->getXsheet();
  int start, end;

  xsh->getCellRange(column, start, end);

  if (start > end) return false;
  std::vector<TXshCell> cell(end - start + 1);

  xsh->getCells(start, column, cell.size(), &(cell[0]));

  TXshSimpleLevel *level = 0;

  for (int i = 0; i < (int)cell.size(); i++) {
    if (cell[i].isEmpty()) continue;
    if (!level) level = cell[i].getSimpleLevel();

    if (cell[i].getSimpleLevel()->getType() != TZP_XSHLEVEL) {
      DVGui::warning(QObject::tr(
          "Match lines can be applied to Toonz raster levels only."));
      return false;
    }
    if (level != cell[i].getSimpleLevel()) {
      DVGui::warning(
          QObject::tr("It is not possible to merge tlv columns containing more "
                      "than one level"));
      return false;
    }
  }

  if (!level) return false;

  if (!level->getPalette()) {
    DVGui::warning(
        QObject::tr("The level you are using has not a valid palette."));
    return false;
  }
  return true;
}

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

void doCloneLevelNoSave(const TCellSelection::Range &range,
                        const QString &newLevelName, bool withUndo);

//-----------------------------------------------------------------------------
namespace {
//-----------------------------------------------------------------------------

class CloneLevelNoSaveUndo final : public TUndo {
  std::map<TXshSimpleLevel *, TXshLevelP> m_createdLevels;
  std::set<int> m_insertedColumnIndices;

  TCellSelection::Range m_range;
  QString m_levelname;

public:
  CloneLevelNoSaveUndo(
      const TCellSelection::Range &range,
      const std::map<TXshSimpleLevel *, TXshLevelP> &createdLevels,
      const std::set<int> &insertedColumnIndices, const QString &levelname)
      : m_createdLevels(createdLevels)
      , m_range(range)
      , m_insertedColumnIndices(insertedColumnIndices)
      , m_levelname(levelname) {}

  void undo() const override {
    TApp *app         = TApp::instance();
    ToonzScene *scene = app->getCurrentScene()->getScene();
    TXsheet *xsh      = scene->getXsheet();
    int i;
    for (i = m_range.getColCount(); i > 0; i--) {
      int index                        = m_range.m_c1 + i;
      std::set<int>::const_iterator it = m_insertedColumnIndices.find(index);
      xsh->removeColumn(index);
      if (it == m_insertedColumnIndices.end()) xsh->insertColumn(index);
    }

    std::map<TXshSimpleLevel *, TXshLevelP>::const_iterator it =
        m_createdLevels.begin();

    for (; it != m_createdLevels.end(); ++it) {
      it->second->addRef();
      scene->getLevelSet()->removeLevel(it->second.getPointer());
    }
    app->getCurrentXsheet()->notifyXsheetChanged();
  }
  void redo() const override {
    doCloneLevelNoSave(m_range, m_levelname, false);
  }

  int getSize() const override {
    return sizeof *this + (sizeof(TXshLevelP) + sizeof(TXshSimpleLevel *)) *
                              m_createdLevels.size();
  }
};

//-----------------------------------------------------------------------------
}  // namespace
//-----------------------------------------------------------------------------

void doCloneLevelNoSave(const TCellSelection::Range &range,
                        const QString &newLevelName = QString(),
                        bool withUndo               = true) {
  std::map<TXshSimpleLevel *, TXshLevelP> createdLevels;

  TApp *app         = TApp::instance();
  TXsheet *xsh      = app->getCurrentXsheet()->getXsheet();
  ToonzScene *scene = app->getCurrentScene()->getScene();

  // Build indices of inserted columns
  std::set<int> insertedColumnIndices;
  int c;
  for (c = 1; c <= range.getColCount(); ++c) {
    int colIndex = range.m_c1 + c;
    if (xsh->isColumnEmpty(colIndex)) continue;

    xsh->insertColumn(colIndex);
    insertedColumnIndices.insert(colIndex);
  }

  bool isOneCellCloned = false;
  for (c = range.m_c0; c <= range.m_c1; ++c) {
    TXshLevelP xl;
    TXshSimpleLevel *sl = 0;
    TFrameId fid(1);

    bool keepOldLevel = false;

    // OverwriteDialog* dialog = new OverwriteDialog();
    for (int r = range.m_r0; r <= range.m_r1; ++r) {
      TXshCell cell = xsh->getCell(r, c);

      TImageP img = cell.getImage(true);
      if (!img) continue;

      fid = cell.getFrameId();

      if (cell.getSimpleLevel() == 0 ||
          cell.getSimpleLevel()->getPath().isUneditable())
        continue;

      std::map<TXshSimpleLevel *, TXshLevelP>::iterator it =
          createdLevels.find(cell.getSimpleLevel());
      if (it == createdLevels.end()) {
        // Create a new level if not already done

        TXshSimpleLevel *oldSl = cell.getSimpleLevel();
        {
          int levelType = oldSl->getType();
          assert(levelType > 0);

          xl = scene->createNewLevel(levelType, newLevelName.toStdWString());
          sl = xl->getSimpleLevel();
          // if(levelType == OVL_XSHLEVEL)
          //  dstPath = dstPath.withType(oldSl->getPath().getType());
          assert(sl);
          // sl->setPath(scene->codeFilePath(dstPath));
          // sl->setName(newName);
          sl->clonePropertiesFrom(oldSl);
          *sl->getHookSet() = *oldSl->getHookSet();

          if (levelType == TZP_XSHLEVEL || levelType == PLI_XSHLEVEL) {
            TPalette *palette = oldSl->getPalette();
            assert(palette);

            sl->setPalette(palette->clone());
          }
        }
        createdLevels[cell.getSimpleLevel()] = xl;
      } else {
        xl = it->second;
        sl = xl->getSimpleLevel();
      }

      TXshCell oldCell(cell);
      cell.m_level = xl;
      int k;
      for (k = range.m_r0; k < r; k++) {
        if (xsh->getCell(k, c).getImage(true).getPointer() ==
            img.getPointer()) {
          TFrameId oldFid = xsh->getCell(k, c).getFrameId();
          assert(fid == oldFid);
          sl->setFrame(fid,
                       xsh->getCell(k, c + range.getColCount()).getImage(true));
          break;
        }
      }

      if (!keepOldLevel && k >= r) {
        TImageP newImg(img->cloneImage());
        assert(newImg);

        sl->setFrame(fid, newImg);
      }

      cell.m_frameId = fid;
      xsh->setCell(r, c + range.getColCount(), cell);
      isOneCellCloned = true;
    }
    if (sl) sl->getProperties()->setDirtyFlag(true);
  }

  // Se non e' stata inserita nessuna cella rimuovo le colonne aggiunte e
  // ritorno.
  if (!isOneCellCloned) {
    if (!insertedColumnIndices.empty()) {
      int i;
      for (i = range.getColCount(); i > 0; i--) {
        int index                        = range.m_c1 + i;
        std::set<int>::const_iterator it = insertedColumnIndices.find(index);
        xsh->removeColumn(index);
      }
    }
    return;
  }
  if (withUndo)
    TUndoManager::manager()->add(new CloneLevelNoSaveUndo(
        range, createdLevels, insertedColumnIndices, newLevelName));

  app->getCurrentXsheet()->notifyXsheetChanged();
  app->getCurrentScene()->setDirtyFlag(true);
  app->getCurrentScene()->notifyCastChange();
}

void cloneColumn(const TCellSelection::Range &cells,
                 const TFilePath &newLevelPath) {
  std::set<std::pair<int, int>> positions;
  for (int i = cells.m_r0; i <= cells.m_r1; i++)
    positions.insert(std::pair<int, int>(i, cells.m_c0));

  TKeyframeSelection ks(positions);
  ks.copyKeyframes();
  doCloneLevelNoSave(cells,
                     QString::fromStdString(newLevelPath.getName() + "_"));
  ColumnCmd::deleteColumn(cells.m_c0);
  ks.pasteKeyframes();
}

}  // namespace

class MergeCmappedCommand final : public MenuItemHandler {
public:
  MergeCmappedCommand() : MenuItemHandler(MI_MergeCmapped) {}
  void execute() override {
    TColumnSelection *selection =
        dynamic_cast<TColumnSelection *>(TSelection::getCurrent());
    if (!selection) {
      DVGui::warning(QObject::tr(
          "It is not possible to merge tlv columns because no column was "
          "selected."));
      return;
    }

    std::set<int> indices = selection->getIndices();

    if (indices.size() < 2) {
      DVGui::warning(QObject::tr(
          "It is not possible to merge tlv columns because at least two "
          "columns have to be selected."));
      return;
    }

    std::set<int>::iterator it = indices.begin();
    int destColumn             = *it;

    TCellSelection::Range cells;
    cells.m_c0 = cells.m_c1 = destColumn;
    TXshColumn *column =
        TApp::instance()->getCurrentXsheet()->getXsheet()->getColumn(
            destColumn);
    column->getRange(cells.m_r0, cells.m_r1);

    // column->getLevelColumn()

    TFilePath newLevelPath;
    TXshCell c = TApp::instance()->getCurrentXsheet()->getXsheet()->getCell(
        cells.m_r0, destColumn);
    if (!c.isEmpty() && c.getSimpleLevel())
      newLevelPath = c.getSimpleLevel()->getPath();

    if (MergeCmappedDialog(newLevelPath).exec() != QDialog::Accepted) return;

    it = indices.begin();
    for (; it != indices.end(); ++it)
      if (!checkColumnValidity(*it)) return;

    DVGui::ProgressDialog progress(QObject::tr("Merging Tlv Levels..."),
                                   QString(), 0, indices.size() - 1,
                                   TApp::instance()->getMainWindow());
    progress.setWindowModality(Qt::WindowModal);
    progress.setWindowTitle(QObject::tr("Merging Tlv Levels..."));
    progress.setValue(0);
    progress.show();

    QCoreApplication::instance()->processEvents();

    TUndoManager::manager()->beginBlock();

    cloneColumn(cells, newLevelPath);

    TFilePath tmpPath = newLevelPath.withName(
        QString::fromStdString(newLevelPath.getName() + "_tmp").toStdWString());

    it = indices.begin();
    ++it;
    for (int count = 0; it != indices.end();) {
      int index = *it;
      it++;
      mergeCmapped(destColumn, index - count,
                   it == indices.end() ? newLevelPath.getQString()
                                       : tmpPath.getQString(),
                   false);
      ColumnCmd::deleteColumn(index - count);
      progress.setValue(++count);
      QCoreApplication::instance()->processEvents();
    }
    TUndoManager::manager()->endBlock();
    TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
  }

} MergeCmappedCommand;

//-----------------------------------------------------------------------------
namespace {
void doDeleteCommand(bool isMatchline) {
  TRect r;

  TCellSelection *sel =
      dynamic_cast<TCellSelection *>(TSelection::getCurrent());
  if (!sel) {
    TFilmstripSelection *filmstripSelection =
        dynamic_cast<TFilmstripSelection *>(TSelection::getCurrent());
    TColumnSelection *columnSelection =
        dynamic_cast<TColumnSelection *>(TSelection::getCurrent());
    std::set<int> indices;
    std::set<TFrameId> fids;
    if (filmstripSelection &&
        (fids = filmstripSelection->getSelectedFids()).size() > 0) {
      TXshSimpleLevel *sl =
          TApp::instance()->getCurrentLevel()->getSimpleLevel();
      if (isMatchline)
        deleteMatchlines(sl, fids);
      else
        deleteInk(sl, fids);
      TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
      return;
    } else if (!columnSelection ||
               (indices = columnSelection->getIndices()).size() != 1) {
      DVGui::warning(
          QObject::tr("It is not possible to delete lines because no column, "
                      "cell or level strip frame was selected."));
      return;
    }
    int from, to;
    int columnIndex = *indices.begin();
    TXsheet *xsh    = TApp::instance()->getCurrentXsheet()->getXsheet();
    if (!xsh->getCellRange(*indices.begin(), from, to)) {
      DVGui::warning(QObject::tr("The selected column is empty."));
      return;
    }
    r.y0 = from;
    r.y1 = to;
    r.x0 = r.x1 = columnIndex;
  } else
    sel->getSelectedCells(r.y0, r.x0, r.y1, r.x1);

  if (r.x0 != r.x1) {
    DVGui::warning(QObject::tr("Selected cells must be in the same column."));
    return;
  }

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

  int i;

  for (i = r.y0; i <= r.y1; i++) {
    TXshCell cell = xsh->getCell(i, r.x0);
    if (cell.isEmpty()) {
      DVGui::warning(
          QObject::tr("It is not possible to delete lines because no column, "
                      "cell or level strip frame was selected."));
      return;
    }
    if (cell.m_level->getType() != TZP_XSHLEVEL) {
      DVGui::warning(QObject::tr(
          "Match lines can be deleted from Toonz raster levels only"));
      return;
    }
  }

  std::set<TFrameId> fids;
  for (i = r.y0; i <= r.y1; i++) {
    const TXshCell &cell = xsh->getCell(i, r.x0);
    fids.insert(cell.getFrameId());
  }

  TXshSimpleLevel *sl = xsh->getCell(r.y0, r.x0).getSimpleLevel();
  assert(sl);

  if (isMatchline)
    deleteMatchlines(sl, fids);
  else
    deleteInk(sl, fids);

  TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
}

}  // namespace

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

class DeleteInkCommand final : public MenuItemHandler {
public:
  DeleteInkCommand() : MenuItemHandler(MI_DeleteInk) {}
  void execute() override { doDeleteCommand(false); }

} DeleteInkCommand;

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

class DeleteMatchlinesCommand final : public MenuItemHandler {
public:
  DeleteMatchlinesCommand() : MenuItemHandler(MI_DeleteMatchLines) {}
  void execute() override { doDeleteCommand(true); }

} DeleteMatchlinesCommand;
