use embedded_hal::{delay::DelayNs, digital::OutputPin, spi::SpiBus};
use rp_pico::hal::{
    gpio::{FunctionSio, Pin, PinId, PullDown, SioOutput},
    spi::{Enabled, SpiDevice, ValidSpiPinout},
    Spi, Timer,
};

use crate::canvas::Canvas;

pub struct Step {
    param_cnt: usize,
    command: u8,
    params: [u8; 4],
    delay: Option<u32>,
}

impl Step {
    pub fn send_command<D, Pinout, P>(
        &self,
        spi: &mut Spi<Enabled, D, Pinout, 8>,
        data_command: &mut Pin<P, FunctionSio<SioOutput>, PullDown>,
    ) where
        D: SpiDevice,
        Pinout: ValidSpiPinout<D>,
        P: PinId,
    {
        let _ = data_command.set_low();
        let _ = spi.write(&[self.command]);
        if self.param_cnt > 0 {
            let _ = data_command.set_high();
            let _ = spi.write(&self.params[0..self.param_cnt]);
        }
    }
}

const NOP: u8 = 0x00;

const SWRESET: Step = Step {
    param_cnt: 0,
    command: 0x01,
    params: [0, 0, 0, 0],
    delay: Some(150),
};
const SLPOUT: Step = Step {
    param_cnt: 0,
    command: 0x11,
    params: [0, 0, 0, 0],
    delay: Some(10),
};
const COLMOD: u8 = 0x3a;
const MADCTL: Step = Step {
    param_cnt: 1,
    command: 0x36,
    params: [0x00, 0, 0, 0],
    delay: None,
};
const CASET: u8 = 0x2a;
const RASET: u8 = 0x2b;
const INVON: Step = Step {
    param_cnt: 0,
    command: 0x21,
    params: [0, 0, 0, 0],
    delay: Some(10),
};
const NORON: Step = Step {
    param_cnt: 0,
    command: 0x13,
    params: [0, 0, 0, 0],
    delay: Some(10),
};
const DISPOFF: Step = Step {
    param_cnt: 0,
    command: 0x28,
    params: [0, 0, 0, 0],
    delay: Some(10),
};
const DISPON: Step = Step {
    param_cnt: 0,
    command: 0x29,
    params: [0, 0, 0, 0],
    delay: Some(10),
};
const RAMWR: u8 = 0x2c;

// Adafruit setup instructions
// SWRESET (0x01), 150ms delay
// SLPOUT (0x11), 10ms delay
// COLMOD (0x3a) 0x55 (65K RGB, 16bit/pixel), 10ms delay
// MADCTL (0x36) 0x00,
//  memory data access control, RGB
// CASET 0x00, 0, 0, 170,
//  column address set, 4 parameters
//  0x00, 0x00 indicates xstart is 0
//  0x00, 170 indicates xend is 170
// RASET 0x00, 0, 320 >> 8, 320 & 0xFF,
//  row address set, 4 parameters
//  0x00, 0x00 indicates ystart is 0
//  3230 >> 8, 320 & 0xff indicates that 320 is the last y address
// INVON, 10ms delay
//  invert the display
// NORON, 10ms delay
//  normal display mode
// DISPON, 10ms delay
//  turn the display on

pub const SETUP_PROGRAM: [Step; 8] = [
    SWRESET,
    SLPOUT,
    Step {
        param_cnt: 1,
        command: COLMOD,
        params: [0x66, 0, 0, 0],
        delay: Some(10),
    },
    MADCTL,
    Step {
        param_cnt: 4,
        command: CASET,
        params: [0, 35, 0, 204],
        delay: None,
    },
    /*
    Step {
        param_cnt: 4,
        command: RASET,
        params: [0, 0, (320 >> 8) as u8, (320 & 0xff) as u8],
        delay: None,
    },
    */
    INVON,
    NORON,
    DISPON,
];

pub struct ST7789Display<
    BoardSelectId: PinId,
    DataCommandId: PinId,
    D: SpiDevice,
    Pinout: ValidSpiPinout<D>,
> {
    inner: ST7789DisplayEnabled<BoardSelectId, DataCommandId, D, Pinout>,
}

impl<BoardSelectId: PinId, DataCommandId: PinId, D: SpiDevice, Pinout: ValidSpiPinout<D>>
    ST7789Display<BoardSelectId, DataCommandId, D, Pinout>
{
    pub fn new(
        board_select: Pin<BoardSelectId, FunctionSio<SioOutput>, PullDown>,
        data_command: Pin<DataCommandId, FunctionSio<SioOutput>, PullDown>,
        spi: Spi<Enabled, D, Pinout, 8>,
    ) -> Self {
        Self {
            inner: ST7789DisplayEnabled {
                board_select,
                data_command,
                spi,
            },
        }
    }

    pub fn acquire(
        &mut self,
    ) -> &mut ST7789DisplayEnabled<BoardSelectId, DataCommandId, D, Pinout> {
        self.inner.board_select.set_low();
        &mut self.inner
    }
}

pub struct ST7789DisplayEnabled<
    BoardSelectId: PinId,
    DataCommandId: PinId,
    D: SpiDevice,
    Pinout: ValidSpiPinout<D>,
> {
    board_select: Pin<BoardSelectId, FunctionSio<SioOutput>, PullDown>,
    data_command: Pin<DataCommandId, FunctionSio<SioOutput>, PullDown>,
    spi: Spi<Enabled, D, Pinout, 8>,
}

impl<BoardSelectId: PinId, DataCommandId: PinId, D: SpiDevice, Pinout: ValidSpiPinout<D>>
    ST7789DisplayEnabled<BoardSelectId, DataCommandId, D, Pinout>
{
    pub fn send_command(&mut self, step: &Step, timer: &mut Timer) {
        step.send_command(&mut self.spi, &mut self.data_command);
        if let Some(delay) = step.delay {
            timer.delay_ms(delay);
        }
    }

    pub fn blit_frame(&mut self, frame: &impl Canvas) {
        // let _ = DISPOFF.send_command(&mut self.spi, &mut self.data_command);
        let _ = self.data_command.set_low();
        let _ = self.spi.write(&[RAMWR]);
        let _ = self.data_command.set_high();
        let _ = self.spi.write(frame.buf());
        // let _ = DISPON.send_command(&mut self.spi, &mut self.data_command);
    }

    pub fn blit_dirty(&mut self, frame: &impl Canvas) {
        if let Some((start_x, start_y, end_x, end_y)) = frame.dirty() {
            let end_y = end_y + 60;
            Step {
                param_cnt: 4,
                command: 0x30,
                params: [
                    (start_y >> 8 & 0xff) as u8,
                    (start_y & 0xff) as u8,
                    (end_y >> 8 & 0xff) as u8,
                    (end_y & 0xff) as u8,
                ],
                delay: None,
            }
            .send_command(&mut self.spi, &mut self.data_command);
            Step {
                param_cnt: 0,
                command: 0x12,
                params: [0, 0, 0, 0],
                delay: None,
            }
            .send_command(&mut self.spi, &mut self.data_command);
            let _ = self.data_command.set_low();
            let _ = self.spi.write(&[RAMWR]);
            let _ = self.data_command.set_high();
            for row in frame.partial(0, start_y, frame.width(), end_y) {
                let _ = self.spi.write(row);
            }
            Step {
                param_cnt: 0,
                command: 0x13,
                params: [0, 0, 0, 0],
                delay: None,
            }
            .send_command(&mut self.spi, &mut self.data_command);
        }
    }
}

impl<BoardSelectId: PinId, DataCommandId: PinId, D: SpiDevice, Pinout: ValidSpiPinout<D>> Drop
    for ST7789DisplayEnabled<BoardSelectId, DataCommandId, D, Pinout>
{
    fn drop(&mut self) {
        self.board_select.set_high();
    }
}