#![no_main]
#![no_std]

mod display;
mod information;
mod pins;
mod radio;
mod sensors;
mod ui;

// TODO: Enable warnings again.
#[allow(unused)]
mod assets {
    include!(concat!(env!("OUT_DIR"), "/assets.rs"));
}

use cortex_m::peripheral::NVIC;
use embedded_hal::timer::CountDown;
use mkl25z4_hal::clocks::ClockConfiguration;
use mkl25z4_hal::mkl25z4::{Interrupt, LPTMR0};
use mkl25z4_hal::time::{CopyableMonoTimer, NonCopyableMonoTimer, U32Ext};
use mkl25z4_hal::timer::Timer;
use mkl25z4_hal::timer::TimerInterrupt;
use panic_semihosting as _;
use protocol::{Location, ValueType};

use display::Display;
use information::{Information, TemperatureHumidity};
use pins::{BME280Pins, DisplayBmeSpi};
use radio::Radio;
use ui::tiles::TileView;
use ui::{Ui, View};

#[rtfm::app(device = mkl25z4_hal::mkl25z4, peripherals = true)]
const APP: () = {
    struct Resources {
        info: Information,
        display: Display,
        display_bme_spi: Option<DisplayBmeSpi>,
        bme_pins: Option<BME280Pins>,
        radio: Option<Radio>,
        time: CopyableMonoTimer,
        update_timer: Timer<LPTMR0>,
    }

    #[init(spawn = [fetch_update])]
    fn init(ctx: init::Context) -> init::LateResources {
        let mut sim = ctx.device.SIM;
        mkl25z4_hal::watchdog::disable(&mut sim);
        let clocks = ClockConfiguration::new()
            .use_irc()
            .core_clock(24.mhz())
            .bus_clock(24.mhz())
            .apply(&mut sim, ctx.device.OSC0, ctx.device.MCG);

        let mut pins = pins::Pins::configure(
            &mut sim,
            clocks,
            ctx.device.GPIOA,
            ctx.device.GPIOB,
            ctx.device.GPIOC,
            ctx.device.GPIOD,
            ctx.device.GPIOE,
            ctx.device.SPI0,
            ctx.device.SPI1,
        );

        let time =
            CopyableMonoTimer::new(NonCopyableMonoTimer::new(ctx.device.PIT, clocks, &mut sim));

        let display = Display::init(pins.display, &mut pins.display_bme_spi, time);

        let radio = Radio::init(pins.nrf, time);

        let update_timer = Timer::lptmr0(ctx.device.LPTMR0, 10.s(), clocks, &mut sim);
        update_timer.enable_interrupt();
        unsafe { NVIC::unmask(Interrupt::LPTMR0) };

        // Fetch the first update.
        ctx.spawn.fetch_update().ok();

        init::LateResources {
            info: Information {
                time: 0,
                valid: false,
                inside: None,
                outside: None,
                bathroom: None,
                pressure: None,
                local: None,
            },
            display,
            display_bme_spi: Some(pins.display_bme_spi),
            bme_pins: Some(pins.bme),
            radio: Some(radio),
            time,
            update_timer,
        }
    }

    #[task(binds = LPTMR0, priority = 3, spawn = [fetch_update], resources = [update_timer])]
    fn lptmr0_interrupt(ctx: lptmr0_interrupt::Context) {
        // Restart the timer.
        ctx.resources.update_timer.start(10.s());
        ctx.resources.update_timer.enable_interrupt();

        // Update the time.
        // TODO

        // Trigger an update of the displayed values.
        ctx.spawn.fetch_update().ok();
    }

    #[task(binds = LLWU, priority = 3)]
    fn llwu_interrupt(_: llwu_interrupt::Context) {
        // TODO
    }

    #[task(priority = 1, spawn = [update_display], resources = [info, bme_pins, display_bme_spi, &time, radio])]
    fn fetch_update(mut ctx: fetch_update::Context) {
        // Fetch local updates.
        let time = ctx.resources.time.clone();
        let pins = ctx.resources.bme_pins.take().unwrap();
        let (pins, values) = ctx.resources.display_bme_spi.lock(|display_bme_spi| {
            let spi = display_bme_spi.take().unwrap();
            let (pins, spi, values) = sensors::bme280(pins, spi, time);
            *display_bme_spi = Some(spi);
            (pins, values)
        });
        *ctx.resources.bme_pins = Some(pins);

        // Send local measurements to the base station. This needs to be done before fetching data
        // from the base station as the base station might return the data to us.
        let mut radio = ctx.resources.radio.as_mut().unwrap().enable();
        radio.send_sensor_values(values);

        // Fetch updates via radio.
        let mut livingroom = [None; 2];
        radio.request_values(
            Location::Livingroom,
            &[ValueType::Temperature, ValueType::Humidity],
            &mut livingroom,
        );
        let mut balcony = [None; 2];
        radio.request_values(
            Location::Balcony,
            &[ValueType::Temperature, ValueType::Humidity],
            &mut balcony,
        );
        let mut bathroom = [None; 2];
        radio.request_values(
            Location::Bathroom,
            &[ValueType::Temperature, ValueType::Humidity],
            &mut bathroom,
        );
        let mut general = [None; 2];
        radio.request_values(
            Location::Livingroom,
            &[ValueType::Time, ValueType::Pressure],
            &mut general,
        );

        ctx.resources.info.lock(|info| {
            if livingroom[0].is_some() && livingroom[1].is_some() {
                info.inside = Some(TemperatureHumidity {
                    temperature: livingroom[0].unwrap() as i32,
                    humidity: livingroom[1].unwrap() as u32,
                });
            } else {
                info.inside = None;
            }
            if balcony[0].is_some() && balcony[1].is_some() {
                info.outside = Some(TemperatureHumidity {
                    temperature: balcony[0].unwrap() as i32,
                    humidity: balcony[1].unwrap() as u32,
                });
            } else {
                info.outside = None;
            }
            if bathroom[0].is_some() && bathroom[1].is_some() {
                info.bathroom = Some(TemperatureHumidity {
                    temperature: bathroom[0].unwrap() as i32,
                    humidity: bathroom[1].unwrap() as u32,
                });
            } else {
                info.bathroom = None;
            }
            if general[1].is_some() {
                info.pressure = Some(general[1].unwrap() as u32);
            }
            if general[0].is_some() {
                info.time = general[0].unwrap() as u64;
            }
            info.local = Some(values);
        });

        // If the update failed, simply increment the time and place a hint that
        // the information might be invalid.
        // TODO

        // Update the display to show the new values.
        ctx.spawn.update_display().ok();
    }

    #[task(priority = 2, resources = [info, display, display_bme_spi, &time])]
    fn update_display(ctx: update_display::Context) {
        let ui = Ui::view(View::Tiles(TileView::new(ctx.resources.info.clone())));
        ctx.resources.display.update(
            ui,
            ctx.resources.display_bme_spi.as_mut().unwrap(),
            ctx.resources.time.clone(),
        );
    }

    #[idle]
    fn idle(_cx: idle::Context) -> ! {
        // Sleep
        loop {
            // wfi needs to be disabled for GDB
            /*// add some side effect to prevent this from turning into a UDF instruction
            // see rust-lang/rust#28728 for details
            atomic::compiler_fence(Ordering::SeqCst);*/
            cortex_m::asm::wfi();
        }
    }

    extern "C" {
        fn I2C0();
        fn I2C1();
    }
};
