#include "../desktop-ui.hpp"
namespace Instances { Instance<Presentation> presentation; }
Presentation& presentation = Instances::presentation();

#define ELLIPSIS "\u2026"

struct WindowSizePreset {
  string name;
  u32 width;
  u32 height;
};

static auto maximumViewportSize() -> Size {
  u32 bestWidth = 1;
  u32 bestHeight = 1;
  u64 bestArea = 1;
  for(u32 monitor : range(Monitor::count())) {
    auto workspace = Monitor::workspace(monitor);
    u32 workspaceWidth = (u32)workspace.width();
    u32 workspaceHeight = (u32)workspace.height();
    u64 area = (u64)workspaceWidth * (u64)workspaceHeight;
    if(area >= bestArea) {
      bestArea = area;
      bestWidth = workspaceWidth;
      bestHeight = workspaceHeight;
    }
  }
  return {bestWidth, bestHeight};
}

static auto minimumConfiguredWindowSize() -> Size {
  return {160, 144};
}

static auto windowSizePresets() -> std::vector<WindowSizePreset> {
  return {
    {"NTSC 4:3",        640,  480},
    {"PAL 4:3",         768,  576},
    {"SVGA 4:3",        800,  600},
    {"qHD 4:3",         960,  720},
    {"XGA 4:3",        1024,  768},
    {"XGA+ 4:3",       1152,  864},
    {"HD 16:9",        1280,  720},
    {"SXGA- 4:3",      1280,  960},
    {"SXGA 5:4",       1280, 1024},
    {"WXGA 16:9",      1366,  768},
    {"HDV 4:3",        1440, 1080},
    {"HD+ 16:9",       1600,  900},
    {"UXGA 4:3",       1600, 1200},
    {"FHD 16:9",       1920, 1080},
    {"WUXGA 16:10",    1920, 1200},
    {"QXGA 4:3",       2048, 1536},
    {"QHD 16:9",       2560, 1440},
    {"WQXGA 16:10",    2560, 1600},
    {"UHD 4K 16:9",    3840, 2160},
  };
}

static auto windowSizeText(u32 width, u32 height) -> string {
  return {width, "x", height};
}

static auto setConfiguredWindowSize(u32 width, u32 height) -> void {
  settings.video.windowWidth = width;
  settings.video.windowHeight = height;
}

static auto snapWindowSizeValue(u32 value, u32 minimum, u32 maximum) -> u32 {
  value = max(minimum, min(maximum, value));
  return minimum + (value - minimum) / 4 * 4;
}

static auto customWindowSizeDialog(Window& relativeTo, Size minimumSize, Size maximumSize) -> Size {
  u32 minimumWidth = (u32)minimumSize.width();
  u32 minimumHeight = (u32)minimumSize.height();
  u32 maximumWidth = max(minimumWidth, (u32)maximumSize.width());
  u32 maximumHeight = max(minimumHeight, (u32)maximumSize.height());
  u32 defaultWidth = snapWindowSizeValue(settings.video.windowWidth, minimumWidth, maximumWidth);
  u32 defaultHeight = snapWindowSizeValue(settings.video.windowHeight, minimumHeight, maximumHeight);
  Size result;

  Window window;
  VerticalLayout layout{&window};
  Label textLabel{&layout, Size{~0, 0}};
  TableLayout sizeLayout{&layout, Size{~0, 0}};
    Label widthLabel{&sizeLayout, Size{0, 0}};
    LineEdit widthEdit{&sizeLayout, Size{128_sx, 0}};
    Label heightLabel{&sizeLayout, Size{0, 0}};
    LineEdit heightEdit{&sizeLayout, Size{128_sx, 0}};
  HorizontalLayout buttonLayout{&layout, Size{~0, 0}};
    Canvas spacer{&buttonLayout, Size{~0, 0}};
    Button acceptButton{&buttonLayout, Size{80_sx, 0}};
    Button cancelButton{&buttonLayout, Size{80_sx, 0}};

  layout.setPadding(5_sx, 5_sy);
  sizeLayout.setSize({2, 2}).setPadding(12_sx, 0);
  sizeLayout.column(0).setAlignment(1.0);
  widthLabel.setText("Width:");
  heightLabel.setText("Height:");
  widthEdit.setEditable(true).setText(integer(defaultWidth));
  heightEdit.setEditable(true).setText(integer(defaultHeight));
  widthEdit.onChange([&] {
    auto text = widthEdit.text();
    string filtered;
    for(auto character : text) {
      if(character >= '0' && character <= '9') filtered.append(character);
    }
    if(filtered != text) widthEdit.setText(filtered);
  });
  heightEdit.onChange([&] {
    auto text = heightEdit.text();
    string filtered;
    for(auto character : text) {
      if(character >= '0' && character <= '9') filtered.append(character);
    }
    if(filtered != text) heightEdit.setText(filtered);
  });

  textLabel.setText({
    "Choose a viewport size from ",
    windowSizeText(minimumWidth, minimumHeight),
    " to ",
    windowSizeText(maximumWidth, maximumHeight),
    " (multiples of 8)."
  });

  acceptButton.setText("Set").onActivate([&] {
    auto requestedWidth = widthEdit.text().integer();
    auto requestedHeight = heightEdit.text().integer();
    u32 width = requestedWidth > 0 ? (u32)requestedWidth : minimumWidth;
    u32 height = requestedHeight > 0 ? (u32)requestedHeight : minimumHeight;
    width = snapWindowSizeValue(width, minimumWidth, maximumWidth);
    height = snapWindowSizeValue(height, minimumHeight, maximumHeight);
    result = {width, height};
    window.doClose();
  });
  cancelButton.setText("Cancel").onActivate([&] {
    window.doClose();
  });
  window.onClose([&] {
    window.setModal(false);
    window.setVisible(false);
  });

  window.setDismissable();
  window.setTitle("Custom Window Size");
  window.setSize({480_sx, layout.minimumSize().height()});
  window.setAlignment(relativeTo, Alignment::Center);
  window.setVisible();
  window.setModal();

  return result;
}


Presentation::Presentation() {
  if(program.kiosk) {
    iconLayout.setCollapsible();
    viewport.setDroppable().onDrop([&](std::vector<string> filenames) {
      Program::Guard guard;
      if(filenames.size() != 1) return;
      if(auto emulator = program.identify(filenames.front())) {
        program.load(emulator, filenames.front());
      }
    });

    Application::onOpenFile([&](auto filename) {
      Program::Guard guard;
      if(auto emulator = program.identify(filename)) {
        program.load(emulator, filename);
      }
    });

    onClose([&] {
      program.quit();
    });

    menuBar.setVisible(false);
    layout.remove(statusLayout);
    resizeWindow();
    setTitle({ares::Name, " ", ares::Version});
    setAssociatedFile();
    setBackgroundColor({0, 0, 0});
    setAlignment(Alignment::Center);
    setVisible();
    return;
  }

  loadMenu.setText("Load");

  systemMenu.setVisible(false);

  settingsMenu.setText("Settings");
  videoSizeMenu.setText("Window Size").setIcon(Icon::Emblem::Image);
  refreshWindowSizeMenu();
  videoOutputMenu.setText("Output").setIcon(Icon::Emblem::Image);

  videoOutputScale.setText("Scale: Best Fit").onActivate([&] {
    settings.video.output = "Scale";
  });
  videoOutputMenu.append(videoOutputScale);
  videoOutputGroup.append(videoOutputScale);

  videoOutputIntegerScale.setText("Scale: Integer (auto)").onActivate([&] {
    settings.video.output = "Integer";
  });
  videoOutputMenu.append(videoOutputIntegerScale);
  videoOutputGroup.append(videoOutputIntegerScale);

  refreshFixedScaleMenu();

  videoOutputStretch.setText("Scale: Stretch to Fill").onActivate([&] {
    settings.video.output = "Stretch";
  });
  videoOutputMenu.append(videoOutputStretch);
  videoOutputGroup.append(videoOutputStretch);

  videoOutputMenu.append(videoOutputSeparator);

  if(settings.video.output == "Integer") videoOutputIntegerScale.setChecked();
  if(settings.video.output == "Scale") videoOutputScale.setChecked();
  if(settings.video.output == "Stretch") videoOutputStretch.setChecked();

  videoAspectCorrectionNone.setText("Aspect: No correction").onActivate([&] {
    settings.video.aspectCorrection = "None";
    if(settings.video.adaptiveSizing) resizeWindow();
  });
  videoOutputMenu.append(videoAspectCorrectionNone);
  videoAspectCorrectionGroup.append(videoAspectCorrectionNone);

  videoAspectCorrectionStandard.setText("Aspect: Standard").onActivate([&] {
    settings.video.aspectCorrection = "Standard";
    if(settings.video.adaptiveSizing) resizeWindow();
  });
  videoOutputMenu.append(videoAspectCorrectionStandard);
  videoAspectCorrectionGroup.append(videoAspectCorrectionStandard);

  videoAspectCorrectionAnamorphic.setText("Aspect: Anamorphic (16:9)").onActivate([&] {
    settings.video.aspectCorrection = "Anamorphic";
    if(settings.video.adaptiveSizing) resizeWindow();
  });
  videoOutputMenu.append(videoAspectCorrectionAnamorphic);
  videoAspectCorrectionGroup.append(videoAspectCorrectionAnamorphic);

  videoOutputMenu.append(videoOutputSeparator2);

  if(settings.video.aspectCorrection == "None") videoAspectCorrectionNone.setChecked();
  if(settings.video.aspectCorrection == "Standard") videoAspectCorrectionStandard.setChecked();
  if(settings.video.aspectCorrection == "Anamorphic") videoAspectCorrectionAnamorphic.setChecked();
  
  videoAdaptiveSizing.setText("Window: Auto resize to content aspect").setChecked(settings.video.adaptiveSizing).onToggle([&] {
    settings.video.adaptiveSizing = videoAdaptiveSizing.checked();
    if(settings.video.adaptiveSizing) resizeWindow();
  });
  videoOutputMenu.append(videoAdaptiveSizing);
  videoAutoCentering.setText("Window: Auto center").setChecked(settings.video.autoCentering).onToggle([&] {
    settings.video.autoCentering = videoAutoCentering.checked();
    if(settings.video.autoCentering) resizeWindow();
  });
  videoOutputMenu.append(videoAutoCentering);
  videoShaderMenu.setText("Shader").setIcon(Icon::Emblem::Image);
  loadShaders();
  bootOptionsMenu.setText("Boot Options").setIcon(Icon::Place::Settings);
  fastBoot.setText("Fast Boot").setChecked(settings.boot.fast).onToggle([&] {
    settings.boot.fast = fastBoot.checked();
  });
  bootOptionsRegionMenu.setText("Region Preference").setIcon(Icon::Application::Browser);
  launchDebugger.setText("Launch Tracer").setChecked(settings.boot.debugger).onToggle([&] {
    settings.boot.debugger = launchDebugger.checked();
  });
  awaitGDBClient.setText("Await GDB Client").setChecked(settings.boot.awaitGDBClient).onToggle([&] {
    settings.boot.awaitGDBClient = awaitGDBClient.checked();
  });

  regionUJE.setText("NTSC-U -> NTSC-J -> PAL").onActivate([&] { settings.boot.prefer = "NTSC-U,NTSC-J,PAL"; });
  regionUEJ.setText("NTSC-U -> PAL -> NTSC-J").onActivate([&] { settings.boot.prefer = "NTSC-U,PAL,NTSC-J"; });
  regionJUE.setText("NTSC-J -> NTSC-U -> PAL").onActivate([&] { settings.boot.prefer = "NTSC-J,NTSC-U,PAL"; });
  regionJEU.setText("NTSC-J -> PAL -> NTSC-U").onActivate([&] { settings.boot.prefer = "NTSC-J,PAL,NTSC-U"; });
  regionEUJ.setText("PAL -> NTSC-U -> NTSC-J").onActivate([&] { settings.boot.prefer = "PAL,NTSC-U,NTSC-J"; });
  regionEJU.setText("PAL -> NTSC-J -> NTSC-U").onActivate([&] { settings.boot.prefer = "PAL,NTSC-J,NTSC-U"; });

  //Backwards compatibility: convert old region preferences to new format
  string prefer = settings.boot.prefer;
  if(prefer == "NTSC-U") prefer = "NTSC-U,NTSC-J,PAL";
  if(prefer == "NTSC-J") prefer = "NTSC-J,NTSC-U,PAL";
  if(prefer == "PAL")    prefer = "PAL,NTSC-U,NTSC-J";

  if(settings.boot.prefer == "NTSC-U,NTSC-J,PAL") regionUJE.setChecked();
  if(settings.boot.prefer == "NTSC-U,PAL,NTSC-J") regionUEJ.setChecked();
  if(settings.boot.prefer == "NTSC-J,NTSC-U,PAL") regionJUE.setChecked();
  if(settings.boot.prefer == "NTSC-J,PAL,NTSC-U") regionJEU.setChecked();
  if(settings.boot.prefer == "PAL,NTSC-U,NTSC-J") regionEUJ.setChecked();
  if(settings.boot.prefer == "PAL,NTSC-J,NTSC-U") regionEJU.setChecked();

  muteAudioSetting.setText("Mute Audio").setChecked(settings.audio.mute).onToggle([&] {
    settings.audio.mute = muteAudioSetting.checked();
  });
  showStatusBarSetting.setText("Show Status Bar").setChecked(settings.general.showStatusBar).onToggle([&] {
    settings.general.showStatusBar = showStatusBarSetting.checked();
    if(!showStatusBarSetting.checked()) {
      layout.remove(statusLayout);
    } else {
      layout.append(statusLayout, Size{~0, StatusHeight});
    }
    refreshWindowSizeMenu();
    if(visible()) resizeWindow();
  }).doToggle();
  videoSettingsAction.setText("Video" ELLIPSIS).setIcon(Icon::Device::Display).onActivate([&] {
    settingsWindow.show("Video");
  });
  audioSettingsAction.setText("Audio" ELLIPSIS).setIcon(Icon::Device::Speaker).onActivate([&] {
    settingsWindow.show("Audio");
  });
  inputSettingsAction.setText("Input" ELLIPSIS).setIcon(Icon::Device::Joypad).onActivate([&] {
    settingsWindow.show("Input");
  });
  hotkeySettingsAction.setText("Hotkeys" ELLIPSIS).setIcon(Icon::Device::Keyboard).onActivate([&] {
    settingsWindow.show("Hotkeys");
  });
  emulatorSettingsAction.setText("Emulators" ELLIPSIS).setIcon(Icon::Place::Server).onActivate([&] {
    settingsWindow.show("Emulators");
  });
  optionSettingsAction.setText("Options" ELLIPSIS).setIcon(Icon::Action::Settings).onActivate([&] {
    settingsWindow.show("Options");
  });
  firmwareSettingsAction.setText("Firmware" ELLIPSIS).setIcon(Icon::Emblem::Binary).onActivate([&] {
    settingsWindow.show("Firmware");
  });
  pathSettingsAction.setText("Paths" ELLIPSIS).setIcon(Icon::Emblem::Folder).onActivate([&] {
    settingsWindow.show("Paths");
  });
  driverSettingsAction.setText("Drivers" ELLIPSIS).setIcon(Icon::Place::Settings).onActivate([&] {
    settingsWindow.show("Drivers");
  });
  debugSettingsAction.setText("Debug" ELLIPSIS).setIcon(Icon::Device::Network).onActivate([&] {
    settingsWindow.show("Debug");
  });
  importExportAction.setText("Settings File" ELLIPSIS).setIcon(Icon::Action::Save).onActivate([&] {
    settingsWindow.show("Settings File");
  });

  toolsMenu.setVisible(false).setText("Tools");
  saveStateMenu.setText("Save State").setIcon(Icon::Media::Record);
  for(u32 slot : range(9)) {
    MenuItem item{&saveStateMenu};
    item.setText({"Slot ", 1 + slot}).onActivate([=, this] {
      Program::Guard guard;
      if(program.stateSave(1 + slot)) {
        undoSaveStateMenu.setEnabled(true);
      }
    });
  }
  loadStateMenu.setText("Load State").setIcon(Icon::Media::Rewind);
  for(u32 slot : range(9)) {
    MenuItem item{&loadStateMenu};
    item.setText({"Slot ", 1 + slot}).onActivate([=, this] {
      Program::Guard guard;
      if(program.stateLoad(1 + slot)) {
        undoLoadStateMenu.setEnabled(true);
      }
    });
  }
  undoSaveStateMenu.setText("Undo Last Save State").setIcon(Icon::Edit::Undo).setEnabled(false);
  undoSaveStateMenu.onActivate([&] {
    Program::Guard guard;
    program.undoStateSave();
    undoSaveStateMenu.setEnabled(false);
  });
  undoLoadStateMenu.setText("Undo Last Load State").setIcon(Icon::Edit::Undo).setEnabled(false);
  undoLoadStateMenu.onActivate([&] {
    Program::Guard guard;
    program.undoStateLoad();
    undoLoadStateMenu.setEnabled(false);
  });
  captureScreenshot.setText("Capture Screenshot").setIcon(Icon::Emblem::Image).onActivate([&] {
    Program::Guard guard;
    program.requestScreenshot = true;
  });
  pauseEmulation.setText("Pause Emulation").onToggle([&] {
    Program::Guard guard;
    program.pause(!program.paused);
  });
  reloadGame.setText("Reload Game").setIcon(Icon::Action::Refresh).onActivate([&] {
    Program::Guard guard;
    program.load(emulator, emulator->game->location);
  });
  frameAdvance.setText("Frame Advance").setIcon(Icon::Media::Play).onActivate([&] {
    Program::Guard guard;
    if (!program.paused) program.pause(true);
    program.requestFrameAdvance = true;
  });
  manifestViewerAction.setText("Manifest").setIcon(Icon::Emblem::Binary).onActivate([&] {
    toolsWindow.show("Manifest");
  });
  cheatEditorAction.setText("Cheats").setIcon(Icon::Emblem::File).onActivate([&] {
    toolsWindow.show("Cheats");
  });
  memoryEditorAction.setText("Memory").setIcon(Icon::Device::Storage).onActivate([&] {
    toolsWindow.show("Memory");
  });
  graphicsViewerAction.setText("Graphics").setIcon(Icon::Emblem::Image).onActivate([&] {
    toolsWindow.show("Graphics");
  });
  streamManagerAction.setText("Streams").setIcon(Icon::Emblem::Audio).onActivate([&] {
    toolsWindow.show("Streams");
  });
  propertiesViewerAction.setText("Properties").setIcon(Icon::Emblem::Text).onActivate([&] {
    toolsWindow.show("Properties");
  });
  traceLoggerAction.setText("Tracer").setIcon(Icon::Emblem::Script).onActivate([&] {
    toolsWindow.show("Tracer");
  });
  tapeViewerAction.setText("Tape").setIcon(Icon::Device::Tape).onActivate([&] {
    toolsWindow.show("Tape");
  });

  helpMenu.setText("Help");
  aboutAction.setText("About" ELLIPSIS).setIcon(Icon::Prompt::Question).onActivate([&] {
    multiFactorImage logo(Resource::Ares::Logo1x, Resource::Ares::Logo2x);
    Program::Guard guard;
    AboutDialog()
    .setName(ares::Name)
    .setLogo(logo)
    .setDescription({ares::Name, " — a simplified multi-system emulator"})
    .setVersion(ares::Version)
    .setCopyright(ares::Copyright)
    .setLicense(ares::License, ares::LicenseURI)
    .setWebsite(ares::Website, ares::WebsiteURI)
    .setAlignment(presentation)
    .show();
  });

  viewport.setDroppable().onDrop([&](std::vector<string> filenames) {
    Program::Guard guard;
    if(filenames.size() != 1) return;
    if(auto emulator = program.identify(filenames.front())) {
      program.load(emulator, filenames.front());
    }
  });
    
  Application::onOpenFile([&](auto filename) {
    Program::Guard guard;
    if(auto emulator = program.identify(filename)) {
      program.load(emulator, filename);
    }
  });

  iconLayout.setCollapsible();

  iconSpacer.setCollapsible().setColor({0, 0, 0}).setDroppable().onDrop([&](auto filenames) {
    viewport.doDrop(filenames);
  });

  multiFactorImage icon(Resource::Ares::Icon1x, Resource::Ares::Icon2x);
  icon.alphaBlend(0x000000);
  iconCanvas.setCollapsible().setIcon(icon).setDroppable().onDrop([&](auto filenames) {
    viewport.doDrop(filenames);
  });

  iconPadding.setCollapsible().setColor({0, 0, 0}).setDroppable().onDrop([&](auto filenames) {
    viewport.doDrop(filenames);
  });

  iconBottom.setCollapsible().setColor({0, 0, 0}).setDroppable().onDrop([&](auto filenames) {
    viewport.doDrop(filenames);
  });

  spacerLeft .setBackgroundColor({32, 32, 32});
  statusLeft .setBackgroundColor({32, 32, 32}).setForegroundColor({255, 255, 255});
  statusRight.setBackgroundColor({32, 32, 32}).setForegroundColor({255, 255, 255});
  spacerRight.setBackgroundColor({32, 32, 32});

  statusLeft .setAlignment(0.0).setFont(Font().setBold());
  statusRight.setAlignment(1.0).setFont(Font().setBold());

  onClose([&] {
    program.quit();
  });

  loadEmulators();

  resizeWindow();
  setTitle({ares::Name, " ", ares::Version});
  setAssociatedFile();
  setBackgroundColor({0, 0, 0});
  setAlignment(Alignment::Center);
  setVisible();

  #if defined(PLATFORM_MACOS)
  Application::Cocoa::onAbout([&] { aboutAction.doActivate(); });
  Application::Cocoa::onActivate([&] { setFocused(); });
  Application::Cocoa::onPreferences([&] { settingsWindow.show("Video"); });
  Application::Cocoa::onQuit([&] { doClose(); });
  #endif
}

auto Presentation::resizeWindow() -> void {
  if(fullScreen()) setFullScreen(false);
  if(maximized()) return;

  u32 viewportWidth = settings.video.windowWidth;
  u32 viewportHeight = settings.video.windowHeight;
  if(!viewportWidth || !viewportHeight) {
    viewportWidth = 800;
    viewportHeight = 576;
    setConfiguredWindowSize(viewportWidth, viewportHeight);
  }

  if(emulator && !program.screens.empty()) {
    auto& node = program.screens.front();
    u32 videoWidth = node->width() * node->scaleX();
    u32 videoHeight = node->height() * node->scaleY();
    if(settings.video.aspectCorrection != "None")       videoWidth = videoWidth * node->aspectX() / node->aspectY();
    if(settings.video.aspectCorrection == "Anamorphic") videoWidth = videoWidth * 4 / 3;
    if(node->rotation() == 90 || node->rotation() == 270) std::swap(videoWidth, videoHeight);

    if(videoWidth && videoHeight && settings.video.adaptiveSizing) {
      u32 targetWidth = viewportWidth;
      u32 targetHeight = viewportHeight;

      if((u64)targetWidth * videoHeight > (u64)targetHeight * videoWidth) {
        targetWidth = targetHeight * videoWidth / videoHeight;
      } else if((u64)targetWidth * videoHeight < (u64)targetHeight * videoWidth) {
        targetHeight = targetWidth * videoHeight / videoWidth;
      }

      if(targetWidth != viewportWidth || targetHeight != viewportHeight) {
        viewportWidth = targetWidth;
        viewportHeight = targetHeight;
      }
    }
  }

  u32 statusHeight = (!program.kiosk && showStatusBarSetting.checked()) ? StatusHeight : 0;

  // Prevent the window frame from going out of bounds
  u32 monitorHeight = 1;
  u32 monitorWidth = 1;
  for(u32 monitor : range(Monitor::count())) {
    monitorHeight = max(monitorHeight, Monitor::workspace(monitor).height());
  }
  for(u32 monitor : range(Monitor::count())) {
    monitorWidth = max(monitorWidth, Monitor::workspace(monitor).width());
  }

  if(viewportWidth > monitorWidth || viewportHeight > monitorHeight) {
    if(settings.video.adaptiveSizing) {
      if(viewportWidth > monitorWidth) {
        viewportHeight = viewportHeight * monitorWidth / viewportWidth;
        viewportWidth = monitorWidth;
      }
      if(viewportHeight > monitorHeight) {
        viewportWidth = viewportWidth * monitorHeight / viewportHeight;
        viewportHeight = monitorHeight;
      }
    } else {
      viewportWidth = min(viewportWidth, monitorWidth);
      viewportHeight = min(viewportHeight, monitorHeight);
    }
  }

  if(settings.video.autoCentering) {
    setGeometry(Alignment::Center, {viewportWidth, viewportHeight + statusHeight});
  } else {
    setSize({viewportWidth, viewportHeight + statusHeight});
  }

  auto minimumSize = minimumConfiguredWindowSize();
  u32 minimumWidth = (u32)minimumSize.width();
  u32 minimumHeight = (u32)minimumSize.height();
  setMinimumSize({(f32)minimumWidth, (f32)(minimumHeight + statusHeight)});
}

auto Presentation::refreshWindowSizeMenu() -> void {
  videoSizeMenu.reset();
  videoSizeGroup = {};

  auto maximumSize = maximumViewportSize();
  u32 maximumWidth = (u32)maximumSize.width();
  u32 maximumHeight = (u32)maximumSize.height();
  u32 statusHeight = (!program.kiosk && showStatusBarSetting.checked()) ? StatusHeight : 0;
  auto minimumSize = minimumConfiguredWindowSize();
  u32 minimumWidth = (u32)minimumSize.width();
  u32 minimumHeight = (u32)minimumSize.height();
  std::vector<WindowSizePreset> available;

  for(auto preset : windowSizePresets()) {
    if(preset.width > maximumWidth) continue;
    if(preset.height + statusHeight > maximumHeight) continue;
    available.push_back(preset);
  }

  if(available.empty()) {
    u32 maximumAvailableHeight = maximumHeight - min(statusHeight, maximumHeight - 1);
    setConfiguredWindowSize(
      max(1u, min(minimumWidth, maximumWidth)),
      max(1u, min(minimumHeight, maximumAvailableHeight))
    );
  } else {
    bool configuredSizeAvailable = false;
    for(auto preset : available) {
      if(settings.video.windowWidth == preset.width && settings.video.windowHeight == preset.height) {
        configuredSizeAvailable = true;
      }
    }

    if(!configuredSizeAvailable) {
      if(settings.video.windowWidth >= minimumWidth
      && settings.video.windowHeight >= minimumHeight
      && settings.video.windowWidth <= maximumWidth
      && settings.video.windowHeight + statusHeight <= maximumHeight
      ) {
      } else {
        auto fallback = available.front();
        for(auto preset : available) {
          if(preset.width == 800 && preset.height == 600) fallback = preset;
          if(preset.width == 768 && preset.height == 576) fallback = preset;
        }
        setConfiguredWindowSize(fallback.width, fallback.height);
      }
    }
  }

  for(auto preset : available) {
    MenuRadioItem item{&videoSizeMenu};
    item.setText({preset.name, " (", windowSizeText(preset.width, preset.height), ")"});
    item.onActivate([=, this] {
      setConfiguredWindowSize(preset.width, preset.height);
      resizeWindow();
    });
    videoSizeGroup.append(item);

    if(settings.video.windowWidth == preset.width && settings.video.windowHeight == preset.height) item.setChecked();
  }

  bool presetSelected = false;
  for(auto item : videoSizeGroup.objects<MenuRadioItem>()) {
    if(item.checked()) presetSelected = true;
  }
  if(!presetSelected
  && settings.video.windowWidth >= minimumWidth
  && settings.video.windowHeight >= minimumHeight
  && settings.video.windowWidth <= maximumWidth
  && settings.video.windowHeight + statusHeight <= maximumHeight
  ) {
    MenuRadioItem item{&videoSizeMenu};
    item.setText({"Custom (", windowSizeText(settings.video.windowWidth, settings.video.windowHeight), ")"});
    item.onActivate([&] {
      resizeWindow();
    });
    videoSizeGroup.append(item);
    item.setChecked();
  }

  videoSizeMenu.append(MenuSeparator());
  MenuItem custom{&videoSizeMenu};
  custom.setText("Custom" ELLIPSIS).onActivate([&, minimumSize, maximumWidth, maximumHeight, statusHeight] {
    auto maximumCustomSize = Size{(f32)maximumWidth, (f32)(maximumHeight - min(statusHeight, maximumHeight - 1))};
    auto size = customWindowSizeDialog(*this, minimumSize, maximumCustomSize);
    if(size.width() <= 0 || size.height() <= 0) return;

    setConfiguredWindowSize((u32)size.width(), (u32)size.height());
    refreshWindowSizeMenu();
    resizeWindow();
  });

  videoSizeMenu.append(MenuSeparator());
  MenuItem centerWindow{&videoSizeMenu};
  centerWindow.setText("Center Window").setIcon(Icon::Place::Settings).onActivate([&] {
    setAlignment(Alignment::Center);
  });
}

auto Presentation::refreshFixedScaleMenu() -> void {
  for(auto item : videoFixedScaleItems) {
    videoOutputMenu.remove(item);
    videoOutputGroup.remove(item);
  }
  videoFixedScaleItems.clear();

  auto maximumSize = maximumViewportSize();
  u32 maximumWidth = (u32)maximumSize.width();
  u32 maximumHeight = (u32)maximumSize.height();
  u32 maximumScale = max(1u, min(maximumWidth / 160, maximumHeight / 144));
  maximumScale = min(maximumScale, 16u);
  settings.video.fixedScale = max(1u, min(maximumScale, settings.video.fixedScale));

  for(u32 scale : range(1, maximumScale + 1)) {
    MenuRadioItem item{&videoOutputMenu};
    item.setText({"Scale: Integer (fixed ", scale, "x)"});
    item.onActivate([=] {
      settings.video.output = "IntegerFixed";
      settings.video.fixedScale = scale;
    });
    videoOutputGroup.append(item);
    videoFixedScaleItems.push_back(item);
    if(settings.video.output == "IntegerFixed" && settings.video.fixedScale == scale) item.setChecked();
  }
}

auto Presentation::loadEmulators() -> void {
  if(program.kiosk) return;
  loadMenu.reset();

  //clean up the recent games history first
  std::vector<string> recentGames;
  for(u32 index : range(9)) {
    auto entry = settings.recent.game[index];
    auto parts = nall::split(entry, ";", 1L);
    parts.resize(2);
    auto system = parts[0];
    auto location = parts[1];
    if(location.length()) {  //remove missing games
      if(std::ranges::find(recentGames, entry) == recentGames.end()) {  //remove duplicate entries
        recentGames.push_back(entry);
      }
    }
    settings.recent.game[index] = {};
  }

  //build recent games list
  u32 count = 0;
  for(auto& game : recentGames) {
    settings.recent.game[count++] = game;
  }
  { Menu recentGames{&loadMenu};
    recentGames.setIcon(Icon::Action::Open);
    recentGames.setText("Recent Games");
    for(u32 index : range(count)) {
      MenuItem item{&recentGames};
      auto entry = settings.recent.game[index];
      auto parts = nall::split(entry, ";", 1L);
      parts.resize(2);
      auto system = parts[0];
      auto location = parts[1];
      item.setIconForFile(location);
      item.setText({Location::base(location).trimRight("/"), " (", system, ")"});
      item.onActivate([=, this] {
        Program::Guard guard;
        if(!inode::exists(location)) {
          MessageDialog()
            .setTitle("Error")
            .setText({location, " does not exist"})
            .setAlignment(presentation)
            .error();

          //remove the entry from the recent games list
          settings.recent.game[index] = {};
          loadEmulators();
          return;
        }
        for(auto& emulator : emulators) {
          if(emulator->name == system) {
            program.load(emulator, location);
            return;
          }
        }
      });
    }
    if(count > 0) {
      recentGames.append(MenuSeparator());
      MenuItem clearHistory{&recentGames};
      clearHistory.setIcon(Icon::Edit::Clear);
      #if !defined(PLATFORM_MACOS)
      clearHistory.setText("Clear History");
      #else
      clearHistory.setText("Clear Menu");
      #endif
      clearHistory.onActivate([&] {
        for(u32 index : range(9)) settings.recent.game[index] = {};
        loadEmulators();
      });
    } else {
      recentGames.setEnabled(false);
    }
  }
  loadMenu.append(MenuSeparator());

  //build emulator load list
  u32 enabled = 0;

  //first pass; make sure "Arcade" is start of list
  for(auto& emulator : emulators) {
    if(!emulator->configuration.visible) continue;
    if(emulator->group() == "Arcade") {
      Menu menu;
      menu.setIcon(Icon::Emblem::Folder);
      menu.setText(emulator->group());
      loadMenu.append(menu);
      break;
    }
  }

  for(auto& emulator : emulators) {
    if (!emulator->configuration.visible) continue;
    enabled++;
    MenuItem item;
    item.setIcon(Icon::Place::Server);
    item.setText({emulator->name, ELLIPSIS});
    item.setVisible(emulator->configuration.visible);
    item.onActivate([=] {
      program.load(emulator);
    });

    Menu menu;
    for(auto& action : loadMenu.actions()) {
      if(auto group = action.cast<Menu>()) {
        if(group.text() == emulator->group()) {
          menu = group;
          break;
        }
      }
    }
    if(!menu) {
      menu.setIcon(Icon::Emblem::Folder);
      menu.setText(emulator->group());
      loadMenu.append(menu);
    }
    menu.append(item);
  }
  if(enabled == 0) {
    //if the user disables every system, give an indication for how to re-add systems to the load menu
    MenuItem item{&loadMenu};
    item.setIcon(Icon::Action::Add);
    item.setText("Add Systems" ELLIPSIS);
    item.onActivate([&] {
      settingsWindow.show("Emulators");
    });
  }

  #if !defined(PLATFORM_MACOS)
  loadMenu.append(MenuSeparator());

  { MenuItem quit{&loadMenu};
    quit.setIcon(Icon::Action::Quit);
    quit.setText("Quit");
    quit.onActivate([&] {
      program.quit();
    });
  }
  #endif
}

auto Presentation::loadEmulator() -> void {
  setTitle(emulator->root->game());
  setAssociatedFile(emulator->game->location);

  if(!program.kiosk) {
    systemMenu.setText(emulator->name);
    systemMenu.setVisible();

    refreshSystemMenu();

    toolsMenu.setVisible(true);
    pauseEmulation.setChecked(false);
  }

  setFocused();
  viewport.setFocused();
}

auto Presentation::refreshSystemMenu() -> void {
  if(program.kiosk) return;
  systemMenu.reset();

  //allow each emulator core to create any specialized menus necessary:
  //for instance, floppy disk and CD-ROM swapping support.
  emulator->load(systemMenu);
  if(systemMenu.actionCount() > 0) systemMenu.append(MenuSeparator());

  //Build the Dip Switch menu if the emulator core has a DIP Switches node
  if(auto dipSwitches = ares::Node::find<ares::Node::Object>(emulator->root, "DIP Switches")) {
    Menu dipSwitchMenu{&systemMenu};
    dipSwitchMenu.setText("DIP Switches");
    
    for(auto dip : ares::Node::enumerate<ares::Node::Setting::Boolean>(emulator->root)) {
      MenuCheckItem item{&dipSwitchMenu};
      item.setText(dip->name());
      item.setAttribute<ares::Node::Setting::Boolean>("dip", dip);
      item.setChecked(dip->value());
      item.onToggle([=] {
        auto dip = item.attribute<ares::Node::Setting::Boolean>("dip");
        dip->setValue(item.checked());
      });
    }

    for(auto& dip : ares::Node::enumerate<ares::Node::Setting::String>(emulator->root)) {
      Group group;
      Menu item{&dipSwitchMenu};
      item.setAttribute<ares::Node::Setting::String>("dip", dip);
      item.setText(dip->name());
      for(auto option : dip->readAllowedValues()) {
        MenuRadioItem optionItem{&item};
        optionItem.setText(option);
        group.append(optionItem);
        if(dip->value() == option) optionItem.setChecked();
        optionItem.onActivate([=] {
          auto dip = item.attribute<ares::Node::Setting::String>("dip");
          dip->setValue(optionItem.text());
        });
      }
    }

    systemMenu.append(MenuSeparator());
  }

  u32 portsFound = 0;
  for(auto port : ares::Node::enumerate<ares::Node::Port>(emulator->root)) {
    //do not add unsupported ports to the port menu
    auto portName = port->name();
    if(std::ranges::find(emulator->portBlacklist, portName) != emulator->portBlacklist.end()) continue;

    if(!port->hotSwappable()) continue;
    if(port->type() != "Controller" && port->type() != "Expansion") continue;

    portsFound++;
    Menu portMenu{&systemMenu};
    if(port->type() == "Controller") portMenu.setIcon(Icon::Device::Joypad);
    if(port->type() == "Expansion" ) portMenu.setIcon(Icon::Device::Storage);
    portMenu.setText(port->name());

    Group peripheralGroup;
    { MenuRadioItem peripheralItem{&portMenu};
      peripheralItem.setAttribute<ares::Node::Port>("port", port);
      peripheralItem.setText("Nothing");
      if(!port->connected()) peripheralItem.setChecked();
      peripheralItem.onActivate([=, this] {
        auto port = peripheralItem.attribute<ares::Node::Port>("port");
        port->disconnect();
        refreshSystemMenu();
      });
      peripheralGroup.append(peripheralItem);
    }
    for(auto peripheral : port->supported()) {
      //do not add unsupported peripherals to the peripheral port menu
      if(std::ranges::find(emulator->inputBlacklist, peripheral) != emulator->inputBlacklist.end()) continue;

      MenuRadioItem peripheralItem{&portMenu};
      peripheralItem.setAttribute<ares::Node::Port>("port", port);
      peripheralItem.setText(peripheral);
      peripheralItem.onActivate([=, this] {
        auto port = peripheralItem.attribute<ares::Node::Port>("port");
        port->disconnect();
        port->allocate(peripheralItem.text());
        port->connect();
        refreshSystemMenu();
      });
      peripheralGroup.append(peripheralItem);
    }

    //check the peripheral item menu option that is currently connected to said port
    if(auto connected = port->connected()) {
      for(auto peripheralItem : peripheralGroup.objects<MenuRadioItem>()) {
        if(peripheralItem.text() == connected->name()) peripheralItem.setChecked();
      }
    }

    // Allow for cores to add additional menu items here
    emulator->portMenu(portMenu, port);
  }

  if(portsFound > 0) systemMenu.append(MenuSeparator());

  if(ares::Node::enumerate<ares::Node::Tape>(emulator->root).size() > 0) {
    MenuItem tapeManager{&systemMenu};
    tapeManager.setText("Tape Manager").setIcon(Icon::Device::Tape).onActivate([&] {
      toolsWindow.show("Tape");
    });
    systemMenu.append(MenuSeparator());
  }

  MenuItem reset{&systemMenu};
  reset.setText("Reset").setIcon(Icon::Action::Refresh).onActivate([&] {
    Program::Guard guard;
    emulator->root->power(true);
    program.showMessage("System reset");
  });
  systemMenu.append(MenuSeparator());

  MenuItem unload{&systemMenu};
  unload.setText("Unload").setIcon(Icon::Media::Eject).onActivate([&] {
    Program::Guard guard;
    program.unload();
    if(settings.video.adaptiveSizing) presentation.resizeWindow();
    presentation.showIcon(true);
  });
}

auto Presentation::unloadEmulator(bool reloading) -> void {
  setTitle({ares::Name, " ", ares::Version});
  setAssociatedFile();
  if(program.kiosk) return;
  systemMenu.setVisible(false);
  systemMenu.reset();

  toolsMenu.setVisible(false);
}

auto Presentation::showIcon(bool visible) -> void {
  iconLayout.setVisible(visible);
  iconSpacer.setVisible(visible);
  iconCanvas.setVisible(visible);
  iconBottom.setVisible(visible);
  layout.resize();
}

auto Presentation::loadShaders() -> void {
  videoShaderMenu.reset();
  videoShaderMenu.setEnabled(ruby::video.hasShader());
  if(!ruby::video.hasShader()) return;

  Group shaders;

  MenuCheckItem none{&videoShaderMenu};
  none.setText("None").onToggle([&] {
    settings.video.shader = "None";
    ruby::video.setShader(settings.video.shader);
    loadShaders();
  });
  shaders.append(none);

  string location = locate("Shaders/");
  #if defined(PLATFORM_LINUX) || defined(PLATFORM_BSD)
  // In some Linux or BSD distro shaders may not be bundled with ares, so try to locate them at a different path
  if(not inode::exists(location)) location = locate("../libretro/shaders/shaders_slang/");
  #endif

  if(shaderDirectories.size() == 0) {
    std::function<void(string)> findShaderDirectories = [&](string path) {
      for(auto &entry: directory::folders(path)) findShaderDirectories({path, entry});
      auto files = directory::files(path, "*.slangp");
      if(files.size() > 0) shaderDirectories.push_back((string({path}).trimLeft(location, 1L)));
    };
    findShaderDirectories(location);

    // Sort by name and depth such that child folders appear after their parents
    std::ranges::sort(shaderDirectories, [](const string &lhs, const string &rhs) {
      auto lhsParts = nall::split(lhs, "/");
      auto rhsParts = nall::split(rhs, "/");
      for(u32 i : range(min(lhsParts.size(), rhsParts.size()))) {
        if(lhsParts[i] != rhsParts[i]) return lhsParts[i] < rhsParts[i];
      }
      return lhsParts.size() < rhsParts.size();
    });
  }

  if(ruby::video.hasShader()) {
    for(auto &directory : shaderDirectories) {
      auto parts = nall::split(directory, "/");
      Menu parent = videoShaderMenu;

      if(directory != "") {
        for (auto &part: parts) {
          if(part == "") continue;
          Menu child;
          bool found = false;
          for(auto &action: parent.actions()) {
            if(auto menu = action.cast<Menu>()) {
              if(menu.text() == part) {
                child = menu;
                found = true;
                break;
              }
            }
          }

          if(found) {
            parent = child;
          } else {
            Menu newMenu{&parent};
            newMenu.setText(part);
            parent = newMenu;
          }
        }
      }

      auto files = directory::files({location, directory}, "*.slangp");
      for(auto &file: files) {
        MenuCheckItem item{&parent};
        item.setAttribute("file", {directory, file});
        item.setText(string{file}.trimRight(".slangp", 1L)).onToggle([=, this] {
          settings.video.shader = {directory, file};
          ruby::video.setShader({location, settings.video.shader});
          loadShaders();
        });
        shaders.append(item);
      }
    }
  }

  if(program.startShader && !shaderArgApplied) {
    shaderArgApplied = true;
    string existingShader = settings.video.shader;

    program.startShader.transform("\\", "/");
    if(!program.startShader.imatch("None")) {
      settings.video.shader = {program.startShader, ".slangp"};
    } else {
      settings.video.shader = program.startShader;
    }

    if(inode::exists({location, settings.video.shader})) {
      ruby::video.setShader({location, settings.video.shader});
      loadShaders();
    } else if(settings.video.shader.imatch("None")) {
      ruby::video.setShader("None");
      loadShaders();
    } else {
      hiro::MessageDialog()
          .setTitle("Warning")
          .setAlignment(hiro::Alignment::Center)
          .setText({"Requested shader not found: ", location, settings.video.shader , 
            "\nUsing existing defined shader: ", location, existingShader})
          .warning();
      settings.video.shader = existingShader;
    }
  }

  if(settings.video.shader.imatch("None")) {none.setChecked(); settings.video.shader = "None";}
  for(auto item : shaders.objects<MenuCheckItem>()) {
    auto temp = item.attribute("file");
    if(settings.video.shader.imatch(temp)) {
      item.setChecked();
      settings.video.shader = item.attribute("file");
      ruby::video.setShader({location, settings.video.shader});
    }
  }
}
