// Aseprite
// Copyright (C) 2019-2024  Igara Studio S.A.
// Copyright (C) 2017  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/ui/dithering_selector.h"

#include "app/app.h"
#include "app/console.h"
#include "app/extensions.h"
#include "app/i18n/strings.h"
#include "app/modules/palettes.h"
#include "app/ui/skin/skin_theme.h"
#include "app/util/conversion_to_surface.h"
#include "doc/image.h"
#include "doc/image_ref.h"
#include "doc/primitives.h"
#include "os/surface.h"
#include "os/system.h"
#include "render/dithering.h"
#include "render/gradient.h"
#include "render/quantization.h"
#include "ui/graphics.h"
#include "ui/listitem.h"
#include "ui/paint_event.h"
#include "ui/size_hint_event.h"

#include <algorithm>

namespace app {

using namespace ui;

namespace {

class DitherItem : public ListItem {
public:
  DitherItem(render::DitheringAlgorithm algo,
             const render::DitheringMatrix& matrix,
             const std::string& text)
    : ListItem(text)
    , m_matrixOnly(false)
    , m_dithering(algo, matrix)
    , m_preview(nullptr)
    , m_palId(0)
    , m_palMods(0)
  {
  }

  DitherItem(const render::DitheringMatrix& matrix, const std::string& text)
    : ListItem(text)
    , m_matrixOnly(true)
    , m_dithering(render::DitheringAlgorithm::None, matrix)
    , m_preview(nullptr)
    , m_palId(0)
    , m_palMods(0)
  {
  }

  render::DitheringAlgorithm algo() const { return m_dithering.algorithm(); }

  render::DitheringMatrix matrix() const { return m_dithering.matrix(); }

private:
  os::Surface* preview()
  {
    const doc::Palette* palette = get_current_palette();
    ASSERT(palette);

    if (m_preview) {
      // Reuse the preview in case that the palette is exactly the same
      if (palette->id() == m_palId && palette->getModifications() == m_palMods)
        return m_preview.get();

      // In other case regenerate the preview for the current palette
      m_preview.reset();
    }

    const int w = 128, h = 16;
    doc::ImageRef image1(doc::Image::create(doc::IMAGE_RGB, w, h));
    render_rgba_linear_gradient(image1.get(),
                                gfx::Point(0, 0),
                                gfx::Point(0, 0),
                                gfx::Point(w - 1, 0),
                                doc::rgba(0, 0, 0, 255),
                                doc::rgba(255, 255, 255, 255),
                                (m_matrixOnly ? m_dithering.matrix() : render::DitheringMatrix()));

    doc::ImageRef image2;
    if (m_matrixOnly) {
      image2 = image1;
    }
    else {
      image2.reset(doc::Image::create(doc::IMAGE_INDEXED, w, h));
      doc::clear_image(image2.get(), 0);
      render::convert_pixel_format(image1.get(),
                                   image2.get(),
                                   IMAGE_INDEXED,
                                   m_dithering,
                                   nullptr,
                                   palette,
                                   true,
                                   -1,
                                   nullptr);
    }

    m_preview = os::System::instance()->makeRgbaSurface(w, h);
    convert_image_to_surface(image2.get(), palette, m_preview.get(), 0, 0, 0, 0, w, h);

    m_palId = palette->id();
    m_palMods = palette->getModifications();
    return m_preview.get();
  }

  void onSizeHint(SizeHintEvent& ev) override
  {
    gfx::Size sz = textSize();

    sz.w = std::max(sz.w, preview()->width() * guiscale()) + 4 * guiscale();
    sz.h += 6 * guiscale() + preview()->height() * guiscale();

    ev.setSizeHint(sz);
  }

  void onPaint(PaintEvent& ev) override
  {
    Graphics* g = ev.graphics();
    auto theme = skin::SkinTheme::get(this);

    gfx::Color fg, bg;
    if (isSelected()) {
      fg = theme->colors.listitemSelectedText();
      bg = theme->colors.listitemSelectedFace();
    }
    else {
      fg = theme->colors.listitemNormalText();
      bg = theme->colors.listitemNormalFace();
    }

    gfx::Rect rc = clientBounds();
    g->fillRect(bg, rc);

    gfx::Size textsz = textSize();
    g->drawText(text(), fg, bg, gfx::Point(rc.x + 2 * guiscale(), rc.y + 2 * guiscale()));

    ui::Paint paint;
    paint.blendMode(os::BlendMode::SrcOver);

    g->drawSurface(preview(),
                   preview()->bounds(),
                   gfx::Rect(rc.x + 2 * guiscale(),
                             rc.y + 4 * guiscale() + textsz.h,
                             preview()->width() * guiscale(),
                             preview()->height() * guiscale()),
                   os::Sampling(),
                   &paint);
  }

  bool m_matrixOnly;
  render::Dithering m_dithering;
  os::SurfaceRef m_preview;
  doc::ObjectId m_palId;
  int m_palMods;
};

} // anonymous namespace

DitheringSelector::DitheringSelector(Type type) : m_type(type)
{
  setId("dithering_selector"); // Used for label buddies.

  Extensions& extensions = App::instance()->extensions();

  // If an extension with "ditheringMatrices" is disable/enable, we
  // regenerate this DitheringSelector
  m_extChanges = extensions.DitheringMatricesChange.connect([this] { regenerate(); });

  setUseCustomWidget(true);
  regenerate();
}

void DitheringSelector::onInitTheme(ui::InitThemeEvent& ev)
{
  ComboBox::onInitTheme(ev);
  if (getItem(0))
    setSizeHint(calcItemSizeHint(0));
}

void DitheringSelector::setSelectedItemByName(const std::string& name)
{
  int index = findItemIndex(name);
  setSelectedItemIndex(index);
  regenerate(index);
}

void DitheringSelector::regenerate(int selectedItemIndex)
{
  deleteAllItems();

  Extensions& extensions = App::instance()->extensions();
  auto ditheringMatrices = extensions.ditheringMatrices();

  switch (m_type) {
    case SelectBoth:
      addItem(new DitherItem(render::DitheringAlgorithm::None,
                             render::DitheringMatrix(),
                             Strings::dithering_selector_no_dithering()));
      for (const auto* it : ditheringMatrices) {
        try {
          addItem(new DitherItem(render::DitheringAlgorithm::Ordered,
                                 it->matrix(),
                                 Strings::dithering_selector_ordered_dithering() + it->name()));
        }
        catch (const std::exception& e) {
          LOG(ERROR, "%s\n", e.what());
          Console::showException(e);
        }
      }
      for (const auto* it : ditheringMatrices) {
        try {
          addItem(new DitherItem(render::DitheringAlgorithm::Old,
                                 it->matrix(),
                                 Strings::dithering_selector_old_dithering() + it->name()));
        }
        catch (const std::exception& e) {
          LOG(ERROR, "%s\n", e.what());
          Console::showException(e);
        }
      }
      addItem(new DitherItem(render::DitheringAlgorithm::ErrorDiffusion,
                             render::DitheringMatrix(),
                             Strings::dithering_selector_floyd_steinberg()));
      break;
    case SelectMatrix:
      addItem(
        new DitherItem(render::DitheringMatrix(), Strings::dithering_selector_no_dithering()));
      for (const auto* it : ditheringMatrices) {
        try {
          addItem(new DitherItem(it->matrix(), it->name()));
        }
        catch (const std::exception& e) {
          LOG(ERROR, "%s\n", e.what());
          Console::showException(e);
        }
      }
      break;
  }
  selectedItemIndex = std::clamp(selectedItemIndex, 0, std::max(0, getItemCount() - 1));
  setSelectedItemIndex(selectedItemIndex);
  setSizeHint(calcItemSizeHint(selectedItemIndex));
}

render::DitheringAlgorithm DitheringSelector::ditheringAlgorithm()
{
  auto item = static_cast<DitherItem*>(getSelectedItem());
  if (item)
    return item->algo();
  else
    return render::DitheringAlgorithm::None;
}

render::DitheringMatrix DitheringSelector::ditheringMatrix()
{
  auto item = static_cast<DitherItem*>(getSelectedItem());
  if (item)
    return item->matrix();
  else
    return render::DitheringMatrix();
}

gfx::Size DitheringSelector::calcItemSizeHint(int index)
{
  auto item = getItem(index);
  if (item) {
    return item->sizeHint()
           // Added offset to prevent unnecessary scrollbar in X dimension
           + gfx::Size(6 * guiscale(), 0);
  }
  else {
    return gfx::Size(0, 0);
  }
}

} // namespace app
