#![no_std]
#![no_main]
#![feature(type_alias_impl_trait, future_join, abi_avr_interrupt, impl_trait_in_assoc_type)]
#![feature(int_roundings)]
#![feature(isqrt)]

extern crate alloc;

use alloc::rc::Rc;
use core::convert::Infallible;
use core::fmt::Write;
use core::mem::MaybeUninit;
use core::ops::Deref;
use core::panic::PanicInfo;
use core::sync::atomic::Ordering;
use embedded_io::Write as _;
use arduino_hal::DefaultClock;
use atmega_hal::pac::USART0;
use atmega_hal::port::{Dynamic, PB5, PD4, Pin};
use avr_async_serial::UsartDriver;
use avr_hal_generic::avr_device;
use avr_hal_generic::port::mode::Output;
use avr_hal_generic::usart::Baudrate;
use avr_tc1_embassy_time::{define_interrupt, init_system_time};
use ehlcd2d::{DisplayControl, EntryMode, HalfWidthBus, LcdPinConfiguration, Lines};
use ehlcd2d::nonblocking::Lcd;
use embassy_executor::Spawner;
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_sync::mutex::Mutex;
use embassy_time::{Duration, Instant, Ticker, Timer, with_timeout};
use embedded_alloc::Heap;
use embedded_hal::blocking::delay::DelayUs;
use embedded_hal::digital::v2::OutputPin;
use embedded_hal_async::delay::DelayNs;
use ufmt::uWrite;
use embedded_io_async::{Read, Seek, SeekFrom, Write as AsyncWrite};
use itertools::Itertools;
use portable_atomic::AtomicBool;


type Se = UsartDriver<USART0>;
define_interrupt!(atmega328p);


const HEAP_SIZE: usize = 64;
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
#[global_allocator]
static HEAP: Heap = Heap::empty();
#[embassy_executor::task]
async fn serial(mut serial: Se, mut led: Pin<Output>, status: Rc<Mutex<NoopRawMutex, (u16, u16)>>) {
    led.set_high();
    let mut write = serial.write().unwrap();
    let mut read = serial.read().unwrap();

    let mut last_status_update = Instant::now();
    let mut last_receive: u8 = 0;
    let mut update_drops = 0;
    let mut updates = 0;
    loop {
        let idx;
        {
            let mut header = [0u8; 1];
            while header[0] & 0b10000000 == 0 {
                let result = with_timeout(Duration::from_secs(1), read.read_exact(&mut header)).await;
                if result.is_err() {
                    DISCONNECTED.store(true, Ordering::SeqCst);
                }
            }
            idx = header[0] & 0b01111111;
            if (last_receive + 1) & 0b01111111 != idx {
                update_drops += 1;
            }
        }

        let mut data = [0u8; 12];
        read.read_exact(&mut data).await.unwrap();
        let mut decoded = [0u32; 3];
        'a: {
            for i in 0..3 {
                for j in (i * 4)..(i * 4 + 4) {
                    if data[j] & 0b10000000 != 0 {
                        break 'a;
                    }
                    decoded[i] <<= 7;
                    decoded[i] |= data[j] as u32;
                }
            }
            last_receive = idx;
            updates += 1;
            *status.deref().lock().await = (decoded[0] as u16, decoded[1] as u16);
        }
        if Instant::now() - last_status_update > Duration::from_secs(1) {
            //*status.deref().lock().await = (updates, update_drops);
            updates = 0;
            update_drops = 0;
            last_status_update = Instant::now();
        }

    }
}

static DISCONNECTED: AtomicBool = AtomicBool::new(false);

#[embassy_executor::task]
async fn blink(mut led: Pin<Output, PB5>) {
    let mut ticker = Ticker::every(Duration::from_millis(50));
    loop {
        led.toggle();
        ticker.next().await;
    }
}

#[embassy_executor::task]
async fn lcd_update(mut lcd: Lcd<
    Pin<Output, Dynamic>,
    Pin<Output, Dynamic>,
    HalfWidthBus<
        Pin<Output, Dynamic>,
        Pin<Output, Dynamic>,
        Pin<Output, Dynamic>,
        Pin<Output, Dynamic>
    >,
    EmbassyDelayNs,
    Infallible
>, status: Rc<Mutex<NoopRawMutex, (u16, u16)>>) {

    let mut ticker = Ticker::every(Duration::from_millis(100));
    let mut b0 = [0u8; 32];
    let mut b1 = [0u8; 32];
    let mut previous_buffer = &mut b0;
    let mut next_buffer = &mut b1;

    lcd.set_display_control(DisplayControl::default()).await.ok();
    loop {
        ticker.next().await;
        next_buffer.fill(0);
        //lcd_first_line(next_buffer).await;
        lcd_second_line(&mut next_buffer[16..], &status).await;
        let mut current_address = 0;
        lcd.seek(SeekFrom::Start(current_address)).await.ok();
        for (lcd_addr, array_addr) in (0u64..16).merge((40u64..56)).zip(0usize..32) {
            if previous_buffer[array_addr] != next_buffer[array_addr] {
                if lcd_addr != current_address {
                    lcd.seek(SeekFrom::Start(lcd_addr)).await.ok();
                    current_address = lcd_addr;
                }
                current_address += 1;
                lcd.write_char(next_buffer[array_addr]).await.ok();
            }
        }
        (previous_buffer, next_buffer) = (next_buffer, previous_buffer);
    }
}

const START: Instant = Instant::from_ticks(0);

// async fn lcd_first_line(mut buffer: &mut [u8]) {
//     let hour = Duration::from_secs(60 * 64).as_millis();
//     let minute = Duration::from_secs(60).as_millis();
//     let second = Duration::from_secs(1).as_millis();
//     let mut now = Instant::now().duration_since(START).as_millis();
//     let hours = now / hour;
//     now %= hour;
//     let minutes = now / minute;
//     now %= minute;
//     let seconds = now / second;
//     now %= second;
//     write!(buffer, "{hours:02}:{minutes:02}:{seconds:02}.{now:03}").ok();
// }

async fn lcd_second_line(mut buffer: &mut [u8], status: &Mutex<NoopRawMutex, (u16, u16)>) {
    let (polling_rate, drop_count) = *status.lock().await;
    if polling_rate != 0 {
        let drop_percentage = (drop_count as u32) * 100 / polling_rate as u32;
        write!(buffer, "x: {polling_rate:04} y: {drop_percentage:04}").unwrap();
        //write!(buffer, "{polling_rate:03}Hz : {drop_percentage:03}%").unwrap();
    } else {
        write!(buffer, "NO CONN!").ok();
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    // Initialize the allocator BEFORE you use it
    unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
    let mut dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    dp.CPU.smcr.write(|w| w.se().set_bit());
    init_system_time(&mut dp.TC1);
    let mut serial1 = UsartDriver::new(dp.USART0, Baudrate::<DefaultClock>::new(57600));
    spawner.spawn(blink(pins.d13.into_output())).unwrap();
    let lcd = Lcd::new(
        LcdPinConfiguration {
            en: pins.d11.into_output().downgrade(),
            rs: pins.d12.into_output().downgrade(),
            bus: HalfWidthBus {
                d4: pins.d10.into_output().downgrade(),
                d5: pins.d9.into_output().downgrade(),
                d6: pins.d8.into_output().downgrade(),
                d7: pins.d7.into_output().downgrade(),
            },
        },
        EmbassyDelayNs,
        Lines::TwoLines,
        EntryMode::default()
    ).await.unwrap();

    let status = Rc::new(Mutex::new((0, 0)));
    spawner.spawn(lcd_update(lcd, status.clone())).unwrap();
    //let stop_channel: Rc<Channel<NoopRawMutex, bool, 1>> = Rc::new(Channel::new());
    spawner.spawn(serial(serial1, pins.d4.into_output().downgrade(), status)).unwrap();

}

pub struct EmbassyDelayNs;

impl DelayNs for EmbassyDelayNs {
    async fn delay_ns(&mut self, ns: u32) {
        embassy_time::Timer::after_micros(ns.div_ceil(1000) as u64).await;
    }

    async fn delay_us(&mut self, us: u32) {
        embassy_time::Timer::after_micros(us as u64).await;
    }

    async fn delay_ms(&mut self, ms: u32) {
        embassy_time::Timer::after_millis(ms as u64).await;
    }
}


#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    avr_device::interrupt::disable();
    let dp = unsafe { arduino_hal::Peripherals::steal() };
    let pins = arduino_hal::pins!(dp);
    let serial = arduino_hal::default_serial!(dp, pins, 57600);
    let mut writer = Writer {
        u_write: serial
    };
    writeln!(&mut writer, "{}", info).ok();
    loop {

    }
}

struct Writer<T> {
    u_write: T
}

impl<T: uWrite> Write for Writer<T> {
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        self.u_write.write_str(s).map_err(|x| match x {
            _ => core::fmt::Error
        })
    }

    fn write_char(&mut self, c: char) -> core::fmt::Result {
        self.u_write.write_char(c).map_err(|x| match x {
            _ => core::fmt::Error
        })
    }
}