#pragma once

#include <libremidi/error_handler.hpp>
#include <libremidi/message.hpp>

#include <cmath>

#include <array>
#include <cinttypes>
#include <cstdint>
#include <cstdio>
#include <functional>
#include <span>

namespace libremidi
{
static constexpr auto to_underlying(auto e)
{
  return static_cast<std::underlying_type_t<decltype(e)>>(e);
}

// A clean-room reverse-engineered remote control protocol compatible with many hardware devices.
// Thanks https://github.com/NicoG60/TouchMCU !
struct remote_control_protocol
{
  enum class device_type : uint8_t
  {
    logic_control = 0x10,
    logic_control_xt = 0x11,
    mackie_control = 0x14
  };

  enum class command_to_device : uint8_t
  {
    device_query = 0x00,
    host_connection_reply = 0x02,

    transport_click = 0x0A,
    lcd_backlight_save = 0x0B,
    touchless_movable_fader = 0x0C,

    faders_touch_sensitivity = 0x0E,
    go_offline = 0x0F,
    update_tc_display = 0x10,
    update_assignment_display = 0x11,
    update_lcd = 0x12,
    firmware_version_request = 0x13,
    version_reply = 0x14,

    firmware_update = 0x18,

    channel_meter_mode = 0x20,
    global_lcd_meter_mode = 0x21,

    faders_to_minimum = 0x61,
    all_leds_off = 0x62,
    reset = 0x63,
  };

  enum class command_from_device : uint8_t
  {
    host_connection_query = 0x01,
    host_connection_confirmation = 0x03,
    host_connection_error = 0x04,
    version_reply = 0x14,
  };

  enum class lcd_meter_mode : uint8_t
  {
    horizontal = 0x00,
    vertical = 0x01,
  };

  enum class fader_sensitivity : uint8_t
  {
    sensitivity_0 = 0x00,
    sensitivity_1 = 0x01,
    sensitivity_2 = 0x02,
    sensitivity_3 = 0x03,
    sensitivity_4 = 0x04,

    sensitivity_default = sensitivity_3,
  };

  // CC Message:
  // 0b0LMMVVVV
  // L: toggle underneath LED
  // MM: mode as led_ring_mode
  // VVVV: value
  enum class led_ring_mode : uint8_t
  {
    mode_0 = 0b00, // one led only
    mode_1 = 0b01, // pan pot
    mode_2 = 0b10, // fill leds from left
    mode_3 = 0b11, // fill leds from middle
  };

  enum class pot : uint8_t
  {
    pot_0 = 0x00,
    pot_1 = 0x01,
    pot_2 = 0x02,
    pot_3 = 0x03,
    pot_4 = 0x04,
    pot_5 = 0x05,
    pot_6 = 0x06,
    pot_7 = 0x07,
  };

  enum class fader : uint8_t
  {
    fader_0 = 0x00,
    fader_1 = 0x01,
    fader_2 = 0x02,
    fader_3 = 0x03,
    fader_4 = 0x04,
    fader_5 = 0x05,
    fader_6 = 0x06,
    fader_7 = 0x07,
    fader_master = 0x08,
  };

  // control changes
  enum class mixer_control : uint8_t
  {
    // rotation: CC
    // 0b00000001 : clockwise
    // 0b01000001 : counter-clockwise
    vpot_rotation_0 = 0x10 + 0x00,
    vpot_rotation_1 = 0x10 + 0x01,
    vpot_rotation_2 = 0x10 + 0x02,
    vpot_rotation_3 = 0x10 + 0x03,
    vpot_rotation_4 = 0x10 + 0x04,
    vpot_rotation_5 = 0x10 + 0x05,
    vpot_rotation_6 = 0x10 + 0x06,
    vpot_rotation_7 = 0x10 + 0x07,

    external_control = 0x2E,

    // led ring: CC
    vpot_led_0 = 0x30 + 0x00,
    vpot_led_1 = 0x30 + 0x01,
    vpot_led_2 = 0x30 + 0x02,
    vpot_led_3 = 0x30 + 0x03,
    vpot_led_4 = 0x30 + 0x04,
    vpot_led_5 = 0x30 + 0x05,
    vpot_led_6 = 0x30 + 0x06,
    vpot_led_7 = 0x30 + 0x07,

    jog_wheel = 0x3C,

    timecode_digit_0 = 0x40 + 0x00,
    timecode_digit_1 = 0x40 + 0x01,
    timecode_digit_2 = 0x40 + 0x02,
    timecode_digit_3 = 0x40 + 0x03,
    timecode_digit_4 = 0x40 + 0x04,
    timecode_digit_5 = 0x40 + 0x05,
    timecode_digit_6 = 0x40 + 0x06,
    timecode_digit_7 = 0x40 + 0x07,
    timecode_digit_8 = 0x40 + 0x08,
    timecode_digit_9 = 0x40 + 0x09,

    assignment_digit_0 = 0x4A,
    assignment_digit_1 = 0x4B,
  };

  // note events
  enum class mixer_command : uint8_t
  {
    vpot_click_0 = 0x20 + 0x00,
    vpot_click_1 = 0x20 + 0x01,
    vpot_click_2 = 0x20 + 0x02,
    vpot_click_3 = 0x20 + 0x03,
    vpot_click_4 = 0x20 + 0x04,
    vpot_click_5 = 0x20 + 0x05,
    vpot_click_6 = 0x20 + 0x06,
    vpot_click_7 = 0x20 + 0x07,

    rec_0 = 0x00 + 0x00,
    rec_1 = 0x00 + 0x01,
    rec_2 = 0x00 + 0x02,
    rec_3 = 0x00 + 0x03,
    rec_4 = 0x00 + 0x04,
    rec_5 = 0x00 + 0x05,
    rec_6 = 0x00 + 0x06,
    rec_7 = 0x00 + 0x07,

    solo_0 = 0x08 + 0x00,
    solo_1 = 0x08 + 0x01,
    solo_2 = 0x08 + 0x02,
    solo_3 = 0x08 + 0x03,
    solo_4 = 0x08 + 0x04,
    solo_5 = 0x08 + 0x05,
    solo_6 = 0x08 + 0x06,
    solo_7 = 0x08 + 0x07,

    mute_0 = 0x10 + 0x00,
    mute_1 = 0x10 + 0x01,
    mute_2 = 0x10 + 0x02,
    mute_3 = 0x10 + 0x03,
    mute_4 = 0x10 + 0x04,
    mute_5 = 0x10 + 0x05,
    mute_6 = 0x10 + 0x06,
    mute_7 = 0x10 + 0x07,

    sel_0 = 0x18 + 0x00,
    sel_1 = 0x18 + 0x01,
    sel_2 = 0x18 + 0x02,
    sel_3 = 0x18 + 0x03,
    sel_4 = 0x18 + 0x04,
    sel_5 = 0x18 + 0x05,
    sel_6 = 0x18 + 0x06,
    sel_7 = 0x18 + 0x07,

    // TODO metering
    assign_track = 0x28,
    assign_send = 0x29,
    assign_pan = 0x2A,
    assign_plugin = 0x2B,
    assign_eq = 0x2C,
    assign_instrument = 0x2D,

    bank_left = 0x2E,
    bank_right = 0x2F,
    channel_left = 0x30,
    channel_right = 0x31,
    flip = 0x32,
    global = 0x33,

    name_value_button = 0x34,
    smpte_beats_button = 0x35,

    f1 = 0x36 + 0x00,
    f2 = 0x36 + 0x01,
    f3 = 0x36 + 0x02,
    f4 = 0x36 + 0x03,
    f5 = 0x36 + 0x04,
    f6 = 0x36 + 0x05,
    f7 = 0x36 + 0x06,
    f8 = 0x36 + 0x07,

    midi_tracks = 0x3E,
    inputs = 0x3F,
    audio_tracks = 0x40,
    audio_instruments = 0x41,
    aux = 0x42,
    busses = 0x43,
    outputs = 0x44,
    user = 0x45,

    shift = 0x46,
    option = 0x47,
    control = 0x48,
    alt = 0x49,

    save = 0x50,
    undo = 0x51,
    cancel = 0x52,
    enter = 0x53,

    markers = 0x54,
    nudge = 0x55,
    cycle = 0x56,
    drop = 0x57,
    replace = 0x58,
    click = 0x59,
    solo = 0x5a,

    rewind = 0x5b,
    forward = 0x5c,
    stop = 0x5d,
    play = 0x5e,
    record = 0x5f,

    up = 0x60,
    down = 0x61,
    left = 0x62,
    right = 0x63,
    zoom = 0x64,
    scrub = 0x65,

    user_switch_1 = 0x66,
    user_switch_2 = 0x67,

    fader_touched_0 = 0x68,
    fader_touched_1 = 0x69,
    fader_touched_2 = 0x6a,
    fader_touched_3 = 0x6b,
    fader_touched_4 = 0x6c,
    fader_touched_5 = 0x6d,
    fader_touched_6 = 0x6e,
    fader_touched_7 = 0x6f,
    fader_touched_master = 0x70,

    smpte_led = 0x71,
    beats_led = 0x72,
    rude_solo_led = 0x73,

    relay_click = 0x76,
  };

  template <std::size_t N>
  using arr = std::array<uint8_t, N>;

  device_type type = device_type::mackie_control;

  static libremidi::message make_command_impl(auto&&... data)
  {
    using namespace std;
    libremidi::message m;
    m.bytes.reserve((std::ssize(data) + ...));
    (m.bytes.insert(m.bytes.end(), begin(data), end(data)), ...);
    return m;
  }

  libremidi::message make_command(command_to_device c, auto&&... data)
  {
    using namespace std;
    const auto type = to_underlying(this->type);
    const auto cmd = to_underlying(c);
    const uint8_t header[6]{0xF0, 0x00, 0x00, 0x66, type, cmd};
    const uint8_t footer[1]{0xF7};
    return make_command_impl(header, data..., footer);
  }

  auto device_query() { return make_command(command_to_device::device_query); }

  auto response_to_challenge(arr<4> c)
  {
    arr<4> r;

    r[0] = 0x7F & (c[0] + (c[1] ^ 0x0A) - c[3]);
    r[1] = 0x7F & ((c[2] >> 4) ^ (c[0] + c[3]));
    r[2] = 0x7F & (c[3] - (c[2] << 2) ^ (c[0] | c[1]));
    r[3] = 0x7F & (c[1] - c[2] + (0xF0 ^ (c[3] << 4)));

    return r;
  }

  auto host_connection_reply(arr<7> serial, arr<4> challenge_code)
  {
    const auto res = response_to_challenge(challenge_code);
    return make_command(command_to_device::host_connection_reply, serial, res);
  }

  auto transport_click(bool enabled)
  {
    return make_command(
        command_to_device::transport_click, arr<1>{uint8_t(enabled ? 0x01 : 0x00)});
  }

  auto lcd_backlight_save(uint8_t timeout)
  {
    // 0: instant off otherwise timeout in minutes
    return make_command(command_to_device::lcd_backlight_save, arr<1>{timeout});
  }

  auto touchless_movable_fader(bool enabled)
  {
    return make_command(
        command_to_device::touchless_movable_fader, arr<1>{uint8_t(enabled ? 0x01 : 0x00)});
  }

  auto faders_touch_sensitivity(uint8_t fader_id, fader_sensitivity sens)
  {
    return make_command(
        command_to_device::faders_touch_sensitivity, arr<2>{fader_id, to_underlying(sens)});
  }

  auto go_offline() { return make_command(command_to_device::go_offline, arr<1>{0x7F}); }

  auto update_tc_display()
  {
    // FIXME 1 .. 10
    return make_command(command_to_device::update_tc_display, arr<10>{});
  }

  auto update_assignment_display()
  {
    // FIXME 1 .. 2
    return make_command(command_to_device::update_assignment_display, arr<2>{});
  }

  auto update_lcd(std::string_view txt, int pos)
  {
    // FIXME
    if (pos < 0 || pos >= 112)
      return libremidi::message{};

    int len = int(std::ssize(txt));

    if (len > (112 - pos))
    {
      txt = txt.substr(0, 112 - pos);
      len = 112 - pos;
    }

    uint8_t buf[128];
    const int N = std::min(len, 112 - pos);
    for (int i = 0; i < N; i++)
    {
      buf[i + pos] = charmap_lcd(txt[i]);
    }
    buf[55] = '\n';
    buf[111] = '\n';

    uint8_t cmd_pos = pos;

    return make_command(command_to_device::update_lcd, arr<1>{cmd_pos}, std::span(buf + pos, len));
  }

  auto update_lcd(std::string_view txt)
  {
    uint8_t buf[112] = {};
    for (int i = 0; i < std::min(int(std::ssize(txt)), 112); i++)
    {
      buf[i] = charmap_lcd(txt[i]);
    }
    buf[55] = '\n';
    buf[111] = '\n';
    return make_command(command_to_device::update_lcd, arr<1>{0}, std::span(buf, 112));
  }

  auto firmware_version_request()
  {
    return make_command(command_to_device::firmware_version_request, arr<1>{0});
  }

  auto firmware_update(std::span<uint8_t> firmware)
  {
    return make_command(command_to_device::firmware_update, firmware);
  }

  auto channel_meter_mode(uint8_t fader_id, bool level_meter, bool peak_hold, bool signal_led)
  {
    uint8_t mode = 0;

    if (signal_led)
      mode |= 0b1;
    if (peak_hold)
      mode |= 0b10;
    if (level_meter)
      mode |= 0b100;

    return make_command(command_to_device::channel_meter_mode, arr<2>{fader_id, mode});
  }

  auto global_lcd_meter_mode(lcd_meter_mode mode)
  {
    return make_command(command_to_device::global_lcd_meter_mode, arr<1>{to_underlying(mode)});
  }

  auto faders_to_minimum() { return make_command(command_to_device::faders_to_minimum); }

  auto all_leds_off() { return make_command(command_to_device::all_leds_off); }

  auto reset() { return make_command(command_to_device::reset); }

  static auto timecode(int hi, int mi, int si, int framei)
  {
    std::vector<libremidi::message> msg;
    auto h = std::to_string(hi);
    while (h.size() < 3)
      h.insert(h.begin(), '0');
    auto m = std::to_string(mi);
    while (m.size() < 2)
      m.insert(m.begin(), '0');
    auto s = std::to_string(si);
    while (s.size() < 2)
      s.insert(s.begin(), '0');
    auto f = std::to_string(framei);
    while (f.size() < 3)
      f.insert(f.begin(), '0');

    using ce = libremidi::channel_events;
    msg.push_back(ce::control_change(1, 0x49, charmap_7segment(h[0])));
    msg.push_back(ce::control_change(1, 0x48, charmap_7segment(h[1])));
    msg.push_back(ce::control_change(1, 0x47, charmap_7segment(h[2])));

    msg.push_back(ce::control_change(1, 0x46, charmap_7segment(m[0])));
    msg.push_back(ce::control_change(1, 0x45, charmap_7segment(m[1])));

    msg.push_back(ce::control_change(1, 0x44, charmap_7segment(s[0])));
    msg.push_back(ce::control_change(1, 0x43, charmap_7segment(s[1])));

    msg.push_back(ce::control_change(1, 0x42, charmap_7segment(f[0])));
    msg.push_back(ce::control_change(1, 0x41, charmap_7segment(f[1])));
    msg.push_back(ce::control_change(1, 0x40, charmap_7segment(f[2])));

    return msg;
  }

  static uint8_t charmap_7segment(char c, bool dot)
  {
    uint8_t res = charmap_7segment(c);
    if (dot)
      res |= 0b00100000;
    return res;
  };

  static uint8_t charmap_7segment(char c)
  {
    // FIXME there are some more characters but what to map them to ? :)
    if (c >= 'a' && c <= 'z')
      return c - 'a' + 1;
    else if (c >= 'A' && c <= 'Z')
      return c - 'A' + 1;
    else if (c >= '0' && c <= '9')
      return c - '0' + 0x30;
    else
      switch (c)
      {
        case '[':
        case '{':
          return 0x1B;
        case '\\':
        case '~': // yen too ?
          return 0x1C;
        case ']':
        case '}':
          return 0x1D;
        case '^':
          return 0x1E;
        case '_':
          return 0x1F;

        case '!':
          return 0x21;
        case '"':
          return 0x22;
        case '#':
          return 0x23;
        case '$':
          return 0x24;
        case '%':
          return 0x25;
        case '&':
          return 0x26;
        case '\'':
          return 0x27;
        case '(':
          return 0x28;
        case ')':
          return 0x29;
        case '*':
          return 0x2A;
        case '+':
          return 0x2B;
        case ',':
          return 0x2C;
        case '-':
          return 0x2D;
        case '.':
          return 0x2E;
        case '/':
          return 0x2F;

        case ':':
          return 0x3A;
        case ';':
          return 0x3B;
        case '<':
          return 0x3C;
        case '=':
          return 0x3D;
        case '>':
          return 0x3E;
        case '?':
          return 0x3F;

        default:
          return 0x00;
      }
  }
  static uint8_t charmap_lcd(char c)
  {
    // FIXME there are some more characters but what to map them to ? :)
    if (c >= 'a' && c <= 'z')
      return c - 'a' + 0x61;
    else if (c >= 'A' && c <= 'Z')
      return c - 'A' + 0x41;
    else if (c >= '0' && c <= '9')
      return c - '0' + 0x30;
    else
      switch (c)
      {
        case '!':
          return 0x21;
        case '"':
          return 0x22;
        case '#':
          return 0x23;
        case '$':
          return 0x24;
        case '%':
          return 0x25;
        case '&':
          return 0x26;
        case '\'':
          return 0x27;
        case '(':
          return 0x28;
        case ')':
          return 0x29;
        case '*':
          return 0x2A;
        case '+':
          return 0x2B;
        case ',':
          return 0x2C;
        case '-':
          return 0x2D;
        case '.':
          return 0x2E;
        case '/':
          return 0x2F;

        case ':':
          return 0x3A;
        case ';':
          return 0x3B;
        case '<':
          return 0x3C;
        case '=':
          return 0x3D;
        case '>':
          return 0x3E;
        case '?':
          return 0x3F;

        case '@':
          return 0x40;
        case '[':
          return 0x5B;
        case '~': // Yen symbol... builtin mojibake?
          return 0x5C;
        case ']':
          return 0x5D;
        case '^':
          return 0x5E;
        case '_':
          return 0x5F;
        case '`':
          return 0x60;
        case '{':
          return 0x7B;
        case '|':
          return 0x7C;
        case '}':
          return 0x7D;
        case '\u000E':
          return 0x7E;
        case '\u000F':
          return 0x7F;
        default:
          return c; // gives access to the bubble first row 0x00 > 0x0F
      }
  }
};

struct rcp_configuration
{
  //! How to send MIDI messages to the device.
  //! Note: this function *will* be called from different thread,
  //! thus it has to be thread-safe, for instance
  //! by storing the message in an event queue.
  std::function<void(libremidi::message&&)> midi_out;

  std::function<void(libremidi::remote_control_protocol::device_type)> on_connected;
  std::function<void(libremidi::remote_control_protocol::mixer_command, bool)> on_command;
  std::function<void(libremidi::remote_control_protocol::mixer_control, int)> on_control;
  std::function<void(libremidi::remote_control_protocol::fader, uint16_t)> on_fader;

  libremidi::midi_error_callback on_error{};
};

struct remote_control_processor : libremidi::error_handler
{
  using rcp = libremidi::remote_control_protocol;
  rcp_configuration configuration;
  rcp impl;

  explicit remote_control_processor(rcp_configuration conf)
      : configuration{std::move(conf)}
  {
    assert(configuration.midi_out);

    if (!configuration.on_error)
      configuration.on_error = [](std::string_view s, auto&&...) {
        std::fprintf(stderr, "libremidi: rcp error: %s\n", s.data());
      };

    if (!configuration.on_connected)
      configuration.on_connected
          = [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_connected"); };

    if (!configuration.on_command)
      configuration.on_command
          = [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_command"); };

    if (!configuration.on_control)
      configuration.on_control
          = [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_control"); };

    if (!configuration.on_fader)
      configuration.on_fader
          = [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_fader"); };
  }

  void start()
  {
    current_state = waiting_for_query;
    configuration.midi_out(impl.device_query());
  }

  void on_midi(const libremidi::message& message)
  {
    switch (message.get_message_type())
    {
      case libremidi::message_type::SYSTEM_EXCLUSIVE:
        if (auto N = message.size(); N >= 7)
        {
          const uint8_t* bytes = message.bytes.data();

          // strip 0xF0 & 0xF7
          bytes += 1;
          N -= 2;

          // Mackie manufacturer ID check
          if (bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0x66)
          {
            impl.type = static_cast<rcp::device_type>(bytes[3]);

            // strip header
            bytes += 4;
            N -= 4;
            on_rcp_command(std::span(bytes, N));
          }
        }
        else
        {
          libremidi_handle_error(configuration, "Invalid sysex");
        }
        break;
      case libremidi::message_type::NOTE_ON:
        configuration.on_command(static_cast<rcp::mixer_command>(message[1]), message[2] > 0);
        break;
      case libremidi::message_type::NOTE_OFF:
        break;
      case libremidi::message_type::CONTROL_CHANGE:
        configuration.on_control(static_cast<rcp::mixer_control>(message[1]), message[2]);
        break;
      case libremidi::message_type::PITCH_BEND: {
        uint16_t value = message.bytes[2] * 128 + message.bytes[1];
        configuration.on_fader(static_cast<rcp::fader>(uint8_t(message.get_channel() - 1)), value);
        break;
      }
      default:
        break;
    }
  }

  void on_rcp_command(std::span<const uint8_t> cmd)
  {
    if (cmd.empty())
    {
      libremidi_handle_error(configuration, "on_rcp_command: empty command");
      return;
    }

    auto command = static_cast<rcp::command_from_device>(cmd[0]);
    cmd = cmd.subspan(1);
    switch (command)
    {
      case rcp::command_from_device::host_connection_query: {
        if (cmd.size() == 11)
        {
          current_state = got_query;

          std::array<uint8_t, 7> serial;
          std::array<uint8_t, 4> challenge;
          std::copy_n(cmd.data(), 7, serial.begin());
          std::copy_n(cmd.data() + 7, 4, challenge.begin());

          configuration.midi_out(impl.host_connection_reply(serial, challenge));
        }
        else
          libremidi_handle_error(configuration, "host_connection_query: invalid size");
        break;
      }
      case rcp::command_from_device::host_connection_confirmation:
        current_state = connected;
        configuration.on_connected(impl.type);
        break;
      case rcp::command_from_device::host_connection_error:
        current_state = errored;
        libremidi_handle_error(configuration, "host_connection_error");
        break;
      case rcp::command_from_device::version_reply: {
        // TODO
        break;
      }
      default:
        break;
    }
  }

  void update_timecode(int h, int m, int s, int f)
  {
    for (auto&& m : rcp::timecode(h, m, s, f))
      configuration.midi_out(std::move(m));
  }

  void update_lcd(std::string_view v)
  {
    auto res = impl.update_lcd(v);
    if (!res.empty())
      configuration.midi_out(std::move(res));
  }

  void update_lcd(std::string_view v, int pos)
  {
    auto res = impl.update_lcd(v, pos);
    if (!res.empty())
      configuration.midi_out(std::move(res));
  }

  void command(remote_control_protocol::mixer_command c, bool press)
  {
    using ce = libremidi::channel_events;
    configuration.midi_out(ce::note_on(1, to_underlying(c), press ? 127 : 0));
    configuration.midi_out(ce::note_off(1, to_underlying(c), press ? 127 : 0));
  }

  void control(remote_control_protocol::mixer_control c, int value)
  {
    using ce = libremidi::channel_events;
    configuration.midi_out(ce::control_change(1, to_underlying(c), value));
  }

  void fader(remote_control_protocol::fader c, uint16_t value)
  {
    int idx = to_underlying(c);

    using ce = libremidi::channel_events;
    configuration.midi_out(ce::pitch_bend(idx + 1, value));
  }

  // State machine
  enum
  {
    waiting_for_query,
    got_query,
    connected,
    errored
  } current_state{waiting_for_query};
};
}
