#ifndef YAZE_APP_GFX_PALETTE_H
#define YAZE_APP_GFX_PALETTE_H

#include <SDL.h>
#include <imgui/imgui.h>

#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <vector>

#include "absl/base/casts.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "app/core/constants.h"
#include "app/gfx/snes_color.h"

namespace yaze {
namespace app {
namespace gfx {

/**
 * @brief Primitive of a SNES color palette.
 */
struct snes_palette {
  uint id;            /**< ID of the palette. */
  uint size;          /**< Size of the palette. */
  snes_color* colors; /**< Pointer to the colors in the palette. */
};
using snes_palette = struct snes_palette;

uint32_t GetPaletteAddress(const std::string& group_name, size_t palette_index,
                           size_t color_index);

/**
 * @brief Represents a palette of colors for the Super Nintendo Entertainment
 * System (SNES).
 *
 * The `SnesPalette` class provides functionality to create, modify, and access
 * colors in an SNES palette. It supports various constructors to initialize the
 * palette with different types of data. The palette can be modified by adding
 * or changing colors, and it can be cleared to remove all colors. Colors in the
 * palette can be accessed using index-based access or through the `GetColor`
 * method. The class also provides a method to create a sub-palette by selecting
 * a range of colors from the original palette.
 */
class SnesPalette {
 public:
  template <typename T>
  explicit SnesPalette(const std::vector<T>& data) {
    for (const auto& item : data) {
      colors.push_back(SnesColor(item));
    }
    size_ = data.size();
  }

  SnesPalette() = default;

  explicit SnesPalette(uint8_t mSize);
  explicit SnesPalette(char* snesPal);
  explicit SnesPalette(const unsigned char* snes_pal);
  explicit SnesPalette(const std::vector<ImVec4>&);
  explicit SnesPalette(const std::vector<snes_color>&);
  explicit SnesPalette(const std::vector<SnesColor>&);

  SDL_Palette* GetSDL_Palette();

  void Create(const std::vector<SnesColor>& cols) {
    for (const auto& each : cols) {
      colors.push_back(each);
    }
    size_ = cols.size();
  }

  void AddColor(SnesColor color) {
    colors.push_back(color);
    size_++;
  }

  void AddColor(snes_color color) {
    colors.emplace_back(color);
    size_++;
  }

  absl::StatusOr<SnesColor> GetColor(int i) const {
    if (i > size_) {
      return absl::InvalidArgumentError("SnesPalette: Index out of bounds");
    }
    return colors[i];
  }

  auto mutable_color(int i) { return &colors[i]; }

  void Clear() {
    colors.clear();
    size_ = 0;
  }

  auto size() const { return colors.size(); }

  SnesColor& operator[](int i) {
    if (i > size_) {
      std::cout << "SNESPalette: Index out of bounds" << std::endl;
      return colors[0];
    }
    return colors[i];
  }

  void operator()(int i, const SnesColor& color) {
    if (i >= size_) {
      throw std::out_of_range("SNESPalette: Index out of bounds");
    }
    colors[i] = color;
  }

  void operator()(int i, const ImVec4& color) {
    if (i >= size_) {
      throw std::out_of_range("SNESPalette: Index out of bounds");
    }
    colors[i].set_rgb(color);
    colors[i].set_modified(true);
  }

  SnesPalette sub_palette(int start, int end) const {
    SnesPalette pal;
    for (int i = start; i < end; i++) {
      pal.AddColor(colors[i]);
    }
    return pal;
  }

 private:
  int size_ = 0;                 /**< The size of the palette. */
  std::vector<SnesColor> colors; /**< The colors in the palette. */
};

SnesPalette ReadPaletteFromRom(int offset, int num_colors, const uint8_t* rom);

std::array<float, 4> ToFloatArray(const SnesColor& color);

/**
 * @brief Represents a group of palettes.
 *
 * Supports adding palettes and colors, clearing the group, and accessing
 * palettes and colors by index.
 */
struct PaletteGroup {
  PaletteGroup() = default;

  explicit PaletteGroup(uint8_t mSize);

  absl::Status AddPalette(SnesPalette pal) {
    palettes.emplace_back(pal);
    size_ = palettes.size();
    return absl::OkStatus();
  }

  absl::Status AddColor(SnesColor color) {
    if (size_ == 0) {
      palettes.emplace_back();
    }
    palettes[0].AddColor(color);
    return absl::OkStatus();
  }

  void Clear() {
    palettes.clear();
    size_ = 0;
  }

  auto name() const { return name_; }
  auto size() const { return palettes.size(); }
  auto mutable_palette(int i) { return &palettes[i]; }
  auto palette(int i) const { return palettes[i]; }

  SnesPalette operator[](int i) {
    if (i > size_) {
      std::cout << "PaletteGroup: Index out of bounds" << std::endl;
      return palettes[0];
    }
    return palettes[i];
  }

  const SnesPalette& operator[](int i) const {
    if (i > size_) {
      std::cout << "PaletteGroup: Index out of bounds" << std::endl;
      return palettes[0];
    }
    return palettes[i];
  }

  absl::Status operator()(int i, const SnesColor& color) {
    if (i >= size_) {
      return absl::InvalidArgumentError("PaletteGroup: Index out of bounds");
    }
    palettes[i](0, color);
    return absl::OkStatus();
  }

  absl::Status operator()(int i, const ImVec4& color) {
    if (i >= size_) {
      return absl::InvalidArgumentError("PaletteGroup: Index out of bounds");
    }
    palettes[i](0, color);
    return absl::OkStatus();
  }

 private:
  int size_ = 0;
  std::string name_;
  std::vector<SnesPalette> palettes;
};

/**
 * @brief Represents a mapping of palette groups.
 *
 * Originally, this was an actual std::unordered_map but since the palette
 * groups supported never change, it was changed to a struct with a method to
 * get the group by name.
 */
struct PaletteGroupMap {
  PaletteGroup overworld_main;
  PaletteGroup overworld_aux;
  PaletteGroup overworld_animated;
  PaletteGroup hud;
  PaletteGroup global_sprites;
  PaletteGroup armors;
  PaletteGroup swords;
  PaletteGroup shields;
  PaletteGroup sprites_aux1;
  PaletteGroup sprites_aux2;
  PaletteGroup sprites_aux3;
  PaletteGroup dungeon_main;
  PaletteGroup grass;
  PaletteGroup object_3d;
  PaletteGroup overworld_mini_map;

  auto get_group(const std::string& group_name) {
    if (group_name == "ow_main") {
      return &overworld_main;
    } else if (group_name == "ow_aux") {
      return &overworld_aux;
    } else if (group_name == "ow_animated") {
      return &overworld_animated;
    } else if (group_name == "hud") {
      return &hud;
    } else if (group_name == "global_sprites") {
      return &global_sprites;
    } else if (group_name == "armors") {
      return &armors;
    } else if (group_name == "swords") {
      return &swords;
    } else if (group_name == "shields") {
      return &shields;
    } else if (group_name == "sprites_aux1") {
      return &sprites_aux1;
    } else if (group_name == "sprites_aux2") {
      return &sprites_aux2;
    } else if (group_name == "sprites_aux3") {
      return &sprites_aux3;
    } else if (group_name == "dungeon_main") {
      return &dungeon_main;
    } else if (group_name == "grass") {
      return &grass;
    } else if (group_name == "3d_object") {
      return &object_3d;
    } else if (group_name == "ow_mini_map") {
      return &overworld_mini_map;
    } else {
      throw std::out_of_range("PaletteGroupMap: Group not found");
    }
  }

  template <typename Func>
  void for_each(Func&& func) {
    func(overworld_main);
    func(overworld_aux);
    func(overworld_animated);
    func(hud);
    func(global_sprites);
    func(armors);
    func(swords);
    func(shields);
    func(sprites_aux1);
    func(sprites_aux2);
    func(sprites_aux3);
    func(dungeon_main);
    func(grass);
    func(object_3d);
    func(overworld_mini_map);
  }
};

absl::StatusOr<PaletteGroup> CreatePaletteGroupFromColFile(
    std::vector<SnesColor>& colors);

absl::StatusOr<PaletteGroup> CreatePaletteGroupFromLargePalette(
    SnesPalette& palette);

/**
 * @brief Loads all the palettes for the game.
 *
 * This function loads all the palettes for the game, including overworld,
 * HUD, armor, swords, shields, sprites, dungeon, grass, and 3D object
 * palettes. It also adds the loaded palettes to their respective palette
 * groups.
 *
 */
absl::Status LoadAllPalettes(const Bytes& rom_data, PaletteGroupMap& groups);

/**
 * @brief Represents a set of palettes used in a SNES graphics system.
 */
struct Paletteset {
  /**
   * @brief Default constructor for Paletteset.
   */
  Paletteset() = default;

  /**
   * @brief Constructor for Paletteset.
   * @param main The main palette.
   * @param animated The animated palette.
   * @param aux1 The first auxiliary palette.
   * @param aux2 The second auxiliary palette.
   * @param background The background color.
   * @param hud The HUD palette.
   * @param spr The sprite palette.
   * @param spr2 The second sprite palette.
   * @param comp The composite palette.
   */
  Paletteset(gfx::SnesPalette main, gfx::SnesPalette animated,
             gfx::SnesPalette aux1, gfx::SnesPalette aux2,
             gfx::SnesColor background, gfx::SnesPalette hud,
             gfx::SnesPalette spr, gfx::SnesPalette spr2, gfx::SnesPalette comp)
      : main(main),
        animated(animated),
        aux1(aux1),
        aux2(aux2),
        background(background),
        hud(hud),
        spr(spr),
        spr2(spr2),
        composite(comp) {}

  gfx::SnesPalette main;      /**< The main palette. */
  gfx::SnesPalette animated;  /**< The animated palette. */
  gfx::SnesPalette aux1;      /**< The first auxiliary palette. */
  gfx::SnesPalette aux2;      /**< The second auxiliary palette. */
  gfx::SnesColor background;  /**< The background color. */
  gfx::SnesPalette hud;       /**< The HUD palette. */
  gfx::SnesPalette spr;       /**< The sprite palette. */
  gfx::SnesPalette spr2;      /**< The second sprite palette. */
  gfx::SnesPalette composite; /**< The composite palette. */
};

}  // namespace gfx
}  // namespace app
}  // namespace yaze

#endif  // YAZE_APP_GFX_PALETTE_H