//
// Space API HTTP Button ("dumbswitch") Main Entrypoint
// Copyright (C) 2025  memdmp
//
// This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

//! Production ESP is built with `cargo r --bin dumbswitch-as-server --features led-as-busy-led -r`
//! Debug ESP is built with `ESP_LOG=debug cargo r --bin dumbswitch-as-server --features led-as-busy-led`
//! To get logs, use while true; do if ls /dev/ | grep ttyACM >/dev/null 2>/dev/null; then echo 'Found Device'; cat /dev/ttyACM*; else sleep 0.001; fi; done
//! To change networks, copy ../network_data.example.rs to ../network_data.rs and change it's contents

//% FEATURES: esp-wifi esp-wifi/wifi esp-wifi/utils esp-hal/unstable
//% CHIPS: esp32 esp32s2 esp32s3 esp32c2 esp32c3 esp32c6

#![no_std]
#![no_main]

use core::cell::{OnceCell, RefCell};
use core::ops::Deref;
use core::str::FromStr;

use blocking_network_stack::Stack;
use critical_section::Mutex;
use dumbswitch::{network_data, network_type::Network};
use embedded_io::*;
use esp_alloc as _;
use esp_backtrace as _;
use esp_hal::delay::Delay;
use esp_hal::gpio::{Event, Input, Io, Level, Output, Pull};
use esp_hal::handler;
use esp_hal::interrupt::InterruptConfigurable;
use esp_hal::{
  clock::CpuClock,
  main,
  rng::Rng,
  time::{self, Duration},
  timer::timg::TimerGroup,
};
use esp_println::println;
use esp_wifi::{
  init,
  wifi::{
    utils::create_network_interface, AccessPointInfo, ClientConfiguration, Configuration,
    WifiError, WifiStaDevice,
  },
};
use log::{debug, error, info, trace, warn};
use smoltcp::iface::{SocketSet, SocketStorage};

pub enum OurOption<T> {
  /// No value.
  None,
  /// Some value of type `T`.
  Some(T),
}

static BUTTON: Mutex<RefCell<Option<Input>>> = Mutex::new(RefCell::new(None));
static mut LED: OurOption<RefCell<Output<'_>>> = OurOption::None;

fn set_open_led_state(val: bool) {
  #[cfg(not(feature = "led-as-busy-led"))]
  {
    let v = unsafe {
      #[allow(static_mut_refs)]
      &LED
    };
    let v = match v {
      OurOption::None => {
        return;
      }
      OurOption::Some(v) => v,
    };
    let mut b = v.borrow_mut();
    b.set_level(if val { Level::High } else { Level::Low });
  }
}

// TODO: in future, add another LED for busy
fn set_busy_led_state(val: bool) {
  #[cfg(feature = "led-as-busy-led")]
  {
    let v = unsafe {
      #[allow(static_mut_refs)]
      &LED
    };
    let v = match v {
      OurOption::None => {
        return;
      }
      OurOption::Some(v) => v,
    };
    let mut b = v.borrow_mut();
    b.set_level(if val { Level::High } else { Level::Low });
  }
}

#[main]
fn main() -> ! {
  esp_println::logger::init_logger_from_env();
  let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
  let peripherals = esp_hal::init(config);

  let delay = Delay::new();

  esp_alloc::heap_allocator!(72 * 1024);

  info!("Preparing GPIO");

  let mut io = Io::new(peripherals.IO_MUX);

  // Set GPIO0 as an output, and set its state low initially.
  let led = RefCell::new(Output::new(peripherals.GPIO0, Level::Low));
  let led = unsafe {
    LED = OurOption::Some(led);
    #[allow(static_mut_refs)]
    match &LED {
      OurOption::None => panic!(),
      OurOption::Some(v) => v,
    }
  };

  // Set GPIO5 as an input
  let mut button = Input::new(peripherals.GPIO5, Pull::Down);

  info!("Waiting 500ms pre-init");
  delay.delay_millis(500);

  led.borrow_mut().set_low();

  let timg0 = TimerGroup::new(peripherals.TIMG0);

  let mut rng = Rng::new(peripherals.RNG);

  let init = init(timg0.timer0, rng.clone(), peripherals.RADIO_CLK).unwrap();

  let mut wifi = peripherals.WIFI;
  let (iface, device, mut controller) =
    create_network_interface(&init, &mut wifi, WifiStaDevice).unwrap();

  let mut socket_set_entries: [SocketStorage; 3] = Default::default();
  let socket_set = SocketSet::new(&mut socket_set_entries[..]);

  let now = || time::now().duration_since_epoch().to_millis();
  let mut stack = Stack::new(iface, device, socket_set, now, rng.random());

  let client_config = Configuration::Client(ClientConfiguration {
    ..Default::default()
  });
  let res = controller.set_configuration(&client_config);
  debug!("wifi_set_configuration returned {:?}", res);

  controller.start().unwrap();
  debug!("is wifi started: {:?}", controller.is_started());

  info!("Start Wifi Scan");
  let res: Result<(heapless::Vec<AccessPointInfo, 30>, usize), WifiError> = controller.scan_n();
  let mut net: Option<Network> = None;
  if let Ok((res, _count)) = res {
    for ap in res {
      info!("Saw AP: {:?}", ap);
      for ap_comp in network_data::NETWORKS {
        if ap_comp.ssid == ap.ssid.clone() {
          let client_config = Configuration::Client(ClientConfiguration {
            ssid: ap_comp.ssid.try_into().unwrap(),
            password: ap_comp.password.try_into().unwrap(),
            ..Default::default()
          });
          controller.set_configuration(&client_config);
          net = Some(ap_comp);
        };
      }
    }
  }

  if net.is_none() {
    loop {
      error!("Failed to connect to wifi: No saved network found.");
      let mut high = false;
      loop {
        delay.delay_millis(1000);
        high = !high;
        if high {
          led.borrow_mut().set_high();
        } else {
          led.borrow_mut().set_low();
        }
      }
    }
  }

  debug!("{:?}", controller.capabilities());
  let connres = controller.connect();
  debug!("wifi_connect {:?}", connres);
  if !connres.is_ok() {
    error!("Failed to connect to wifi! {:#?}", connres.unwrap_err());

    let mut high = false;
    loop {
      delay.delay_millis(2000);
      high = !high;
      if high {
        led.borrow_mut().set_high();
      } else {
        led.borrow_mut().set_low();
      }
    }
  }

  let net = net.unwrap();

  // wait to get connected
  info!("Wait to get connected");
  loop {
    match controller.is_connected() {
      Ok(true) => break,
      Ok(false) => {
        set_busy_led_state(true);
        delay.delay_millis(10);
        set_busy_led_state(false);
        delay.delay_millis(500);
      }
      Err(err) => {
        error!("Failed to connect to wifi: {:?}", err);
        let mut high = false;
        loop {
          delay.delay_millis(1000);
          high = !high;
          if high {
            led.borrow_mut().set_high();
          } else {
            led.borrow_mut().set_low();
          }
        }
      }
    }
  }
  debug!("{:?}", controller.is_connected());

  info!("Setting static IP {}", net.static_ip);

  stack
    .set_iface_configuration(&blocking_network_stack::ipv4::Configuration::Client(
      blocking_network_stack::ipv4::ClientConfiguration::Fixed(
        blocking_network_stack::ipv4::ClientSettings {
          ip: blocking_network_stack::ipv4::Ipv4Addr::from(parse_ip(net.static_ip)),
          subnet: blocking_network_stack::ipv4::Subnet {
            gateway: blocking_network_stack::ipv4::Ipv4Addr::from(parse_ip(net.gateway_ip)),
            mask: blocking_network_stack::ipv4::Mask(24),
          },
          dns: None,
          secondary_dns: None,
        },
      ),
    ))
    .unwrap();

  for i in 0..4 {
    delay.delay_millis(100);
    if i % 2 == 0 {
      led.borrow_mut().set_high();
    } else {
      led.borrow_mut().set_low();
    }
  }

  info!(
    "Start busy loop on main. Point your browser to http://{}:8080/",
    net.static_ip
  );

  let mut rx_buffer = [0u8; 1536];
  let mut tx_buffer = [0u8; 1536];
  let mut socket = stack.get_socket(&mut rx_buffer, &mut tx_buffer);

  socket.listen(8080).unwrap();

  // fn update_open(is_open: bool, button: &Input<'_>, led: &mut Output<'_>, delay: &Delay) -> bool {
  //   let mut is_open = if is_open { true } else { false };
  //   if button.is_low() {
  //     is_open = !is_open;

  //     if is_open {
  //       led.set_high();
  //     } else {
  //       led.set_low();
  //     }
  //     while button.is_low() {
  //       trace!("waiting for unpress btn");
  //       delay.delay_millis(10);
  //     }
  //   }
  //   is_open
  // }

  loop {
    socket.work();

    if !socket.is_open() {
      socket.listen(8080).unwrap();
    }

    set_busy_led_state(true);
    if socket.is_connected() {
      let is_open = button.is_high();
      unsafe {
        set_open_led_state(is_open);
      }

      let r = socket.write_all(
        // 0b01100001 - true (open)
        (if is_open {
          b"HTTP/1.0 200 OK\r\n\
\r\n\
a"
        } else {
          // 0b01111010 - false (closed)
          b"HTTP/1.0 200 OK\r\n\
\r\n\
z"
        }),
      );
      if !r.is_ok() {
        error!("{:#?}", r.unwrap_err());
        continue;
      };

      let r = socket.flush();
      if !r.is_ok() {
        error!("{:#?}", r.unwrap_err());
        continue;
      };
      socket.work();
      // }

      socket.close();
    }
    set_busy_led_state(false);
    // TODO: what
    let deadline = time::now() + Duration::millis(100);
    while time::now() < deadline && !socket.is_connected() {
      socket.work();
    }

    let is_open = button.is_high();
    set_open_led_state(is_open);
  }
}

fn parse_ip(ip: &str) -> [u8; 4] {
  let mut result = [0u8; 4];
  for (idx, octet) in ip.split(".").into_iter().enumerate() {
    result[idx] = u8::from_str_radix(octet, 10).unwrap();
  }
  result
}