// Copyright 2011 Emilie Gillet.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU 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 General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// -----------------------------------------------------------------------------
//
// Bootloader supporting MIDI SysEx update.
//
// Caveat: assumes the firmware flashing is always done from first to last
// block, in increasing order. Random access flashing is not supported!

#include "avrlibx/io/gpio.h"
#include "avrlibx/io/usart.h"
#include "avrlibx/system/init.h"
#include "avrlibx/system/time.h"

#include "avrlibx/third_party/sp_driver/sp_driver.h"

#include "edges/hardware_config.h"

using namespace avrlibx;
using namespace edges;

MidiIO midi;
Leds leds;
Switches switches;

uint16_t page = 0;
uint8_t rx_buffer[APP_SECTION_PAGE_SIZE + 1];

typedef void (*MainEntryPoint)(void) __attribute__ ((noreturn));
MainEntryPoint main_entry_point = (MainEntryPoint)0x0000;

inline void Init() {
  SysInit();
  leds.set_direction(OUTPUT);
  switches.set_direction(INPUT);
  switches.set_mode(PORT_MODE_PULL_UP);
  midi.Init();
}

void WriteBufferToFlash() {
  SP_LoadFlashPage(rx_buffer);
  SP_EraseWriteApplicationPage(page);
  SP_WaitForSPM();
  
  NVM_CMD = NVM_CMD_NO_OPERATION_gc;
}

void FlashLedsOk() {
  for (uint8_t i = 0; i < 8; ++i) {
    leds.Write(0xf);
    ConstantDelay(50);
    leds.Write(8);
    ConstantDelay(50);
  }
}

void FlashLedsError() {
  leds.Write(0);
  for (uint8_t i = 0; i < 5; ++i) {
    ConstantDelay(100);
    leds.Toggle();
  }
}

static const uint8_t sysex_header[] = {
  0xf0,  // <SysEx>
  0x00, 0x21, 0x02,  // Mutable instruments manufacturer id.
  0x00, 0x0a,  // Product ID for "any product".
};

enum SysExReceptionState {
  MATCHING_HEADER = 0,
  READING_COMMAND = 1,
  READING_DATA = 2,
};

inline void MidiLoop() {
  uint8_t byte;
  uint16_t bytes_read = 0;
  uint16_t rx_buffer_index;
  uint8_t state = MATCHING_HEADER;
  uint8_t checksum;
  uint8_t sysex_commands[2];
  uint8_t page_byte = 0;

  midi.Init();
  page = 0;
  
  while (1) {
    leds.Write(8 | (4 >> ((page_byte + 3) & 0x3)));
    byte = midi.Read();
    // In case we see a realtime message in the stream, safely ignore it.
    if (byte > 0xf0 && byte != 0xf7) {
      continue;
    }
    switch (state) {
      case MATCHING_HEADER:
        if (byte == sysex_header[bytes_read]) {
          ++bytes_read;
          if (bytes_read == sizeof(sysex_header)) {
            bytes_read = 0;
            state = READING_COMMAND;
          }
        } else {
          bytes_read = 0;
        }
        break;

      case READING_COMMAND:
        if (byte < 0x80) {
          sysex_commands[bytes_read++] = byte;
          if (bytes_read == 2) {
            bytes_read = 0;
            rx_buffer_index = 0;
            checksum = 0;
            state = READING_DATA;
          }
        } else {
          state = MATCHING_HEADER;
          bytes_read = 0;
        }
        break;

      case READING_DATA:
        if (byte < 0x80) {
          if (bytes_read & 1) {
            rx_buffer[rx_buffer_index] |= byte & 0xf;
            if (rx_buffer_index < APP_SECTION_PAGE_SIZE) {
              checksum += rx_buffer[rx_buffer_index];
            }
            ++rx_buffer_index;
          } else {
            rx_buffer[rx_buffer_index] = (byte << 4);
          }
          ++bytes_read;
        } else if (byte == 0xf7) {
          if (sysex_commands[0] == 0x7f &&
              sysex_commands[1] == 0x00 &&
              bytes_read == 0) {
            // Reset.
            return;
          } else if (rx_buffer_index == APP_SECTION_PAGE_SIZE + 1 &&
                     sysex_commands[0] == 0x7e &&
                     sysex_commands[1] == 0x00 &&
                     rx_buffer[rx_buffer_index - 1] == checksum) {
            // Block write.
            WriteBufferToFlash();
            page += APP_SECTION_PAGE_SIZE;
            ++page_byte;
          } else {
            FlashLedsError();
          }
          state = MATCHING_HEADER;
          bytes_read = 0;
        }
        break;
    }
  }
}

int main(void) {
  Init();
  ConstantDelay(25);
  if (!(switches.Read() & 1)) {
    FlashLedsOk();
    MidiLoop();
    FlashLedsOk();
  }
  SP_LockSPM();
  EIND = 0x00;
  main_entry_point();
}
