use std::cell::RefCell;
use std::num::NonZeroU16;
use std::ops::Deref;
use std::sync::{Arc, RwLock};
use std::{borrow::Cow};
use std::str::FromStr;

use anyhow::Context;
use chrono::{DateTime, Datelike, Days, FixedOffset, NaiveTime, TimeDelta, Weekday};
use embedded_graphics::prelude::DrawTarget;
use embedded_graphics::{
    prelude::{Dimensions, Point},
    text::{Text},
    Drawable,
};
use embedded_hal::delay::DelayNs;
use epd_waveshare::{color::Color, epd7in5_v2::Epd7in5, prelude::WaveshareDisplay};
use esp_idf_hal::{
    delay::{Ets},
    gpio::{Gpio19, PinDriver},
    spi::{config, SpiDriverConfig},
};
use esp_idf_svc::http::Method;
use esp_idf_svc::http::client::{Configuration as HttpConfig, EspHttpConnection};
use esp_idf_svc::http::server::EspHttpServer;
use esp_idf_svc::wifi::{BlockingWifi, EspWifi};
use esp_idf_svc::{
    eventloop::EspSystemEventLoop,
    hal::{prelude::*, spi::SpiDeviceDriver},
    nvs::EspDefaultNvsPartition,
    wifi::Configuration,
};
use esp_idf_sys::{esp_deep_sleep, esp_deep_sleep_wake_stub_fn_t};
use u8g2_fonts::U8g2TextStyle;
use untis::{Date, Lesson, LessonCode};
use embedded_layout::{layout::linear::{spacing::DistributeFill, FixedMargin, LinearLayout}, prelude::*};

type Timetable = [[Option<Lesson>;8]; 5];

#[unsafe(link_section = ".rtc.data")]
static mut WAKEUP_COUNTER: u64 = 0;

#[unsafe(link_section = ".rtc.data")]
static mut FETCHING_FAIL_COUNT: Option<NonZeroU16> = None;

#[unsafe(link_section = ".rtc.data")]
static mut ERROR_MSG: Option<[u8; 2048]> = None;

static mut WEEK_OFFSET: i32 = 0;

fn main() -> anyhow::Result<()> {
    // It is necessary to call this function once. Otherwise some patches to the runtime
    // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
    esp_idf_svc::sys::link_patches();

    // Bind the log crate to the ESP Logging facilities
    esp_idf_svc::log::EspLogger::initialize_default();
    let peripherals = Peripherals::take().unwrap();

    log::info!("Took peripherals");

    let mut creds = include_str!("../wifi-creds.txt").split('\n');
    let ssid: heapless::String<32> =
    heapless::String::<32>::from_str(creds.next().unwrap()).unwrap();
    let password: heapless::String<64> =
    heapless::String::<64>::from_str(creds.next().unwrap()).unwrap();
    
    std::panic::set_hook(Box::new(|panic|{
        let msg: String = format!("{}", panic);/* if let Some(s) = panic.payload().downcast_ref::<&str>() {
            format!("{s:?}")
        } else if let Some(s) = panic.payload().downcast_ref::<String>() {
            format!("{s:?}")
        } else {
            format!("Failed to downcast panic msg")
        }; */
        let msg_bytes = msg.into_bytes();

        // Copy the string buffer into the array
        let mut buffer = [0u8; 2048];
        for i in 0..(buffer.len().min(msg_bytes.len())){
            buffer[i] = msg_bytes[i];
        }

        unsafe{
            ERROR_MSG = Some(buffer);

            // Sleep for a second
            esp_deep_sleep(1_000_000)
        }
    }));


    let busy = PinDriver::input(peripherals.pins.gpio17).unwrap();
    let rst = PinDriver::output(peripherals.pins.gpio16).unwrap();
    let cs = peripherals.pins.gpio5;
    let clk = peripherals.pins.gpio18;
    let mosi = peripherals.pins.gpio23;
    let miso = peripherals.pins.gpio19; // aka. dc

    let dc = PinDriver::output(miso).unwrap();

    log::info!("Initialized pins");

    let config = config::Config::new();

    let mut spi = SpiDeviceDriver::new_single(
        peripherals.spi2, // 2 oder 3?
        clk,
        mosi,
        Option::<Gpio19>::None,
        Some(cs),
        &SpiDriverConfig::default(),
        &config,
    )
    .unwrap();

    log::info!("Initialized spi interface");





    // Initialize the draw buffer
    // Allocating on the stack makes the chip crash. We gotta put it on the heap
    let mut display = Box::new(epd_waveshare::epd7in5_v2::Display7in5::default());
    display.set_rotation(epd_waveshare::prelude::DisplayRotation::Rotate0);

    let mut delay = Ets;

    let mut epd7in5 = Epd7in5::new(&mut spi, busy, dc, rst, &mut delay, None).unwrap();
    epd7in5.sleep(&mut spi, &mut delay)?;
    log::info!("Completed");

    unsafe{
        if let Some(error) = ERROR_MSG{

            // Draw to the display
            let text_style = U8g2TextStyle::new(u8g2_fonts::fonts::u8g2_font_7x14_mr, Color::White);

            let error_label = Text::new("An error occured:", Point::zero(), &text_style);

            let error_message = String::from_utf8_lossy(&error);
            let error_message = match &error_message{
                Cow::Borrowed(C) => *C,
                Cow::Owned(C) => &C
            } ;


            let error_message_label = Text::new(error_message, Point::zero(), &text_style);
            let mut vg = [error_label, error_message_label];
            LinearLayout::vertical(Views::new(&mut vg))
            .arrange()
            .draw(&mut *display).unwrap();

            epd7in5.update_and_display_frame(&mut spi, &*display.buffer(), &mut delay)?;
            epd7in5.sleep(&mut spi, &mut delay)?;

            // Sleep for 12 hours
            esp_deep_sleep(12*60*60*60*1000_000)
        }
    }




    // Connecting to wifi
    let sysloop = EspSystemEventLoop::take()?;
    let nvs = EspDefaultNvsPartition::take()?;

    let mut wifi = BlockingWifi::wrap(
        EspWifi::new(peripherals.modem, sysloop.clone(), Some(nvs))?,
        sysloop.clone(),
    )?;

    wifi.set_configuration(&Configuration::Client(
        esp_idf_svc::wifi::ClientConfiguration {
            ssid,
            password,
            auth_method: esp_idf_svc::wifi::AuthMethod::WPA2WPA3Personal,
            ..Default::default()
        },
    ))?;

    fn err() -> !{
        println!("No wifi available. Sleeping for 5 minutes");
        unsafe{
            // retry in 5 minutes
            esp_deep_sleep(5*60*1000_000)
        }
    }

    wifi.start().unwrap_or_else(|_| err());
    wifi.connect().unwrap_or_else(|_| err());
    wifi.wait_netif_up().unwrap_or_else(|_| err());
    loop {if wifi.is_connected().unwrap_or_else(|_| err()) {break;}};

    unsafe {
        FETCHING_FAIL_COUNT = None;
    }


    let mut httpserver = EspHttpServer::new(&esp_idf_svc::http::server::Configuration{
        http_port: 80,
        ..Default::default()
    })?;

    let do_update = Arc::new(RwLock::new(Option::<Command>::None));
    let writer = do_update.clone();
    httpserver.fn_handler("/", Method::Get, move |request| -> anyhow::Result<()> {

        *writer.write().unwrap() = Some(Command::Timetable);

        unsafe{
            WEEK_OFFSET = request.header("week").and_then(|w| w.parse::<i32>().ok()).unwrap_or_default();
        }

        // Retrieve html String
        let html = String::from("OK");
        // Respond with OK status
        let mut response = request.into_ok_response()?;
        // Return Requested Object (Index Page)
        response.write(html.as_bytes())?;
        Ok(())
    })?;

    let writer = do_update.clone();
    httpserver.fn_handler("/msg", Method::Get, move |request| -> anyhow::Result<()> {

        *writer.write().unwrap() = Some(Command::Message(request.header("msg").unwrap().to_string()));

        // Retrieve html String
        let html = String::from("OK");
        // Respond with OK status
        let mut response = request.into_ok_response()?;
        // Return Requested Object (Index Page)
        response.write(html.as_bytes())?;
        Ok(())
    })?;

    loop {
        delay.delay_ns(100);

        match &*do_update.read().unwrap(){
            Some(Command::Timetable) => {
                update_display_buffer(&mut display);
                println!("Rendered to buffer");
                epd7in5.wake_up(&mut spi, &mut delay)?;
                epd7in5.update_and_display_frame(&mut spi, &*display.buffer(), &mut delay)?;
                println!("Updated frame");
                epd7in5.sleep(&mut spi, &mut delay)?;
                println!("Sleep");
                unsafe {
                    WAKEUP_COUNTER += 1;
                }
            },
            Some(Command::Message(msg)) => {                
                display_custom_message(&mut display, &msg);

                epd7in5.wake_up(&mut spi, &mut delay)?;
                epd7in5.update_and_display_frame(&mut spi, &*display.buffer(), &mut delay)?;
                epd7in5.sleep(&mut spi, &mut delay)?;
            }
            None => continue
        }

        if let Ok(mut writer) = do_update.try_write(){
            *writer = Option::<Command>::None;
            println!("Successfully reset rw lock")
        }
    }



    drop(httpserver);
    drop(epd7in5);
    drop(spi);

    //let next_wakeup_time = calculate_next_wakeuptime(now).context(format!("Failed to calculate next wakeup time for time {now}")).unwrap();
    //let seconds_until_wakeup = next_wakeup_time.timestamp() - now.timestamp();

    wifi.disconnect().unwrap();
    wifi.stop().unwrap();

/*     unsafe{
        esp_deep_sleep(seconds_until_wakeup as u64 * 1000_000);
    } */
   Ok(())
}


#[derive(Clone)]
enum Command{
    Timetable,
    Message(String)
}

fn update_display_buffer(display: &mut epd_waveshare::epd7in5_v2::Display7in5){
    let (tt, now) = match fetch_data(){
        Ok(d) => d,
        Err(e) => {
            let fail_count: u16 = unsafe {
                let new_val = match FETCHING_FAIL_COUNT{
                    Some(C) => C.saturating_add(1),
                    None => {
                        NonZeroU16::new_unchecked(1)
                    }
                };
                FETCHING_FAIL_COUNT = Some(new_val);
                new_val
            }.into();

            println!("Failed to fetch timetable for the {}th time. Error: {}", fail_count, e);
            
            if fail_count > 10{
                panic!("10 failed attempts. Last error:\n{}\nBacktrace:\n{}", e, e.backtrace());
            }

            // Quadratic falloff maxing out at 1 hours intervals
            let sleep_time = u32::min((fail_count as u32).pow(2u32) * 4, 3600);
            println!("Going to sleep for {} seconds", sleep_time);

            unsafe {
                esp_deep_sleep(sleep_time as u64 * 1000_000)
            }
        }
    };

    // We wanna panic here if this fails because retrying this in a couple of seconds is stupid
    let next_wakeup_time = calculate_next_wakeuptime(now).context(format!("Failed to calculate next wakeup time for time {now}")).unwrap();

    display.clear(Color::Black).unwrap();
    // Render the layout
    draw_layout(display, &tt, now, next_wakeup_time, include_str!("../wifi-creds.txt").split('\n').next().unwrap());
}


fn display_custom_message(display: &mut epd_waveshare::epd7in5_v2::Display7in5, msg: &str){
    display.clear(Color::Black).unwrap();
    let text_style = U8g2TextStyle::new(u8g2_fonts::fonts::u8g2_font_profont22_mr, Color::White);

    let display_area = display.bounding_box();
    let text = Text::new(msg, Point::zero(), &text_style);

    LinearLayout::vertical(Chain::new(text))
        .with_alignment(horizontal::Center)
        .arrange()
        .align_to(&display_area, horizontal::Center, vertical::Center)
        .draw(display)
        .unwrap();
}

fn get_full_subject_name(shortname: &str) -> Option<&'static str>{
    if shortname.len() < 2{
        return None;
    }
    let shortname = shortname.to_lowercase();
    Some(match shortname[..2].as_ref() {
        "in" => "Informatik",
        "en" => "Englisch",
        "de" => "Deutsch",
        "ds" => "Theater",
        "ph" => "Physik",
        "ma" => "Mathematik",
        "ch" => "Chemie",
        "ge" => "Geschichte",
        "sp" => "Sport",
        _ => {
            return None;
        }
    })
}


fn calculate_next_wakeuptime(now: DateTime<FixedOffset>) -> Option<DateTime<FixedOffset>>{

    if now.weekday().num_days_from_monday() > 4 || (now.weekday() == Weekday::Fri && now.time() > NaiveTime::from_hms_opt(15, 15, 0)?){
        // On weekends, wait until 5am on the next day to update

        let d = now.checked_add_signed(TimeDelta::days(1))?;
        let d = d.with_time(NaiveTime::from_hms_opt(5, 0, 0).unwrap()).single()?;

        Some(d)
    }else if now.time() > NaiveTime::from_hms_opt(15, 15, 0)? || now.time() < NaiveTime::from_hms_opt(5, 0, 0)?{
        // If it's a weekday but it's after 15:15 or before 5am, update after an hour
        Some(now.checked_add_signed(TimeDelta::minutes(60))?)
    }else{
        // In any other case, update 5 minutes before the end of a lesson but at least 10 minutes from now
        let (_, end_time) = get_school_period_and_end_time(now.time());

        let before_lesson_end = now.with_time(end_time.overflowing_sub_signed(TimeDelta::minutes(5)).0).earliest()?;
        let in_10_minutes = now.checked_add_signed(TimeDelta::minutes(10))?;
        Some(before_lesson_end.max(in_10_minutes))
    }
}

fn fetch_data() -> anyhow::Result<(Box<Timetable>, DateTime<FixedOffset>)>{    
    let httpconnection = EspHttpConnection::new(&HttpConfig {
        use_global_ca_store: true,
        crt_bundle_attach: Some(esp_idf_sys::esp_crt_bundle_attach),
        ..Default::default()
    })?;

    let res = fetch_timetable(httpconnection);

    res
}


fn is_lesson_cancelled(l: &Lesson) -> bool{
    l.code == LessonCode::Cancelled || l.subst_text.as_ref().is_some_and(|s| s == "Aufgaben")
}

fn draw_layout(display: &mut epd_waveshare::epd7in5_v2::Display7in5, timetable: &Timetable, now: DateTime<FixedOffset>, next_wakeup_time: DateTime<FixedOffset>, wifi_details: &str){
    let display_area = display.bounding_box();

    // let text_style = MonoTextStyle::new(&embedded_graphics::mono_font::ascii::FONT_10X20, Color::White);
    let text_style = U8g2TextStyle::new(u8g2_fonts::fonts::u8g2_font_profont22_mr, Color::White);

    let mut inverted_text_style = U8g2TextStyle::new(u8g2_fonts::fonts::u8g2_font_profont22_mr, Color::Black);
    inverted_text_style.background_color = Some(Color::White);

    let text_style_small = U8g2TextStyle::new(u8g2_fonts::fonts::u8g2_font_7x14_mr, Color::White);

    let empty_str = String::from(" ");


    let day_of_week = now.weekday().num_days_from_monday(); // zero-indexed
    let current_period = get_school_period(now.time());

    let mut texts = timetable.iter().enumerate().map(|(day_index, day)| day.iter().enumerate().map(|(period, l)| {
        let s = if let Some(l) = l && !is_lesson_cancelled(l){
            (||{
                Some(l.subjects.first()?.name.as_str())
            })()
        }else{
            None
        }.and_then(get_full_subject_name);

        let room_str = if let Some(l) = l && !is_lesson_cancelled(l){
            (||{
                Some(l.rooms.first()?.name.as_str())
            })()
        }else{
            None
        };

        let text_style = if period as u8 == current_period && day_index as u32 == day_of_week{
            &inverted_text_style
        }else{
            &text_style
        };
        LinearLayout::vertical(
            Chain::new(
                Text::new(s.unwrap_or_else(|| &empty_str), Point::zero(), text_style)
            ).append(
                Text::new(room_str.unwrap_or_else(|| &empty_str), Point::zero(), &text_style_small)
            )
        )
        .with_spacing(FixedMargin(2)).arrange()
        
    }).collect::<Vec<_>>()).collect::<Vec<_>>();

    let mut columns = texts.iter_mut().map(|day|
        LinearLayout::vertical(Views::new(day))
            .align_to(&display_area, horizontal::Center, vertical::Top)
            .with_spacing(FixedMargin(15))
            .arrange()
    ).collect::<Vec<_>>();

    let columns = Views::new(&mut columns);

    // Unsafe is needed to access static mutable variable
    let time_str = format!("{} ({})", now.format("%Y-%m-%d %H:%M"), unsafe {WAKEUP_COUNTER});
    let time_text = Text::new(&time_str, Point::zero(), &text_style_small);

    let next_wakeup_time_str = format!("Next wakup: {}", next_wakeup_time.naive_local());

    let time_table_grid = LinearLayout::horizontal(columns)
        .with_alignment(vertical::Center)
        .with_spacing(FixedMargin(18))
        .arrange();

    let subtext = LinearLayout::vertical(Chain::new(time_text).append(Text::new(&next_wakeup_time_str, Point::zero(), &text_style_small))).with_alignment(horizontal::Right).with_spacing(FixedMargin(2)).arrange();

    let bottom_bar = LinearLayout::horizontal(Chain::new(
        Text::new(wifi_details, Point::zero(), &text_style_small)
        ).append(
            subtext
        )
    ).with_alignment(vertical::Bottom)
    .with_spacing(DistributeFill(display_area.size.width))
    .arrange();
    LinearLayout::vertical(Chain::new(time_table_grid).append(bottom_bar))
        .with_alignment(horizontal::Center)
        .with_spacing(DistributeFill(display_area.size.height-10))
        .arrange()
        .align_to(&display_area, horizontal::Center, vertical::Bottom)
        .draw(display)
        .unwrap();
}


const PERIOD_LOOKUP: [NaiveTime; 8] = [
    (NaiveTime::from_hms_opt(8, 45, 0).unwrap()),
    (NaiveTime::from_hms_opt(9, 35, 0).unwrap()),
    (NaiveTime::from_hms_opt(10, 40, 0).unwrap()),
    (NaiveTime::from_hms_opt(11, 30, 0).unwrap()),
    (NaiveTime::from_hms_opt(12, 40, 0).unwrap()),
    (NaiveTime::from_hms_opt(13, 30, 0).unwrap()),
    (NaiveTime::from_hms_opt(14, 25, 0).unwrap()),
    (NaiveTime::from_hms_opt(15, 15, 0).unwrap()),
];

fn get_school_period(time: NaiveTime) -> u8{


    for (index, period_time) in PERIOD_LOOKUP.iter().enumerate(){
        if time < *period_time{
            return index as u8;
        }
    }
    7u8
}

fn get_school_period_and_end_time(time: NaiveTime) -> (u8, NaiveTime){
    for (index, period_time) in PERIOD_LOOKUP.iter().enumerate(){
        if time < *period_time{
            return (index as u8, *period_time);
        }
    }
    (7u8, PERIOD_LOOKUP[7])
}


fn transform_lessons(lessons: Vec<Lesson>) -> Box<Timetable>{

    let mut table: Box<[[Option<Lesson>; 8]; 5]> = Box::new([0u8; 5].map(|_| [0u8; 8].map(|_| Option::<Lesson>::None)));

    for lesson in lessons {
        let day_idx = lesson.date.weekday().num_days_from_monday() as usize;

        let mut l = Cow::<Lesson>::Owned(lesson);
        for (period_idx, time) in PERIOD_LOOKUP.iter().enumerate(){
            if l.start_time.0 < *time && &l.end_time.0 >= time{
                table[day_idx][period_idx] = Some(l.into_owned());
                l = Cow::Borrowed(&table[day_idx][period_idx].as_ref().unwrap());
            }
        }
    }
    table
}

//TODO: Make this return result
fn fetch_timetable(client: EspHttpConnection) -> anyhow::Result<(Box<Timetable>, DateTime<FixedOffset>)> {
    let mut creds = include_str!("../untis-creds.txt").split('\n');

    let server = creds.next().unwrap();
    let school_id = creds.next().unwrap();
    let usr = creds.next().unwrap();
    let password = creds.next().unwrap();

    let mut s = untis::Client::login(
        server, 
        school_id, 
        usr, 
        password,
        client
    ).or_else(|e| None.context(format!("{:?}", e)))?;
    let cet = chrono::FixedOffset::east_opt(1 * 3600).unwrap();
    let now = s.date().context("Untis client provided no time but time is necessary for the requesting the timetable")?.with_timezone(&cet);
    println!("Current time: {}", now);
    let date = now.date_naive().checked_add_days(Days::new(2)).unwrap().checked_add_signed(TimeDelta::weeks(unsafe { WEEK_OFFSET } as i64 )).unwrap();
    let lessons = s.own_timetable_for_week(&Date(date)).or_else(|e| None.context(format!("{:?}", e)))?;
    
    Ok((transform_lessons(lessons), now))
}