/******************************************************************************
*
* Copyright (C) 2026  Brian Berlin
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
#include <stdio.h>
#include "../src/core/antic.h"
#include "../src/core/boom6502.h"
#include "../src/core/console.h"
#include "../src/core/pokey.h"

extern t_antic antic;
extern t_cpu cpu6502;
extern t_pokey pokey;
extern unsigned int pokey_cpu_clock;

static int failure = 0;

static void output_result ( int result, const char *test, const char *error ) {

    if ( result ) {
        printf ( "\e[32m[PASS]\e[m %s\n", test );
    }
    else {
        ++failure;
        printf ( "\e[31m[FAIL]\e[m %s %s\n", test, error );
    }
}

static void set_clock ( unsigned int clock ) {

    pokey_cpu_clock = clock;
    antic.sl_cycle_count = 1;
    antic.refresh_cycles = 0;
    cpu6502.i_cnt = 0;
}

static void setup_pokey_timer_test ( void ) {

    console_set_machine_type ( MACHINE_TYPE_5200 );
    pokey_reset();
    set_clock ( 0 );
    cpu6502.status = 0;
    cpu6502.irq_assert = 0;
    cpu6502.irq_pending = 0;
    cpu6502.i_req = INT_NONE;
}

static void arm_timer ( KWORD audf_reg, KBYTE audf_value, KBYTE irq_bit,
                        KBYTE audctl ) {

    setup_pokey_timer_test();
    pokey_write_register ( audf_reg, audf_value );
    pokey_write_register ( AUDCTL, audctl );
    pokey_write_register ( IRQEN, 0x00 );
    pokey_write_register ( STIMER, 0x00 );
    pokey_write_register ( IRQEN, irq_bit );
}

static void check_basic_timer ( const char *name, KWORD audf_reg,
                                KBYTE irq_bit ) {

    KBYTE irqst;

    arm_timer ( audf_reg, 0x02, irq_bit, 0x00 );

    set_clock ( 83 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & irq_bit) != 0, name, "fired before deadline" );

    set_clock ( 84 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & irq_bit) == 0, name, "did not fire at deadline" );
}

static void check_15khz_timer ( void ) {

    KBYTE irqst;

    arm_timer ( AUDF1, 0x00, IRQ_TIMER1, 0x01 );

    set_clock ( 113 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) != 0, "Timer 1 15kHz before deadline",
                    "fired before deadline" );

    set_clock ( 114 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) == 0, "Timer 1 15kHz deadline",
                    "did not fire at deadline" );
}

static void check_linked_timer2 ( void ) {

    KBYTE irqst;

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0x01 );
    pokey_write_register ( AUDF2, 0x01 );
    pokey_write_register ( AUDCTL, 0x10 );
    pokey_write_register ( IRQEN, 0x00 );
    pokey_write_register ( STIMER, 0x00 );
    pokey_write_register ( IRQEN, IRQ_TIMER2 );

    set_clock ( 7223 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER2) != 0, "Linked Timer 2 before deadline",
                    "fired before 16-bit deadline" );

    set_clock ( 7224 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER2) == 0, "Linked Timer 2 deadline",
                    "did not fire at 16-bit deadline" );
}

static void check_linked_timer4 ( void ) {

    KBYTE irqst;

    setup_pokey_timer_test();
    pokey_write_register ( AUDF3, 0x01 );
    pokey_write_register ( AUDF4, 0x01 );
    pokey_write_register ( AUDCTL, 0x08 );
    pokey_write_register ( IRQEN, 0x00 );
    pokey_write_register ( STIMER, 0x00 );
    pokey_write_register ( IRQEN, IRQ_TIMER4 );

    set_clock ( 7223 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER4) != 0, "Linked Timer 4 before deadline",
                    "fired before 16-bit deadline" );

    set_clock ( 7224 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER4) == 0, "Linked Timer 4 deadline",
                    "did not fire at 16-bit deadline" );
}

static void check_disabled_expiration ( void ) {

    KBYTE irqst;

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0x00 );
    pokey_write_register ( AUDCTL, 0x00 );
    pokey_write_register ( IRQEN, 0x00 );
    pokey_write_register ( STIMER, 0x00 );

    set_clock ( 200 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) != 0, "Disabled Timer 1 expiration",
                    "latched while disabled" );

    pokey_write_register ( IRQEN, IRQ_TIMER1 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) != 0, "Timer 1 enable after miss",
                    "latched immediately after enable" );

    set_clock ( 224 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) == 0, "Timer 1 after disabled miss",
                    "did not fire at next deadline" );
}

static void check_catchup_phase ( void ) {

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0x00 );
    pokey_write_register ( AUDCTL, 0x00 );
    pokey_write_register ( IRQEN, IRQ_TIMER1 );
    pokey_write_register ( STIMER, 0x00 );

    set_clock ( 100 );
    pokey_read_register ( IRQST );
    output_result ( pokey.timer_irq[0].next_fire_clock == 112,
                    "Timer 1 catch-up phase", "phase did not catch up" );
}

static void check_large_disabled_catchup_phase ( void ) {

    unsigned int current_clock = 1000000;
    unsigned int expected_next;

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0x00 );
    pokey_write_register ( AUDCTL, 0x00 );
    pokey_write_register ( IRQEN, 0x00 );
    pokey_write_register ( STIMER, 0x00 );

    set_clock ( current_clock );
    pokey_write_register ( IRQEN, IRQ_TIMER1 );
    expected_next = current_clock + 28 - ((current_clock - 28) % 28);

    output_result ( pokey.timer_irq[0].next_fire_clock == expected_next &&
                    pokey.timer_irq_next_clock == expected_next,
                    "Large disabled timer catch-up phase",
                    "phase did not catch up with modulo step" );
}

static void check_cpu_boundary_hook ( void ) {

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0x00 );
    pokey_write_register ( AUDCTL, 0x00 );
    pokey_write_register ( IRQEN, IRQ_TIMER1 );
    pokey_write_register ( STIMER, 0x00 );

    set_clock ( 28 );
    pokey_check_timer_irq();
    output_result ( (pokey.irq_status & IRQ_TIMER1) == 0 &&
                    cpu6502.irq_assert,
                    "CPU boundary timer IRQ hook", "did not assert IRQ" );
}

static void check_disabled_timer_hook_is_quiet ( void ) {

    unsigned int next_fire_clock;

    setup_pokey_timer_test();
    next_fire_clock = pokey.timer_irq[0].next_fire_clock;

    output_result ( pokey.timer_irq[0].armed &&
                    !pokey.timer_irq_next_clock_valid,
                    "Reset timer armed without IRQ hook deadline",
                    "reset timer was stopped or scheduled with IRQ disabled" );

    set_clock ( next_fire_clock + 100 );
    pokey_check_timer_irq();
    output_result ( pokey.timer_irq[0].next_fire_clock == next_fire_clock &&
                    (pokey.irq_status & IRQ_TIMER1) != 0,
                    "Disabled timer IRQ hook quiet",
                    "hook serviced disabled timer IRQ" );

    pokey_write_register ( IRQEN, IRQ_TIMER1 );
    output_result ( pokey.timer_irq_next_clock_valid &&
                    pokey.timer_irq[0].next_fire_clock > next_fire_clock,
                    "Timer IRQEN enables hook deadline",
                    "IRQEN did not catch up and schedule enabled timer" );

    set_clock ( pokey.timer_irq[0].next_fire_clock );
    pokey_check_timer_irq();
    output_result ( (pokey.irq_status & IRQ_TIMER1) == 0 &&
                    cpu6502.irq_assert,
                    "Enabled timer IRQ hook fires",
                    "enabled timer IRQ did not fire through hook" );
}

static void check_skctl_exit_arms_timer4 ( void ) {

    KBYTE irqst;

    setup_pokey_timer_test();
    pokey_write_register ( AUDF4, 0x00 );
    pokey_write_register ( IRQEN, 0x00 );
    pokey_write_register ( SKCTL, 0x00 );

    set_clock ( 10 );
    pokey_write_register ( SKCTL, SK_KEY_SCAN );
    pokey_write_register ( IRQEN, IRQ_TIMER4 );

    set_clock ( 33 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER4) != 0,
                    "SKCTL exit Timer 4 before deadline",
                    "fired immediately after init exit" );

    set_clock ( 34 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER4) == 0,
                    "SKCTL exit Timer 4 deadline",
                    "did not fire without STIMER" );
}

static void check_reset_arms_timer4 ( void ) {

    KBYTE irqst;

    setup_pokey_timer_test();
    set_clock ( 1000 );
    pokey_write_register ( AUDF4, 0x23 );
    pokey_write_register ( IRQEN, IRQ_TIMER4 );

    set_clock ( 1003 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER4) != 0,
                    "Reset Timer 4 before deadline",
                    "fired before running 64kHz timer edge" );

    set_clock ( 1004 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER4) == 0,
                    "Reset Timer 4 deadline",
                    "did not fire without STIMER after reset" );
}

static void check_skctl_init_suspends_timer ( void ) {

    KBYTE irqst;

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0x00 );
    pokey_write_register ( IRQEN, IRQ_TIMER1 );
    pokey_write_register ( STIMER, 0x00 );

    set_clock ( 10 );
    pokey_write_register ( SKCTL, 0x00 );

    set_clock ( 50 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) != 0,
                    "SKCTL init suspends timer",
                    "timer fired while init held clocks reset" );
}

static void check_stimer_during_init_respects_clock_domain ( void ) {

    KBYTE irqst;

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0x00 );
    pokey_write_register ( IRQEN, IRQ_TIMER1 );
    pokey_write_register ( SKCTL, 0x00 );
    pokey_write_register ( STIMER, 0x00 );

    set_clock ( 200 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) != 0,
                    "STIMER init slow timer frozen",
                    "slow timer fired while init held clocks reset" );

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0x00 );
    pokey_write_register ( AUDCTL, 0x40 );
    pokey_write_register ( IRQEN, IRQ_TIMER1 );
    pokey_write_register ( SKCTL, 0x00 );
    pokey_write_register ( STIMER, 0x00 );

    set_clock ( 3 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) != 0,
                    "STIMER init fast timer before deadline",
                    "fast timer fired before deadline" );

    set_clock ( 4 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) == 0,
                    "STIMER init fast timer deadline",
                    "fast timer did not run during init" );
}

static void check_audctl_during_init_resyncs_timers ( void ) {

    KBYTE irqst;

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0x00 );
    pokey_write_register ( AUDCTL, 0x40 );
    pokey_write_register ( IRQEN, IRQ_TIMER1 );
    pokey_write_register ( STIMER, 0x00 );
    pokey_write_register ( SKCTL, 0x00 );
    pokey_write_register ( AUDCTL, 0x00 );

    set_clock ( 50 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) != 0,
                    "AUDCTL init fast-to-slow freezes timer",
                    "timer fired after switching to frozen slow clock" );

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0x00 );
    pokey_write_register ( AUDCTL, 0x00 );
    pokey_write_register ( IRQEN, IRQ_TIMER1 );
    pokey_write_register ( STIMER, 0x00 );
    pokey_write_register ( SKCTL, 0x00 );
    pokey_write_register ( AUDCTL, 0x40 );

    set_clock ( 3 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) != 0,
                    "AUDCTL init slow-to-fast before deadline",
                    "fast timer fired before deadline" );

    set_clock ( 4 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & IRQ_TIMER1) == 0,
                    "AUDCTL init slow-to-fast deadline",
                    "fast timer did not start during init" );
}

static void check_long_delay_no_stray ( void ) {

    KBYTE irqst;

    setup_pokey_timer_test();
    pokey_write_register ( AUDF1, 0xff );
    pokey_write_register ( AUDF2, 0xff );
    pokey_write_register ( AUDCTL, 0x00 );
    pokey_write_register ( IRQEN, 0x00 );
    pokey_write_register ( STIMER, 0x00 );
    pokey_write_register ( IRQEN, IRQ_TIMER1 | IRQ_TIMER2 );

    set_clock ( 4 * 114 );
    irqst = pokey_read_register ( IRQST );
    output_result ( (irqst & (IRQ_TIMER1 | IRQ_TIMER2)) ==
                    (IRQ_TIMER1 | IRQ_TIMER2),
                    "Long Timer 1/2 no stray IRQ", "unexpected timer IRQ" );
}

static void check_allpot_reports_per_pot_bits ( void ) {

    KBYTE allpot;
    int i;

    setup_pokey_timer_test();
    pokey.pot_scanline = 10;
    for ( i = 0; i < 8; ++i )
        pokey.pot[i] = 0;
    pokey.pot[1] = 11;
    pokey.pot[4] = 20;
    pokey.pot[7] = 228;

    allpot = pokey_read_register ( ALLPOT );
    output_result ( allpot == ((1 << 1) | (1 << 4) | (1 << 7)),
                    "ALLPOT reports per-pot bits", "incorrect bit mask" );
}

static void check_skctl_fast_pot_does_not_start_scan ( void ) {

    setup_pokey_timer_test();
    pokey.pot_scanline = 0;

    pokey_write_register ( SKCTL, SK_FAST_POT | SK_KEY_SCAN | SK_KEY_DEB );
    output_result ( pokey.pot_scanline == 0,
                    "SKCTL fast pot does not start scan",
                    "unexpected pot scan progress" );

    pokey_write_register ( POTGO, 0x00 );
    output_result ( pokey.pot_scanline == 226,
                    "POTGO fast pot keeps fast behavior",
                    "fast POTGO did not advance scan" );
}

int main ( void ) {

    console_init();

    check_basic_timer ( "Timer 1 64kHz", AUDF1, IRQ_TIMER1 );
    check_basic_timer ( "Timer 2 64kHz", AUDF2, IRQ_TIMER2 );
    check_basic_timer ( "Timer 4 64kHz", AUDF4, IRQ_TIMER4 );
    check_15khz_timer();
    check_linked_timer2();
    check_linked_timer4();
    check_disabled_expiration();
    check_catchup_phase();
    check_large_disabled_catchup_phase();
    check_cpu_boundary_hook();
    check_disabled_timer_hook_is_quiet();
    check_skctl_exit_arms_timer4();
    check_reset_arms_timer4();
    check_skctl_init_suspends_timer();
    check_stimer_during_init_respects_clock_domain();
    check_audctl_during_init_resyncs_timers();
    check_long_delay_no_stray();
    check_allpot_reports_per_pot_bits();
    check_skctl_fast_pot_does_not_start_scan();

    return failure ? -1 : 0;
}
