// Aseprite
// Copyright (C) 2019-2024  Igara Studio S.A.
// Copyright (C) 2001-2018  David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.

#ifdef HAVE_CONFIG_H
  #include "config.h"
#endif

#include "app/app.h"
#include "app/cmd/set_palette.h"
#include "app/commands/new_params.h"
#include "app/console.h"
#include "app/context.h"
#include "app/context_access.h"
#include "app/job.h"
#include "app/pref/preferences.h"
#include "app/sprite_job.h"
#include "app/transaction.h"
#include "app/ui/color_bar.h"
#include "app/ui/rgbmap_algorithm_selector.h"
#include "app/ui_context.h"
#include "doc/palette.h"
#include "doc/sprite.h"
#include "render/quantization.h"
#include "ui/manager.h"

#include "palette_from_sprite.xml.h"

#include <algorithm>

namespace app {

struct ColorQuantizationParams : public NewParams {
  Param<bool> ui{ this, true, "ui" };
  Param<bool> withAlpha{ this, true, "withAlpha" };
  Param<int> maxColors{ this, 256, "maxColors" };
  Param<bool> useRange{ this, false, "useRange" };
  Param<RgbMapAlgorithm> algorithm{ this, RgbMapAlgorithm::DEFAULT, "algorithm" };
};

class PaletteFromSpriteWindow : public app::gen::PaletteFromSprite {
public:
  PaletteFromSpriteWindow()
  {
    rgbmapAlgorithmPlaceholder()->addChild(&m_algoSelector);

    advancedCheck()->Click.connect([this]() {
      advanced()->setVisible(advancedCheck()->isSelected());
      expandWindow(sizeHint());
    });
  }

  doc::RgbMapAlgorithm algorithm() { return m_algoSelector.algorithm(); }

  void algorithm(const doc::RgbMapAlgorithm mapAlgo) { m_algoSelector.algorithm(mapAlgo); }

private:
  RgbMapAlgorithmSelector m_algoSelector;
};

class ColorQuantizationCommand : public CommandWithNewParams<ColorQuantizationParams> {
public:
  ColorQuantizationCommand();

protected:
  bool onEnabled(Context* context) override;
  void onExecute(Context* context) override;
};

ColorQuantizationCommand::ColorQuantizationCommand()
  : CommandWithNewParams<ColorQuantizationParams>(CommandId::ColorQuantization(), CmdRecordableFlag)
{
}

bool ColorQuantizationCommand::onEnabled(Context* ctx)
{
  return ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable);
}

void ColorQuantizationCommand::onExecute(Context* ctx)
{
  const bool ui = (params().ui() && ctx->isUIAvailable());
  auto& pref = Preferences::instance();
  bool withAlpha = params().withAlpha();
  int maxColors = params().maxColors();
  RgbMapAlgorithm algorithm = params().algorithm();
  bool createPal;

  Site site = ctx->activeSite();
  PalettePicks entries = site.selectedColors();

  if (ui) {
    PaletteFromSpriteWindow window;
    {
      ContextReader reader(ctx);
      const Palette* curPalette = site.sprite()->palette(site.frame());

      if (!params().algorithm.isSet())
        algorithm = pref.quantization.rgbmapAlgorithm();
      if (!params().withAlpha.isSet())
        withAlpha = pref.quantization.withAlpha();

      const bool advanced = pref.quantization.advanced();
      window.advancedCheck()->setSelected(advanced);
      window.advanced()->setVisible(advanced);

      window.algorithm(algorithm);
      window.newPalette()->setSelected(true);
      window.ncolors()->setTextf("%d", maxColors);

      if (entries.picks() > 1) {
        window.currentRange()->setTextf("%s, %d color(s)",
                                        window.currentRange()->text().c_str(),
                                        entries.picks());
      }
      else
        window.currentRange()->setEnabled(false);

      window.currentPalette()->setTextf("%s, %d color(s)",
                                        window.currentPalette()->text().c_str(),
                                        curPalette->size());
    }

    window.openWindowInForeground();
    if (window.closer() != window.ok())
      return;

    maxColors = window.ncolors()->textInt();
    withAlpha = window.alphaChannel()->isSelected();
    algorithm = window.algorithm();

    pref.quantization.withAlpha(withAlpha);
    pref.quantization.advanced(window.advancedCheck()->isSelected());

    if (window.newPalette()->isSelected()) {
      createPal = true;
    }
    else {
      createPal = false;
      if (window.currentPalette()->isSelected()) {
        entries = PalettePicks(site.palette()->size());
        entries.all();
      }
    }
  }
  else {
    createPal = (!params().useRange());
  }

  if (createPal) {
    entries = PalettePicks(std::max(1, maxColors));
    entries.all();
  }
  if (entries.picks() == 0)
    return;

  try {
    Doc* doc = site.document();
    Sprite* sprite = site.sprite();
    frame_t frame = site.frame();
    const Palette* curPalette = site.sprite()->palette(frame);
    Palette tmpPalette(frame, entries.picks());

    SpriteJob job(ctx, doc, "Color Quantization", ui);
    const bool newBlend = pref.experimental.newBlend();
    job.startJobWithCallback([sprite,
                              withAlpha,
                              curPalette,
                              &tmpPalette,
                              &job,
                              &entries,
                              newBlend,
                              algorithm,
                              createPal,
                              site,
                              frame](Tx& tx) {
      render::create_palette_from_sprite(sprite,
                                         0,
                                         sprite->lastFrame(),
                                         withAlpha,
                                         &tmpPalette,
                                         &job, // SpriteJob is a render::TaskDelegate
                                         newBlend,
                                         algorithm);

      std::unique_ptr<Palette> newPalette(new Palette(createPal ? tmpPalette : *site.palette()));

      if (createPal) {
        entries = PalettePicks(newPalette->size());
        entries.all();
      }

      int i = 0, j = 0;
      for (bool state : entries) {
        if (state)
          newPalette->setEntry(i, tmpPalette.getEntry(j++));
        ++i;
      }

      if (*curPalette != *newPalette)
        tx(new cmd::SetPalette(sprite, frame, newPalette.get()));
    });
    job.waitJob();
    if (job.isCanceled())
      return;
  }
  catch (const base::Exception& e) {
    Console::showException(e);
  }
}

Command* CommandFactory::createColorQuantizationCommand()
{
  return new ColorQuantizationCommand;
}

} // namespace app
