#include "board.h"
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <string.h>
#include <util/delay.h>

#define LED PB5

void Board::toggle_led() { PORTB ^= (1 << LED); }

void Board::init_start() {
  // Initialize the application including WDT, PORTB.5

  // We will now disable the watchdog.
  // Service the watchdog just to be sure to avoid pending timeout.
  wdt_reset();

  // Clear WDRF in MCUSR.
  MCUSR &= ~(1U << WDRF);

  // Write logical one to WDCE and WDE.
  // Keep the old prescaler setting to prevent unintentional time-out.
  WDTCSR |= (1U << WDCE) | (1U << WDE);

  // Turn off the WDT.
  WDTCSR = 0x00;

  // We will now initialize PORTB.5 to be used as an LED driver port.
  // Set PORTB.5 value to low.
  PORTB &= ~(1U << PORTB5);

  /* initializing PB5 which is connected to port 13 of uno as output*/
  DDRB |= (1 << LED);
}

void Board::enable_timer() {
  // We will now initialize the TIMER0 clock and interrupt.
  // Clear the TIMER0 overflow flag.
  TIFR0 = static_cast<std::uint8_t>(1U << TOV0);

  // Enable the TIMER0 overflow interrupt.
  TIMSK0 = static_cast<std::uint8_t>(1U << TOIE0);

  // Set the TIMER0 clock source to f_osc/8 = 2MHz and begin counting.
  TCCR0B = static_cast<std::uint8_t>(1U << CS01);
}

void Board::disable_timer() {
  // erase TIMER0 clock source;
  TCCR0B = 0;

  // Clear the TIMER0 overflow flag.
  TIMSK0 = 0;

  // Enable the TIMER0 overflow interrupt.
  TIMSK0 = 0;
}

std::uint64_t gpt_system_tick = 0;

rl::TimePoint Board::get_now() {
  // The entire system-tick is composed of the static
  // 64-bit variable mcal_gpt_system_tick and the 8-bit
  // timer register TCNT0. These are concatenated together
  // in software as a cohesive, consistent 64-bit tick.

  // This subroutine returns the concatenated 64-bit
  // mcal_gpt_system_tick/TCNT0 64-bit system tick.
  // A multiple read of the tick is used in order
  // to ensure data consistency.

  // Do the first read of the TIMER0 counter and the system tick.
  const auto tim0_cnt_1 = TCNT0;
  const auto sys_tick_1 = gpt_system_tick;

  // Do the second read of the TIMER0 counter.
  const auto tim0_cnt_2 = TCNT0;

  // Perform the consistency check to obtain the concatenated,
  // consistent, 64-bit system-tick.
  const auto consistent_microsecond_tick =
      ((tim0_cnt_2 >= tim0_cnt_1)
           ? static_cast<std::uint64_t>(
                 sys_tick_1 | static_cast<std::uint8_t>(tim0_cnt_1 >> 1U))
           : static_cast<std::uint64_t>(
                 gpt_system_tick |
                 static_cast<std::uint8_t>(tim0_cnt_2 >> 1U)));

  return consistent_microsecond_tick;
}

void adjust_timer(const rotor_light::Duration &value) {
  gpt_system_tick += value;
}

static UARTRxCallback uart_rx;
static UARTTxCallback uart_tx;

void Board::enable_uart(UARTRxCallback rx, UARTTxCallback tx) {
  uart_rx = rx;
  uart_tx = tx;
  // USART
#define BAUD 9600
#include <util/setbaud.h>
  UBRR0H = UBRRH_VALUE;
  UBRR0L = UBRRL_VALUE;
  UCSR0B = (1 << RXEN0)    // enable rx
           | (1 << TXEN0)  // enable tx
           | (1 << RXCIE0) // enable rx interrupt
      ;

  /* Set frame format: 8data, 2stop bit */
  UCSR0C = (1 << USBS0) | (3 << UCSZ00);
}

void Board::send_uart(char value) {
  // enable empty data buffer interrupt
  UCSR0B |= (1 << UDRIE0);
  while (!(UCSR0A & (1 << UDRE0))) {
    // NO-OP
  }
  UDR0 = value;
}

void Board::enable_usart() {
  // USART
#define BAUD 9600
#include <util/setbaud.h>
  UBRR0H = UBRRH_VALUE;
  UBRR0L = UBRRL_VALUE;
  UCSR0B = (1 << RXEN0) | (1 << TXEN0);

  /* Set frame format: 8data, 2stop bit */
  UCSR0C = (1 << USBS0) | (3 << UCSZ00);
}

void Board::send_usart(const char *data) { send_usart(data, strlen(data)); }

void Board::send_usart(const char *data, size_t count) {
  auto end = data + count;
  while (data != end) {
    while (!(UCSR0A & (1 << UDRE0)))
      ;
    UDR0 = static_cast<uint8_t>(*data++);
  }
}

void Board::sleep(const rl::TimePoint &until) {
  auto now = get_now();
  auto left = until - now;
  auto amount = rl::Duration{};
  auto sleep_constant = uint8_t{};

  cli();
  disable_timer();
  while (left > 0) {
    cli();
    if (left < 15) {
      break;
    } else if (left < 30) {
      amount = 15;
      sleep_constant = WDTO_15MS;
    } else if (left < 60) {
      amount = 30;
      sleep_constant = WDTO_30MS;
    } else if (left < 120) {
      amount = 60;
      sleep_constant = WDTO_60MS;
    } else if (left < 250) {
      amount = 120;
      sleep_constant = WDTO_120MS;
    } else if (left < 500) {
      amount = 250;
      sleep_constant = WDTO_250MS;
    } else if (left < 1000) {
      amount = 500;
      sleep_constant = WDTO_500MS;
    } else if (left < 2000) {
      amount = 1000;
      sleep_constant = WDTO_1S;
    } else if (left < 4000) {
      amount = 2000;
      sleep_constant = WDTO_2S;
    } else if (left < 8000) {
      amount = 4000;
      sleep_constant = WDTO_4S;
    } else {
      amount = 8000;
      sleep_constant = WDTO_8S;
    }
    TCNT0 = 0;
    adjust_timer(amount * 1000);
    wdt_reset();
    wdt_enable(sleep_constant);
    // enable watchdog interrupt
    WDTCSR |= (1 << WDIE);

    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_enable();
    sei();
    sleep_cpu();
    sleep_disable();
    sei();
    left -= amount;
  }
  enable_timer();
  sei();
}

void Board::delay() { _delay_ms(1000); }

ISR(TIMER0_OVF_vect) {
  // Increment the 64-bit system tick with 0x80, representing 128 microseconds.
  gpt_system_tick += UINT8_C(0x80);
}

ISR(USART_RX_vect) {
  if (UCSR0A & (1 << RXC0)) {
    uart_rx(static_cast<char>(UDR0));
  }
}

ISR(USART_UDRE_vect) {
  // disable empty data buffer interrupt
  UCSR0B &= ~(1 << UDRIE0);
  uart_tx();
}

ISR(WDT_vect) {
  // WDIE & WDIF is cleared in hardware upon entering this ISR
  wdt_disable();
}
