#include "../../internal-include/GPU/gpu_internal.hpp"
#include <PSX/Timer/frame_timer.hpp>
#include <PSX/System/IOPorts/interrupt_io.hpp>
#include <PSX/System/syscalls.hpp>

namespace JabyEngine {
    namespace GPU {
        uint8_t Display :: current_id = 0;
        namespace internal {
            #ifdef __SUPPORT_PS3__
                uintptr_t DMA :: MADR = 0;
            #endif // __SUPPORT_PS3__

            static SysCall::InterruptVerifierResult interrupt_verifier();
            static void                             interrupt_handler(uint32_t);

            auto           irq_callback        = SysCall::InterruptCallback::from(interrupt_verifier, interrupt_handler);
            VSyncCallback  vsync_callback      = nullptr;
            static uint8_t vsync_counter       = 0;
            bool           vsync_lock_callback = false;

            static SysCall::InterruptVerifierResult interrupt_verifier() {
                if(Interrupt::is_irq(Interrupt::VBlank)) {
                    Interrupt::ack_irq(Interrupt::VBlank);
                    return SysCall::InterruptVerifierResult::ExecuteHandler;
                }
                
                else {
                    return SysCall::InterruptVerifierResult::SkipHandler;
                }
            }

            static void interrupt_handler(uint32_t) {
                vsync_counter++;
                MasterTime::value++;

                if(vsync_callback && !vsync_lock_callback) {
                    vsync_callback();
                }
                //Callback::internal::VSync::execute();
                SysCall::ReturnFromException();
            }

            uint32_t Display :: exchange_buffer_and_display() {
                static constexpr uint16_t TexturePageHeight = 256;

                const uint16_t draw_area_y = (TexturePageHeight*PublicDisplay::current_id);

                GPU::internal::set_draw_area(GPU::PositionU16::create(0, draw_area_y));
                PublicDisplay::current_id ^= 1;
                GPU_IO::GP1.set_display_area(GPU::PositionU16::create(0, static_cast<uint16_t>((TexturePageHeight*PublicDisplay::current_id))));
                return draw_area_y;
            }

            void wait_vsync(uint8_t syncs) {
                volatile auto& vsync_count = reinterpret_cast<volatile uint8_t&>(vsync_counter);

                const uint8_t vsync_dst_value = vsync_count + syncs;
                while(vsync_count != vsync_dst_value);
            }

            void render(const uint32_t* data, size_t words) {
                wait_ready_for_CMD();
                for(size_t n = 0; n < words; n++) {
                    GPU_IO::GP0.write({data[n]});
                }
            }

            void render_dma(const uint32_t* data) {
                // DPCR already enabled
                DMA_IO::GPU.wait();
                GPU_IO::GP1.set_dma_direction(GPU_IO_Values::GPUSTAT::DMADirection::CPU2GPU);
                DMA_IO::GPU.set_adr(reinterpret_cast<uintptr_t>(data));
                DMA_IO::GPU.block_ctrl.write(DMA_IO_Values::BCR::SyncMode2::for_gpu_cmd());

                wait_ready_for_CMD();
                DMA_IO::GPU.channel_ctrl.write(DMA_IO_Values::CHCHR::StartGPULinked());
            }
        }

        void Display :: set_offset(int16_t x, int16_t y) {
            // Does not matter really - The original offset is good enough
            #ifdef __ADJUST_PS3_SCREEN_OFFSET__
                static constexpr auto PS3_CorrectionX = 2;
                static constexpr auto PS3_CorrectionY = 1;
            #else
                static constexpr auto PS3_CorrectionX = 0;
                static constexpr auto PS3_CorrectionY = 0;
            #endif // __ADJUST_PS3_SCREEN_OFFSET__
            
            x += (internal::Display::DisplayRange.x + Configuration::DisplayDefaultOffset.x);
            y += (internal::Display::DisplayRange.y + Configuration::DisplayDefaultOffset.y);

            GPU_IO::GP1.set_horizontal_display_range((x << 3), (x + Display::Width + PS3_CorrectionX) << 3);
            GPU_IO::GP1.set_vertical_display_range(   y,        y + Display::Height + PS3_CorrectionY);
        }

        void set_vsync_callback(VSyncCallback callback) {
            internal::vsync_callback = callback;
        }

        void swap_buffers(bool clear_screen) {
            const int16_t draw_offset_y = internal::Display::exchange_buffer_and_display();
            internal::set_draw_offset(GPU::PositionI16::create(0, draw_offset_y));
            if(clear_screen) {
                internal::quick_fill_fast(Color24::Black(), AreaU16::create(0, static_cast<uint16_t>(draw_offset_y), Display::Width, Display::Height));
            }
        }

        uint8_t swap_buffers_vsync(uint8_t syncs, bool clear_screen) {
            // Waits for finish FIFO
            internal::wait_ready_for_CMD();
            internal::wait_vsync(syncs);

            swap_buffers(clear_screen);
            return Display::current_id;
        }
    }
}