// Aseprite Code Generator
// Copyright (c) 2021-2024 Igara Studio S.A.
// Copyright (c) 2014-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.

#include "gen/ui_class.h"

#include "base/exception.h"
#include "base/file_handle.h"
#include "base/fs.h"
#include "base/string.h"
#include "gen/common.h"

#include <iostream>
#include <set>
#include <vector>

using namespace tinyxml2;
using XmlElements = std::vector<XMLElement*>;

namespace {

struct Item {
  std::string xmlid;
  std::string cppid;
  std::string type;
  std::string incl;
  const Item& typeIncl(const char* type, const char* incl)
  {
    this->type = type;
    this->incl = incl;
    return *this;
  }
};

} // namespace

static XMLElement* find_element_by_id(XMLElement* elem, const std::string& thisId)
{
  const char* id = elem->Attribute("id");
  if (id && id == thisId)
    return elem;

  XMLElement* child = elem->FirstChildElement();
  while (child) {
    XMLElement* match = find_element_by_id(child, thisId);
    if (match)
      return match;

    child = child->NextSiblingElement();
  }

  return NULL;
}

static void collect_widgets_with_ids(XMLElement* elem, XmlElements& widgets)
{
  XMLElement* child = elem->FirstChildElement();
  while (child) {
    const char* id = child->Attribute("id");
    if (id)
      widgets.push_back(child);
    collect_widgets_with_ids(child, widgets);
    child = child->NextSiblingElement();
  }
}

static Item convert_to_item(XMLElement* elem)
{
  static std::string parent;
  const std::string name = elem->Value();
  if (name != "item")
    parent = name;

  Item item;
  if (elem->Attribute("id")) {
    item.xmlid = elem->Attribute("id");
    item.cppid = convert_xmlid_to_cppid(item.xmlid, false);
  }

  if (name == "box")
    return item.typeIncl("ui::Box", "ui/box.h");
  if (name == "boxfiller")
    return item.typeIncl("ui::Box", "ui/box.h");
  if (name == "button")
    return item.typeIncl("ui::Button", "ui/button.h");
  if (name == "buttonset")
    return item.typeIncl("app::ButtonSet", "app/ui/button_set.h");
  if (name == "check") {
    if (elem->Attribute("pref") != nullptr)
      return item.typeIncl("BoolPrefWidget<ui::CheckBox>", "app/ui/pref_widget.h");
    else
      return item.typeIncl("ui::CheckBox", "ui/button.h");
  }
  if (name == "colorpicker")
    return item.typeIncl("app::ColorButton", "app/ui/color_button.h");
  if (name == "combobox")
    return item.typeIncl("ui::ComboBox", "ui/combobox.h");
  if (name == "dropdownbutton")
    return item.typeIncl("app::DropDownButton", "app/ui/drop_down_button.h");
  if (name == "entry")
    return item.typeIncl("ui::Entry", "ui/entry.h");
  if (name == "textedit")
    return item.typeIncl("ui::TextEdit", "ui/textedit.h");
  if (name == "expr")
    return item.typeIncl("app::ExprEntry", "app/ui/expr_entry.h");
  if (name == "filename")
    return item.typeIncl("app::FilenameField", "app/ui/filename_field.h");
  if (name == "font")
    return item.typeIncl("app::FontEntry", "app/ui/font_entry.h");
  if (name == "grid")
    return item.typeIncl("ui::Grid", "ui/grid.h");
  if (name == "hbox")
    return item.typeIncl("ui::HBox", "ui/box.h");
  if (name == "image")
    return item.typeIncl("ui::ImageView", "ui/image_view.h");
  if (name == "item" && parent == "buttonset")
    return item.typeIncl("app::ButtonSet::Item", "app/ui/button_set.h");
  if (name == "label")
    return item.typeIncl("ui::Label", "ui/label.h");
  if (name == "link")
    return item.typeIncl("ui::LinkLabel", "ui/link_label.h");
  if (name == "listbox")
    return item.typeIncl("ui::ListBox", "ui/listbox.h");
  if (name == "listitem")
    return item.typeIncl("ui::ListItem", "ui/listitem.h");
  if (name == "panel")
    return item.typeIncl("ui::Panel", "ui/panel.h");
  if (name == "popupwindow")
    return item.typeIncl("ui::PopupWindow", "ui/popup_window.h");
  if (name == "radio")
    return item.typeIncl("ui::RadioButton", "ui/button.h");
  if (name == "search")
    return item.typeIncl("app::SearchEntry", "app/ui/search_entry.h");
  if (name == "separator")
    return item.typeIncl("ui::Separator", "ui/separator.h");
  if (name == "slider")
    return item.typeIncl("ui::Slider", "ui/slider.h");
  if (name == "alphaslider" || name == "opacityslider")
    return item.typeIncl("app::AlphaSlider", "app/ui/alpha_slider.h");
  if (name == "splitter")
    return item.typeIncl("ui::Splitter", "ui/splitter.h");
  if (name == "textbox")
    return item.typeIncl("ui::TextBox", "ui/textbox.h");
  if (name == "tipwindow")
    return item.typeIncl("ui::TipWindow", "ui/tooltips.h");
  if (name == "vbox")
    return item.typeIncl("ui::VBox", "ui/box.h");
  if (name == "view")
    return item.typeIncl("ui::View", "ui/view.h");
  if (name == "window")
    return item.typeIncl("ui::Window", "ui/window.h");

  throw base::Exception("Unknown widget name: " + name);
}

void gen_ui_class(XMLDocument* doc, const std::string& inputFn, const std::string& widgetId)
{
  std::cout << "// Don't modify, generated file from " << inputFn << "\n"
            << "\n";

  XMLHandle handle(doc);
  XMLElement* elem = handle.FirstChildElement("gui").ToElement();
  elem = find_element_by_id(elem, widgetId);
  if (!elem) {
    std::cout << "#error Widget not found: " << widgetId << "\n";
    return;
  }

  bool hasTooltips = false;
  std::vector<Item> items;
  {
    XmlElements xmlWidgets;
    collect_widgets_with_ids(elem, xmlWidgets);
    for (XMLElement* elem : xmlWidgets) {
      const char* id = elem->Attribute("id");
      if (!id)
        continue;
      items.push_back(convert_to_item(elem));

      if (!hasTooltips && elem->Attribute("tooltip"))
        hasTooltips = true;
    }
  }

  if (hasTooltips)
    items.push_back({ "tooltip_manager", "tooltipManager", "ui::TooltipManager", "ui/tooltips.h" });

  std::string className = convert_xmlid_to_cppid(widgetId, true);
  std::string fnUpper = base::string_to_upper(base::get_file_title(inputFn));
  Item mainWidget = convert_to_item(elem);

  std::set<std::string> headerFiles;
  headerFiles.insert("app/find_widget.h");
  headerFiles.insert("app/load_widget.h");
  headerFiles.insert(mainWidget.incl);
  for (const Item& item : items)
    headerFiles.insert(item.incl);

  std::cout << "#ifndef GENERATED_" << fnUpper << "_H_INCLUDED\n"
            << "#define GENERATED_" << fnUpper << "_H_INCLUDED\n"
            << "#pragma once\n"
            << "\n";
  for (const auto& incl : headerFiles)
    std::cout << "#include \"" << incl << "\"\n";
  std::cout << "\n"
            << "namespace app {\n"
            << "namespace gen {\n"
            << "\n"
            << "  class " << className << " : public " << mainWidget.type << " {\n"
            << "  public:\n"
            << "    " << className << "()";

  // Special ctor for base class
  if (mainWidget.type == "ui::Window") {
    const char* desktop = elem->Attribute("desktop");
    if (desktop && std::string(desktop) == "true")
      std::cout << " : ui::Window(ui::Window::DesktopWindow)";
    else
      std::cout << " : ui::Window(ui::Window::WithTitleBar)";
  }

  std::cout << " {\n"
            << "      app::load_widget(\"" << base::get_file_name(inputFn) << "\", \"" << widgetId
            << "\", this);\n"
            << "      app::finder(this)\n";

  for (const Item& item : items) {
    std::cout << "        >> \"" << item.xmlid << "\" >> m_" << item.cppid << "\n";
  }

  std::cout << "      ;\n"
            << "    }\n"
            << "\n";

  for (const Item& item : items) {
    std::cout << "    " << item.type << "* " << item.cppid << "() const { return m_" << item.cppid
              << "; }\n";
  }

  std::cout << "\n"
            << "  private:\n";

  for (const Item& item : items) {
    std::cout << "    " << item.type << "* m_" << item.cppid << ";\n";
  }

  std::cout << "  };\n"
            << "\n"
            << "} // namespace gen\n"
            << "} // namespace app\n"
            << "\n"
            << "#endif\n";
}
