#include "../../internal-include/CD/cd_internal.hpp"
#include "../../internal-include/System/callbacks_internal.hpp"
#include <PSX/System/IOPorts/dma_io.hpp>
#include <PSX/System/IOPorts/interrupt_io.hpp>
#include <PSX/System/syscalls.hpp>
#include <stdio.hpp>

namespace JabyEngine {
    namespace CD {
        namespace internal {
            extern SectorBufferAllocator sector_allocator;
            extern File                  cur_file;

            static constexpr auto AudioSectorMode   = CD_IO_Values::Mode::from(CD_IO_Values::Mode::SingleSpeed, CD_IO_Values::Mode::AutoPauseTrack, CD_IO_Values::Mode::CDDA);
            static constexpr auto DataSectorMode    = CD_IO_Values::Mode::from(CD_IO_Values::Mode::DoubleSpeed, CD_IO_Values::Mode::DataSector);
            static constexpr auto XAAudioSectorMode = CD_IO_Values::Mode::from(CD_IO_Values::Mode::SingleSpeed, CD_IO_Values::Mode::XADPCM, CD_IO_Values::Mode::WholeSector, CD_IO_Values::Mode::UseXAFilter);
            
            namespace IRQ {
                SysCall::InterruptVerifierResult verifier();
                void                             handler(uint32_t);
            }
            
            auto    irq_callback    = SysCall::InterruptCallback::from(IRQ::verifier, IRQ::handler);
            State   current_state   = State::Ready;
            uint8_t irq_bit_pending = CD_IO::Interrupt::None;
            
            static void read_sector_dma(uint32_t* dst, size_t bytes) {
                static const auto WaitSectorReady = []() {
                    while(!CD_IO::IndexStatus.read().is_set(CD_IO_Values::IndexStatus::HasDataFifoData));
                };

                static const auto ReadSector = [](uint32_t* dst, size_t bytes) {
                    DMA_IO::CDROM.set_adr(reinterpret_cast<uintptr_t>(dst));
                    DMA_IO::CDROM.block_ctrl.write(DMA_IO_Values::BCR::SyncMode0::for_cd(bytes >> 2));
                    DMA_IO::CDROM.channel_ctrl.write(DMA_IO_Values::CHCHR::StartCDROM());
                    
                    DMA_IO::CDROM.wait();
                    CD_IO::PortIndex0::change_to();

                    CD_IO::PortIndex0::Request.write(CD_IO_Values::Request::reset());
                };

                WaitSectorReady();
                ReadSector(dst, bytes);
            }

            static BCDTimeStamp send_read_cmd(uint32_t lba, CD_IO::Command::Desc cmd) {
                const auto loc = BCDTimeStamp::from(lba);
                
                Command::send(CD_IO::Command::SetLoc, loc.min, loc.sec, loc.sector);
                Command::send(cmd);
                return loc;
            }

            static void send_read_n(uint32_t lba) {
                send_read_cmd(lba, CD_IO::Command::ReadN);
                current_state = State::Reading;
            }

            void read_file(AutoLBAEntry file_info, const SectorBufferAllocator& buffer_allocator) {
                cur_file.set_from(file_info);
                sector_allocator = buffer_allocator;

                enable_CD();
                send_read_n(cur_file.cur_lba);
            }

            void end_read_file() {
                sector_allocator = SectorBufferAllocator::invalid();
            }

            void continue_reading() {
                if(current_state == State::BufferFull) {
                    CD_IO::PortIndex0::change_to();
                    send_read_n(cur_file.cur_lba);    
                }                
            }

            void copy_from_sector(uint32_t* dst, size_t bytes) {
                read_sector_dma(dst, bytes);
            }

            BCDTimeStamp get_loc() {
                Command::send_wait_response(CD_IO::Command::GetLocP);

                // XEBRA does not support the mirror register so we go for the original...
                CD_IO::PortIndex1::change_to();
                const auto track   = CD_IO::PortIndex1::ResponseFifo.read().raw; // track number (AAh=Lead-out area) (FFh=unknown, toc, none?)
                const auto index   = CD_IO::PortIndex1::ResponseFifo.read().raw; // index number (Usually 01h)
                const auto mm      = CD_IO::PortIndex1::ResponseFifo.read().raw; // minute number within track (00h and up)
                const auto ss      = CD_IO::PortIndex1::ResponseFifo.read().raw; // second number within track (00h to 59h)
                const auto sect    = CD_IO::PortIndex1::ResponseFifo.read().raw; // sector number within track (00h to 74h)

                const auto min     = CD_IO::PortIndex1::ResponseFifo.read().raw; // minute number on entire disk (00h and up)
                const auto sec     = CD_IO::PortIndex1::ResponseFifo.read().raw; // second number on entire disk (00h to 59h)
                const auto sectors = CD_IO::PortIndex1::ResponseFifo.read().raw; // sector number on entire disk (00h to 74h) 

                return BCDTimeStamp{min, sec, sectors};
            }

            BCDTimeStamp get_locL() {
                Command::send_wait_response(CD_IO::Command::GetLocL);

                const auto min    = CD_IO::PortIndex0::ResponseFifo.read().raw;
                const auto sec    = CD_IO::PortIndex0::ResponseFifo.read().raw;
                const auto sector = CD_IO::PortIndex0::ResponseFifo.read().raw;

                return BCDTimeStamp{min, sec, sector};
            }

            void enable_CD() {
                Command::send(CD_IO::Command::SetMode, DataSectorMode);
            }

            void enable_CDDA() {
                Command::send(CD_IO::Command::SetMode, AudioSectorMode);
            }

            void enable_CDXA(bool double_speed) {
                static constexpr uint8_t SingleSpeedBit = 0x0;
                static constexpr uint8_t DoubleSpeedBit = static_cast<uint8_t>(CD_IO_Values::Mode::DoubleSpeed);

                const uint8_t mode = XAAudioSectorMode.raw | (double_speed ? DoubleSpeedBit : SingleSpeedBit);

                Command::send(CD_IO::Command::SetMode, mode);
                current_state = State::XAMode;
            }
        }
    }
}