;; Reset the background cycle so a new column can be loaded even if the cycle
;; was stopped at the end of the loading of the previous screen.
.macro RESET_BACKGROUND_CYCLE
    lda #$FF
    sta Background::zp_background_cycle
.endmacro

;; All functions and related variables for keeping up with the background
;; elements. This is by far the most extensive and complex piece of code from
;; the scrolling examples, and it's the core of the render engine for them.
;;
;; Documentation has been added for it as much as possible, so follow along to
;; better grasp how to use and grok this piece of code.
.scope Background
    ;; Current value for the scroll on the X axis.
    zp_scroll = $95

    ;; Instead of attempting to load a column every time the player movement
    ;; requires it, stall things a bit (as much as defined in
    ;; `BACKGROUND_CYCLE_MAX`). This allows for loading columns at a slower
    ;; pace. This cycle is diregarded if you set it to
    ;; `DISABLED_BACGROUND_CYCLE`, which it just means something like "no more
    ;; columns to load".
    zp_background_cycle = $96

    ;; See `zp_background_cycle`.
    DISABLED_BACKGROUND_CYCLE = $FE
    BACKGROUND_CYCLE_MAX = $02

    ;; The offset for metatile rows. That is, from where should the engine start
    ;; counting rows of metatiles. By default it's 0, but it could be set to
    ;; something else to reserve top space for a HUD or something similar.
    ;;
    ;; NOTE: configurable.
    .ifndef BACKGROUND_ROW_OFFSET
        BACKGROUND_ROW_OFFSET = 0
    .endif

    ;; The maximum row coordinate this engine is allowed to go. That is, until
    ;; which row each column is supposed to be rendered. By default it's $0F,
    ;; but it could be set to something else to allow a status element down the
    ;; screen.
    ;;
    ;; NOTE: configurable.
    .ifndef BACKGROUND_ROW_MAX
        BACKGROUND_ROW_MAX = $0F
    .endif

    ;; Current column (or the next to be loaded by functions like
    ;; `load_column`).
    zp_cur_column = $75

    ;; Current row (or the next to be loaded by functions like `load_column`).
    zp_cur_row = $76

    .proc init
        ;; Load the first level.
        ldx #$00
        JAL load_level_x
        ;; NOTE: returned value ignored as the level *must* have loaded at this
        ;; point.
    .endproc

    ;; Resets all the variables which touch in any way the loading of columns,
    ;; levels, etc. This is already being called by `load_level_x`, so outside
    ;; of this usage you shouldn't be calling it.
    .proc reset
        RESET_BACKGROUND_CYCLE

        lda #$00
        sta zp_scroll
        sta Buffer::zp_vram_idx
        sta zp_cur_column

        lda #BACKGROUND_ROW_OFFSET
        sta zp_cur_row

        rts
    .endproc

    ;; Load the level indexed by the `x` register. This function requires that
    ;; the PPU is disabled. If this is not the case, then it sets PPU::zp_mask
    ;; in a way so to disable the PPU on the next `nmi` call, and finally sets
    ;; `a` to 0 (i.e. level could not be loaded). Otherwise you can expect the
    ;; `a` register to contain 1 after calling this function.
    ;;
    ;; Moreover, the screen pointer is going to be left at the start of the
    ;; second screen, so future `load_column` calls can rely on this pointer
    ;; being set properly.
    .proc load_level_x
        ;; Is the PPU set to be disabled?
        lda PPU::zp_mask
        and #%00111100
        beq @load_level

        ;; The PPU is not disabled, set the mask so the PPU is disabled.
        lda #$00
        sta PPU::zp_mask

        ;; Reset the nametable being used.
        lda #%11111100
        and PPU::zp_control
        sta PPU::zp_control

        ;; Enable the `ppu` flag so these changes are not ignored.
        lda Globals::zp_flags
        ora #%01000000
        and #%11011111
        sta Globals::zp_flags
        lda #0
        rts

    @load_level:
        ;; Start by resetting any variables that touch on the loading of columns
        ;; in any way.
        jsr Background::reset

        ;; Reset all relevant flags.
        lda #%11000011
        and Globals::zp_flags
        sta Globals::zp_flags

        ;; Initialize the screen pointer by using the argument as passed through
        ;; the `x` register.
        lda levels_lo, x
        sta Metatile::zp_screen_ptr
        lda levels_hi, x
        sta Metatile::zp_screen_ptr + 1

    @loop:
        jsr load_column
        ;; NOTE: returned value ignored.

        ;; Increase the column being used and loop if we are not at the end of
        ;; the screen yet.
        lda #BACKGROUND_ROW_OFFSET
        sta zp_cur_row
        inc zp_cur_column
        lda zp_cur_column
        cmp #$10
        bne @loop

        ;; Advance the screen pointer so it points to the second screen.
        ADVANCE_SCREEN_PTR(1)

        ;; Enable back the PPU on the next NMI call.
        lda #%00011110
        sta PPU::zp_mask

        ;; And set the `ppu`, `level` and `nametable` flags.
        lda #%01100100
        ora Globals::zp_flags
        sta Globals::zp_flags

        ;; Reset things again just in case.
        jsr Background::reset

        lda #1
        rts
    .endproc

    ;; Translates the given metatile coordinate byte as given on the `a`
    ;; register and sets the first four argument variables from it. A "metatile
    ;; coordinate byte" is the first byte of a metatile reference on the screen
    ;; definition. Hence, the high nibble contains the Y metatile coordinate,
    ;; and the low nibble contains the X metatile coordinate.
    ;;
    ;; After calling this function you will have the following:
    ;;
    ;;  - zp_arg0: Y metatile coordinate (i.e. the high nibble as given on the
    ;;    `a` register shifted 4 times right).
    ;;  - zp_arg1: X metatile coordinate (i.e. the low nibble as given on the
    ;;    `a` register and the high nibble zero'ed out).
    ;;  - zp_arg2: low byte for the corresponding PPU base address.
    ;;  - zp_arg3: high byte for the corresponding PPU base address.
    .proc translate_mt_coordinates_to_arguments
        ;; Save the X metatile coordinate. This is also saved into a temporary
        ;; variable so it can be messed up when figuring out the PPU address.
        tay
        and #%00001111
        sta Globals::zp_arg1
        sta Globals::zp_tmp0

        ;; Save the Y metatile coordinate.
        tya
        and #%11110000
        lsr
        lsr
        lsr
        lsr
        sta Globals::zp_arg0

        ;; Now that the metatile coordinates have been isolated, let's go for
        ;; the PPU address.

        ;; The initial value for the high byte of the PPU address depends on the
        ;; nametable being used. For this we can take advantage of the fact that
        ;; this information is on bit 2 of the global flags. If we mask this
        ;; out, and add to it the base value of `$20`, we get either `$20` or
        ;; `$24`, which is the base high byte for either nametable.
        lda #%00000100
        and Globals::zp_flags
        clc
        adc #$20
        sta Globals::zp_arg3

        ;; Every 4 rows the high byte of the PPU address increases by one. This
        ;; means that with the Y metatile position, divide it by 4 (shift right
        ;; twice), and you can add that to the high byte directly.
        lda Globals::zp_arg0
        lsr
        lsr
        clc
        adc Globals::zp_arg3
        sta Globals::zp_arg3

        ;; After the high byte has been sorted out, notice that for the low byte
        ;; the base can only be the four values as defined in `ppu_offsets`,
        ;; as they repeat every four times.
        lda Globals::zp_arg0
        and #%00000011
        tax
        lda ppu_offsets, x

        ;; To that base we need to apply the X coordinate. Since each metatile
        ;; takes two tiles on the PPU, we can simply take the value on the X and
        ;; shift it left once. And finally add the base low byte from the row
        ;; with this shifted X value.
        asl Globals::zp_tmp0
        clc
        adc Globals::zp_tmp0
        sta Globals::zp_arg2

        rts
    ppu_offsets:
        .byte $00, $40, $80, $C0
    .endproc

    ;; Load the column as indexed by `Background::zp_cur_column` and
    ;; `Background:zp_cur_row`. This means that this function can be called at
    ;; any time, and so rendering columns in multiple iterations is indeed
    ;; possible. This is important to keep a small VRAM buffer.
    ;;
    ;; Returns 1 if the column has been completely loaded, 0 if there are rows
    ;; still to be loaded for the current column after calling this function.
    ;;
    ;; This function also sets the `ppu` and the `column` flags as expected in
    ;; each scenario, but it will *not* call functions such as
    ;; `prepare_next_column`, as that is to be done by the caller if necessary.
    .proc load_column
    @start:
        ;; First, compute the offset of the current metatile definition. That
        ;; is, what's left from the current row until the Y position of the next
        ;; metatile to push. First things first, check whether we are at the end
        ;; of the screen definition.
        ldy #0
        lda (Metatile::zp_screen_ptr), y
        cmp #$FF
        bne @fetch_offset

        ;; We are actually at the end of the screen definition. Then the offset
        ;; is simply the current row until the end. If that turns out to be
        ;; zero, then we are done.
        lda #BACKGROUND_ROW_MAX
        sec
        sbc zp_cur_row
        beq @done
        sta Globals::zp_idx
        jmp @default_loop

    @fetch_offset:
        ;; Otherwise, if we weren't at the end of the screen definition, fetch
        ;; the offset now. Note that if the offset is zero, then we can simply
        ;; go and push the metatile definition.
        jsr find_offset
        beq @push

        ;; This is an inner loop, which pushes the default metatile
        ;; `Globals::zp_idx` - 1 times.
        sta Globals::zp_idx
    @default_loop:
        ;; Make up a fake metatile position at the currently evaluated pair of
        ;; row/column. Afterwards just push it. Doing all this dance is
        ;; certainly not too expensive, but at the same time it could be easily
        ;; optimized. This is not done out of simplicity.
        lda zp_cur_row
        asl
        asl
        asl
        asl
        ora zp_cur_column
        jsr translate_mt_coordinates_to_arguments
        lda #$00
        jsr Buffer::push_metatile
        beq @end

        ;; The metatile was successfully pushed, now increase the row and check
        ;; if we have to push another default metatile.
        inc zp_cur_row
        dec Globals::zp_idx
        bne @default_loop

        ;; We are done pushing default metatiles. Check if we are done with the
        ;; column entirely.
        lda zp_cur_row
        cmp #BACKGROUND_ROW_MAX
        beq @done

        ;; Push the metatile pointed by the screen pointer.
    @push:
        ;; Pick up the current metatile position byte and set the arguments as
        ;; expected by `Buffer::push_metatile`,
        ;; `Collision::set_background_collision` and related functions.
        ldy #0
        lda (Metatile::zp_screen_ptr), y
        jsr translate_mt_coordinates_to_arguments

        ;; And now call `push_metatile` with the proper metatile definition.
        ldy #1
        lda (Metatile::zp_screen_ptr), y
        jsr Buffer::push_metatile
        beq @end

        ;; The two bytes for the metatile have been consumed, now advance the
        ;; screen pointer two bytes to point to the next metatile for the
        ;; screen.
        ADVANCE_SCREEN_PTR(2)

        ;; Increase the row being evaluated and go for another iteration if this
        ;; wasn't the last row for the column.
        inc zp_cur_row
        lda zp_cur_row
        cmp #$10
        bne @start

    @done:
        ;; Set the `ppu` flag and unset the `column` one. That is, a column is
        ;; now fully buffered and the PPU needs to update things.
        lda Globals::zp_flags
        ora #%01000000
        and #%11101111
        sta Globals::zp_flags

        lda #1
        rts
    @end:
        ;; Set the `ppu` and the `column` flags. Even if the column is not fully
        ;; buffered yet, we can already consume the VRAM buffer and empty it.
        ;; Setting the `column` flag will ask code elsewhere to beware of a
        ;; pending column to be loaded and act on it (e.g. by calling
        ;; `load_column` again to finish the task).
        lda Globals::zp_flags
        ora #%01010000
        sta Globals::zp_flags

        lda #0
        rts
    .endproc

    ;; Load a new column if the engine detects that it was needed. That is, it
    ;; will ask to load a column unless a column is already being rendered, or
    ;; the background cycle has not been consumed yet (or disabled entirely).
    .proc load_column_if_needed
        ;; If the `column` or the `level` flags are set, then we can skip the
        ;; loading altogether.
        lda #%00011000
        and Globals::zp_flags
        bne @end

        ;; Is the background cycle disabled? If so just go to the end.
        lda Background::zp_background_cycle
        cmp #DISABLED_BACKGROUND_CYCLE
        beq @end

        ;; Instead of loading a column every time it can be loaded, we add a bit
        ;; of a hiccup (i.e. "background cycle") so an entire level is not
        ;; loaded right away after the player moves one bit.
        inc Background::zp_background_cycle
        beq @do_load
        lda Background::zp_background_cycle
        cmp #BACKGROUND_CYCLE_MAX
        bne @end
        lda #$00
        sta Background::zp_background_cycle

    @do_load:
        ;; Load the column and increase the current column if it was fully
        ;; buffered. Otherwise it will be picked up by the `update` function.
        jsr load_column
        beq @end
        JAL prepare_next_column

    @end:
        rts
    .endproc

    ;; Load the next background column. You want to call this function on the
    ;; outside whenever a new column might be needed. This function will
    ;; eventually call `load_column_if_needed`, and it will also handle
    ;; overflows on the scroll values, changing the nametable being used, etc.
    ;;
    ;; Because of the above, this is the most suitable function to call whenever
    ;; you want to load a new column from the outside code, even if you are
    ;; unsure whether it's really needed.
    .proc load_next_background
        ;; Was there an overflow on the scroll's value?
        lda Background::zp_scroll
        bne @load_column_if_needed

        ;; Reset the background cycle so the next screen is picked up.
        RESET_BACKGROUND_CYCLE

        ;; Flip the nametable being used so the scroll makes sense for the
        ;; current context. After that, skip the loading of the column because
        ;; otherwise it would start loading on the currently displayed nametable
        ;; and that would make for weird glitches.
        lda PPU::zp_control
        eor #%00000001
        sta PPU::zp_control
        jmp @update_ppu_and_quit

    @load_column_if_needed:
        jsr Background::load_column_if_needed

    @update_ppu_and_quit:
        ;; Regardless of whether the loading of the column happened or not, the
        ;; scroll register has to change. Hence, set the `ppu` flag so at least
        ;; the PPU::SCROLL register is updated.
        lda Globals::zp_flags
        ora #%01000000
        sta Globals::zp_flags

        rts
    .endproc

    ;; Reset all the variables in preparation for the loading of a new column.
    ;; This function assumes that a column has already been fully buffered; and
    ;; it will also handle the case where it was the last column of the
    ;; screen/level by poking the right flags.
    .proc prepare_next_column
        ;; The row index always has to be set to zero.
        lda #BACKGROUND_ROW_OFFSET
        sta zp_cur_row

        ;; Is this the last column? If not, just quit.
        lda zp_cur_column
        cmp #$0F
        bne @end

        ;; Flip the nametable to be used.
        lda Globals::zp_flags
        eor #%00000100
        sta Globals::zp_flags

        ;; Mark the background cycle as over. This will make functions like
        ;; `load_column_if_needed` to refrain from loading a new column
        ;; altogether.
        lda #DISABLED_BACKGROUND_CYCLE
        sta zp_background_cycle

        ;; Reset the column to use. Note that the `@end` section will increase
        ;; the column to be used.
        lda #$FF
        sta zp_cur_column

        ;; If we were at the last column, we were out of metatiles for the
        ;; current screen. Hence, move to the next screen if possible.
        ADVANCE_SCREEN_PTR(1)

        ;; Do we have another "$FF" byte? If so then we are at the end of the
        ;; level entirely.
        ldy #0
        lda (Metatile::zp_screen_ptr), y
        cmp #$FF
        bne @end

        ;; Mark the end of the level.
        lda #%00001000
        ora Globals::zp_flags
        sta Globals::zp_flags

    @end:
        ;; Increase the current column and quit.
        inc zp_cur_column
        rts
    .endproc

    ;;;
    ;; Find the distance between the currently evaluated row and the one pointed
    ;; by the given argument. The argument is a byte where the Y value is
    ;; located on the high nibble, as expected from a metatile definition.
    ;;
    ;; NOTE: the argument and the returned value are on the `a` register.
    .proc find_offset
        ;; Are we at the right column?
        sta Globals::zp_arg0
        and #%00001111
        cmp zp_cur_column
        bne @to_the_end

        ;; We are: go until the current Y index.
        lda Globals::zp_arg0
        lsr
        lsr
        lsr
        lsr
        jmp @compute

    @to_the_end:
        ;; We aren't: go until the last row.
        lda #BACKGROUND_ROW_MAX

    @compute:
        ;; Subtract the current row with the one we are trying to reach.
        sec
        sbc zp_cur_row
        rts
    .endproc
.endscope
