#include "widget/TransferPakDetectionWidget.h"
#include "transferpak/TransferPakManager.h"
#include "transferpak/TransferPakRomReader.h"
#include "transferpak/TransferPakSaveManager.h"
#include "gen1/Gen1GameReader.h"
#include "gen2/Gen2GameReader.h"
#include "tpak.h"

/**
 * @brief This function allows you to specify a 32 bit RGBA color by specifying separate color components
 * and converting it to a RGBA16 uint16_t value at compile time
 * 
 * @param r 8 bit red value
 * @param g 8 bit green value
 * @param b 8 bit blue value
 * @param a 8 bit alpha value 
 */
constexpr uint16_t colorToRGBA16(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
{
    return (((uint16_t)r >> 3) << 11) | (((uint16_t)g >> 3) << 6) | (((uint16_t)b >> 3) << 1) | (a >> 7);
}

static const Rectangle textBounds = {0, 100, 200, 20};
static const Rectangle cartridgeLabelBounds = {9, 26, 70, 62};

static const uint16_t paletteBlue[] = {0, colorToRGBA16(0x20, 0x30, 0x81, 0xFF), colorToRGBA16(0x15, 0x3B, 0xB0, 0xFF), 0, 0, 0, 0, 0};
static const uint16_t paletteRed[] = {0, colorToRGBA16(0xA7, 0x1F, 0x1B, 0xFF), colorToRGBA16(0xAA, 0x2A, 0x2A, 0xFF), 0, 0, 0, 0, 0};
static const uint16_t paletteYellow[] = {0, colorToRGBA16(0xEA, 0xA8, 0x2C, 0xFF), colorToRGBA16(0xF4, 0xB0, 0x2E, 0xFF), 0, 0, 0, 0, 0};
static const uint16_t paletteGold[] = {0, colorToRGBA16(0x8A, 0x86, 0x48, 0xFF), colorToRGBA16(0x88, 0x89, 0x4B, 0xFF), 0, 0, 0, 0, 0};
static const uint16_t paletteSilver[] = {0, colorToRGBA16(0x90, 0x8D, 0x85, 0xFF), colorToRGBA16(0xA2, 0x9E, 0x98, 0xFF), 0, 0, 0, 0, 0};
static const uint16_t paletteCrystal[] = {0, colorToRGBA16(0x55, 0x7A, 0x77, 0xFF), colorToRGBA16(0x72, 0x9E, 0xA4, 0xFF), 0, 0, 0, 0, 0};

TransferPakDetectionWidget::TransferPakDetectionWidget(AnimationManager& animManager, TransferPakManager& pakManager)
    : style_({0})
    , animManager_(animManager)
    , tpakManager_(pakManager)
    , bounds_({0})
    , currentState_(TransferPakWidgetState::UNKNOWN)
    , previousInputState_({0})
    , gen1Type_(Gen1GameType::INVALID)
    , gen2Type_(Gen2GameType::INVALID)
    , stateChangedCallback_(nullptr)
    , stateChangedCallbackContext_(nullptr)
    , cartridgeIconSprite_(nullptr)
    , cartridgeLabelSprite_(nullptr)
    , cartridgeIconRenderSettings_()
    , cartridgeLabelRenderSettings_()
    , focused_(false)
    , visible_(true)
{
    cartridgeIconSprite_ = sprite_load("rom://cartridge-icon.sprite");
    cartridgeLabelSprite_ = sprite_load("rom://cartridge-label-unknown.sprite");
}

TransferPakDetectionWidget::~TransferPakDetectionWidget()
{
    sprite_free(cartridgeIconSprite_);
    cartridgeIconSprite_ = nullptr;

    if(cartridgeLabelSprite_)
    {
        sprite_free(cartridgeLabelSprite_);
        cartridgeLabelSprite_ = nullptr;
    }
}

bool TransferPakDetectionWidget::isFocused() const
{
    return focused_;
}

void TransferPakDetectionWidget::setFocused(bool focused)
{
    focused_ = focused;
}

bool TransferPakDetectionWidget::isVisible() const
{
    return visible_;
}

void TransferPakDetectionWidget::setVisible(bool visible)
{
    visible_ = visible;
}

Rectangle TransferPakDetectionWidget::getBounds() const
{
    return bounds_;
}

void TransferPakDetectionWidget::setBounds(const Rectangle& bounds)
{
    bounds_ = bounds;
}

Dimensions TransferPakDetectionWidget::getSize() const
{
    return Dimensions{ .width = bounds_.width, .height = bounds_.height };
}

bool TransferPakDetectionWidget::handleUserInput(const joypad_inputs_t& userInput)
{
    bool handled = false;
    if(previousInputState_.btn.a && !userInput.btn.a)
    {
        switch(currentState_)
        {
        case TransferPakWidgetState::UNKNOWN:
            switchState(currentState_, TransferPakWidgetState::DETECTING_PAK);
            handled = true;
            break;
        default:
            break;
        }
    }
    else if(currentState_ == TransferPakWidgetState::VALIDATING_GAME_SAVE)
    {
        // We don't want to do this in the switchState flow in order to have the widget actually render something before starting this step
        // (because validating the game save CRC might take a few seconds)
        tpakManager_.setRAMEnabled(true);
        const bool ret = validateGameSave();
        tpakManager_.setRAMEnabled(false);
        const TransferPakWidgetState newState = (ret) ? TransferPakWidgetState::VALID_SAVE_FOUND : TransferPakWidgetState::NO_SAVE_FOUND;

        switchState(currentState_, newState);
    }

    previousInputState_ = userInput;
    return handled;
}

void TransferPakDetectionWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds)
{
    //gfx.fillRectangle(bounds_, RGBA32(0xFF, 0, 0, 0xFF));
    const Rectangle absoluteIconBounds = {bounds_.x + ((bounds_.width - 88) / 2), bounds_.y, 88, 100};
    gfx.drawSprite(absoluteIconBounds, cartridgeIconSprite_, cartridgeIconRenderSettings_);

    if(cartridgeLabelSprite_)
    {
        const Rectangle absoluteLabelBounds = addOffset(cartridgeLabelBounds, absoluteIconBounds);
        gfx.drawSprite(absoluteLabelBounds, cartridgeLabelSprite_, cartridgeLabelRenderSettings_);
    }

    switch(currentState_)
    {
    case TransferPakWidgetState::UNKNOWN:
        renderUnknownState(gfx, parentBounds);
        break;
    case TransferPakWidgetState::NO_TRANSFER_PAK_FOUND:
    case TransferPakWidgetState::GB_HEADER_VALIDATION_FAILED:
    case TransferPakWidgetState::NO_GAME_FOUND:
        renderErrorState(gfx, parentBounds);
    default:
        break;
    }
}

TransferPakWidgetState TransferPakDetectionWidget::getState() const
{
    return currentState_;
}

void TransferPakDetectionWidget::retrieveGameType(Gen1GameType& outGen1Type, Gen2GameType& outGen2Type)
{
    outGen1Type = gen1Type_;
    outGen2Type = gen2Type_;
}

void TransferPakDetectionWidget::setStyle(const TransferPakDetectionWidgetStyle& style)
{
    style_ = style;
}

void TransferPakDetectionWidget::setStateChangedCallback(void (*callback)(void*, TransferPakWidgetState), void* context)
{
    stateChangedCallback_ = callback;
    stateChangedCallbackContext_ = context;
}

void TransferPakDetectionWidget::switchState(TransferPakWidgetState previousState, TransferPakWidgetState state)
{
    TransferPakWidgetState newState;
    bool ret;

    currentState_ = state;
    switch(state)
    {
    case TransferPakWidgetState::DETECTING_PAK:
        ret = selectTransferPak();
        newState = (ret) ? TransferPakWidgetState::VALIDATING_GB_HEADER : TransferPakWidgetState::NO_TRANSFER_PAK_FOUND;
        switchState(state, newState);
        return;
    case TransferPakWidgetState::VALIDATING_GB_HEADER:
        ret = validateGameboyHeader();
        newState = (ret) ? TransferPakWidgetState::DETECTING_GAME : TransferPakWidgetState::GB_HEADER_VALIDATION_FAILED;
        switchState(state, newState);
        return;
    case TransferPakWidgetState::DETECTING_GAME:
        ret = detectGameType();
        newState = (ret) ? TransferPakWidgetState::GAME_FOUND : TransferPakWidgetState::NO_GAME_FOUND;
        switchState(state, newState);
        return;
    case TransferPakWidgetState::GAME_FOUND:
        updateCartridgeIcon();
        newState = TransferPakWidgetState::VALIDATING_GAME_SAVE;
        switchState(state, newState);
        break;
    default:
        break;
    }

    // now notify the callback (if any) that the state has changed
    if(stateChangedCallback_)
    {
        stateChangedCallback_(stateChangedCallbackContext_, state);
    }
}

void TransferPakDetectionWidget::renderUnknownState(RDPQGraphics& gfx, const Rectangle& parentBounds)
{
    const Rectangle absoluteTextBounds = addOffset(textBounds, bounds_);
    gfx.drawText(absoluteTextBounds, "Press A to start", style_.textSettings);
}

void TransferPakDetectionWidget::renderValidatingSaveState(RDPQGraphics& gfx, const Rectangle& parentBounds)
{
    const Rectangle absoluteTextBounds = addOffset(textBounds, bounds_);
    gfx.drawText(absoluteTextBounds, "Checking save...", style_.textSettings);
}

void TransferPakDetectionWidget::renderErrorState(RDPQGraphics& gfx, const Rectangle& parentBounds)
{
    const Rectangle absoluteTextBounds = addOffset(textBounds, bounds_);
    const char* errorText;

    switch(currentState_)
    {
    case TransferPakWidgetState::NO_TRANSFER_PAK_FOUND:
        errorText = "ERROR: No Transfer Pak found!";
        break;
    case TransferPakWidgetState::GB_HEADER_VALIDATION_FAILED:
        errorText = "ERROR: Gameboy Header validation failed!";
        break;
    case TransferPakWidgetState::NO_GAME_FOUND:
        // TODO: technically this is not correct
        // We just didn't find a Pkmn game.
        errorText = "ERROR: No game found!";
        break;
    default:
        errorText = "ERROR: this should never happen!";
        break;
    }

    gfx.drawText(absoluteTextBounds, errorText, style_.textSettings);
}

bool TransferPakDetectionWidget::selectTransferPak()
{
    joypad_poll();
    for(uint8_t i=0; i < 4; ++i)
    {
        tpakManager_.setPort((joypad_port_t)i);
        if(tpakManager_.hasTransferPak())
        {
            debugf("[Application]: Transfer pak found at controller %hu\r\n", i);
            // power the transfer pak off in case it was powered before
            tpakManager_.setPower(false);
            return true;
        }
    }
    debugf("[Application]: ERROR: no transfer pak found!\r\n");
    return false;
}

bool TransferPakDetectionWidget::validateGameboyHeader()
{
    gameboy_cartridge_header cartridgeHeader;
    if(!tpakManager_.setPower(true))
    {
        return false;
    }


    if(!tpakManager_.readCartridgeHeader(cartridgeHeader))
    {
        return false;
    }

    if(!tpak_check_header(&cartridgeHeader))
    {
        debugf("[TransferPakDetectionWidget]: ERROR: tpak_check_header returned false!\r\n");
        return false;
    }

    // For MBC1 cartridges, we need to set the MBC1 banking mode to 1.
    // If we don't, we can't actually switch SRAM banks.
    // It looks like only the Japanese cartridges use MBC1 though.
    switch(cartridgeHeader.cartridge_type)
    {
    case GB_MBC1_RAM:
    case GB_MBC1_RAM_BATTERY:
        tpakManager_.switchMBC1BankingMode(1);
        break;
    default:
        // We don't need to do anything
        break;
    }

    return true;
}

bool TransferPakDetectionWidget::detectGameType()
{
    GameboyCartridgeHeader cartridgeHeader;
    TransferPakRomReader romReader(tpakManager_);

    readGameboyCartridgeHeader(romReader, cartridgeHeader);

    gen1Type_ = gen1_determineGameType(cartridgeHeader);
    gen2Type_ = gen2_determineGameType(cartridgeHeader);

    return (gen1Type_ != Gen1GameType::INVALID || gen2Type_ != Gen2GameType::INVALID);
}

void TransferPakDetectionWidget::updateCartridgeIcon()
{
    const char* labelSpritePath = nullptr;
    if(gen1Type_ != Gen1GameType::INVALID)
    {
        switch(gen1Type_)
        {
        case Gen1GameType::BLUE:
            cartridgeIconRenderSettings_.customPalette.numColors = sizeof(paletteBlue) / sizeof(paletteBlue[0]);
            cartridgeIconRenderSettings_.customPalette.colorsRGBA16 = paletteBlue;
            labelSpritePath = "rom://cartridge-label-blue.sprite";
            break;
        case Gen1GameType::RED:
            cartridgeIconRenderSettings_.customPalette.numColors = sizeof(paletteRed) / sizeof(paletteRed[0]);
            cartridgeIconRenderSettings_.customPalette.colorsRGBA16 = paletteRed;
            labelSpritePath = "rom://cartridge-label-red.sprite";
            break;
        case Gen1GameType::YELLOW:
            cartridgeIconRenderSettings_.customPalette.numColors = sizeof(paletteYellow) / sizeof(paletteYellow[0]);
            cartridgeIconRenderSettings_.customPalette.colorsRGBA16 = paletteYellow;
            labelSpritePath = "rom://cartridge-label-yellow.sprite";
            break;
        case Gen1GameType::GREEN:
            labelSpritePath = "rom://cartridge-label-green.sprite";
            break;
        default:
            break;
        }
    }
    else if(gen2Type_ != Gen2GameType::INVALID)
    {
        switch(gen2Type_)
        {
        case Gen2GameType::GOLD:
            cartridgeIconRenderSettings_.customPalette.numColors = sizeof(paletteGold) / sizeof(paletteGold[0]);
            cartridgeIconRenderSettings_.customPalette.colorsRGBA16 = paletteGold;
            labelSpritePath = "rom://cartridge-label-gold.sprite";
            break;
        case Gen2GameType::SILVER:
            cartridgeIconRenderSettings_.customPalette.numColors = sizeof(paletteSilver) / sizeof(paletteSilver[0]);
            cartridgeIconRenderSettings_.customPalette.colorsRGBA16 = paletteSilver;
            labelSpritePath = "rom://cartridge-label-silver.sprite";
            break;
        case Gen2GameType::CRYSTAL:
            cartridgeIconRenderSettings_.customPalette.numColors = sizeof(paletteCrystal) / sizeof(paletteCrystal[0]);
            cartridgeIconRenderSettings_.customPalette.colorsRGBA16 = paletteCrystal;
            labelSpritePath = "rom://cartridge-label-crystal.sprite";
            break;
        default:
            break;
        }
    }

    if(labelSpritePath)
    {
        if(cartridgeLabelSprite_)
        {
            sprite_free(cartridgeLabelSprite_);
        }
        cartridgeLabelSprite_ = sprite_load(labelSpritePath);
    }
}

bool TransferPakDetectionWidget::validateGameSave()
{
    TransferPakRomReader romReader(tpakManager_);
    TransferPakSaveManager saveManager(tpakManager_);
    if(gen1Type_ != Gen1GameType::INVALID)
    {
        Gen1GameReader gen1Reader(romReader, saveManager, gen1Type_);

        return gen1Reader.isMainChecksumValid();
    }
    else if(gen2Type_ != Gen2GameType::INVALID)
    {
        Gen2GameReader gen2Reader(romReader, saveManager, gen2Type_);

        return gen2Reader.isMainChecksumValid();
    }
    return false;
}