use crate::warnln;

pub trait SerialDevice {
    /// Sends a byte (u8) to the attached serial connection.
    fn send(&mut self) -> u8;

    /// Receives a byte (u8) from the attached serial connection,
    /// can be either another device or the host.
    fn receive(&mut self, byte: u8);

    /// Whether the serial device "driver" supports slave mode
    /// simulating an external clock source. Or if instead the
    /// clock should always be generated by the running device.
    fn allow_slave(&self) -> bool;

    /// Returns a short description of the serial device.
    fn description(&self) -> String;

    /// Returns a string describing the current state of the
    /// serial device. Could be used for debugging purposes.
    fn state(&self) -> String;
}

pub struct Serial {
    data: u8,
    control: u8,
    shift_clock: bool,
    clock_speed: bool,
    transferring: bool,
    timer: i16,
    length: u16,
    bit_count: u8,
    byte_send: u8,
    byte_receive: u8,
    int_serial: bool,
    device: Box<dyn SerialDevice>,
}

impl Serial {
    pub fn new() -> Self {
        Self {
            data: 0x0,
            control: 0x0,
            shift_clock: false,
            clock_speed: false,
            transferring: false,
            timer: 0,
            length: 512,
            bit_count: 0,
            byte_send: 0x0,
            byte_receive: 0x0,
            int_serial: false,
            device: Box::<NullDevice>::default(),
        }
    }

    pub fn reset(&mut self) {
        self.data = 0x0;
        self.control = 0x0;
        self.shift_clock = false;
        self.clock_speed = false;
        self.transferring = false;
        self.timer = 0;
        self.length = 512;
        self.bit_count = 0;
        self.byte_send = 0x0;
        self.byte_receive = 0x0;
        self.int_serial = false;
    }

    pub fn clock(&mut self, cycles: u16) {
        if !self.transferring {
            return;
        }

        self.timer = self.timer.saturating_sub(cycles as i16);
        if self.timer <= 0 {
            let bit = (self.byte_receive >> (7 - self.bit_count)) & 0x01;
            self.data = (self.data << 1) | bit;

            self.tick_transfer();

            self.timer = self.length as i16;
        }
    }

    pub fn read(&mut self, addr: u16) -> u8 {
        match addr {
            // 0xFF01 — SB: Serial transfer data
            0xff01 => self.data,
            // 0xFF02 — SC: Serial transfer control
            0xff02 =>
            {
                #[allow(clippy::bool_to_int_with_if)]
                (if self.shift_clock { 0x01 } else { 0x00 }
                    | if self.clock_speed { 0x02 } else { 0x00 }
                    | if self.transferring { 0x80 } else { 0x00 })
            }
            _ => {
                warnln!("Reding from unknown Serial location 0x{:04x}", addr);
                0xff
            }
        }
    }

    pub fn write(&mut self, addr: u16, value: u8) {
        match addr {
            // 0xFF01 — SB: Serial transfer data
            0xff01 => self.data = value,
            // 0xFF02 — SC: Serial transfer control
            0xff02 => {
                self.shift_clock = value & 0x01 == 0x01;
                self.clock_speed = value & 0x02 == 0x02;
                self.transferring = value & 0x80 == 0x80;

                // in case the clock is meant to be set by the attached device
                // and the current Game Boy is meant to be running in slave mode
                // then checks if the attached device "driver" allows clock set
                // by external device and if not then ignores the transfer request
                // by immediately disabling the transferring flag
                if !self.shift_clock && !self.device.allow_slave() {
                    self.transferring = false;
                }

                // in case a transfer of byte has been requested and
                // this is the then we need to start the transfer setup
                if self.transferring {
                    // @TODO: if the GBC mode exists there should
                    // be special check logic here
                    //self.length = if self.gb.is_cgb() && self.clock_speed { 16 } else { 512 };
                    self.length = 512;
                    self.bit_count = 0;
                    self.timer = self.length as i16;

                    // executes the send and receive operation immediately
                    // this is considered an operational optimization with
                    // no real effect on the emulation (ex: no timing issues)
                    // then stores the byte to be sent to the device so that
                    // it's sent by the end of the send cycle
                    self.byte_receive = self.device.send();
                    self.byte_send = self.data;
                }
            }
            _ => warnln!("Writing to unknown Serial location 0x{:04x}", addr),
        }
    }

    pub fn send(&self) -> bool {
        if self.shift_clock {
            true
        } else {
            self.data & 0x80 == 0x80
        }
    }

    pub fn receive(&mut self, bit: bool) {
        if !self.shift_clock {
            self.data = (self.data << 1) | bit as u8;
            self.tick_transfer();
        }
    }

    #[inline(always)]
    pub fn int_serial(&self) -> bool {
        self.int_serial
    }

    #[inline(always)]
    pub fn set_int_serial(&mut self, value: bool) {
        self.int_serial = value;
    }

    #[inline(always)]
    pub fn ack_serial(&mut self) {
        self.set_int_serial(false);
    }

    pub fn device(&self) -> &dyn SerialDevice {
        self.device.as_ref()
    }

    pub fn set_device(&mut self, device: Box<dyn SerialDevice>) {
        self.device = device;
    }

    fn tick_transfer(&mut self) {
        self.bit_count += 1;
        if self.bit_count == 8 {
            self.transferring = false;
            self.length = 0;
            self.bit_count = 0;

            // received the byte on the device as the
            // complete send operation has been performed
            self.device.receive(self.byte_send);

            // signals the interrupt for the serial
            // transfer completion, indicating that
            // a new byte is ready to be read
            self.int_serial = true;
        }
    }
}

impl Default for Serial {
    fn default() -> Self {
        Self::new()
    }
}

pub struct NullDevice {}

impl NullDevice {
    pub fn new() -> Self {
        Self {}
    }
}

impl SerialDevice for NullDevice {
    fn send(&mut self) -> u8 {
        0xff
    }

    fn receive(&mut self, _: u8) {}

    fn allow_slave(&self) -> bool {
        false
    }

    fn description(&self) -> String {
        String::from("Null")
    }

    fn state(&self) -> String {
        String::from("")
    }
}

impl Default for NullDevice {
    fn default() -> Self {
        Self::new()
    }
}