package org._1935711.gosia;

import javacard.framework.Util;

public class Gfx {
    // @formatter:off
    // static private final byte APDU_CLA = (byte) 0xA0;
    // static private final short APDU_OFFSET_CLA = 0;
    // static private final short APDU_OFFSET_INS = 1;
    // static private final short APDU_OFFSET_P1 = 2;
    // static private final short APDU_OFFSET_P2 = 3;
    // static private final short APDU_OFFSET_P3 = 4;
    // static private final short APDU_OFFSET_LE = 4;
    // static private final short APDU_OFFSET_LC = 4;
    // static private final short APDU_OFFSET_DATA = 5;
    // @formatter:on

    static private final byte APDU_INS_BLOCK_0 = (byte) 0xB0;
    static private final byte APDU_INS_BLOCK_1 = (byte) (APDU_INS_BLOCK_0 + 1);

    static private final short FRAME_WIDTH = 36;
    static private final short FRAME_WIDTH_HALF = FRAME_WIDTH / 2;
    static private final short FRAME_HEIGHT = 14;
    static private final short FRAME_HEIGHT_HALF = FRAME_HEIGHT / 2;
    static private final short BLOCK_HEIGHT = FRAME_HEIGHT / 2; // Block width is equal to frame width by definition.
    static public final short BLOCK_LENGTH = FRAME_WIDTH * BLOCK_HEIGHT;
    static public final short STATE_LENGTH = 3;
    static public final short MEMORY_SIZE = BLOCK_LENGTH + STATE_LENGTH * 1;
    public byte[] memory;

    static public final short MEMORY_INDEX_BLOCK = 0;
    static public final short MEMORY_INDEX_STATE_FLIP = MEMORY_INDEX_BLOCK + BLOCK_LENGTH;
    static public final short MEMORY_INDEX_STATE_BACKWARD = MEMORY_INDEX_STATE_FLIP + 1;
    static public final short MEMORY_INDEX_STATE_EFFECT_LAST = MEMORY_INDEX_STATE_BACKWARD + 1;

    static private final byte HOURGLASS_HEIGHT = 36;
    static private final byte HOURGLASS_HEIGHT_HALF = HOURGLASS_HEIGHT / 2;
    static private final byte[] GLASS_X = {
            // By placing the glass in the center, the first few pixels go 'under' the
            // hourglass base, hence are hidden.
            FRAME_WIDTH_HALF, FRAME_WIDTH_HALF, FRAME_WIDTH_HALF, FRAME_WIDTH_HALF,
            11, 10, 10, 10, 10, 10, 11, 11, 12, 13, 14, 15, 15, 16, 16,
    };
    /**
     * X positions of the hourglass rod through animation frames.
     */
    static private final byte[] ROD_X = {
            2, 2, 3, 3, 4, 5, 6, 7, 9, 11, 13, 15, 17, 19, 20, 22, 24, 25, 26, 27, 27, 28, 28
    };
    /**
     * These offsets are relative to left edge of rod data.
     */
    static private final byte[] ROD_X_SKIP = {
            0, 0,
            0, 0,
            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    };
    /**
     * How many bytes should be copied from each row of rod data onto the
     * hourglass.
     */
    static private final byte[] ROD_X_LENGTH = {
            0, 0,
            4, 4,
            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    };

    /**
     * Offset of the sand on the hourglass, i.e., 11 pixels from the left edge of
     * hourglass
     */
    static private final byte SAND_X_OFFSET = 11;
    /**
     * These offsets are relative to left edge of sand data.
     */
    static private final byte[] SAND_X_SKIP = {
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            1, 2, 3, 4, 5, 5, 6, 6,
            6, 6, 6, 6, 6, 6, 6, 6, 6, 5,
            0, 0, 1,
            0, 0, 0, 0,
    };
    /**
     * How many bytes should be copied from each row of sand data onto the
     * hourglass.
     */
    static private final byte[] SAND_X_LENGTH = {
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            12, 10, 8, 6, 4, 4, 2, 2,
            2, 2, 2, 2, 2, 2, 2, 2, 2, 4,
            14, 14, 12,
            0, 0, 0, 0,
    };

    // @formatter:off
    static private final short EFFECT_LEN_TEST = 0;
    /**
     * 56 * 3 steps to finish each planet scroll. -18 for audio sync.
     */
    static private final short EFFECT_LEN_SOLARSYSTEM = (56 * 3) - 18;
    /**
     * 23 frames * 1 steps, down+up anim repeat 2x. +15 for audio sync.
     */
    static private final short EFFECT_LEN_HOURGLASS = (23 * 1 * 2) + 15;
    /**
     * 7 frames * 1 step * 2 (reverse), full anim repeat x4. +6 for audio sync.
     */
    static private final short EFFECT_LEN_GREENWAVE = (7 * 1 * 2 * 4) + 6;
    /**
     * 5 frames * 1 step, repeat 14x. +4 for audio sync.
     */
    static private final short EFFECT_LEN_BLUETUNNEL = (5 * 1 * 14) + 4;
    /**
     * 3 frames * 1 step, repeat 7x. +1 for audio sync
     */
    static private final short EFFECT_LEN_SWIRLYDIRLY = (3 * 1 * 7) + 1;
    /**
     * 24 frames * 2 step, repeat 2x. +26 for audio sync.
     */
    static private final short EFFECT_LEN_DUNGEON = (24 * 2 * 2) + 26;
    /**
     * 4 frames * 1 step, repeat 8x. +8 for audio sync.
     */
    static private final short EFFECT_LEN_BLINDINGSQUARES = (4 * 1 * 8) + 8;
    // static private final short EFFECT_LEN_SPIRALFRACTAL = 0; // Skipped
    /**
     * 60 frames * 1 step, repeat 1x. +1 for audio sync.
     */
    static private final short EFFECT_LEN_XENIUMFLAGS = (60 * 1 * 1) + 1;
    /**
     * 3 frames * 5 step * 2 (reverse), repeat 3x. +15 for audio sync.
     */
    static private final short EFFECT_LEN_TITLE = (3 * 5 * 2 * 3) + 15;
    /**
     * (45 + 4) frames * 3 step, repeat 1x.
     */
    static private final short EFFECT_LEN_CREDITS = (45 + 4) * 3 * 1;
    /**
     * 3 frames * 8 step, repeat 1x.
     */
    static private final short EFFECT_LEN_FAREWELL = 3 * 8 * 1;
    // @formatter:on

    public Gfx(byte[] _memory) {
        memory = _memory;
    }

    private static byte color(final byte r, final byte g, final byte b) {
        return (byte) (((r & 0x07) << 5) | ((g & 0x07) << 2) | (b & 0x03));
    }

    static private final short EFFECT_LEN_0 = EFFECT_LEN_TEST;
    static private final short EFFECT_LEN_1 = EFFECT_LEN_SOLARSYSTEM;
    static private final short EFFECT_LEN_2 = EFFECT_LEN_HOURGLASS;
    static private final short EFFECT_LEN_3 = EFFECT_LEN_GREENWAVE;
    static private final short EFFECT_LEN_4 = EFFECT_LEN_BLUETUNNEL;
    static private final short EFFECT_LEN_5 = EFFECT_LEN_SWIRLYDIRLY;
    static private final short EFFECT_LEN_6 = EFFECT_LEN_DUNGEON;
    static private final short EFFECT_LEN_7 = EFFECT_LEN_BLINDINGSQUARES;
    static private final short EFFECT_LEN_8 = EFFECT_LEN_XENIUMFLAGS;
    static private final short EFFECT_LEN_9 = EFFECT_LEN_TITLE;
    static private final short EFFECT_LEN_10 = EFFECT_LEN_CREDITS;
    static private final short EFFECT_LEN_11 = EFFECT_LEN_FAREWELL;

    // @formatter:off
    static private final short EFFECT_END_0 = EFFECT_LEN_0;
    static private final short EFFECT_END_1 = EFFECT_LEN_0 + EFFECT_LEN_1;
    static private final short EFFECT_END_2 = EFFECT_LEN_0 + EFFECT_LEN_1 + EFFECT_LEN_2;
    static private final short EFFECT_END_3 = EFFECT_LEN_0 + EFFECT_LEN_1 + EFFECT_LEN_2 + EFFECT_LEN_3;
    static private final short EFFECT_END_4 = EFFECT_LEN_0 + EFFECT_LEN_1 + EFFECT_LEN_2 + EFFECT_LEN_3 + EFFECT_LEN_4;
    static private final short EFFECT_END_5 = EFFECT_LEN_0 + EFFECT_LEN_1 + EFFECT_LEN_2 + EFFECT_LEN_3 + EFFECT_LEN_4 + EFFECT_LEN_5;
    static private final short EFFECT_END_6 = EFFECT_LEN_0 + EFFECT_LEN_1 + EFFECT_LEN_2 + EFFECT_LEN_3 + EFFECT_LEN_4 + EFFECT_LEN_5 + EFFECT_LEN_6;
    static private final short EFFECT_END_7 = EFFECT_LEN_0 + EFFECT_LEN_1 + EFFECT_LEN_2 + EFFECT_LEN_3 + EFFECT_LEN_4 + EFFECT_LEN_5 + EFFECT_LEN_6 + EFFECT_LEN_7;
    static private final short EFFECT_END_8 = EFFECT_LEN_0 + EFFECT_LEN_1 + EFFECT_LEN_2 + EFFECT_LEN_3 + EFFECT_LEN_4 + EFFECT_LEN_5 + EFFECT_LEN_6 + EFFECT_LEN_7 + EFFECT_LEN_8;
    static private final short EFFECT_END_9 = EFFECT_LEN_0 + EFFECT_LEN_1 + EFFECT_LEN_2 + EFFECT_LEN_3 + EFFECT_LEN_4 + EFFECT_LEN_5 + EFFECT_LEN_6 + EFFECT_LEN_7 + EFFECT_LEN_8 + EFFECT_LEN_9;
    static private final short EFFECT_END_10 = EFFECT_LEN_0 + EFFECT_LEN_1 + EFFECT_LEN_2 + EFFECT_LEN_3 + EFFECT_LEN_4 + EFFECT_LEN_5 + EFFECT_LEN_6 + EFFECT_LEN_7 + EFFECT_LEN_8 + EFFECT_LEN_9 + EFFECT_LEN_10;
    static private final short EFFECT_END_11 = EFFECT_LEN_0 + EFFECT_LEN_1 + EFFECT_LEN_2 + EFFECT_LEN_3 + EFFECT_LEN_4 + EFFECT_LEN_5 + EFFECT_LEN_6 + EFFECT_LEN_7 + EFFECT_LEN_8 + EFFECT_LEN_9 + EFFECT_LEN_10 + EFFECT_LEN_11;
    // @formatter:on

    public void control(final byte ins, final byte p1, final byte p2) {
        if (ins == APDU_INS_BLOCK_0 || ins == APDU_INS_BLOCK_1) {
            final short frameIndex = (short) ((((p1 & 0xFF) << 8) | (p2 & 0xFF)));
            final byte blockYAbsolute = (byte) ((ins - APDU_INS_BLOCK_0) * FRAME_HEIGHT_HALF);

            // For Testing.
            // @formatter:off
            // if (frameIndex >= 0 && frameIndex < EFFECT_END_0) {
            //     if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 0) {
            //         this.clear();
            //         memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 0;
            //     }
            //     final short frameIndexRelative = frameIndex;
            //     this.paletteTest(frameIndexRelative, blockYAbsolute);
            // }
            // else if (frameIndex >= EFFECT_END_0) {
            //     if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 1) {
            //         this.clear();
            //         memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 1;
            //     }
            //     final short frameIndexRelative = (short) (frameIndex - EFFECT_END_0);
            //     this.solarsystem(frameIndexRelative, blockYAbsolute);
            // }
            // @formatter:on

            // @formatter:off
            if (frameIndex >= 0 && frameIndex < EFFECT_END_0) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 0) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 0;
                }
                final short frameIndexRelative = frameIndex;
                this.paletteTest(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_0 && frameIndex < EFFECT_END_1) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 1) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 1;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_0);
                this.solarsystem(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_1 && frameIndex < EFFECT_END_2) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 2) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 2;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_1);
                this.hourglass(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_2 && frameIndex < EFFECT_END_3) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 3) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 3;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_2);
                this.greenwave(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_3 && frameIndex < EFFECT_END_4) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 4) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 4;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_3);
                this.bluetunnel(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_4 && frameIndex < EFFECT_END_5) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 5) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 5;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_4);
                this.swirlydirly(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_5 && frameIndex < EFFECT_END_6) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 6) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 6;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_5);
                this.dungeon(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_6 && frameIndex < EFFECT_END_7) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 7) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 7;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_6);
                this.blindingsquares(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_7 && frameIndex < EFFECT_END_8) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 8) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 8;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_7);
                this.xeniumflags(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_8 && frameIndex < EFFECT_END_9) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 9) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 9;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_8);
                this.title(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_9 && frameIndex < EFFECT_END_10) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 10) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 10;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_9);
                this.credits(frameIndexRelative, blockYAbsolute);
            } else if (frameIndex >= EFFECT_END_10 && frameIndex < EFFECT_END_11) {
                if (memory[MEMORY_INDEX_STATE_EFFECT_LAST] != 11) {
                    this.clear();
                    memory[MEMORY_INDEX_STATE_EFFECT_LAST] = 11;
                }
                final short frameIndexRelative = (short) (frameIndex - EFFECT_END_10);
                this.farewell(frameIndexRelative, blockYAbsolute);
            } else {
                this.clear();
            }
            // @formatter:on
        }
    }

    private short animationIndex(final short frameIndexRelative, final short frameDuration,
            final short frameCount, final byte mode) {
        if (mode == 1) {
            final byte animationIndexRawFull = (byte) ((frameIndexRelative
                    % (short) (frameCount * 4 * frameDuration))
                    / frameDuration);
            final byte animationIndexRawHalf = (byte) (animationIndexRawFull % (short) (frameCount * 2));
            // @formatter:off
            // final boolean flipRaw = animationIndexRawFull >= (short) (frameCount * 2);
            // @formatter:on
            final boolean backwardRaw = animationIndexRawHalf >= frameCount;
            memory[MEMORY_INDEX_STATE_FLIP] = animationIndexRawFull >= (short) (frameCount * 2) ? (byte) 1 : (byte) 0;
            memory[MEMORY_INDEX_STATE_BACKWARD] = animationIndexRawHalf >= frameCount ? (byte) 1 : (byte) 0;
            return backwardRaw ? (byte) ((frameCount - 1) - (animationIndexRawHalf - frameCount))
                    : animationIndexRawHalf;
        } else if (mode == 2) {
            final byte animationIndexRawFull = (byte) ((frameIndexRelative
                    % (short) (frameCount * 2 * frameDuration))
                    / frameDuration);
            // @formatter:off
            // final boolean flipRaw = false;
            // @formatter:on
            final boolean backwardRaw = animationIndexRawFull >= frameCount;
            memory[MEMORY_INDEX_STATE_FLIP] = 0;
            memory[MEMORY_INDEX_STATE_BACKWARD] = animationIndexRawFull >= frameCount ? (byte) 1 : (byte) 0;
            return backwardRaw ? (byte) ((frameCount - 1) - (animationIndexRawFull - frameCount))
                    : animationIndexRawFull;
        } else if (mode == 0) {
            memory[MEMORY_INDEX_STATE_FLIP] = 0;
            memory[MEMORY_INDEX_STATE_BACKWARD] = 0;
            return (short) ((frameIndexRelative % (short) (frameCount * frameDuration)) / frameDuration);
        } else {
            return (short) 0;
        }
    }

    private void clear() {
        Util.arrayFillNonAtomic(memory, MEMORY_INDEX_BLOCK, BLOCK_LENGTH, (byte) 0);
    }

    private void paletteTest(final short frameIndexRelative, final byte yAbsolute) {
        for (byte y = 0, blockY = 0; y < BLOCK_HEIGHT; ++y, ++blockY) {
            final short blockOffset = (short) ((short) (blockY) * FRAME_WIDTH);
            for (byte x = 0; x < FRAME_WIDTH; ++x) {
                memory[(short) (blockOffset + x)] = color(x, y, (byte) (x / 8));
            }
        }
    }

    private byte[] solarsystemData(final short animationIndex) {
        switch (animationIndex) {
            case 0:
                return A0.BLUE_PLANET__DATA;
            case 1:
                return A0.CUTE_PLANET__DATA;
            default:
                return A0.LESS_CUTE_PLANET__DATA;
        }
    }

    /**
     * Shows a set of 3 planets sliding across the screen from outside the screen on
     * the right to outside the screen on the left. The planet images must have at
     * least 2 columns of black pixels on the right (in case of 4 cards) because
     * that is how the screen gets cleared correctly.
     *
     * @param frameIndexRelative
     * @param blockYAbsolute
     */
    private void solarsystem(final short frameIndexRelative, final byte blockYAbsolute) {
        final short animationDuration = (short) (FRAME_WIDTH + A0.BLUE_PLANET__WIDTH);
        final short animIdx = animationIndex(frameIndexRelative, animationDuration, (short) 3, (byte) 0);

        final byte[] animationData = solarsystemData(animIdx);
        final short frameIndexShitfout = (short) (frameIndexRelative % animationDuration);

        if (frameIndexShitfout <= A0.BLUE_PLANET__WIDTH) {
            final short O__blockIndexAsset = (short) (FRAME_WIDTH - frameIndexShitfout);
            final short O__yRelativeInit = O__blockIndexAsset;
            final short O__yAbsoluteInit = (short) (blockYAbsolute * A0.BLUE_PLANET__WIDTH);
            final short O__forEnd = (short) ((short) (BLOCK_HEIGHT * FRAME_WIDTH) + O__yRelativeInit);

            final short assetCopyLength = (short) (A0.BLUE_PLANET__WIDTH
                    - (A0.BLUE_PLANET__WIDTH - frameIndexShitfout));
            for (short yAbsolute = O__yAbsoluteInit,
                    yRelative = O__yRelativeInit; yRelative < O__forEnd; yAbsolute += A0.BLUE_PLANET__WIDTH, yRelative += FRAME_WIDTH) {
                // blockIndexAsset = yRelative
                // assetIndex = yAbsolute
                Util.arrayCopyNonAtomic(animationData, yAbsolute, memory, yRelative, assetCopyLength);
            }
        } else if (frameIndexShitfout <= FRAME_WIDTH) {
            final short O__blockIndexAsset = (short) (FRAME_WIDTH - frameIndexShitfout);
            final short O__yRelativeInit = O__blockIndexAsset;
            final short O__yAbsoluteInit = (short) (blockYAbsolute * A0.BLUE_PLANET__WIDTH);
            final short O__forEnd = (short) ((short) (BLOCK_HEIGHT * FRAME_WIDTH) + O__yRelativeInit);

            for (short yAbsolute = O__yAbsoluteInit,
                    yRelative = O__yRelativeInit; yRelative < O__forEnd; yAbsolute += A0.BLUE_PLANET__WIDTH, yRelative += FRAME_WIDTH) {
                // blockIndexAsset = yRelative
                // assetIndex = yAbsolute
                Util.arrayCopyNonAtomic(animationData, yAbsolute, memory, yRelative, A0.BLUE_PLANET__WIDTH);
            }
        } else {
            final short O__forEnd = BLOCK_HEIGHT * FRAME_WIDTH;
            final short O__assetIndex = (short) (frameIndexShitfout - FRAME_WIDTH);
            final short O__yAbsoluteInit = (short) ((short) (blockYAbsolute * A0.BLUE_PLANET__WIDTH) + O__assetIndex);

            final short assetCopyLength = (short) (A0.BLUE_PLANET__WIDTH - (frameIndexShitfout - FRAME_WIDTH));
            for (short yAbsolute = O__yAbsoluteInit,
                    yRelative = 0; yRelative < O__forEnd; yAbsolute += A0.BLUE_PLANET__WIDTH, yRelative += FRAME_WIDTH) {
                // blockIndexAsset = yRelative
                // assetIndex = yAbsolute
                Util.arrayCopyNonAtomic(animationData, yAbsolute, memory, yRelative, assetCopyLength);
            }
        }
    }

    private byte[] hourglassSandData(final short animationIndex) {
        switch (animationIndex) {
            case 0:
                return A1.HOURGLASS_SAND__FRAME_0__DATA;
            case 1:
                return A1.HOURGLASS_SAND__FRAME_1__DATA;
            case 2:
                return A1.HOURGLASS_SAND__FRAME_2__DATA;
            default:
                return A1.HOURGLASS_SAND__FRAME_3__DATA;
        }
    }

    /**
     * Spinning hourglass effect with a parallax effect that uses hourglass rods
     * that go in front and behind the hourglass.
     *
     * The sand slightly breaks the effect because some of the black pixels from
     * sand are drawn over the rod when they are in the center.
     *
     * @param frameIndexRelative
     * @param blockYAbsolute
     */
    private void hourglass(final short frameIndexRelative, final byte blockYAbsolute) {
        final short animIdxSand = animationIndex(frameIndexRelative, (short) 1, (short) 4, (byte) 0);
        final short animIdx = animationIndex(frameIndexRelative, (short) 1, (short) 23, (byte) 2);

        final byte[] sandData = hourglassSandData(animIdxSand);
        final byte rodX = ROD_X[animIdx];

        this.clear();

        final short forEnd = BLOCK_HEIGHT * FRAME_WIDTH;

        for (short assetYRaw = (short) (blockYAbsolute + animIdx),
                blockIndex = 0; blockIndex < forEnd; ++assetYRaw, blockIndex += FRAME_WIDTH) {
            final short assetY = assetYRaw < HOURGLASS_HEIGHT_HALF ? assetYRaw
                    : (short) ((HOURGLASS_HEIGHT_HALF - (short) 1) - (assetYRaw - HOURGLASS_HEIGHT_HALF));

            final short blockGlassIndexLeft = (short) (blockIndex + (short) GLASS_X[assetY]);
            final short blockGlassIndexRight = (short) (blockIndex
                    + (short) ((short) (FRAME_WIDTH_HALF - (short) 1)
                            + (short) (FRAME_WIDTH_HALF - (short) GLASS_X[assetY])));

            final short blockIndexRodBase = (short) (blockIndex + (short) ROD_X_SKIP[assetY]);
            final short blockIndexRodLeft = (short) (blockIndexRodBase + (short) rodX);
            final short blockIndexRodRight = (short) (blockIndexRodBase
                    + (short) (FRAME_WIDTH - A0.HOURGLASS_ROD__WIDTH - (short) rodX));
            final short rodIndex = (short) ((short) (assetY * A0.HOURGLASS_ROD__WIDTH) + (short) ROD_X_SKIP[assetY]);

            if (assetY < 4) {
                final short hourglassIndex = (short) (assetY * A0.HOURGLASS__WIDTH);
                Util.arrayCopyNonAtomic(A0.HOURGLASS__DATA, hourglassIndex, memory, blockIndex, A0.HOURGLASS__WIDTH);
                Util.arrayCopyNonAtomic(A0.HOURGLASS_ROD__DATA, rodIndex, memory, blockIndexRodRight,
                        (short) ROD_X_LENGTH[assetY]);
                Util.arrayCopyNonAtomic(A0.HOURGLASS_ROD__DATA, rodIndex, memory, blockIndexRodLeft,
                        (short) ROD_X_LENGTH[assetY]);
            } else {
                final short blockIndexSand = (short) (blockIndex + (short) SAND_X_OFFSET
                        + (short) SAND_X_SKIP[assetYRaw]);
                final short sandIndex = (short) ((short) (assetYRaw * A1.HOURGLASS_SAND__FRAME_0__WIDTH)
                        + (short) SAND_X_SKIP[assetYRaw]);

                if (memory[MEMORY_INDEX_STATE_BACKWARD] == 0) {
                    Util.arrayCopyNonAtomic(A0.HOURGLASS_ROD__DATA, rodIndex, memory, blockIndexRodRight,
                            (short) ROD_X_LENGTH[assetY]);
                    memory[blockGlassIndexLeft] = (byte) -33; // Light blue glass.
                    memory[blockGlassIndexRight] = (byte) -33; // Light blue glass.
                    Util.arrayCopyNonAtomic(sandData, sandIndex, memory, blockIndexSand,
                            (short) SAND_X_LENGTH[assetYRaw]);
                    Util.arrayCopyNonAtomic(A0.HOURGLASS_ROD__DATA, rodIndex, memory, blockIndexRodLeft,
                            (short) ROD_X_LENGTH[assetY]);
                } else {
                    Util.arrayCopyNonAtomic(A0.HOURGLASS_ROD__DATA, rodIndex, memory, blockIndexRodLeft,
                            (short) ROD_X_LENGTH[assetY]);
                    memory[blockGlassIndexLeft] = (byte) -33; // Light blue glass.
                    memory[blockGlassIndexRight] = (byte) -33; // Light blue glass.
                    Util.arrayCopyNonAtomic(sandData, sandIndex, memory, blockIndexSand,
                            (short) SAND_X_LENGTH[assetYRaw]);
                    Util.arrayCopyNonAtomic(A0.HOURGLASS_ROD__DATA, rodIndex, memory, blockIndexRodRight,
                            (short) ROD_X_LENGTH[assetY]);
                }
            }
        }
    }

    private byte[] greenwaveData(final short animationIndex) {
        switch (animationIndex) {
            case 0:
                return A0.GREEN_WAVE__FRAME_6__DATA;
            case 1:
                return A0.GREEN_WAVE__FRAME_5__DATA;
            case 2:
                return A0.GREEN_WAVE__FRAME_4__DATA;
            case 3:
                return A0.GREEN_WAVE__FRAME_3__DATA;
            case 4:
                return A0.GREEN_WAVE__FRAME_2__DATA;
            case 5:
                return A0.GREEN_WAVE__FRAME_1__DATA;
            default:
                return A0.GREEN_WAVE__FRAME_0__DATA;
        }
    }

    /**
     * Effect that consists of pulsing wave sprites that are repeated in multiple
     * locations on the screen, with mirroring and flipping.
     *
     * @param frameIndexRelative
     * @param blockYAbsolute
     */
    private void greenwave(final short frameIndexRelative, final byte blockYAbsolute) {
        final short animIdx = animationIndex(frameIndexRelative, (short) 1, (short) 7, (byte) 1);

        this.clear();

        final byte[] animationData = greenwaveData(animIdx);
        if (memory[MEMORY_INDEX_STATE_FLIP] == 0) {
            if (blockYAbsolute == 0) {
                for (byte yRelative = 0; yRelative < BLOCK_HEIGHT; ++yRelative) {
                    final short blockIndex0 = (short) (yRelative * FRAME_WIDTH);
                    final short blockIndex1 = (short) (blockIndex0 + (short) 14);
                    final short blockIndex2 = (short) (blockIndex0 + (short) 30);

                    final short assetIndexBase = (short) (yRelative * A0.GREEN_WAVE__FRAME_0__WIDTH);
                    final short assetIndexStartCut = (short) (assetIndexBase + (short) 2);

                    Util.arrayCopyNonAtomic(animationData, assetIndexStartCut, memory, blockIndex0, (short) 7);
                    Util.arrayCopyNonAtomic(animationData, assetIndexBase, memory, blockIndex1,
                            A0.GREEN_WAVE__FRAME_0__WIDTH);
                    Util.arrayCopyNonAtomic(animationData, assetIndexBase, memory, blockIndex2, (short) 6);
                }
            } else {
                for (byte yRelative = 0; yRelative < BLOCK_HEIGHT; ++yRelative) {
                    final short blockIndex0 = (short) ((short) (yRelative * FRAME_WIDTH) + (short) 6);
                    final short blockIndex1 = (short) (blockIndex0 + (short) 16);

                    final short assetIndex = (short) (((short) (A0.GREEN_WAVE__FRAME_0__HEIGHT - (short) 1) - yRelative)
                            * A0.GREEN_WAVE__FRAME_0__WIDTH);

                    Util.arrayCopyNonAtomic(animationData, assetIndex, memory, blockIndex0,
                            A0.GREEN_WAVE__FRAME_0__WIDTH);
                    Util.arrayCopyNonAtomic(animationData, assetIndex, memory, blockIndex1,
                            A0.GREEN_WAVE__FRAME_0__WIDTH);
                }
            }
        } else {
            if (blockYAbsolute == 0) {
                for (byte yRelative = 0; yRelative < BLOCK_HEIGHT; ++yRelative) {
                    final short blockIndex0 = (short) ((short) (yRelative * FRAME_WIDTH) + (short) 6);
                    final short blockIndex1 = (short) (blockIndex0 + (short) 16);

                    final short assetIndex = (short) (yRelative * A0.GREEN_WAVE__FRAME_0__WIDTH);

                    Util.arrayCopyNonAtomic(animationData, assetIndex, memory, blockIndex0,
                            A0.GREEN_WAVE__FRAME_0__WIDTH);
                    Util.arrayCopyNonAtomic(animationData, assetIndex, memory, blockIndex1,
                            A0.GREEN_WAVE__FRAME_0__WIDTH);
                }
            } else {
                for (byte yRelative = 0; yRelative < BLOCK_HEIGHT; ++yRelative) {
                    final short blockIndex0 = (short) (yRelative * FRAME_WIDTH);
                    final short blockIndex1 = (short) (blockIndex0 + (short) 14);
                    final short blockIndex2 = (short) (blockIndex0 + (short) 30);

                    final short assetIndexBase = (short) (((short) (A0.GREEN_WAVE__FRAME_0__HEIGHT - (short) 1)
                            - yRelative) * A0.GREEN_WAVE__FRAME_0__WIDTH);
                    final short assetIndexStartCut = (short) (assetIndexBase + (short) 2);

                    Util.arrayCopyNonAtomic(animationData, assetIndexStartCut, memory, blockIndex0, (short) 7);
                    Util.arrayCopyNonAtomic(animationData, assetIndexBase, memory, blockIndex1,
                            A0.GREEN_WAVE__FRAME_0__WIDTH);
                    Util.arrayCopyNonAtomic(animationData, assetIndexBase, memory, blockIndex2, (short) 6);
                }

            }
        }
    }

    private byte[] bluetunnelData(final short animationIndex) {
        switch (animationIndex) {
            case 0:
                return A0.BLUE_TUNNEL__FRAME_0__DATA;
            case 1:
                return A0.BLUE_TUNNEL__FRAME_1__DATA;
            case 2:
                return A0.BLUE_TUNNEL__FRAME_2__DATA;
            case 3:
                return A0.BLUE_TUNNEL__FRAME_3__DATA;
            default:
                return A0.BLUE_TUNNEL__FRAME_4__DATA;
        }
    }

    /**
     * Simple effect that mirrors a pre-rendered tunnel.
     *
     * @param frameIndexRelative
     * @param blockYAbsolute
     */
    private void bluetunnel(final short frameIndexRelative, final byte blockYAbsolute) {
        final short animIdx = animationIndex(frameIndexRelative, (short) 1, (short) 5, (byte) 0);

        final byte[] animationData = bluetunnelData(animIdx);

        if (blockYAbsolute == 0) {
            for (byte yRelative = 0; yRelative < BLOCK_HEIGHT; ++yRelative) {
                final short blockIndex = (short) (yRelative * FRAME_WIDTH);
                final short assetIndex = blockIndex;
                Util.arrayCopyNonAtomic(animationData, assetIndex, memory, blockIndex, FRAME_WIDTH);
            }
        } else {
            for (byte yRelative = 0; yRelative < BLOCK_HEIGHT; ++yRelative) {
                final short blockIndex = (short) (yRelative * FRAME_WIDTH);
                final short assetIndex = (short) ((short) (BLOCK_HEIGHT - yRelative)
                        * FRAME_WIDTH);
                Util.arrayCopyNonAtomic(animationData, assetIndex, memory, blockIndex, FRAME_WIDTH);
            }
        }
    }

    private byte[] swirlydirlyData(final short animationIndex) {
        switch (animationIndex) {
            case 0:
                return A0.SWIRLY_DIRLY__FRAME_0__DATA;
            case 1:
                return A0.SWIRLY_DIRLY__FRAME_1__DATA;
            case 2:
                return A0.SWIRLY_DIRLY__FRAME_2__DATA;
            default:
                return A0.SWIRLY_DIRLY__FRAME_3__DATA;
        }
    }

    /**
     * A spiral created by displaying a slideshow... I wonder if the demo would look
     * better with the spiral fractal instead of this but we were just trying to
     * wrap things up soon before the compo.
     *
     * @param frameIndexRelative
     * @param blockYAbsolute
     */
    private void swirlydirly(final short frameIndexRelative, final byte blockYAbsolute) {
        final short animIdx = animationIndex(frameIndexRelative, (short) 1, (short) 3, (byte) 0);

        final byte[] animationData = swirlydirlyData(animIdx);

        if (blockYAbsolute == 0) {
            Util.arrayCopyNonAtomic(animationData, (short) 0, memory, (short) 0,
                    (short) (A0.SWIRLY_DIRLY__FRAME_0__DATA.length / 2));
        } else {
            Util.arrayCopyNonAtomic(animationData, (short) (A0.SWIRLY_DIRLY__FRAME_0__DATA.length / 2), memory,
                    (short) 0, (short) (A0.SWIRLY_DIRLY__FRAME_0__DATA.length / 2));
        }
    }

    private byte[] dungeonData(final short animationIndex) {
        switch (animationIndex) {
            case 0:
                return A1.DUNGEON__FRAME_0__DATA;
            case 1:
                return A1.DUNGEON__FRAME_1__DATA;
            case 2:
                return A1.DUNGEON__FRAME_2__DATA;
            case 3:
                return A1.DUNGEON__FRAME_3__DATA;
            case 4:
                return A1.DUNGEON__FRAME_4__DATA;
            case 5:
                return A1.DUNGEON__FRAME_5__DATA;
            case 6:
                return A1.DUNGEON__FRAME_6__DATA;
            case 7:
                return A1.DUNGEON__FRAME_7__DATA;
            case 8:
                return A1.DUNGEON__FRAME_8__DATA;
            case 9:
                return A1.DUNGEON__FRAME_9__DATA;
            case 10:
                return A1.DUNGEON__FRAME_10__DATA;
            case 11:
                return A1.DUNGEON__FRAME_11__DATA;
            case 12:
                return A1.DUNGEON__FRAME_12__DATA;
            case 13:
                return A2.DUNGEON__FRAME_13__DATA;
            case 14:
                return A2.DUNGEON__FRAME_14__DATA;
            case 15:
                return A2.DUNGEON__FRAME_15__DATA;
            case 16:
                return A2.DUNGEON__FRAME_16__DATA;
            case 17:
                return A2.DUNGEON__FRAME_17__DATA;
            case 18:
                return A2.DUNGEON__FRAME_18__DATA;
            case 19:
                return A2.DUNGEON__FRAME_19__DATA;
            case 20:
                return A2.DUNGEON__FRAME_20__DATA;
            case 21:
                return A2.DUNGEON__FRAME_21__DATA;
            case 22:
                return A2.DUNGEON__FRAME_22__DATA;
            default:
                return A2.DUNGEON__FRAME_23__DATA;
        }
    }

    /**
     * Displays a pre-rendered raycast slideshow with minor computation taking care
     * of mirroring and shifting each frame.
     *
     * @param frameIndexRelative
     * @param blockYAbsolute
     */
    private void dungeon(final short frameIndexRelative, final byte blockYAbsolute) {
        final short animIdx = animationIndex(frameIndexRelative, (short) 2, (short) 24, (byte) 0);

        final byte[] animationData = dungeonData(animIdx);

        if (blockYAbsolute == 0) {
            for (byte yRelative = 0; yRelative < BLOCK_HEIGHT; ++yRelative) {
                final short blockIndex = (short) (yRelative * FRAME_WIDTH);
                final short assetIndex = (short) ((short) (yRelative + (short) 3) * FRAME_WIDTH);
                Util.arrayCopyNonAtomic(animationData, assetIndex, memory, blockIndex, FRAME_WIDTH);
            }
        } else {
            for (byte yRelative = 0; yRelative < BLOCK_HEIGHT; ++yRelative) {
                final short blockIndex = (short) (yRelative * FRAME_WIDTH);
                final short assetIndex = (short) ((short) ((short) (BLOCK_HEIGHT - yRelative) + (short) 3)
                        * FRAME_WIDTH);
                Util.arrayCopyNonAtomic(animationData, assetIndex, memory, blockIndex, FRAME_WIDTH);
            }
        }
    }

    private byte[] blindingsquaresData(final short animationIndex, final byte indexOffset) {
        final short animationIndexTmp = (short) ((short) (animationIndex + indexOffset) % 4);
        switch (animationIndexTmp) {
            case 0:
                return A0.BLINDING_SQUARES__FRAME_0__DATA;
            case 1:
                return A0.BLINDING_SQUARES__FRAME_1__DATA;
            case 2:
                return A0.BLINDING_SQUARES__FRAME_2__DATA;
            default:
                return A0.BLINDING_SQUARES__FRAME_3__DATA;
        }
    }

    /**
     * Animating a square sprite repeated in x and y directions. Each neighboring
     * sprite has its animation offset by 2 steps.
     *
     * @param frameIndexRelative
     * @param blockYAbsolute
     */
    private void blindingsquares(final short frameIndexRelative, final byte blockYAbsolute) {
        final short animIdx = animationIndex(frameIndexRelative, (short) 1, (short) 4, (byte) 0);

        final byte[] animationData0 = blindingsquaresData(animIdx, (byte) 0);
        final byte[] animationData2 = blindingsquaresData(animIdx, (byte) 2);

        if (blockYAbsolute == 0) {
            for (byte yRelative = 0; yRelative < BLOCK_HEIGHT; ++yRelative) {
                final short blockIndex0 = (short) (yRelative * FRAME_WIDTH);
                final short blockIndex1 = (short) (blockIndex0 + A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                final short blockIndex2 = (short) (blockIndex1 + A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                final short blockIndex3 = (short) (blockIndex2 + A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                final short blockIndex4 = (short) (blockIndex3 + A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                final short blockIndex5 = (short) (blockIndex4 + A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                final short assetIndex = (short) (yRelative * A0.BLINDING_SQUARES__FRAME_0__WIDTH);

                Util.arrayCopyNonAtomic(animationData0, assetIndex, memory, blockIndex0,
                        A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                Util.arrayCopyNonAtomic(animationData2, assetIndex, memory, blockIndex1,
                        A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                Util.arrayCopyNonAtomic(animationData0, assetIndex, memory, blockIndex2,
                        A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                Util.arrayCopyNonAtomic(animationData2, assetIndex, memory, blockIndex3,
                        A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                Util.arrayCopyNonAtomic(animationData0, assetIndex, memory, blockIndex4,
                        A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                Util.arrayCopyNonAtomic(animationData2, assetIndex, memory, blockIndex5,
                        (short) 1);
            }
        } else {
            for (byte yRelative = 0; yRelative < BLOCK_HEIGHT; ++yRelative) {
                final short blockIndex0 = (short) (yRelative * FRAME_WIDTH);
                final short blockIndex1 = (short) (blockIndex0 + A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                final short blockIndex2 = (short) (blockIndex1 + A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                final short blockIndex3 = (short) (blockIndex2 + A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                final short blockIndex4 = (short) (blockIndex3 + A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                final short blockIndex5 = (short) (blockIndex4 + A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                final short assetIndex = (short) (yRelative * A0.BLINDING_SQUARES__FRAME_0__WIDTH);

                Util.arrayCopyNonAtomic(animationData2, assetIndex, memory, blockIndex0,
                        A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                Util.arrayCopyNonAtomic(animationData0, assetIndex, memory, blockIndex1,
                        A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                Util.arrayCopyNonAtomic(animationData2, assetIndex, memory, blockIndex2,
                        A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                Util.arrayCopyNonAtomic(animationData0, assetIndex, memory, blockIndex3,
                        A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                Util.arrayCopyNonAtomic(animationData2, assetIndex, memory, blockIndex4,
                        A0.BLINDING_SQUARES__FRAME_0__WIDTH);
                Util.arrayCopyNonAtomic(animationData0, assetIndex, memory, blockIndex5,
                        (short) 1);
            }
        }
    }

    // @formatter:off
    // private byte[] spiralfractalData(final short animationIndex) {
    //     switch (animationIndex) {
    //         case 0:
    //             return A0.SPIRAL_FRACTAL__FRAME_0__DATA;
    //         case 1:
    //             return A0.SPIRAL_FRACTAL__FRAME_1__DATA;
    //         case 2:
    //             return A0.SPIRAL_FRACTAL__FRAME_2__DATA;
    //         case 3:
    //             return A0.SPIRAL_FRACTAL__FRAME_3__DATA;
    //         case 4:
    //             return A0.SPIRAL_FRACTAL__FRAME_4__DATA;
    //         case 5:
    //             return A0.SPIRAL_FRACTAL__FRAME_5__DATA;
    //         default:
    //             return A0.SPIRAL_FRACTAL__FRAME_6__DATA;
    //     }
    // }

    // /**
    //  * Shows a pre-rendered spiral. Unfortunately was cut from the final demo by the
    //  * musicians haha. Granted, it was a pretty simple effect, essentially a
    //  * slideshow once again.
    //  *
    //  * @param frameIndexRelative
    //  * @param blockYAbsolute
    //  */
    // private void spiralfractal(final short frameIndexRelative, final byte blockYAbsolute) {
    //     final short animIdx = animationIndex(frameIndexRelative, (short) 1, (short) 7, (byte) 0);

    //     final byte[] animationData = spiralfractalData(animIdx);

    //     if (blockYAbsolute == 0) {
    //         Util.arrayCopyNonAtomic(animationData, (short) 0, memory, (short) 0,
    //                 (short) (A0.SPIRAL_FRACTAL__FRAME_0__DATA.length / 2));
    //     } else {
    //         Util.arrayCopyNonAtomic(animationData, (short) (A0.SPIRAL_FRACTAL__FRAME_0__DATA.length / 2), memory,
    //                 (short) 0, (short) (A0.SPIRAL_FRACTAL__FRAME_0__DATA.length / 2));
    //     }
    // }
    // @formatter:on

    /**
     * Splits the screen in half horizontally. "Xenium 2024" gets rendered in top
     * half by sliding a sprite across the screen. Bottom is made up of a narrow
     * sprite with flags that get repeated and then shifted left as the text
     * scrolls. The flags are mainly a memory copy, but there is an edge case on the
     * left and right edges of screen that require a length and position adjustment
     * of 2 additional flag sprites.
     *
     * @param frameIndexRelative
     * @param blockYAbsolute
     */
    private void xeniumflags(final short frameIndexRelative, final byte blockYAbsolute) {
        final short animIdx = animationIndex(frameIndexRelative, (short) 1, (short) (A0.XENIUM__WIDTH + 1), (byte) 0);

        this.clear();

        if (blockYAbsolute == 0) {
            final short animIdxDelay = (short) (animIdx - (short) (A0.XENIUM__WIDTH - FRAME_WIDTH));
            if (animIdxDelay <= 0) {
                for (byte yRelative = 0, blockY = (short) (BLOCK_HEIGHT
                        - A0.XENIUM__HEIGHT); yRelative < A0.XENIUM__HEIGHT; ++yRelative, ++blockY) {
                    final short blockIndex0 = (short) (blockY * FRAME_WIDTH);
                    final short assetIndex0 = (short) ((short) (yRelative * A0.XENIUM__WIDTH) + animIdx);
                    Util.arrayCopyNonAtomic(A0.XENIUM__DATA, assetIndex0, memory, blockIndex0,
                            FRAME_WIDTH);
                }
            } else {
                for (byte yRelative = 0, blockY = (short) (BLOCK_HEIGHT
                        - A0.XENIUM__HEIGHT); yRelative < A0.XENIUM__HEIGHT; ++yRelative, ++blockY) {
                    final short assetLength0 = (short) (FRAME_WIDTH - animIdxDelay);
                    final short assetLength1 = (short) (FRAME_WIDTH - assetLength0);

                    final short blockIndex0 = (short) (blockY * FRAME_WIDTH);
                    final short blockIndex1 = (short) (blockIndex0 + assetLength0);

                    final short assetIndex1 = (short) (yRelative * A0.XENIUM__WIDTH);
                    final short assetIndex0 = (short) (assetIndex1 + animIdx);

                    Util.arrayCopyNonAtomic(A0.XENIUM__DATA, assetIndex0, memory, blockIndex0, assetLength0);
                    Util.arrayCopyNonAtomic(A0.XENIUM__DATA, assetIndex1, memory, blockIndex1, assetLength1);
                }
            }
        } else {
            final short animIdxWraparound = (short) (animIdx % (short) (A0.FLAGS__WIDTH - (short) 1));
            for (byte yRelative = 0, blockY = (short) (BLOCK_HEIGHT - 1
                    - A0.FLAGS__HEIGHT); yRelative < A0.FLAGS__HEIGHT; ++yRelative, ++blockY) {
                final short blockIndex0 = (short) (blockY * FRAME_WIDTH);
                final short blockIndexBase = (short) (blockIndex0 - animIdxWraparound);
                final short blockIndex1 = (short) (blockIndexBase + (short) (A0.FLAGS__WIDTH - (short) 1));
                final short blockIndex2 = (short) (blockIndex1 + (short) (A0.FLAGS__WIDTH - (short) 1));
                final short blockIndex3 = (short) (blockIndex2 + (short) (A0.FLAGS__WIDTH - (short) 1));
                final short blockIndex4 = (short) (blockIndex3 + (short) (A0.FLAGS__WIDTH - (short) 1));
                final short assetIndex0 = (short) (yRelative * A0.FLAGS__WIDTH);

                final short assetIndexLeft = (short) (assetIndex0 + animIdxWraparound);
                final short assetLengthLeft = (short) (A0.FLAGS__WIDTH - animIdxWraparound);

                Util.arrayCopyNonAtomic(A0.FLAGS__DATA, assetIndexLeft, memory, blockIndex0, assetLengthLeft);
                Util.arrayCopyNonAtomic(A0.FLAGS__DATA, assetIndex0, memory, blockIndex1, A0.FLAGS__WIDTH);
                Util.arrayCopyNonAtomic(A0.FLAGS__DATA, assetIndex0, memory, blockIndex2, A0.FLAGS__WIDTH);
                Util.arrayCopyNonAtomic(A0.FLAGS__DATA, assetIndex0, memory, blockIndex3, A0.FLAGS__WIDTH);
                Util.arrayCopyNonAtomic(A0.FLAGS__DATA, assetIndex0, memory, blockIndex4, animIdxWraparound);
            }
        }
    }

    private byte[] titleData(final short animationIndex) {
        switch (animationIndex) {
            case 0:
                return A1.TITLE__FRAME_0__DATA;
            case 1:
                return A1.TITLE__FRAME_1__DATA;
            default:
                return A1.TITLE__FRAME_2__DATA;
        }
    }

    /**
     * Shows an animated title using pre-rendered frames that it cycles through.
     *
     * @param frameIndexRelative
     * @param blockYAbsolute
     */
    private void title(final short frameIndexRelative, final byte blockYAbsolute) {
        final short animIdx = animationIndex(frameIndexRelative, (short) 5, (short) 3, (byte) 2);

        final byte[] animationData = titleData(animIdx);

        final short titleHeightTop = 6;
        final short titleHeightBottom = 7;
        final short titleLengthTop = (short) (A1.TITLE__FRAME_0__WIDTH * titleHeightTop);

        this.clear();
        if (blockYAbsolute == 0) {
            Util.arrayCopyNonAtomic(animationData, (short) 0, memory, FRAME_WIDTH, titleLengthTop);
        } else {
            final short TITLE__LENGTH__BOTTOM = (short) (A1.TITLE__FRAME_0__WIDTH * titleHeightBottom);
            Util.arrayCopyNonAtomic(animationData, titleLengthTop,
                    memory, (short) 0, TITLE__LENGTH__BOTTOM);
        }
    }

    private byte[] creditsData(final short animationIndex) {
        switch (animationIndex) {
            case 0:
            case 1:
            case 2:
            case 3:
                return A4.CREDITS__FRAME_0__DATA;
            case 4:
                return A4.CREDITS__FRAME_1__DATA;
            case 5:
                return A4.CREDITS__FRAME_2__DATA;
            case 6:
            case 7:
            case 8:
            case 9:
            case 10:
                return A4.CREDITS__FRAME_3__DATA;
            case 11:
                return A4.CREDITS__FRAME_4__DATA;
            case 12:
                return A4.CREDITS__FRAME_5__DATA;
            case 13:
            case 14:
            case 15:
            case 16:
                return A4.CREDITS__FRAME_6__DATA;
            case 17:
                return A4.CREDITS__FRAME_7__DATA;
            case 18:
                return A4.CREDITS__FRAME_8__DATA;
            case 19:
            case 20:
            case 21:
            case 22:
            case 23:
                return A4.CREDITS__FRAME_9__DATA;
            case 24:
            case 25:
            case 26:
            case 27:
            case 28:
                return A4.CREDITS__FRAME_10__DATA;
            case 29:
                return A4.CREDITS__FRAME_11__DATA;
            case 30:
                return A4.CREDITS__FRAME_12__DATA;
            case 31:
                return A4.CREDITS__FRAME_13__DATA;
            case 32:
            case 33:
            case 34:
            case 35:
                return A4.CREDITS__FRAME_14__DATA;
            case 36:
                return A4.CREDITS__FRAME_15__DATA;
            case 37:
                return A4.CREDITS__FRAME_16__DATA;
            case 38:
                return A4.CREDITS__FRAME_17__DATA;
            case 39:
                return A4.CREDITS__FRAME_18__DATA;
            case 40:
                return A4.CREDITS__FRAME_19__DATA;
            case 41:
            case 42:
            case 43:
            case 44:
            case 45:
            default:
                return A4.CREDITS__FRAME_20__DATA;
        }
    }

    private void credits(final short frameIndexRelative, final byte blockYAbsolute) {
        final short animIdx = animationIndex(frameIndexRelative, (short) 3, (short) (45 + 4), (byte) 0);

        final byte[] animationData = creditsData(animIdx);

        if (animIdx > 46) {
            this.clear();
            return;
        }

        if (blockYAbsolute == 0) {
            Util.arrayCopyNonAtomic(animationData, (short) 0, memory, (short) 0,
                    (short) (A4.CREDITS__FRAME_0__DATA.length / 2));
        } else {
            Util.arrayCopyNonAtomic(animationData, (short) (A4.CREDITS__FRAME_0__DATA.length / 2), memory,
                    (short) 0, (short) (A4.CREDITS__FRAME_0__DATA.length / 2));
        }
    }

    private byte[] farewellData(final short animationIndex) {
        switch (animationIndex) {
            case 0:
                return A3.FAREWELL__FRAME_3__DATA;
            case 1:
                return A3.FAREWELL__FRAME_2__DATA;
            default:
                return A3.FAREWELL__FRAME_1__DATA;
        }
    }

    /**
     * Effect showing fading "farewell" text. Works as a slideshow for pre-rendered
     * sprites.
     *
     * @param frameIndexRelative
     * @param blockYAbsolute
     */
    private void farewell(final short frameIndexRelative, final byte blockYAbsolute) {
        if (frameIndexRelative >= 30) {
            this.clear();
        } else {
            final short animIdx = animationIndex(frameIndexRelative, (short) 8, (short) 3, (byte) 0);

            final byte[] animationData = farewellData(animIdx);

            if (blockYAbsolute == 0) {
                Util.arrayCopyNonAtomic(animationData, (short) 0, memory, (short) 0,
                        (short) (A3.FAREWELL__FRAME_1__DATA.length / 2));
            } else {
                Util.arrayCopyNonAtomic(animationData, (short) (A3.FAREWELL__FRAME_1__DATA.length / 2), memory,
                        (short) 0, (short) (A3.FAREWELL__FRAME_1__DATA.length / 2));
            }
        }
    }
}
