/*
 * Copyright 2023 jacqueline <me@jacqueline.id.au>
 *
 * SPDX-License-Identifier: GPL-3.0-only
 */

#include "screen_settings.hpp"
#include <stdint.h>
#include <string>

#include "audio_events.hpp"
#include "bluetooth.hpp"
#include "bluetooth_types.hpp"
#include "core/lv_event.h"
#include "core/lv_obj.h"
#include "core/lv_obj_tree.h"
#include "display.hpp"
#include "esp_log.h"

#include "core/lv_group.h"
#include "core/lv_obj_pos.h"
#include "event_queue.hpp"
#include "extra/layouts/flex/lv_flex.h"
#include "extra/widgets/list/lv_list.h"
#include "extra/widgets/menu/lv_menu.h"
#include "extra/widgets/spinbox/lv_spinbox.h"
#include "extra/widgets/spinner/lv_spinner.h"
#include "hal/lv_hal_disp.h"
#include "index.hpp"
#include "lv_api_map.h"
#include "misc/lv_anim.h"
#include "misc/lv_area.h"
#include "model_top_bar.hpp"
#include "nvs.hpp"
#include "screen.hpp"
#include "themes.hpp"
#include "ui_events.hpp"
#include "ui_fsm.hpp"
#include "widget_top_bar.hpp"
#include "widgets/lv_bar.h"
#include "widgets/lv_btn.h"
#include "widgets/lv_dropdown.h"
#include "widgets/lv_label.h"
#include "widgets/lv_slider.h"
#include "widgets/lv_switch.h"
#include "wm8523.hpp"

namespace ui {
namespace screens {

using Page = internal::ShowSettingsPage::Page;

static void open_sub_menu_cb(lv_event_t* e) {
  Page next_page = static_cast<Page>(reinterpret_cast<uintptr_t>(e->user_data));
  events::Ui().Dispatch(internal::ShowSettingsPage{
      .page = next_page,
  });
}

static void sub_menu(lv_obj_t* list,
                     lv_group_t* group,
                     const std::pmr::string& text,
                     Page page) {
  lv_obj_t* item = lv_list_add_btn(list, NULL, text.c_str());
  lv_group_add_obj(group, item);
  lv_obj_add_event_cb(item, open_sub_menu_cb, LV_EVENT_CLICKED,
                      reinterpret_cast<void*>(static_cast<uintptr_t>(page)));
}

Settings::Settings(models::TopBar& bar) : MenuScreen(bar, "Settings") {
  lv_obj_t* list = lv_list_create(content_);
  lv_obj_set_size(list, lv_pct(100), lv_pct(100));

  themes::Theme::instance()->ApplyStyle(lv_list_add_text(list, "Audio"),
                                        themes::Style::kMenuSubheadFirst);
  sub_menu(list, group_, "Bluetooth", Page::kBluetooth);
  sub_menu(list, group_, "Headphones", Page::kHeadphones);

  themes::Theme::instance()->ApplyStyle(lv_list_add_text(list, "Interface"),
                                        themes::Style::kMenuSubhead);
  sub_menu(list, group_, "Appearance", Page::kAppearance);
  sub_menu(list, group_, "Input Method", Page::kInput);

  themes::Theme::instance()->ApplyStyle(lv_list_add_text(list, "System"),
                                        themes::Style::kMenuSubhead);
  sub_menu(list, group_, "Storage", Page::kStorage);
  sub_menu(list, group_, "Firmware Update", Page::kFirmwareUpdate);
  sub_menu(list, group_, "About", Page::kAbout);
}

static auto settings_container(lv_obj_t* parent) -> lv_obj_t* {
  lv_obj_t* res = lv_obj_create(parent);
  lv_obj_set_layout(res, LV_LAYOUT_FLEX);
  lv_obj_set_size(res, lv_pct(100), LV_SIZE_CONTENT);
  lv_obj_set_flex_flow(res, LV_FLEX_FLOW_ROW);
  lv_obj_set_flex_align(res, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER,
                        LV_FLEX_ALIGN_START);
  return res;
}

static auto label_pair(lv_obj_t* parent,
                       const std::pmr::string& left,
                       const std::pmr::string& right) -> lv_obj_t* {
  lv_obj_t* container = settings_container(parent);
  lv_obj_t* left_label = lv_label_create(container);
  lv_label_set_text(left_label, left.c_str());
  lv_obj_t* right_label = lv_label_create(container);
  lv_label_set_text(right_label, right.c_str());
  return right_label;
}

static auto toggle_bt_cb(lv_event_t* ev) {
  Bluetooth* instance = reinterpret_cast<Bluetooth*>(ev->user_data);
  instance->ChangeEnabledState(lv_obj_has_state(ev->target, LV_STATE_CHECKED));
}

static auto select_device_cb(lv_event_t* ev) {
  Bluetooth* instance = reinterpret_cast<Bluetooth*>(ev->user_data);
  instance->OnDeviceSelected(lv_obj_get_index(ev->target));
}

Bluetooth::Bluetooth(models::TopBar& bar,
                     drivers::Bluetooth& bt,
                     drivers::NvsStorage& nvs)
    : MenuScreen(bar, "Bluetooth"), bt_(bt), nvs_(nvs) {
  lv_obj_t* toggle_container = settings_container(content_);
  lv_obj_t* toggle_label = lv_label_create(toggle_container);
  lv_label_set_text(toggle_label, "Enable");
  lv_obj_set_flex_grow(toggle_label, 1);
  lv_obj_t* toggle = lv_switch_create(toggle_container);
  lv_group_add_obj(group_, toggle);

  if (bt.IsEnabled()) {
    lv_obj_add_state(toggle, LV_STATE_CHECKED);
  }

  lv_obj_add_event_cb(toggle, toggle_bt_cb, LV_EVENT_VALUE_CHANGED, this);

  lv_obj_t* devices_label = lv_label_create(content_);
  lv_label_set_text(devices_label, "Devices");

  devices_list_ = lv_list_create(content_);
  RefreshDevicesList();
}

auto Bluetooth::ChangeEnabledState(bool enabled) -> void {
  if (enabled) {
    events::System().RunOnTask([&]() { bt_.Enable(); });
    nvs_.OutputMode(drivers::NvsStorage::Output::kBluetooth);
  } else {
    events::System().RunOnTask([&]() { bt_.Disable(); });
    nvs_.OutputMode(drivers::NvsStorage::Output::kHeadphones);
  }
  events::Audio().Dispatch(audio::OutputModeChanged{});
  RefreshDevicesList();
}

auto Bluetooth::RefreshDevicesList() -> void {
  if (!bt_.IsEnabled()) {
    // Bluetooth is disabled, so we just clear the list.
    RemoveAllDevices();
    return;
  }

  auto devices = bt_.KnownDevices();
  std::optional<drivers::bluetooth::mac_addr_t> preferred_device =
      nvs_.PreferredBluetoothDevice();

  // If the user's current selection is within the devices list, then we need
  // to be careful not to rearrange the list items underneath them.
  lv_obj_t* current_selection = lv_group_get_focused(group_);
  bool is_in_list = current_selection != NULL &&
                    lv_obj_get_parent(current_selection) == devices_list_;

  if (!is_in_list) {
    // The user isn't in the list! We can blow everything away and recreate it
    // without issues.
    RemoveAllDevices();

    // First look to see if the user's preferred device is in the list. If it
    // is, we hoist it up to the top of the list.
    if (preferred_device) {
      for (size_t i = 0; i < devices.size(); i++) {
        if (devices[i].address == *preferred_device) {
          AddPreferredDevice(devices[i]);
          devices.erase(devices.begin() + i);
          break;
        }
      }
    }

    // The rest of the list is already sorted by signal strength.
    for (const auto& device : devices) {
      AddDevice(device);
    }
  } else {
    // The user's selection is within the device list. We need to work out
    // which devices are new, then add them to the end.
    for (const auto& mac : macs_in_list_) {
      auto pos = std::find_if(
          devices.begin(), devices.end(),
          [&mac](const auto& device) { return device.address == mac; });

      if (pos != devices.end()) {
        devices.erase(pos);
      }
    }

    // The remaining list is now just the new devices.
    for (const auto& device : devices) {
      if (preferred_device && device.address == *preferred_device) {
        AddPreferredDevice(device);
      } else {
        AddDevice(device);
      }
    }
  }
}

auto Bluetooth::RemoveAllDevices() -> void {
  while (lv_obj_get_child_cnt(devices_list_) > 0) {
    lv_obj_del(lv_obj_get_child(devices_list_, 0));
  }
  macs_in_list_.clear();
  preferred_device_ = nullptr;
}

auto Bluetooth::AddPreferredDevice(const drivers::bluetooth::Device& dev)
    -> void {
  preferred_device_ = lv_list_add_btn(devices_list_, NULL, dev.name.c_str());
  macs_in_list_.push_back(dev.address);
}

auto Bluetooth::AddDevice(const drivers::bluetooth::Device& dev) -> void {
  lv_obj_t* item = lv_list_add_btn(devices_list_, NULL, dev.name.c_str());
  lv_group_add_obj(group_, item);
  lv_obj_add_event_cb(item, select_device_cb, LV_EVENT_CLICKED, this);
  macs_in_list_.push_back(dev.address);
}

auto Bluetooth::OnDeviceSelected(size_t index) -> void {
  // Tell the bluetooth driver that our preference changed.
  auto it = macs_in_list_.begin();
  std::advance(it, index);
  events::System().RunOnTask([=]() { bt_.SetPreferredDevice(*it); });

  // Update which devices are selectable.
  if (preferred_device_) {
    lv_group_add_obj(group_, preferred_device_);
    // Bubble the newly added object up to its visible position in the list.
    size_t pos = lv_obj_get_index(preferred_device_);
    while (pos > 0) {
      lv_group_swap_obj(preferred_device_,
                        lv_obj_get_child(devices_list_, pos - 1));
      pos--;
    }
  }

  preferred_device_ = lv_obj_get_child(devices_list_, index);
  lv_group_remove_obj(preferred_device_);
}

static void change_vol_limit_cb(lv_event_t* ev) {
  int selected_index = lv_dropdown_get_selected(ev->target);
  Headphones* instance = reinterpret_cast<Headphones*>(ev->user_data);
  instance->ChangeMaxVolume(selected_index);
}

static void increase_vol_limit_cb(lv_event_t* ev) {
  Headphones* instance = reinterpret_cast<Headphones*>(ev->user_data);
  instance->ChangeCustomVolume(2);
}

static void decrease_vol_limit_cb(lv_event_t* ev) {
  Headphones* instance = reinterpret_cast<Headphones*>(ev->user_data);
  instance->ChangeCustomVolume(-2);
}

Headphones::Headphones(models::TopBar& bar, drivers::NvsStorage& nvs)
    : MenuScreen(bar, "Headphones"), nvs_(nvs), custom_limit_(0) {
  uint16_t reference = drivers::wm8523::kLineLevelReferenceVolume;
  index_to_level_.push_back(reference - (10 * 4));
  index_to_level_.push_back(reference + (6 * 4));
  index_to_level_.push_back(reference + (9.5 * 4));

  lv_obj_t* vol_label = lv_label_create(content_);
  lv_label_set_text(vol_label, "Volume Limit");
  lv_obj_t* vol_dropdown = lv_dropdown_create(content_);
  lv_obj_set_width(vol_dropdown, lv_pct(100));
  lv_dropdown_set_options(
      vol_dropdown,
      "Line Level (-10 dB)\nCD Level (+6 dB)\nMaximum (+10dB)\nCustom");
  lv_group_add_obj(group_, vol_dropdown);
  themes::Theme::instance()->ApplyStyle(lv_dropdown_get_list(vol_dropdown),
                                        themes::Style::kPopup);

  uint16_t level = nvs.AmpMaxVolume();
  for (int i = 0; i < index_to_level_.size() + 1; i++) {
    if (i == index_to_level_.size() || index_to_level_[i] == level) {
      lv_dropdown_set_selected(vol_dropdown, i);
      break;
    }
  }

  lv_obj_add_event_cb(vol_dropdown, change_vol_limit_cb, LV_EVENT_VALUE_CHANGED,
                      this);

  custom_vol_container_ = settings_container(content_);

  lv_obj_t* decrease_btn = lv_btn_create(custom_vol_container_);
  lv_obj_t* btn_label = lv_label_create(decrease_btn);
  lv_label_set_text(btn_label, "-");
  lv_obj_add_event_cb(decrease_btn, decrease_vol_limit_cb, LV_EVENT_CLICKED,
                      this);

  custom_vol_label_ = lv_label_create(custom_vol_container_);
  UpdateCustomVol(level);

  lv_obj_t* increase_btn = lv_btn_create(custom_vol_container_);
  btn_label = lv_label_create(increase_btn);
  lv_label_set_text(btn_label, "+");
  lv_obj_add_event_cb(increase_btn, increase_vol_limit_cb, LV_EVENT_CLICKED,
                      this);

  if (lv_dropdown_get_selected(vol_dropdown) != index_to_level_.size()) {
    lv_obj_add_flag(custom_vol_container_, LV_OBJ_FLAG_HIDDEN);
  }

  lv_obj_t* spacer = lv_obj_create(content_);
  lv_obj_set_size(spacer, 1, 4);

  lv_obj_t* balance_label = lv_label_create(content_);
  lv_label_set_text(balance_label, "Left/Right Balance");

  spacer = lv_obj_create(content_);
  lv_obj_set_size(spacer, 1, 4);

  lv_obj_t* balance = lv_slider_create(content_);
  lv_obj_set_size(balance, lv_pct(100), 5);
  lv_slider_set_range(balance, -10, 10);
  lv_slider_set_value(balance, 0, LV_ANIM_OFF);
  lv_slider_set_mode(balance, LV_SLIDER_MODE_SYMMETRICAL);
  lv_group_add_obj(group_, balance);
  lv_obj_t* current_balance_label = lv_label_create(content_);
  lv_label_set_text(current_balance_label, "0dB");
  lv_obj_set_size(current_balance_label, lv_pct(100), LV_SIZE_CONTENT);

  lv_obj_move_foreground(lv_dropdown_get_list(vol_dropdown));
}

auto Headphones::ChangeMaxVolume(uint8_t index) -> void {
  if (index >= index_to_level_.size()) {
    lv_obj_clear_flag(custom_vol_container_, LV_OBJ_FLAG_HIDDEN);
    return;
  }
  auto vol = index_to_level_[index];
  lv_obj_add_flag(custom_vol_container_, LV_OBJ_FLAG_HIDDEN);
  UpdateCustomVol(vol);
  events::Audio().Dispatch(audio::ChangeMaxVolume{.new_max = vol});
}

auto Headphones::ChangeCustomVolume(int8_t diff) -> void {
  UpdateCustomVol(custom_limit_ + diff);
}

auto Headphones::UpdateCustomVol(uint16_t level) -> void {
  custom_limit_ = level;
  int16_t db = (static_cast<int32_t>(level) -
                drivers::wm8523::kLineLevelReferenceVolume) /
               4;
  int16_t db_parts = (static_cast<int32_t>(level) -
                      drivers::wm8523::kLineLevelReferenceVolume) %
                     4;

  std::ostringstream builder;
  if (db >= 0) {
    builder << "+";
  }
  builder << db << ".";
  builder << (db_parts * 100 / 4);
  builder << " dBV";

  lv_label_set_text(custom_vol_label_, builder.str().c_str());
}

static void change_brightness_cb(lv_event_t* ev) {
  Appearance* instance = reinterpret_cast<Appearance*>(ev->user_data);
  instance->ChangeBrightness(lv_slider_get_value(ev->target));
}

static void release_brightness_cb(lv_event_t* ev) {
  Appearance* instance = reinterpret_cast<Appearance*>(ev->user_data);
  instance->CommitBrightness();
}

static auto brightness_str(uint_fast8_t percent) -> std::string {
  return std::to_string(percent) + "%";
}

Appearance::Appearance(models::TopBar& bar,
                       drivers::NvsStorage& nvs,
                       drivers::Display& display)
    : MenuScreen(bar, "Appearance"), nvs_(nvs), display_(display) {
  lv_obj_t* toggle_container = settings_container(content_);
  lv_obj_t* toggle_label = lv_label_create(toggle_container);
  lv_obj_set_flex_grow(toggle_label, 1);
  lv_label_set_text(toggle_label, "Dark Mode");
  lv_obj_t* toggle = lv_switch_create(toggle_container);
  lv_group_add_obj(group_, toggle);

  uint_fast8_t initial_brightness = nvs_.ScreenBrightness();

  lv_obj_t* brightness_label = lv_label_create(content_);
  lv_label_set_text(brightness_label, "Brightness");
  lv_obj_t* brightness = lv_slider_create(content_);
  lv_obj_set_size(brightness, lv_pct(100), 5);
  lv_slider_set_range(brightness, 10, 100);
  lv_slider_set_value(brightness, initial_brightness, LV_ANIM_OFF);
  lv_group_add_obj(group_, brightness);
  current_brightness_label_ = lv_label_create(content_);
  lv_label_set_text(current_brightness_label_,
                    brightness_str(initial_brightness).c_str());
  lv_obj_set_size(current_brightness_label_, lv_pct(100), LV_SIZE_CONTENT);

  lv_obj_add_event_cb(brightness, change_brightness_cb, LV_EVENT_VALUE_CHANGED,
                      this);
  lv_obj_add_event_cb(brightness, release_brightness_cb, LV_EVENT_RELEASED,
                      this);
}

auto Appearance::ChangeBrightness(uint_fast8_t new_level) -> void {
  current_brightness_ = new_level;
  display_.SetBrightness(new_level);
  lv_label_set_text(current_brightness_label_,
                    brightness_str(new_level).c_str());
}

auto Appearance::CommitBrightness() -> void {
  nvs_.ScreenBrightness(current_brightness_);
}

InputMethod::InputMethod(models::TopBar& bar, drivers::NvsStorage& nvs)
    : MenuScreen(bar, "Input Method"), nvs_(nvs) {
  lv_obj_t* primary_label = lv_label_create(content_);
  lv_label_set_text(primary_label, "Control scheme");
  lv_obj_t* primary_dropdown = lv_dropdown_create(content_);
  lv_dropdown_set_options(
      primary_dropdown,
      "Side buttons only\nButtons and touch\nD-Pad\nClickwheel");
  lv_group_add_obj(group_, primary_dropdown);

  lv_dropdown_set_selected(primary_dropdown,
                           static_cast<uint16_t>(nvs.PrimaryInput()));
  themes::Theme::instance()->ApplyStyle(lv_dropdown_get_list(primary_dropdown),
                                        themes::Style::kPopup);

  lv_bind(primary_dropdown, LV_EVENT_VALUE_CHANGED, [this](lv_obj_t* obj) {
    drivers::NvsStorage::InputModes mode;
    switch (lv_dropdown_get_selected(obj)) {
      case 0:
        mode = drivers::NvsStorage::InputModes::kButtonsOnly;
        break;
      case 1:
        mode = drivers::NvsStorage::InputModes::kButtonsWithWheel;
        break;
      case 2:
        mode = drivers::NvsStorage::InputModes::kDirectionalWheel;
        break;
      case 3:
        mode = drivers::NvsStorage::InputModes::kRotatingWheel;
        break;
      default:
        return;
    }
    nvs_.PrimaryInput(mode);
    events::Ui().Dispatch(internal::ControlSchemeChanged{});
  });
}

Storage::Storage(models::TopBar& bar) : MenuScreen(bar, "Storage") {
  label_pair(content_, "Storage Capacity:", "32 GiB");
  label_pair(content_, "Currently Used:", "6 GiB");
  label_pair(content_, "DB Size:", "1.2 MiB");

  lv_obj_t* usage_bar = lv_bar_create(content_);
  lv_bar_set_range(usage_bar, 0, 32);
  lv_bar_set_value(usage_bar, 6, LV_ANIM_OFF);

  lv_obj_t* container = lv_obj_create(content_);
  lv_obj_set_size(container, lv_pct(100), 30);
  lv_obj_set_layout(container, LV_LAYOUT_FLEX);
  lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW);
  lv_obj_set_flex_align(container, LV_FLEX_ALIGN_SPACE_EVENLY,
                        LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);

  lv_obj_t* reset_btn = lv_btn_create(container);
  lv_obj_t* reset_label = lv_label_create(reset_btn);
  lv_label_set_text(reset_label, "Update Database");
  lv_group_add_obj(group_, reset_btn);
  themes::Theme::instance()->ApplyStyle(reset_btn,
                                        themes::Style::kButtonPrimary);

  lv_bind(reset_btn, LV_EVENT_CLICKED, [&](lv_obj_t*) {
    events::Ui().Dispatch(internal::ReindexDatabase{});
  });
}

FirmwareUpdate::FirmwareUpdate(models::TopBar& bar)
    : MenuScreen(bar, "Firmware Update") {
  lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
                        LV_FLEX_ALIGN_CENTER);

  label_pair(content_, "SAMD21 FW:", "vIDKLOL");

  lv_obj_t* spacer = lv_obj_create(content_);
  lv_obj_set_size(spacer, 1, 4);

  lv_obj_t* flash_esp_btn = lv_btn_create(content_);
  lv_obj_t* flash_esp_label = lv_label_create(flash_esp_btn);
  lv_label_set_text(flash_esp_label, "Update");
  lv_group_add_obj(group_, flash_esp_btn);
  themes::Theme::instance()->ApplyStyle(flash_esp_btn,
                                        themes::Style::kButtonPrimary);

  spacer = lv_obj_create(content_);
  lv_obj_set_size(spacer, 1, 8);

  label_pair(content_, "ESP32 FW:", "vIDKLOL");

  spacer = lv_obj_create(content_);
  lv_obj_set_size(spacer, 1, 4);

  lv_obj_t* flash_samd_btn = lv_btn_create(content_);
  lv_obj_t* flash_samd_label = lv_label_create(flash_samd_btn);
  lv_label_set_text(flash_samd_label, "Update");
  lv_group_add_obj(group_, flash_samd_btn);
  themes::Theme::instance()->ApplyStyle(flash_samd_btn,
                                        themes::Style::kButtonPrimary);
}

About::About(models::TopBar& bar) : MenuScreen(bar, "About") {
  lv_obj_t* label = lv_label_create(content_);
  lv_label_set_text(label, "Some licenses or whatever");
}

}  // namespace screens
}  // namespace ui
