#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <vector>

#include "core/Image.h"

namespace bmp2tile
{

constexpr int TileSize = 8;

// One tile's pixel data: TileSize*TileSize palette-index bytes, row-major.
using TilePixels = std::array<uint8_t, TileSize * TileSize>;

struct Tile
{
    TilePixels Pixels;
};

// One cell of the tilemap: which de-duplicated tile it uses, and whether
// that tile needs to be flipped to reproduce this cell (the "use tile
// mirroring" optimisation).
struct TilemapEntry
{
    int TileIndex = 0;
    bool bHFlip = false;
    bool bVFlip = false;
    bool bUseSpritePalette = false;  // bit 11 in the PNT word
    bool bHighPriority = false;      // bit 12 in the PNT word (in front of sprites)
};

struct TileSet
{
    std::vector<Tile> Tiles;            // de-duplicated unique tiles, in first-seen order
    std::vector<TilemapEntry> Tilemap;  // one entry per cell, row-major
    int TilesWide = 0;
    int TilesHigh = 0;
};

// Splits `image` into tiles, scanning left-to-right, top-to-bottom. Image
// dimensions don't need to be multiples of 8 (or 16); cells that run past
// the right/bottom edge are padded with palette index 0.
//
//   removeDuplicates: merge cells with pixel-identical content into one tile.
//   useMirroring:     also merge cells that are a horizontal, vertical, or
//                     180-degree-flip match of an already-seen tile.
//                     Has no effect if removeDuplicates is false.
//   sprite8x16:       extract 8x16 cells instead of 8x8. Tiles are stored as
//                     consecutive even/odd pairs (top half at index 2n, bottom
//                     half at 2n+1) matching the SMS/GG sprite hardware.
//                     TilemapEntry.TileIndex is always the even index; the
//                     hardware applies bHFlip/bVFlip to the whole 16-row
//                     sprite at display time. TilesHigh counts 16-pixel rows.
TileSet ExtractTiles(const IndexedImage& image, bool removeDuplicates = true, bool useMirroring = true,
                     bool sprite8x16 = false, bool useSpritePalette = false, bool highPriority = false);

// Packs one tile's chunky palette-index pixels into devkitSMS/SMSlib's
// native 32-byte separated-bitplane format: 8 rows, 4 bytes (bitplanes)
// per row, bit 7 of each byte is the leftmost pixel. Exposed (not just an
// implementation detail of SaveTilesBin) because the PSGaiden compressor
// needs tiles in this exact layout before it deinterleaves them further.
std::array<uint8_t, 32> PackTilePlanar(const Tile& tile);

// Formats `tiles`'/`tilemap`'s packed bytes (PackTilePlanar/
// PackTilemapEntry, concatenated -- i.e. exactly what SaveTilesBin/
// SaveTilemapBin write for CompressionFormat::Uncompressed) as a
// comma-separated list of hex byte literals, e.g. "$00,$FF,...", wrapped
// every 16 bytes onto its own line for readability. Mirrors
// FormatPaletteAsSmsHex's (core/Palette.h) minimal "just the hex list"
// convention -- suitable for pasting into a WLA DX .db directive, but
// doesn't wrap the directive keyword itself around it.
std::string FormatTilesAsSmsHex(const std::vector<Tile>& tiles);
// tilesWide: number of entries per row (= TileSet::TilesWide). Needed to emit
// one ".dw ..." line per tilemap row, matching the original's IncludeTextWriter.
std::string FormatTilemapAsSmsHex(const std::vector<TilemapEntry>& tilemap, int tilesWide);

// The compression formats devkitSMS/SMSlib can load, per SMSlib.h's
// SMS_load*compressedTiles*/SMS_loadSTMcompressedTileMap functions:
// STC0/STC4/PSGaiden/ZX7/aPLib/1bpp/2bpp for tiles, STM for tilemaps only.
//
// Every format here is implemented (see core/compressors/), each ported
// from real reference source rather than guessed at a bitstream -- a
// subtly-wrong compressed-tile encoding compiles fine and corrupts VRAM
// silently on real hardware, which is why none of these were written
// from a description alone:
//   - STC0/STC4: sverx's gfxcomp_stc0.c/gfxcomp_stc4.c.
//   - PSGaiden: bmp2tilecompressors' gfxcomp_phantasystargaiden.cpp.
//   - ZX7: Einar Saukas's public-domain reference encoder.
//   - STM: sverx's gfxcomp_stm.c.
//   - Raw1Bpp/Raw2Bpp: bmp2tilecompressors' gfxcomp_1bppraw.cpp/
//     gfxcomp_2bppraw.cpp -- not compression at all, just keeping the
//     first 1 or 2 bitplanes of each tile row and dropping the rest, for
//     tiles that only need 2 or 4 colours (see core/compressors/RawBpp.h).
//   - APLib: a hybrid port -- see core/compressors/ApLib.h for why (in
//     short: the two available permissively-licensed references both do
//     real suffix-array optimal parsing, more than this project needs;
//     the actual bitstream format is faithfully ported, the search for
//     what to encode is a simpler from-scratch one).
//
// NOTE: CompressionFormatCombo.cpp's dropdown loop iterates
// 0..CompressionFormat::STM inclusive, so STM needs to stay the last
// enumerator here -- add new formats before it, not after, or update
// that loop bound too.
enum class CompressionFormat
{
    Uncompressed,  // "raw"/"bin" in bmp2tilecompressors
    STC0,          // tiles only
    STC4,          // tiles only
    PSGaiden,      // tiles only
    ZX7,           // tiles + tilemap (via generic decompress-to-RAM)
    APLib,         // tiles + tilemap (ditto), UNSAFE_ for tiles
    Raw1Bpp,       // tiles only
    Raw2Bpp,       // tiles only
    STM,           // tilemap only -- keep this last, see NOTE above
};

const char* CompressionFormatName(CompressionFormat format);

// Suggested file extension for `format`, for save dialogs. Confirmed
// against real reference source for ZX7 ("zx7"), PSGaiden ("psgcompr"),
// STC0 ("stc0compr"), STC4 ("stc4compr"), STM ("stmcompr"), and
// Raw1Bpp/Raw2Bpp ("1bpp"/"2bpp") -- each is that format's own getExt().
// APLib's ("aplib") is a reasonable placeholder -- none of the reference
// implementations this was ported from/against were the canonical aPLib
// source, so there's no single getExt() to confirm it against.
const char* CompressionFormatFileExtension(CompressionFormat format);

bool SupportsTiles(CompressionFormat format);
bool SupportsTilemap(CompressionFormat format);

// Packs `tiles` into devkitSMS/SMSlib's native tile format -- 32 bytes per
// tile, 8 rows of 4 separated-bitplane bytes each (bit 7 = leftmost pixel)
// -- and writes the concatenated result to `path`. Throws
// std::runtime_error if `format` doesn't support tile data, or if the
// file can't be written.
void SaveTilesBin(const std::string& path, const std::vector<Tile>& tiles,
                   CompressionFormat format = CompressionFormat::Uncompressed);

// Returns a copy of `tilemap` with `offset` added to every entry's
// TileIndex. Matches the original tool's "index of first tile" option --
// useful when your tiles won't be loaded starting at VRAM tile 0 (e.g. a
// font loaded after other graphics, at tile 112). Throws
// std::runtime_error if any resulting index would fall outside the
// tilemap format's 9-bit tile index field (0-511) -- silently wrapping
// would silently corrupt the tilemap, which is worse than refusing.
std::vector<TilemapEntry> OffsetTilemapTileIndices(const std::vector<TilemapEntry>& tilemap, int offset);

// Packs `tilemap` into devkitSMS/SMSlib's native PNT entry format -- 2
// bytes per cell, little-endian, tile index in bits 0-8 with
// TILE_FLIPPED_X (bit 9) / TILE_FLIPPED_Y (bit 10) set from bHFlip/bVFlip
// -- and writes the concatenated result to `path`. `tilesWide` is only
// used by STM (its 1-byte width header; must be 0-255 for that format).
// `tileIndexOffset`, if non-zero, is applied via OffsetTilemapTileIndices
// before packing. Throws std::runtime_error if `format` doesn't support
// tilemap data, if `tileIndexOffset` pushes any index out of range, or if
// the file can't be written.
void SaveTilemapBin(const std::string& path, const std::vector<TilemapEntry>& tilemap, int tilesWide,
                     CompressionFormat format = CompressionFormat::Uncompressed, int tileIndexOffset = 0);

}  // namespace bmp2tile
