#include "tapp.h"
#include "tpalette.h"
#include "toonz/txsheet.h"
#include "toonz/toonzscene.h"
#include "toonz/levelset.h"
#include "toonz/txshsimplelevel.h"
#include "toonz/txshlevelcolumn.h"
#include "toonz/txshcell.h"
//#include "tw/action.h"
#include "tropcm.h"
#include "ttoonzimage.h"
#include "matchline.h"
#include "toonz/scenefx.h"
#include "toonz/dpiscale.h"
#include "toonz/txsheethandle.h"
#include "toonz/palettecontroller.h"
#include "toonz/tpalettehandle.h"
#include "toonz/txshlevelhandle.h"
#include "toonz/txshleveltypes.h"
#include "toonz/tscenehandle.h"
#include "toonz/tframehandle.h"
#include "toonz/preferences.h"
#include "toonzqt/icongenerator.h"
#include <map>
#include <QRadioButton>
#include <QPushButton>
#include <QApplication>
#include "tundo.h"
#include "tools/toolutils.h"
#include "timagecache.h"
#include "tcolorstyles.h"
#include "toonz/levelproperties.h"
#include "toonz/toonzscene.h"
#include "toonz/childstack.h"

#include <algorithm>

using namespace DVGui;

namespace {

class MergeCmappedPair {
public:
  const TXshCell *m_cell;
  TAffine m_imgAff;
  const TXshCell *m_mcell;
  TAffine m_matchAff;

  MergeCmappedPair(const TXshCell &cell, const TAffine &imgAff,
                   const TXshCell &mcell, const TAffine &matchAff)
      : m_cell(&cell)
      , m_imgAff(imgAff)
      , m_mcell(&mcell)
      , m_matchAff(matchAff){};
};

void mergeCmapped(const std::vector<MergeCmappedPair> &matchingLevels) {
  if (matchingLevels.empty()) return;

  TPalette *palette = matchingLevels[0].m_cell->getImage(false)->getPalette();
  TPalette *matchPalette =
      matchingLevels[0].m_mcell->getImage(false)->getPalette();

  TPalette::Page *page;

  // upInkId -> downInkId
  std::map<int, int> usedColors;

  int i = 0;
  for (i = 0; i < (int)matchingLevels.size(); i++) {
    TToonzImageP img = (TToonzImageP)matchingLevels[i].m_cell->getImage(true);
    TToonzImageP match =
        (TToonzImageP)matchingLevels[i].m_mcell->getImage(false);
    if (!img || !match)
      throw TRopException("Can merge only cmapped raster images!");
    // img->lock();
    TRasterCM32P ras      = img->getRaster();    // img->getCMapped(false);
    TRasterCM32P matchRas = match->getRaster();  // match->getCMapped(true);
    if (!ras || !matchRas)
      throw TRopException("Can merge only cmapped images!");

    TAffine aff =
        matchingLevels[i].m_imgAff.inv() * matchingLevels[i].m_matchAff;
    int mlx = matchRas->getLx();
    int mly = matchRas->getLy();
    int rlx = ras->getLx();
    int rly = ras->getLy();

    TRectD in = convert(matchRas->getBounds()) - matchRas->getCenterD();

    TRectD out = aff * in;

    TPoint offs((rlx - mlx) / 2 + tround(out.getP00().x - in.getP00().x),
                (rly - mly) / 2 + tround(out.getP00().y - in.getP00().y));

    int lxout = tround(out.getLx()) + 1 + ((offs.x < 0) ? offs.x : 0);
    int lyout = tround(out.getLy()) + 1 + ((offs.y < 0) ? offs.y : 0);

    if (lxout <= 0 || lyout <= 0 || offs.x >= rlx || offs.y >= rly) {
      // tmsg_error("no intersections between matchline and level");
      continue;
    }

    aff = aff.place((double)(in.getLx() / 2.0), (double)(in.getLy() / 2.0),
                    (out.getLx()) / 2.0 + ((offs.x < 0) ? offs.x : 0),
                    (out.getLy()) / 2.0 + ((offs.y < 0) ? offs.y : 0));

    if (offs.x < 0) offs.x = 0;
    if (offs.y < 0) offs.y = 0;

    if (lxout + offs.x > rlx || lyout + offs.y > rly) {
      // PRINTF("TAGLIO L'IMMAGINE\n");
      lxout = (lxout + offs.x > rlx) ? (rlx - offs.x) : lxout;
      lyout = (lyout + offs.y > rly) ? (rly - offs.y) : lyout;
    }

    if (!aff.isIdentity(1e-4)) {
      TRasterCM32P aux(lxout, lyout);
      TRop::resample(aux, matchRas, aff);
      matchRas = aux;
    }
    ras->lock();
    matchRas->lock();
    TRect raux = matchRas->getBounds() + offs;
    TRasterP r = ras->extract(raux);
    TRop::overlayCmapped(r, matchRas, palette, matchPalette, usedColors);
    ras->unlock();
    matchRas->unlock();

    img->setSavebox(img->getSavebox() + (matchRas->getBounds() + offs));
  }

  std::map<int, int>::iterator it = usedColors.begin();
  for (; it != usedColors.end(); ++it)
    if (it->first != it->second) break;

  if (it == usedColors.end())  // this means that the merged palette does not
                               // differ from source palette.(all usedColors are
                               // not new color )
    return;

  std::wstring pageName = L"merged palettes";

  for (i = 0; i < palette->getPageCount(); i++)
    if (palette->getPage(i)->getName() == pageName) {
      page = palette->getPage(i);
      break;
    }
  if (i == palette->getPageCount()) page = palette->addPage(pageName);

  it        = usedColors.begin();
  int count = 0;
  for (; it != usedColors.end(); ++it) {
    if (it->first == it->second) continue;
    while (palette->getStyleCount() <= it->second)
      palette->addStyle(TPixel32::Red);
    palette->setStyle(it->second, matchPalette->getStyle(it->first)->clone());
    page->addStyle(it->second);
  }
  if (usedColors.size() > 0) palette->setDirtyFlag(true);
}

/*------------------------------------------------------------------------*/

void applyDeleteMatchline(TXshSimpleLevel *sl,
                          const std::vector<TFrameId> &fids,
                          const std::vector<int> &_inkIndexes) {
  TPalette::Page *page = 0;
  int i, j, pageIndex = 0;
  std::vector<int> inkIndexes = _inkIndexes;

  if (fids.empty()) return;

  TPalette *palette = 0;

  if (inkIndexes.empty()) {
    palette = sl->getFrame(fids[0], true)->getPalette();

    for (i = 0; i < palette->getPageCount(); i++)
      if (palette->getPage(i)->getName() == L"match lines") {
        page = palette->getPage(i);
        break;
      }

    if (!page) return;
    pageIndex = i;
  }

  for (i = 0; i < (int)fids.size(); i++) {
    // level[i]->lock();
    TToonzImageP image = sl->getFrame(fids[i], true);
    assert(image);
    TRasterCM32P ras = image->getRaster();  // level[i]->getCMapped(false);
    ras->lock();
    if (inkIndexes.empty())
      for (j = 0; j < page->getStyleCount(); j++)
        inkIndexes.push_back(page->getStyleId(j));

    TRop::eraseColors(ras, &inkIndexes, true);

    ras->unlock();
    TRect savebox;
    TRop::computeBBox(ras, savebox);
    image->setSavebox(savebox);
    // level[i]->unlock();
  }

  // if (page)
  //  {
  //  while (page->getStyleCount())
  //   page->removeStyle(0);
  // palette->erasePage(pageIndex);
  // }
}

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

class DeleteMatchlineUndo final : public TUndo {
public:
  TXshLevel *m_xl;
  TXshSimpleLevel *m_sl;
  std::vector<TFrameId> m_fids;
  std::vector<int> m_indexes;
  TPaletteP m_matchlinePalette;

  DeleteMatchlineUndo(
      TXshLevel *xl, TXshSimpleLevel *sl, const std::vector<TFrameId> &fids,
      const std::vector<int> &indexes)  //, TPalette*matchPalette)
      : TUndo(),
        m_xl(xl),
        m_sl(sl),
        m_fids(fids),
        m_indexes(indexes)
  //, m_matchlinePalette(matchPalette->clone())
  {
    // assert(matchPalette);
    int i;
    for (i = 0; i < fids.size(); i++) {
      QString id = "DeleteMatchlineUndo" + QString::number((uintptr_t)this) +
                   "-" + QString::number(i);
      TToonzImageP image = sl->getFrame(fids[i], false);
      assert(image);
      TImageCache::instance()->add(id, image->clone());
    }
  }

  void undo() const override {
    int i;
    // TPalette *palette = m_matchlinePalette->clone();
    // m_sl->setPalette(palette);
    for (i = 0; i < m_fids.size(); i++) {
      QString id = "DeleteMatchlineUndo" + QString::number((uintptr_t)this) +
                   "-" + QString::number(i);
      TImageP img = TImageCache::instance()->get(id, false)->cloneImage();

      m_sl->setFrame(m_fids[i], img);
      ToolUtils::updateSaveBox(m_sl, m_fids[i]);
    }
    // TApp::instance()->getPaletteController()->getCurrentLevelPalette()->setPalette(palette);

    if (m_xl) invalidateIcons(m_xl, m_fids);
    m_sl->setDirtyFlag(true);
    TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
  }

  void redo() const override {
    int i;

    // for (i=0; i<m_fids.size(); i++)
    //  images.push_back(m_sl->getFrame(m_fids[i], true));

    applyDeleteMatchline(m_sl, m_fids, m_indexes);
    for (i = 0; i < m_fids.size(); i++) {
      ToolUtils::updateSaveBox(m_sl, m_fids[i]);
    }
    if (m_xl) invalidateIcons(m_xl, m_fids);
    m_sl->setDirtyFlag(true);
    TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
  }

  int getSize() const override { return sizeof(*this); }

  ~DeleteMatchlineUndo() {
    int i;
    for (i = 0; i < m_fids.size(); i++)
      TImageCache::instance()->remove("DeleteMatchlineUndo" +
                                      QString::number((uintptr_t)this) + "-" +
                                      QString::number(i));
  }
};

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

class DeleteLevelUndo final : public TUndo {
  TXshLevelP m_xl;
public:
  DeleteLevelUndo(TXshLevel *xl) : m_xl(xl) {}

  void undo() const {
                ToonzScene *scene =
TApp::instance()->getCurrentScene()->getScene();
    scene->getLevelSet()->insertLevel(m_xl.getPointer());
                TApp::instance()->getCurrentScene()->notifyCastChange();
  }
  void redo() const {
                ToonzScene *scene =
TApp::instance()->getCurrentScene()->getScene();
    scene->getLevelSet()->removeLevel(m_xl.getPointer());
    TApp::instance()->getCurrentScene()->notifyCastChange();
  }

  int getSize() const {
    return sizeof *this + 100;
  }
};

} //namespace
*/

static bool removeLevel(TXshLevel *level) {
  TApp *app         = TApp::instance();
  ToonzScene *scene = app->getCurrentScene()->getScene();
  // if(scene->getChildStack()->getTopXsheet()->isLevelUsed(level))
  //	DVGui::error(QObject::tr("It is not possible to delete the used level
  //%1.").arg(QString::fromStdWString(level->getName())));//"E_CantDeleteUsedLevel_%1"
  // else
  {
    // TUndoManager *um = TUndoManager::manager();
    // um->add(new DeleteLevelUndo(level));
    scene->getLevelSet()->removeLevel(level, false);
    TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
    TApp::instance()->getCurrentScene()->notifyCastChange();
  }
  return true;
}

class MergeCmappedUndo final : public TUndo {
  TXshLevel *m_xl;
  int m_mergeCmappedSessionId;
  std::map<TFrameId, QString> m_images;
  TXshSimpleLevel *m_level;
  TPalette *m_palette;
  int m_column, m_mColumn;
  std::wstring m_fullpath;

public:
  MergeCmappedUndo(TXshLevel *xl, int mergeCmappedSessionId, int column,
                   TXshSimpleLevel *level,
                   const std::map<TFrameId, QString> &images, int mColumn,
                   TPalette *palette)
      : TUndo()
      , m_xl(xl)
      , m_mergeCmappedSessionId(mergeCmappedSessionId)
      , m_palette(palette->clone())
      , m_level(level)
      , m_column(column)
      , m_mColumn(mColumn)
      , m_images(images)

  {
    m_fullpath = m_xl->getPath().getWideString();
  }

  void undo() const override {
    std::map<TFrameId, QString>::const_iterator it = m_images.begin();
    TPalette *palette = m_palette->clone();
    m_level->setPalette(palette);
    std::vector<TFrameId> fids;
    for (; it != m_images.end(); ++it)  //, ++mit)
    {
      QString id = "MergeCmappedUndo" +
                   QString::number(m_mergeCmappedSessionId) + "-" +
                   QString::number(it->first.getNumber());
      TImageP img = TImageCache::instance()->get(id, false)->cloneImage();
      img->setPalette(palette);
      m_level->setFrame(it->first, img);
      fids.push_back(it->first);
    }

    removeLevel(m_xl);

    TApp::instance()
        ->getPaletteController()
        ->getCurrentLevelPalette()
        ->setPalette(palette);
    m_level->setDirtyFlag(true);
    TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
  }

  void redo() const override {
    mergeCmapped(m_column, m_mColumn, QString::fromStdWString(m_fullpath),
                 true);
  }

  int getSize() const override { return sizeof(*this); }

  ~MergeCmappedUndo() {
    std::map<TFrameId, QString>::const_iterator it = m_images.begin();
    for (; it != m_images.end(); ++it)  //, ++mit)
    {
      QString id = "MergeCmappedUndo" + QString::number((uintptr_t)this) + "-" +
                   QString::number(it->first.getNumber());
      TImageCache::instance()->remove(id);
    }
    delete m_palette;
  }
};

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

static int LastMatchlineIndex = -1;

class MergedPair {
public:
  TFrameId m_f0, m_f1;
  TAffine m_aff;
  MergedPair(const TFrameId &f0, const TFrameId &f1, const TAffine &aff)
      : m_f0(f0), m_f1(f1), m_aff(aff) {}

  bool operator<(const MergedPair &p) const {
    if (m_f0 != p.m_f0) return m_f0 < p.m_f0;
    if (m_f1 != p.m_f1) return m_f1 < p.m_f1;
    if (m_aff.a11 != p.m_aff.a11) return m_aff.a11 < p.m_aff.a11;
    if (m_aff.a12 != p.m_aff.a12) return m_aff.a12 < p.m_aff.a12;
    if (m_aff.a13 != p.m_aff.a13) return m_aff.a13 < p.m_aff.a13;
    if (m_aff.a21 != p.m_aff.a21) return m_aff.a21 < p.m_aff.a21;
    if (m_aff.a22 != p.m_aff.a22) return m_aff.a22 < p.m_aff.a22;
    if (m_aff.a23 != p.m_aff.a23) return m_aff.a23 < p.m_aff.a23;
    return false;
  }
};

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

void mergeCmapped(int column, int mColumn, const QString &fullpath,
                  bool isRedo) {
  static int MergeCmappedSessionId = 0;
  MergeCmappedSessionId++;

  TXsheet *xsh = TApp::instance()->getCurrentXsheet()->getXsheet();
  int start, end;
  int mStart, mEnd;
  xsh->getCellRange(column, start, end);
  xsh->getCellRange(mColumn, mStart, mEnd);

  if (start > end) return;
  std::vector<TXshCell> cell(std::max(end, mEnd) - std::min(start, mStart) + 1);
  std::vector<TXshCell> mCell(cell.size());

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

  if (mColumn != -1)
    xsh->getCells(std::min(start, mStart), mColumn, cell.size(), &(mCell[0]));

  TXshColumn *col  = xsh->getColumn(column);
  TXshColumn *mcol = xsh->getColumn(mColumn);

  std::vector<MergeCmappedPair> matchingLevels;

  std::map<MergedPair, TFrameId> computedMergedMap;

  TXshSimpleLevel *level = 0, *mLevel = 0;
  TXshLevel *xl;

  std::map<TFrameId, QString> images;
  double dpix = 0, dpiy = 0;
  for (int i = 0; i < (int)cell.size(); i++) {
    if (!cell[i].isEmpty() && dpix == 0)
      ((TToonzImageP)(cell[i].getImage(false)))->getDpi(dpix, dpiy);

    if (!level) {
      level = cell[i].getSimpleLevel();
      xl    = cell[i].m_level.getPointer();
    }
    if (!mLevel) mLevel = mCell[i].getSimpleLevel();
  }

  if (!level || !mLevel) return;

  TFilePath fp(fullpath.toStdString());

  TXshLevel *txl = level->getScene()->createNewLevel(
      level->getType(), fp.getWideName(), level->getResolution());
  TXshSimpleLevel *newLevel = txl->getSimpleLevel();
  newLevel->setPalette(level->getPalette());
  newLevel->clonePropertiesFrom(level);
  newLevel->setPath(fp);

  TApp::instance()->getCurrentScene()->notifySceneChanged();
  TApp::instance()->getCurrentScene()->notifyCastChange();
  TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();

  int count = 0;
  for (int i = 0; i < (int)cell.size(); i++) {
    if (cell[i].isEmpty() && mCell[i].isEmpty()) continue;

    TAffine imgAff, matchAff;

    getColumnPlacement(imgAff, xsh, std::min(start, mStart) + i, column, false);
    getColumnPlacement(matchAff, xsh, std::min(start, mStart) + i, mColumn,
                       false);

    // std::map<TFrameId, TFrameId>::iterator it;
    MergedPair mp(cell[i].isEmpty() ? TFrameId() : cell[i].getFrameId(),
                  mCell[i].isEmpty() ? TFrameId() : mCell[i].getFrameId(),
                  imgAff.inv() * matchAff);

    std::map<MergedPair, TFrameId>::iterator computedMergedIt =
        computedMergedMap.find(mp);

    if (computedMergedIt != computedMergedMap.end()) {
      TXshCell newCell(newLevel, computedMergedIt->second);
      xsh->setCell(i, column, newCell);
      cell[i] = newCell;
      continue;
    }

    TFrameId newFid(++count);  // level->getLastFid().getNumber()+1);
    TDimension dim = level->getResolution();
    TToonzImageP newImage;
    if (cell[i].isEmpty()) {
      newImage =
          TToonzImageP(TRasterCM32P(dim), TRect(0, 0, dim.lx - 1, dim.ly - 1));
      newImage->setDpi(dpix, dpiy);
    } else
      newImage = (TToonzImageP)(cell[i].getImage(false)->cloneImage());

    newImage->setPalette(level->getPalette());

    newLevel->setFrame(newFid, newImage);
    TXshCell newCell(newLevel, newFid);
    xsh->setCell(i, column, newCell);
    computedMergedMap[mp] = newCell.getFrameId();

    cell[i] = newCell;

    TImageP img   = cell[i].getImage(true);
    TImageP match = mCell[i].getImage(true);
    TFrameId fid  = cell[i].m_frameId;
    TFrameId mFid = mCell[i].m_frameId;

    if (!img || !match) continue;

    TToonzImageP timg   = (TToonzImageP)img;
    TToonzImageP tmatch = (TToonzImageP)match;

    QString id = "MergeCmappedUndo" + QString::number(MergeCmappedSessionId) +
                 "-" + QString::number(fid.getNumber());
    TImageCache::instance()->add(id, timg->clone());
    images[fid] = id;

    TAffine dpiAff  = getDpiAffine(level, fid);
    TAffine mdpiAff = getDpiAffine(mLevel, mFid);
    matchingLevels.push_back(MergeCmappedPair(cell[i], imgAff * dpiAff,
                                              mCell[i], matchAff * mdpiAff));
  }

  if (!isRedo) {
    TPalette *plt = level->getPalette();

    TPaletteHandle *pltHandle = new TPaletteHandle();
    pltHandle->setPalette(plt);
    int styleCount = plt->getStyleCount();

    TUndoManager::manager()->add(new MergeCmappedUndo(
        txl, MergeCmappedSessionId, column, level, images, mColumn, plt));
  }

  removeLevel(xl);
  QApplication::setOverrideCursor(Qt::WaitCursor);
  mergeCmapped(matchingLevels);
  QApplication::restoreOverrideCursor();

  for (int i = 0; i < (int)cell.size(); i++)  // the saveboxes must be updated
  {
    if (cell[i].isEmpty() || mCell[i].isEmpty()) continue;

    if (!cell[i].getImage(false) || !mCell[i].getImage(false)) continue;

    TXshSimpleLevel *sl = cell[i].getSimpleLevel();
    const TFrameId &fid = cell[i].m_frameId;

    ToolUtils::updateSaveBox(sl, fid);
    IconGenerator::instance()->invalidate(sl, fid);
    sl->setDirtyFlag(true);
  }

  newLevel->setDirtyFlag(true);
  TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
}

namespace {

const TXshCell *findCell(int column, const TFrameId &fid) {
  TXsheet *xsh = TApp::instance()->getCurrentXsheet()->getXsheet();
  int i;
  for (i = 0; i < xsh->getColumn(column)->getMaxFrame(); i++)
    if (xsh->getCell(i, column).getFrameId() == fid)
      return &(xsh->getCell(i, column));
  return 0;
}

bool contains(const std::vector<TFrameId> &v, const TFrameId &val) {
  int i;
  for (i = 0; i < (int)v.size(); i++)
    if (v[i] == val) return true;
  return false;
}

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

QString indexes2string(const std::set<TFrameId> fids) {
  if (fids.empty()) return "";

  QString str;

  std::set<TFrameId>::const_iterator it = fids.begin();

  str = QString::number(it->getNumber());

  while (it != fids.end()) {
    std::set<TFrameId>::const_iterator it1 = it;
    it1++;

    int lastVal = it->getNumber();
    while (it1 != fids.end() && it1->getNumber() == lastVal + 1) {
      lastVal = it1->getNumber();
      it1++;
    }

    if (lastVal != it->getNumber()) str += "-" + QString::number(lastVal);
    if (it1 == fids.end()) return str;

    str += ", " + QString::number(it1->getNumber());

    it = it1;
  }

  return str;
}

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

std::vector<int> string2Indexes(const QString &values) {
  std::vector<int> ret;
  int i, j;
  bool ok;
  QStringList vals = values.split(',', QString::SkipEmptyParts);
  for (i = 0; i < vals.size(); i++) {
    if (vals.at(i).contains('-')) {
      QStringList vals1 = vals.at(i).split('-', QString::SkipEmptyParts);
      if (vals1.size() != 2) return std::vector<int>();
      int from = vals1.at(0).toInt(&ok);
      if (!ok) return std::vector<int>();
      int to = vals1.at(1).toInt(&ok);
      if (!ok) return std::vector<int>();

      for (j = std::min(from, to); j <= std::max(from, to); j++)
        ret.push_back(j);
    } else {
      int val = vals.at(i).toInt(&ok);
      if (!ok) return std::vector<int>();
      ret.push_back(val);
    }
  }
  std::sort(ret.begin(), ret.end());
  return ret;
}

}  // namespace

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