#include "Cli.h"

#include <algorithm>
#include <cctype>
#include <cstdio>
#include <optional>
#include <stdexcept>
#include <string>

#include "core/Image.h"
#include "core/Palette.h"
#include "core/TileSet.h"

namespace bmp2tile
{

namespace
{

std::string ToLower(std::string s)
{
    std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
    return s;
}

std::optional<CompressionFormat> ParseCompressionFormat(const std::string& raw)
{
    const std::string s = ToLower(raw);
    if (s == "uncompressed" || s == "raw" || s == "bin") return CompressionFormat::Uncompressed;
    if (s == "zx7") return CompressionFormat::ZX7;
    if (s == "psgaiden") return CompressionFormat::PSGaiden;
    if (s == "stc0") return CompressionFormat::STC0;
    if (s == "stc4") return CompressionFormat::STC4;
    if (s == "stm") return CompressionFormat::STM;
    if (s == "aplib") return CompressionFormat::APLib;
    if (s == "1bpp") return CompressionFormat::Raw1Bpp;
    if (s == "2bpp") return CompressionFormat::Raw2Bpp;
    return std::nullopt;
}

std::optional<PaletteFormat> ParsePaletteFormat(const std::string& raw)
{
    const std::string s = ToLower(raw);
    if (s == "sms") return PaletteFormat::Sms;
    if (s == "gg") return PaletteFormat::Gg;
    if (s == "cl123") return PaletteFormat::Cl123;
    return std::nullopt;
}

void PrintUsage(FILE* out)
{
    std::fprintf(out,
                  "Usage: bmp2tile <source-image> [options]\n"
                  "\n"
                  "  -savetiles <path> [format]     Save extracted tiles to <path>.\n"
                  "                                  format: uncompressed (default), zx7,\n"
                  "                                  psgaiden, stc0, stc4, aplib, 1bpp, 2bpp\n"
                  "  -savetilemap <path> [format]   Save the tilemap to <path>.\n"
                  "                                  format: uncompressed (default), zx7, stm, aplib\n"
                  "  -savepalette <path> [format]   Save the palette to <path>.\n"
                  "                                  format: sms (default), gg\n"
                  "  -palettetext <format>          Print the palette as text (sms, gg, or\n"
                  "                                  cl123) instead of/as well as saving it.\n"
                  "  -removedupes / -noremovedupes  Remove duplicate tiles (default: on)\n"
                  "  -mirror / -nomirror            Also detect mirrored duplicates (default: on)\n"
                  "  -sprite8x16                    Extract 8x16 sprite pairs instead of 8x8 tiles;\n"
                  "                                  tiles stored as even/odd pairs, TilesHigh counts\n"
                  "                                  16-pixel rows\n"
                  "  -spritepalette                 Set bit 11 in every tilemap word (use sprite\n"
                  "                                  palette). Affects -savetilemap output.\n"
                  "  -infrontofsprites              Set bit 12 in every tilemap word (high priority,\n"
                  "                                  tile drawn in front of sprites).\n"
                  "  -emit16colours                 Pad the palette to 16 entries with black if\n"
                  "                                  the image uses fewer (affects -savepalette\n"
                  "                                  and -palettetext both)\n"
                  "  -tileoffset <N>                Add N to every tilemap entry's tile index\n"
                  "                                  before saving (for tiles not loaded at VRAM\n"
                  "                                  tile 0). Affects -savetilemap only.\n"
                  "  -h, -help, --help              Show this help and exit\n"
                  "\n"
                  "Indexed (1/4/8bpp) BMP is recommended so the palette order is preserved\n"
                  "exactly. PCX is not supported. Formats are case-insensitive.\n");
}

struct ParsedArgs
{
    std::string SourcePath;
    std::string TilesPath;
    CompressionFormat TilesFormat = CompressionFormat::Uncompressed;
    bool bSaveTiles = false;
    std::string TilemapPath;
    CompressionFormat TilemapFormat = CompressionFormat::Uncompressed;
    bool bSaveTilemap = false;
    std::string PalettePath;
    PaletteFormat SavePaletteFormat = PaletteFormat::Sms;
    bool bSavePalette = false;
    std::optional<PaletteFormat> PaletteTextFormat;
    bool bRemoveDuplicates = true;
    bool bUseMirroring = true;
    bool bAlwaysEmit16Colours = false;
    int TileIndexOffset = 0;
    bool bSprite8x16 = false;
    bool bUseSpritePalette = false;
    bool bHighPriority = false;
    bool bShowHelp = false;
};

// Returns std::nullopt on a parse error (having already printed a message
// to stderr).
std::optional<ParsedArgs> ParseArgs(int argc, char** argv)
{
    ParsedArgs args;

    auto needValue = [&](int& i, const char* flag) -> const char*
    {
        if (i + 1 >= argc)
        {
            std::fprintf(stderr, "%s needs a value.\n", flag);
            return nullptr;
        }
        return argv[++i];
    };

    auto maybeFormat = [&](int& i) -> std::optional<std::string>
    {
        if (i + 1 < argc && argv[i + 1][0] != '-')
        {
            return std::string(argv[++i]);
        }
        return std::nullopt;
    };

    for (int i = 1; i < argc; ++i)
    {
        const std::string arg = argv[i];

        if (arg == "-h" || arg == "-help" || arg == "--help")
        {
            args.bShowHelp = true;
        }
        else if (arg == "-savetiles")
        {
            const char* path = needValue(i, "-savetiles");
            if (!path) return std::nullopt;
            args.TilesPath = path;
            args.bSaveTiles = true;
            if (auto fmt = maybeFormat(i))
            {
                auto parsed = ParseCompressionFormat(*fmt);
                if (!parsed)
                {
                    std::fprintf(stderr, "Unknown tile compression format '%s'.\n", fmt->c_str());
                    return std::nullopt;
                }
                args.TilesFormat = *parsed;
            }
        }
        else if (arg == "-savetilemap")
        {
            const char* path = needValue(i, "-savetilemap");
            if (!path) return std::nullopt;
            args.TilemapPath = path;
            args.bSaveTilemap = true;
            if (auto fmt = maybeFormat(i))
            {
                auto parsed = ParseCompressionFormat(*fmt);
                if (!parsed)
                {
                    std::fprintf(stderr, "Unknown tilemap compression format '%s'.\n", fmt->c_str());
                    return std::nullopt;
                }
                args.TilemapFormat = *parsed;
            }
        }
        else if (arg == "-savepalette")
        {
            const char* path = needValue(i, "-savepalette");
            if (!path) return std::nullopt;
            args.PalettePath = path;
            args.bSavePalette = true;
            if (auto fmt = maybeFormat(i))
            {
                auto parsed = ParsePaletteFormat(*fmt);
                if (!parsed || *parsed == PaletteFormat::Cl123)
                {
                    std::fprintf(stderr,
                                  "Unknown or unsupported palette save format '%s' (use sms or gg -- cl123 has no "
                                  "binary form, see -palettetext).\n",
                                  fmt->c_str());
                    return std::nullopt;
                }
                args.SavePaletteFormat = *parsed;
            }
        }
        else if (arg == "-palettetext")
        {
            const char* fmt = needValue(i, "-palettetext");
            if (!fmt) return std::nullopt;
            auto parsed = ParsePaletteFormat(fmt);
            if (!parsed)
            {
                std::fprintf(stderr, "Unknown palette text format '%s'.\n", fmt);
                return std::nullopt;
            }
            args.PaletteTextFormat = *parsed;
        }
        else if (arg == "-removedupes")
        {
            args.bRemoveDuplicates = true;
        }
        else if (arg == "-noremovedupes")
        {
            args.bRemoveDuplicates = false;
        }
        else if (arg == "-mirror")
        {
            args.bUseMirroring = true;
        }
        else if (arg == "-nomirror")
        {
            args.bUseMirroring = false;
        }
        else if (arg == "-emit16colours")
        {
            args.bAlwaysEmit16Colours = true;
        }
        else if (arg == "-sprite8x16")
        {
            args.bSprite8x16 = true;
        }
        else if (arg == "-spritepalette")
        {
            args.bUseSpritePalette = true;
        }
        else if (arg == "-infrontofsprites")
        {
            args.bHighPriority = true;
        }
        else if (arg == "-tileoffset")
        {
            const char* value = needValue(i, "-tileoffset");
            if (!value) return std::nullopt;
            try
            {
                size_t consumed = 0;
                const int offset = std::stoi(value, &consumed);
                if (consumed != std::string(value).size())
                {
                    throw std::invalid_argument("trailing characters");
                }
                args.TileIndexOffset = offset;
            }
            catch (const std::exception&)
            {
                std::fprintf(stderr, "-tileoffset needs an integer, got '%s'.\n", value);
                return std::nullopt;
            }
        }
        else if (!arg.empty() && arg[0] == '-')
        {
            std::fprintf(stderr, "Unknown option '%s'. Try -help.\n", arg.c_str());
            return std::nullopt;
        }
        else
        {
            if (!args.SourcePath.empty())
            {
                std::fprintf(stderr, "Only one source image may be given (already have '%s', also got '%s').\n",
                              args.SourcePath.c_str(), arg.c_str());
                return std::nullopt;
            }
            args.SourcePath = arg;
        }
    }

    return args;
}

}  // namespace

int RunCli(int argc, char** argv)
{
    const std::optional<ParsedArgs> parsed = ParseArgs(argc, argv);
    if (!parsed)
    {
        return 1;
    }
    const ParsedArgs& args = *parsed;

    if (args.bShowHelp)
    {
        PrintUsage(stdout);
        return 0;
    }

    if (args.SourcePath.empty())
    {
        std::fprintf(stderr, "No source image given.\n");
        PrintUsage(stderr);
        return 1;
    }

    IndexedImage image;
    try
    {
        image = LoadImage(args.SourcePath);
    }
    catch (const std::exception& e)
    {
        std::fprintf(stderr, "Couldn't load '%s': %s\n", args.SourcePath.c_str(), e.what());
        return 1;
    }

    std::printf("Loaded '%s': %dx%d, %zu colours.\n", args.SourcePath.c_str(), image.Width, image.Height, image.Palette.size());

    const TileSet tileSet = ExtractTiles(image, args.bRemoveDuplicates, args.bUseMirroring, args.bSprite8x16,
                                         args.bUseSpritePalette, args.bHighPriority);
    const int totalCells = tileSet.TilesWide * tileSet.TilesHigh;
    std::printf("%zu unique tile%s from %d cell%s (%dx%d grid).\n", tileSet.Tiles.size(), tileSet.Tiles.size() == 1 ? "" : "s",
                totalCells, totalCells == 1 ? "" : "s", tileSet.TilesWide, tileSet.TilesHigh);

    bool ok = true;

    if (args.bSaveTiles)
    {
        try
        {
            SaveTilesBin(args.TilesPath, tileSet.Tiles, args.TilesFormat);
            std::printf("Saved tiles (%s) to '%s'.\n", CompressionFormatName(args.TilesFormat), args.TilesPath.c_str());
        }
        catch (const std::exception& e)
        {
            std::fprintf(stderr, "Couldn't save tiles: %s\n", e.what());
            ok = false;
        }
    }

    if (args.bSaveTilemap)
    {
        try
        {
            SaveTilemapBin(args.TilemapPath, tileSet.Tilemap, tileSet.TilesWide, args.TilemapFormat, args.TileIndexOffset);
            std::printf("Saved tilemap (%s) to '%s'.\n", CompressionFormatName(args.TilemapFormat), args.TilemapPath.c_str());
        }
        catch (const std::exception& e)
        {
            std::fprintf(stderr, "Couldn't save tilemap: %s\n", e.what());
            ok = false;
        }
    }

    const std::vector<Color> outputPalette = args.bAlwaysEmit16Colours ? PadPaletteTo16(image.Palette) : image.Palette;

    if (args.bSavePalette)
    {
        try
        {
            SavePaletteBin(args.PalettePath, outputPalette, args.SavePaletteFormat);
            std::printf("Saved %zu colours to '%s'.\n", outputPalette.size(), args.PalettePath.c_str());
        }
        catch (const std::exception& e)
        {
            std::fprintf(stderr, "Couldn't save palette: %s\n", e.what());
            ok = false;
        }
    }

    if (args.PaletteTextFormat)
    {
        switch (*args.PaletteTextFormat)
        {
            case PaletteFormat::Sms:
                std::printf("%s\n", FormatPaletteAsSmsHex(outputPalette).c_str());
                break;
            case PaletteFormat::Gg:
                std::printf("%s\n", FormatPaletteAsGgHex(outputPalette).c_str());
                break;
            case PaletteFormat::Cl123:
                std::printf("%s\n", FormatPaletteAsCl123Constants(outputPalette).c_str());
                break;
        }
    }

    if (!args.bSaveTiles && !args.bSaveTilemap && !args.bSavePalette && !args.PaletteTextFormat)
    {
        std::printf("Nothing to save -- pass -savetiles/-savetilemap/-savepalette/-palettetext. Try -help.\n");
    }

    return ok ? 0 : 1;
}

}  // namespace bmp2tile
