#include "global.h"
#include "bg.h"
#include "decompress.h"
#include "event_data.h"
#include "event_object_movement.h"
#include "field_weather.h"
#include "gpu_regs.h"
#include "graphics.h"
#include "malloc.h"
#include "main.h"
#include "menu.h"
#include "overworld.h"
#include "palette.h"
#include "random.h"
#include "scanline_effect.h"
#include "script.h"
#include "sound.h"
#include "sprite.h"
#include "task.h"
#include "constants/event_objects.h"
#include "constants/rgb.h"
#include "constants/songs.h"
#include "constants/weather.h"

#define GOING_DOWN gSpecialVar_0x8004

#define STATE_END 0xFF

#define TAG_CABLE_CAR 1
#define TAG_DOOR      2
#define TAG_CABLE     3

struct CableCar
{
    u8 bgTaskId;
    u8 state;
    u8 weather;
    u16 weatherDelay;
    u16 timer;
    u8 bg0HorizontalOffset;
    u8 bg0VerticalOffset;
    u8 unused0[2];
    u8 bg1HorizontalOffset;
    u8 bg1VerticalOffset;
    u8 unused1[6];
    u8 bg3HorizontalOffset;
    u8 bg3VerticalOffset;
    u8 unused2[2];
    u8 groundTileIdx;
    u8 groundSegmentXStart;
    u8 groundSegmentYStart;
    u8 groundTilemapOffset;
    u8 groundTimer; // Incremented, but does nothing
    u8 groundXOffset;
    u8 groundYOffset;
    u8 groundXBase;
    u8 groundYBase;
    u16 groundTileBuffer[9][12];
    u8 unused3[2];
    u16 bgTilemapBuffers[4][BG_SCREEN_SIZE];
    u16 *groundTilemap;
    u16 *treesTilemap;
    u16 *bgMountainsTilemap;
    const u16 *pylonHookTilemapEntries;
    u8 *pylonStemTilemap;
};

static EWRAM_DATA struct CableCar *sCableCar = NULL;
static EWRAM_DATA u8 sGroundX_Up = 0;
static EWRAM_DATA u8 sGroundY_Up = 0;
static EWRAM_DATA u8 sGroundSegmentY_Up = 0;
static EWRAM_DATA u8 sGroundX_Down = 0;
static EWRAM_DATA u8 sGroundY_Down = 0;
static EWRAM_DATA u8 sGroundSegmentY_Down = 0;

static void CB2_LoadCableCar(void);
static void SetBgRegs(bool8);
static void CreateCableCarSprites(void);
static void InitGroundTilemapData(bool8);
static void Task_CableCar(u8);
static void Task_AnimateBgGoingUp(u8);
static void Task_AnimateBgGoingDown(u8);
static void VBlankCB_CableCar(void);
static void CB2_CableCar(void);
static void AnimateGroundGoingUp(void);
static void AnimateGroundGoingDown(void);
static void SpriteCB_CableCar(struct Sprite *);
static void SpriteCB_Cable(struct Sprite *);
static void DrawNextGroundSegmentGoingUp(void);
static void DrawNextGroundSegmentGoingDown(void);

static const struct BgTemplate sBgTemplates[4] = {
    {
        .bg = 0,
        .charBaseIndex = 0,
        .mapBaseIndex = 28,
        .screenSize = 0,
        .paletteMode = 0,
        .priority = 1,
        .baseTile = 0
    },
    {
        .bg = 1,
        .charBaseIndex = 0,
        .mapBaseIndex = 29,
        .screenSize = 0,
        .paletteMode = 0,
        .priority = 2,
        .baseTile = 0
    },
    {
        .bg = 2,
        .charBaseIndex = 0,
        .mapBaseIndex = 30,
        .screenSize = 0,
        .paletteMode = 0,
        .priority = 3,
        .baseTile = 0
    },
    {
        .bg = 3,
        .charBaseIndex = 0,
        .mapBaseIndex = 31,
        .screenSize = 0,
        .paletteMode = 0,
        .priority = 0,
        .baseTile = 0
    },
};

static const u8 sGround_Tilemap[] = INCBIN_U8("graphics/cable_car/ground.bin.lz");
static const u8 sTrees_Tilemap[] = INCBIN_U8("graphics/cable_car/trees.bin.lz");
static const u8 sBgMountains_Tilemap[] = INCBIN_U8("graphics/cable_car/bg_mountains.bin.lz");

static const u16 sPylonHook_TilemapEntries[] = {
    0x3000,
    0x3001,
    0x3002,
    0x3003,
    0x3004,
    0x3005,
    0x3006,
    0x3007,
    0x3008,
    0x3009,
};

static const u8 sPylonStems_Tilemap[] = INCBIN_U8("graphics/cable_car/pylons.bin.lz");

static const struct CompressedSpriteSheet sSpriteSheets[] = {
    { gCableCar_Gfx,      0x800, TAG_CABLE_CAR },
    { gCableCarDoor_Gfx,   0x40, TAG_DOOR },
    { gCableCarCable_Gfx,  0x80, TAG_CABLE },
    { },
};

static const struct SpritePalette sSpritePalettes[] = {
    { gCableCar_Pal, TAG_CABLE_CAR },
    { }
};

static const struct OamData sOam_CableCar =
{
    .y = 0,
    .affineMode = ST_OAM_AFFINE_DOUBLE,
    .objMode = ST_OAM_OBJ_NORMAL,
    .bpp = ST_OAM_4BPP,
    .shape = SPRITE_SHAPE(64x64),
    .x = 0,
    .size = SPRITE_SIZE(64x64),
    .tileNum = 0,
    .priority = 2,
    .paletteNum = 0,
};

static const struct OamData sOam_CableCarDoor =
{
    .y = 0,
    .affineMode = ST_OAM_AFFINE_DOUBLE,
    .objMode = ST_OAM_OBJ_NORMAL,
    .bpp = ST_OAM_4BPP,
    .shape = SPRITE_SHAPE(16x8),
    .x = 0,
    .size = SPRITE_SIZE(16x8),
    .tileNum = 0,
    .priority = 2,
    .paletteNum = 0,
};

static const struct OamData sOam_Cable =
{
    .y = 0,
    .affineMode = ST_OAM_AFFINE_DOUBLE,
    .objMode = ST_OAM_OBJ_NORMAL,
    .bpp = ST_OAM_4BPP,
    .shape = SPRITE_SHAPE(16x16),
    .x = 0,
    .size = SPRITE_SIZE(16x16),
    .tileNum = 0,
    .priority = 2,
    .paletteNum = 0,
};

static const struct SpriteTemplate sSpriteTemplate_CableCar[] =
{
    {
        .tileTag = TAG_CABLE_CAR,
        .paletteTag = TAG_CABLE_CAR,
        .oam = &sOam_CableCar,
        .anims = gDummySpriteAnimTable,
        .images = NULL,
        .affineAnims = gDummySpriteAffineAnimTable,
        .callback = SpriteCB_CableCar,
    },
    {
        .tileTag = TAG_DOOR,
        .paletteTag = TAG_CABLE_CAR,
        .oam = &sOam_CableCarDoor,
        .anims = gDummySpriteAnimTable,
        .images = NULL,
        .affineAnims = gDummySpriteAffineAnimTable,
        .callback = SpriteCB_CableCar,
    },
};

static const struct SpriteTemplate sSpriteTemplate_Cable = {
    .tileTag = TAG_CABLE,
    .paletteTag = TAG_CABLE_CAR,
    .oam = &sOam_Cable,
    .anims = gDummySpriteAnimTable,
    .images = NULL,
    .affineAnims = gDummySpriteAffineAnimTable,
    .callback = SpriteCB_Cable,
};

static void Task_LoadCableCar(u8 taskId)
{
    if (!gPaletteFade.active)
    {
        SetMainCallback2(CB2_LoadCableCar);
        DestroyTask(taskId);
    }
}

void CableCar(void)
{
    ScriptContext2_Enable();
    CreateTask(Task_LoadCableCar, 1);
    BeginNormalPaletteFade(0xFFFFFFFF, 0, 0, 16, RGB(0, 0, 0));
}

static void CB2_LoadCableCar(void)
{
    u16 imebak;
    u8 i = 0;
    u32 sizeOut = 0;

    switch (gMain.state)
    {
    case 0:
    default:
        SetVBlankCallback(NULL);
        SetBgRegs(FALSE);
        ScanlineEffect_Stop();
        DmaFillLarge16(3, 0, (void *)VRAM, VRAM_SIZE, 0x1000);
        DmaFill32Defvars(3, 0, (void *)OAM, OAM_SIZE);
        DmaFill16Defvars(3, 0, (void *)PLTT, PLTT_SIZE);
        sCableCar = AllocZeroed(sizeof(*sCableCar));
        gMain.state++;
        break;
    case 1:
        ResetSpriteData();
        ResetTasks();
        FreeAllSpritePalettes();
        ResetPaletteFade();
        ResetTempTileDataBuffers();
        StartWeather();
        for (i = 0; i < NUM_ASH_SPRITES; i++)
            gWeatherPtr->sprites.s2.ashSprites[i] = NULL;

        InitMapMusic();
        ResetMapMusic();
        ResetBgsAndClearDma3BusyFlags(0);
        InitBgsFromTemplates(0, sBgTemplates, ARRAY_COUNT(sBgTemplates));
        SetBgTilemapBuffer(0, sCableCar->bgTilemapBuffers[0]);
        SetBgTilemapBuffer(1, sCableCar->bgTilemapBuffers[1]);
        SetBgTilemapBuffer(2, sCableCar->bgTilemapBuffers[2]);
        SetBgTilemapBuffer(3, sCableCar->bgTilemapBuffers[3]);
        gSpriteCoordOffsetX = gSpriteCoordOffsetY = 0;
        gMain.state++;
        break;
    case 2:
        for (i = 0; i < ARRAY_COUNT(sSpriteSheets) - 1; i++)
            LoadCompressedSpriteSheet(&sSpriteSheets[i]);

        LoadSpritePalettes(sSpritePalettes);
        sCableCar->groundTilemap = malloc_and_decompress(sGround_Tilemap, &sizeOut);
        sCableCar->treesTilemap = malloc_and_decompress(sTrees_Tilemap, &sizeOut);
        sCableCar->bgMountainsTilemap = malloc_and_decompress(sBgMountains_Tilemap, &sizeOut);
        sCableCar->pylonStemTilemap = malloc_and_decompress(sPylonStems_Tilemap, &sizeOut);
        sCableCar->pylonHookTilemapEntries = sPylonHook_TilemapEntries;
        DecompressAndCopyTileDataToVram(0, gCableCarBg_Gfx, 0, 0, 0);
        gMain.state++;
        break;
    case 3:
        if (!FreeTempTileDataBuffersIfPossible())
        {
            LoadPalette(gCableCarBg_Pal, 0, 0x80);
            gMain.state++;
        }
        break;
    case 4:
        CreateCableCarSprites();
        RunTasks();
        gMain.state++;
        break;
    case 5:
        if (sCableCar->weather == WEATHER_VOLCANIC_ASH)
        {
            gMain.state++;
        }
        else if (gWeatherPtr->sprites.s2.ashSprites[0])
        {
            for (i = 0; i < NUM_ASH_SPRITES; i++)
            {
                if (gWeatherPtr->sprites.s2.ashSprites[i])
                    gWeatherPtr->sprites.s2.ashSprites[i]->oam.priority = 0;
            }

            gMain.state++;
        }
        break;
    case 6:
        CopyToBgTilemapBufferRect_ChangePalette(1, sCableCar->treesTilemap, 0, 17, 32, 15, 17);
        CopyToBgTilemapBufferRect_ChangePalette(2, sCableCar->bgMountainsTilemap, 0, 0, 30, 20, 17);
        CopyToBgTilemapBufferRect_ChangePalette(3, sCableCar->pylonHookTilemapEntries, 0, 0, 5, 2, 17);
        CopyToBgTilemapBufferRect_ChangePalette(3, sCableCar->pylonStemTilemap, 0, 2, 2, 20, 17);
        gMain.state++;
        break;
    case 7:
        InitGroundTilemapData(GOING_DOWN);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +  0x48,  0, 14, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +  0x6C, 12, 17, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +  0x90, 24, 20, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +   0x0,  0, 17, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +  0x24,  0, 20, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +   0x0, 12, 20, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +  0x24, 12, 23, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +   0x0, 24, 23, 12, 3, 17);
        gMain.state++;
        break;
    case 8:
        BeginNormalPaletteFade(0xFFFFFFFF, 3, 16, 0, RGB(0, 0, 0));
        FadeInNewBGM(MUS_CABLE_CAR, 1);
        SetBgRegs(TRUE);
        gMain.state++;
        break;
    case 9:
        imebak = REG_IME;
        REG_IME = 0;
        REG_IE |= INTR_FLAG_VBLANK;
        REG_IME = imebak;
        SetVBlankCallback(VBlankCB_CableCar);
        SetMainCallback2(CB2_CableCar);
        CreateTask(Task_CableCar, 0);
        if (!GOING_DOWN)
            sCableCar->bgTaskId = CreateTask(Task_AnimateBgGoingUp, 1);
        else
            sCableCar->bgTaskId = CreateTask(Task_AnimateBgGoingDown, 1);
        break;
    }
}

static void CB2_CableCar(void)
{
    RunTasks();
    AnimateSprites();
    BuildOamBuffer();
    UpdatePaletteFade();
    MapMusicMain();
}

static void CB2_EndCableCar(void)
{
    u8 i = 0;

    HideBg(0);
    HideBg(1);
    HideBg(2);
    HideBg(3);
    SetBgRegs(FALSE);
    gSpriteCoordOffsetX = 0;
    SetCurrentAndNextWeatherNoDelay(WEATHER_NONE);
    for (i = 0; i < NUM_ASH_SPRITES; i++)
        gWeatherPtr->sprites.s2.ashSprites[i] = NULL;

    ResetTasks();
    ResetSpriteData();
    ResetPaletteFade();
    UnsetBgTilemapBuffer(0);
    UnsetBgTilemapBuffer(1);
    UnsetBgTilemapBuffer(2);
    UnsetBgTilemapBuffer(3);
    ResetBgsAndClearDma3BusyFlags(0);
    sCableCar->pylonHookTilemapEntries = NULL;
    FREE_AND_SET_NULL(sCableCar->pylonStemTilemap);
    FREE_AND_SET_NULL(sCableCar->bgMountainsTilemap);
    FREE_AND_SET_NULL(sCableCar->treesTilemap);
    FREE_AND_SET_NULL(sCableCar->groundTilemap);
    FREE_AND_SET_NULL(sCableCar);
    DmaFillLarge16(3, 0, (void *)VRAM, VRAM_SIZE, 0x1000);
    DmaFill32Defvars(3, 0, (void *)OAM, OAM_SIZE);
    DmaFill16Defvars(3, 0, (void *)PLTT, PLTT_SIZE);
    WarpIntoMap();
    gFieldCallback = NULL;
    SetMainCallback2(CB2_LoadMap);
}

static void Task_CableCar(u8 taskId)
{
    u8 i = 0;

    sCableCar->timer++;
    switch (sCableCar->state)
    {
    case 0:
        // Wait to change weather
        if (sCableCar->timer == sCableCar->weatherDelay)
        {
            SetNextWeather(sCableCar->weather);
            sCableCar->state = 1;
        }
        break;
    case 1:
        // Update ash sprites
        switch (sCableCar->weather)
        {
        case WEATHER_VOLCANIC_ASH:
            if (gWeatherPtr->sprites.s2.ashSprites[0] != NULL && gWeatherPtr->sprites.s2.ashSprites[0]->oam.priority != 0)
            {
                for (; i < NUM_ASH_SPRITES; i++)
                {
                    if (gWeatherPtr->sprites.s2.ashSprites[i])
                        gWeatherPtr->sprites.s2.ashSprites[i]->oam.priority = 0;
                }

                sCableCar->state = 2;
            }
            break;
        case WEATHER_SUNNY:
            if (gWeatherPtr->currWeather == WEATHER_SUNNY)
            {
                sCableCar->state = 2;
            }
            else if (sCableCar->timer >= sCableCar->weatherDelay + 8)
            {
                for (; i < NUM_ASH_SPRITES; i++)
                {
                    if (gWeatherPtr->sprites.s2.ashSprites[i])
                        gWeatherPtr->sprites.s2.ashSprites[i]->invisible ^= 1;
                }
            }
            break;
        }
        break;
    case 2:
        // Wait to fade out
        if (sCableCar->timer == 570)
        {
            sCableCar->state = 3;
            BeginNormalPaletteFade(0xFFFFFFFF, 3, 0, 16, RGB(0, 0, 0));
            FadeOutBGM(4);
        }
        break;
    case 3:
        // Wait for fade out
        if (!gPaletteFade.active)
            sCableCar->state = STATE_END;
        break;
    case STATE_END:
        SetVBlankCallback(NULL);
        DestroyTask(taskId);
        DestroyTask(sCableCar->bgTaskId);
        SetMainCallback2(CB2_EndCableCar);
        break;
    }
}

static void Task_AnimateBgGoingUp(u8 taskId)
{
    if (sCableCar->state != STATE_END)
    {
        sCableCar->bg3HorizontalOffset--;
        if ((sCableCar->timer % 2) == 0)
            sCableCar->bg3VerticalOffset--;

        if ((sCableCar->timer % 8) == 0)
        {
            sCableCar->bg1HorizontalOffset--;
            sCableCar->bg1VerticalOffset--;
        }

        switch (sCableCar->bg3HorizontalOffset)
        {
        case 175:
            FillBgTilemapBufferRect(3, 0, 0, 22, 2, 10, 17);
            break;
        case 40:
            FillBgTilemapBufferRect(3, 0, 3, 0, 2, 2, 17);
            break;
        case 32:
            FillBgTilemapBufferRect(3, 0, 2, 0, 1, 2, 17);
            break;
        case 16:
            CopyToBgTilemapBufferRect_ChangePalette(3, sCableCar->pylonHookTilemapEntries, 0, 0, 5,  2, 17);
            CopyToBgTilemapBufferRect_ChangePalette(3, sCableCar->pylonStemTilemap, 0, 2, 2, 30, 17);
            sCableCar->bg3VerticalOffset = 64;
            break;
        }
    }

    AnimateGroundGoingUp();
    gSpriteCoordOffsetX = (gSpriteCoordOffsetX + 1) % 128;
}

static void Task_AnimateBgGoingDown(u8 taskId)
{
    if (sCableCar->state != STATE_END)
    {
        sCableCar->bg3HorizontalOffset++;
        if ((sCableCar->timer % 2) == 0)
            sCableCar->bg3VerticalOffset++;

        if ((sCableCar->timer % 8) == 0)
        {
            sCableCar->bg1HorizontalOffset++;
            sCableCar->bg1VerticalOffset++;
        }

        switch (sCableCar->bg3HorizontalOffset)
        {
        case 176:
            CopyToBgTilemapBufferRect_ChangePalette(3, sCableCar->pylonStemTilemap, 0, 2, 2, 30, 17);
            break;
        case 16:
            FillBgTilemapBufferRect(3, 0, 2,  0, 3,  2, 17);
            FillBgTilemapBufferRect(3, 0, 0, 22, 2, 10, 17);
            sCableCar->bg3VerticalOffset = 192;
            break;
        case 32:
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[2], 2, 0, 1, 1, 17);
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[3], 3, 0, 1, 1, 17);
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[7], 2, 1, 1, 1, 17);
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[8], 3, 1, 1, 1, 17);
            break;
        case 40:
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[4], 4, 0, 1, 1, 17);
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[9], 4, 1, 1, 1, 17);
            break;
        }
    }

    AnimateGroundGoingDown();
    if (sCableCar->timer < sCableCar->weatherDelay)
        gSpriteCoordOffsetX = (gSpriteCoordOffsetX + 247) % 248;
    else
        gWeatherPtr->ashBaseSpritesX = (gWeatherPtr->ashBaseSpritesX + 247) % 248;
}

static void VBlankCB_CableCar(void)
{
    CopyBgTilemapBufferToVram(0);
    CopyBgTilemapBufferToVram(3);
    SetGpuReg(REG_OFFSET_BG3HOFS, sCableCar->bg3HorizontalOffset);
    SetGpuReg(REG_OFFSET_BG3VOFS, sCableCar->bg3VerticalOffset);
    SetGpuReg(REG_OFFSET_BG1HOFS, sCableCar->bg1HorizontalOffset);
    SetGpuReg(REG_OFFSET_BG1VOFS, sCableCar->bg1VerticalOffset);
    SetGpuReg(REG_OFFSET_BG0HOFS, sCableCar->bg0HorizontalOffset);
    SetGpuReg(REG_OFFSET_BG0VOFS, sCableCar->bg0VerticalOffset);
    LoadOam();
    ProcessSpriteCopyRequests();
    TransferPlttBuffer();
}

static void SpriteCB_Cable(struct Sprite *sprite)
{

}

#define sXPos  data[0]
#define sYPos  data[1]

static void SpriteCB_CableCar(struct Sprite *sprite)
{
    if (sCableCar->state != STATE_END)
    {
        if (!GOING_DOWN)
        {
            sprite->pos1.x = sprite->sXPos - (u8)(0.14f * S16TOPOSFLOAT(sCableCar->timer));
            sprite->pos1.y = sprite->sYPos - (u8)(0.067f * S16TOPOSFLOAT(sCableCar->timer));
        }
        else
        {
            sprite->pos1.x = sprite->sXPos + (u8)(0.14f * S16TOPOSFLOAT(sCableCar->timer));
            sprite->pos1.y = sprite->sYPos + (u8)(0.067f * S16TOPOSFLOAT(sCableCar->timer));
        }
    }
}

#define sState data[2]
#define sTimer data[3]

static void SpriteCB_Player(struct Sprite *sprite)
{
    if (sCableCar->state != STATE_END)
    {
        // Move along with cable car
        if (!GOING_DOWN)
        {
            sprite->pos1.x = sprite->sXPos - (u8)(0.14f * S16TOPOSFLOAT(sCableCar->timer));
            sprite->pos1.y = sprite->sYPos - (u8)(0.067f * S16TOPOSFLOAT(sCableCar->timer));
        }
        else
        {
            sprite->pos1.x = sprite->sXPos + (u8)(0.14f * S16TOPOSFLOAT(sCableCar->timer));
            sprite->pos1.y = sprite->sYPos + (u8)(0.067f * S16TOPOSFLOAT(sCableCar->timer));
        }

        // Bounce up and down
        switch (sprite->sState)
        {
        case 0:
            sprite->pos2.y = 17;
            if (sprite->sTimer++ > 9)
            {
                sprite->sTimer = 0;
                sprite->sState++;
            }
            break;
        default:
            sprite->pos2.y = 16;
            if (sprite->sTimer++ > 9)
            {
                sprite->sTimer = 0;
                sprite->sState = 0;
            }
            break;
        }
    }
}

#undef sState
#undef sTimer

#define sTimer      data[0]
#define sSameDir    data[1] // Whether or not the hiker is headed the same dir as the Cable Car
#define sDelay      data[2]

static void SpriteCB_HikerGoingUp(struct Sprite *sprite)
{
    if (sprite->sTimer == 0)
    {
        sprite->pos1.x += 2 * sprite->centerToCornerVecX;
        sprite->pos1.y += 16 + sprite->centerToCornerVecY;
    }

    if (++sprite->sTimer >= sprite->sDelay)
    {
        switch (sprite->sSameDir)
        {
        case FALSE:
            sprite->pos1.x++;
            if ((sprite->sTimer % 4) == 0)
                sprite->pos1.y++;
            break;
        case TRUE:
            // Hiker moves slower if travelling with the Cable Car
            if ((sprite->sTimer % 2) != 0)
            {
                sprite->pos1.x++;
                if ((sprite->pos1.x % 4) == 0)
                    sprite->pos1.y++;
            }
            break;
        }

        if (sprite->pos1.y > 160)
            DestroySprite(sprite);
    }
}

static void SpriteCB_HikerGoingDown(struct Sprite *sprite)
{
    if (sprite->sTimer == 0)
        sprite->pos1.y += 16 + sprite->centerToCornerVecY;

    if (++sprite->sTimer >= sprite->sDelay)
    {
        switch (sprite->sSameDir)
        {
        case FALSE:
            sprite->pos1.x--;
            if ((sprite->sTimer % 4) == 0)
                sprite->pos1.y--;
            break;
        case TRUE:
            // Hiker moves slower if travelling with the Cable Car
            if ((sprite->sTimer % 2) != 0)
            {
                sprite->pos1.x--;
                if ((sprite->pos1.x % 4) == 0)
                    sprite->pos1.y--;
            }
            break;
        }

        if (sprite->pos1.y < 80)
            DestroySprite(sprite);
    }
}

#undef sTimer

static void SetBgRegs(bool8 active)
{
    switch (active)
    {
    case FALSE:
    default:
        SetGpuReg(REG_OFFSET_WININ, 0);
        SetGpuReg(REG_OFFSET_WINOUT, 0);
        SetGpuReg(REG_OFFSET_WIN0H, 0);
        SetGpuReg(REG_OFFSET_WIN1H, 0);
        SetGpuReg(REG_OFFSET_WIN0V, 0);
        SetGpuReg(REG_OFFSET_WIN1V, 0);
        SetGpuReg(REG_OFFSET_DISPCNT, 0);
        SetGpuReg(REG_OFFSET_BG3CNT, 0);
        SetGpuReg(REG_OFFSET_BG2CNT, 0);
        SetGpuReg(REG_OFFSET_BG1CNT, 0);
        SetGpuReg(REG_OFFSET_BG0CNT, 0);
        SetGpuReg(REG_OFFSET_BG3HOFS, 0);
        SetGpuReg(REG_OFFSET_BG3VOFS, 0);
        SetGpuReg(REG_OFFSET_BG2HOFS, 0);
        SetGpuReg(REG_OFFSET_BG2VOFS, 0);
        SetGpuReg(REG_OFFSET_BG1HOFS, 0);
        SetGpuReg(REG_OFFSET_BG1VOFS, 0);
        SetGpuReg(REG_OFFSET_BG0HOFS, 0);
        SetGpuReg(REG_OFFSET_BG0VOFS, 0);
        SetGpuReg(REG_OFFSET_BLDCNT, 0);
        break;
    case TRUE:
        SetGpuReg(REG_OFFSET_WININ, 0);
        SetGpuReg(REG_OFFSET_WINOUT, 0);
        SetGpuReg(REG_OFFSET_WIN0H, 0);
        SetGpuReg(REG_OFFSET_WIN1H, 0);
        SetGpuReg(REG_OFFSET_WIN0V, 0);
        SetGpuReg(REG_OFFSET_WIN1V, 0);
        if (!GOING_DOWN)
        {
            sCableCar->bg3HorizontalOffset = 176;
            sCableCar->bg3VerticalOffset = 16;
            sCableCar->bg1HorizontalOffset = 0;
            sCableCar->bg1VerticalOffset = 80;
            sCableCar->bg0VerticalOffset = 0;
            sCableCar->bg0VerticalOffset = 0;
        }
        else
        {
            sCableCar->bg3HorizontalOffset = 96;
            sCableCar->bg3VerticalOffset = 232;
            sCableCar->bg1HorizontalOffset = 0;
            sCableCar->bg1VerticalOffset = 4;
            sCableCar->bg0VerticalOffset = 0;
            sCableCar->bg0VerticalOffset = 0;
        }

        SetGpuReg(REG_OFFSET_BG3HOFS, sCableCar->bg3HorizontalOffset);
        SetGpuReg(REG_OFFSET_BG3VOFS, sCableCar->bg3VerticalOffset);
        SetGpuReg(REG_OFFSET_BG2HOFS, 0);
        SetGpuReg(REG_OFFSET_BG2VOFS, 0);
        SetGpuReg(REG_OFFSET_BG1HOFS, sCableCar->bg1HorizontalOffset);
        SetGpuReg(REG_OFFSET_BG1VOFS, sCableCar->bg1VerticalOffset);
        SetGpuReg(REG_OFFSET_BG0HOFS, sCableCar->bg0HorizontalOffset);
        SetGpuReg(REG_OFFSET_BG0VOFS, sCableCar->bg0VerticalOffset);
        SetGpuReg(REG_OFFSET_DISPCNT, DISPCNT_OBJ_ON | DISPCNT_OBJ_1D_MAP);
        CopyBgTilemapBufferToVram(1);
        CopyBgTilemapBufferToVram(2);
        ShowBg(0);
        ShowBg(1);
        ShowBg(2);
        ShowBg(3);
        SetGpuReg(REG_OFFSET_BLDCNT, BLDCNT_TGT2_ALL);
        break;
    }
}

static void CreateCableCarSprites(void)
{
    u8 spriteId;
    u8 i;

    u8 playerGraphicsIds[2] = {
        [MALE]   = OBJ_EVENT_GFX_RIVAL_BRENDAN_NORMAL,
        [FEMALE] = OBJ_EVENT_GFX_RIVAL_MAY_NORMAL
    };
    u16 rval = Random();
    u8 hikerGraphicsIds[4] = {
        OBJ_EVENT_GFX_HIKER,
        OBJ_EVENT_GFX_CAMPER,
        OBJ_EVENT_GFX_PICNICKER,
        OBJ_EVENT_GFX_ZIGZAGOON_1
    };
    s16 hikerCoords[2][2] = {
        {   0,  80 }, // Going up
        { 240, 146 }  // Going down
    };
    u8 hikerMovementDelayTable[4] = { 0, 60, 120, 170};
    void (*hikerCallbacks[2])(struct Sprite *) = {
        SpriteCB_HikerGoingUp,
        SpriteCB_HikerGoingDown
    };

    switch (GOING_DOWN)
    {
        case FALSE:
        default:
            // Create player sprite
            spriteId = AddPseudoObjectEvent(playerGraphicsIds[gSaveBlock2Ptr->playerGender], SpriteCB_Player, 200, 73, 102);
            if (spriteId != MAX_SPRITES)
            {
                gSprites[spriteId].oam.priority = 2;
                gSprites[spriteId].pos2.x = 8;
                gSprites[spriteId].pos2.y = 16;
                gSprites[spriteId].sXPos = 200;
                gSprites[spriteId].sYPos = 73;
            }
            // Create car sprite
            spriteId = CreateSprite(&sSpriteTemplate_CableCar[0], 176, 43, 0x67);
            gSprites[spriteId].pos2.x = gSprites[spriteId].pos2.y = 32;
            gSprites[spriteId].sXPos = 176;
            gSprites[spriteId].sYPos = 43;
            // Create door sprite
            spriteId = CreateSprite(&sSpriteTemplate_CableCar[1], 200, 99, 0x65);
            gSprites[spriteId].pos2.x = 8;
            gSprites[spriteId].pos2.y = 4;
            gSprites[spriteId].sXPos = 200;
            gSprites[spriteId].sYPos = 99;
            // Init weather
            sCableCar->weather = WEATHER_VOLCANIC_ASH;
            sCableCar->weatherDelay = 350;
            SetCurrentAndNextWeatherNoDelay(WEATHER_SUNNY);
            break;
        case TRUE:
            CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap + 0x24, 24, 26, 12, 3, 17);
            // Create player sprite
            spriteId = AddPseudoObjectEvent(playerGraphicsIds[gSaveBlock2Ptr->playerGender], SpriteCB_Player, 128, 39, 102);
            if (spriteId != MAX_SPRITES)
            {
                gSprites[spriteId].oam.priority = 2;
                gSprites[spriteId].pos2.x = 8;
                gSprites[spriteId].pos2.y = 16;
                gSprites[spriteId].sXPos = 128;
                gSprites[spriteId].sYPos = 39;
            }
            // Create car sprite
            spriteId = CreateSprite(&sSpriteTemplate_CableCar[0], 104, 9, 0x67);
            gSprites[spriteId].pos2.x = gSprites[spriteId].pos2.y = 32;
            gSprites[spriteId].sXPos = 104;
            gSprites[spriteId].sYPos = 9;
            // Create door sprite
            spriteId = CreateSprite(&sSpriteTemplate_CableCar[1], 128, 65, 0x65);
            gSprites[spriteId].pos2.x = 8;
            gSprites[spriteId].pos2.y = 4;
            gSprites[spriteId].sXPos = 128;
            gSprites[spriteId].sYPos = 65;
            // Init weather
            sCableCar->weather = WEATHER_SUNNY;
            sCableCar->weatherDelay = 265;
            SetCurrentAndNextWeatherNoDelay(WEATHER_VOLCANIC_ASH);
            break;
    }
    for (i = 0; i < 9; i++)
    {
        spriteId = CreateSprite(&sSpriteTemplate_Cable, 16 * i + 96, 8 * i - 8, 0x68);
        gSprites[spriteId].pos2.x = 8;
        gSprites[spriteId].pos2.y = 8;
    }

    // 1/64 chance for an NPC to appear hiking on the ground below the Cable Car
    if ((rval % 64) == 0)
    {
        // Unclear if this was intentional, but the - 1 in the below ARRAY_COUNT means the Zigzagoon is never used
        spriteId = AddPseudoObjectEvent(hikerGraphicsIds[rval % (ARRAY_COUNT(hikerGraphicsIds) - 1)], hikerCallbacks[GOING_DOWN], hikerCoords[GOING_DOWN][0], hikerCoords[GOING_DOWN][1], 106);
        if (spriteId != MAX_SPRITES)
        {
            gSprites[spriteId].oam.priority = 2;
            gSprites[spriteId].pos2.x = -gSprites[spriteId].centerToCornerVecX;
            gSprites[spriteId].pos2.y = -gSprites[spriteId].centerToCornerVecY;
            
            // Randomly choose which direction the NPC is going
            if (!GOING_DOWN)
            {
                if (rval % 2)
                {
                    // Do walking west anim
                    StartSpriteAnim(&gSprites[spriteId], 6);
                    gSprites[spriteId].sSameDir = TRUE;
                    gSprites[spriteId].pos1.y += 2;
                }
                else
                {
                    // Do walking east anim
                    StartSpriteAnim(&gSprites[spriteId], 7);
                    gSprites[spriteId].sSameDir = FALSE;
                }
            }
            else
            {
                if (rval % 2)
                {
                    // Do walking east anim
                    StartSpriteAnim(&gSprites[spriteId], 7);
                    gSprites[spriteId].sSameDir = TRUE;
                    gSprites[spriteId].pos1.y += 2;
                }
                else
                {
                    // Do walking west anim
                    StartSpriteAnim(&gSprites[spriteId], 6);
                    gSprites[spriteId].sSameDir = FALSE;
                }
            }
            gSprites[spriteId].sDelay = hikerMovementDelayTable[rval % ARRAY_COUNT(hikerMovementDelayTable)];
        }
    }
}

static void BufferNextGroundSegment(void)
{
    u8 i, j, k;
    u8 offset;

    for (i = 0, k = 0, offset = 0x24 * (sCableCar->groundTilemapOffset + 2); i < 3; i++)
    {
        for (j = 0; j < ARRAY_COUNT(sCableCar->groundTileBuffer[0]); j++)
        {
            sCableCar->groundTileBuffer[i][j] = sCableCar->groundTilemap[offset++];
            sCableCar->groundTileBuffer[i + 3][j] = sCableCar->groundTilemap[k];
            sCableCar->groundTileBuffer[i + 6][j] = (sCableCar->groundTilemap + 0x24)[k];
            k++;
        }
    }

    sCableCar->groundTilemapOffset = (sCableCar->groundTilemapOffset + 1) % 3;
}

static void AnimateGroundGoingUp(void)
{
    sCableCar->groundTimer = (sCableCar->groundTimer + 1) % 96;
    sCableCar->bg0HorizontalOffset = sCableCar->groundXBase - sCableCar->groundXOffset;
    sCableCar->bg0VerticalOffset = sCableCar->groundYBase - sCableCar->groundYOffset;
    sCableCar->groundXOffset++;
    if ((sCableCar->groundXOffset % 4) == 0)
        sCableCar->groundYOffset++;

    if (sCableCar->groundXOffset > 16)
        DrawNextGroundSegmentGoingUp();
}

static void AnimateGroundGoingDown(void)
{
    sCableCar->groundTimer = (sCableCar->groundTimer + 1) % 96;
    sCableCar->bg0HorizontalOffset = sCableCar->groundXBase + sCableCar->groundXOffset;
    sCableCar->bg0VerticalOffset = sCableCar->groundYBase + sCableCar->groundYOffset;
    sCableCar->groundXOffset++;
    if ((sCableCar->groundXOffset % 4) == 0)
        sCableCar->groundYOffset++;

    if (sCableCar->groundXOffset > 16)
        DrawNextGroundSegmentGoingDown();
}

static void DrawNextGroundSegmentGoingUp(void)
{
    u8 i = 0;

    sCableCar->groundXOffset = sCableCar->groundYOffset = 0;
    sCableCar->groundXBase = sCableCar->bg0HorizontalOffset;
    sCableCar->groundYBase = sCableCar->bg0VerticalOffset;
    sCableCar->groundSegmentXStart = (sCableCar->groundSegmentXStart + 30) % 32;
    sCableCar->groundTileIdx -= 2;
    sGroundSegmentY_Up = (sCableCar->groundSegmentYStart + 23) % 32;

    // Draw next segment
    for (i = 0; i < ARRAY_COUNT(sCableCar->groundTileBuffer); i++)
    {
        sGroundX_Up = sCableCar->groundSegmentXStart;
        sGroundY_Up = (sGroundSegmentY_Up + i) % 32;
        FillBgTilemapBufferRect(0, sCableCar->groundTileBuffer[i][sCableCar->groundTileIdx], sGroundX_Up, sGroundY_Up, 1, 1, 17);
        sGroundX_Up = (sGroundX_Up + 1) % 32;
        FillBgTilemapBufferRect(0, sCableCar->groundTileBuffer[i][sCableCar->groundTileIdx + 1], sGroundX_Up, sGroundY_Up, 1, 1, 17);
    }

    // Erase old segment
    sGroundX_Up = (sCableCar->groundSegmentXStart + 30) % 32;
    FillBgTilemapBufferRect(0, 0, sGroundX_Up, 0, 2, 32, 17);
    if (sCableCar->groundTileIdx == 0)
    {
        sCableCar->groundSegmentYStart = (sCableCar->groundSegmentYStart + 29) % 32;
        sCableCar->groundTileIdx = 12;
        BufferNextGroundSegment();
        sGroundX_Up = (sCableCar->groundSegmentYStart + 1) % 32;
        FillBgTilemapBufferRect(0, 0, 0, sGroundX_Up, 32, 9, 17);
    }
}

static void DrawNextGroundSegmentGoingDown(void)
{
    u8 i = 0;

    sCableCar->groundXOffset = sCableCar->groundYOffset = 0;
    sCableCar->groundXBase = sCableCar->bg0HorizontalOffset;
    sCableCar->groundYBase = sCableCar->bg0VerticalOffset;
    sCableCar->groundSegmentXStart = (sCableCar->groundSegmentXStart + 2) % 32;
    sCableCar->groundTileIdx += 2;
    sGroundSegmentY_Down = sCableCar->groundSegmentYStart;
    
    // Draw next segment
    for (i = 0; i < ARRAY_COUNT(sCableCar->groundTileBuffer); i++)
    {
        sGroundX_Down = sCableCar->groundSegmentXStart;
        sGroundY_Down = (sGroundSegmentY_Down + i) % 32;
        FillBgTilemapBufferRect(0, sCableCar->groundTileBuffer[i][sCableCar->groundTileIdx], sGroundX_Down, sGroundY_Down, 1, 1, 17);
        sGroundX_Down = (sGroundX_Down + 1) % 32;
        FillBgTilemapBufferRect(0, sCableCar->groundTileBuffer[i][sCableCar->groundTileIdx + 1], sGroundX_Down, sGroundY_Down, 1, 1, 17);
    }

    // Erase old segment
    sGroundY_Down = (sCableCar->groundSegmentYStart + 23) % 32;
    FillBgTilemapBufferRect(0, 0, sCableCar->groundSegmentXStart, sGroundY_Down, 2, 9, 17);
    if (sCableCar->groundTileIdx == 10)
    {
        sCableCar->groundSegmentYStart = (sCableCar->groundSegmentYStart + 3) % 32;
        sCableCar->groundTileIdx = -2;
        BufferNextGroundSegment();
    }
}

static void InitGroundTilemapData(bool8 goingDown)
{
    switch (goingDown)
    {
        case FALSE:
        default:
            sCableCar->groundTilemapOffset = 2;
            sCableCar->groundSegmentXStart = 0;
            sCableCar->groundSegmentYStart = 20;
            sCableCar->groundTileIdx = 12;
            BufferNextGroundSegment();
            DrawNextGroundSegmentGoingUp();
            break;
        case TRUE:
            sCableCar->groundTilemapOffset = 2;
            sCableCar->groundSegmentXStart = 28;
            sCableCar->groundSegmentYStart = 20;
            sCableCar->groundTileIdx = 4;
            BufferNextGroundSegment();
            DrawNextGroundSegmentGoingDown();
            break;
    }

    sCableCar->groundTimer = 0;
}