// Copyright 2009 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include <algorithm>
#include <array>
#include <cmath>
#include <string>
#include <wx/app.h>
#include <wx/bitmap.h>
#include <wx/choice.h>
#include <wx/combo.h>
#include <wx/combobox.h>
#include <wx/display.h>
#include <wx/gdicmn.h>
#include <wx/image.h>
#include <wx/msgdlg.h>
#include <wx/sizer.h>
#include <wx/spinctrl.h>
#include <wx/toolbar.h>
#include <wx/toplevel.h>
#include <wx/utils.h>

#include "Common/CommonPaths.h"
#include "Common/FileUtil.h"
#include "Common/StringUtil.h"
#include "Core/ConfigManager.h"

#include "DolphinWX/WxUtils.h"

#ifdef _WIN32
#include <Windows.h>
#endif

namespace WxUtils
{
// Launch a file according to its mime type
void Launch(const std::string& filename)
{
  if (!::wxLaunchDefaultBrowser(StrToWxStr(filename)))
  {
    // WARN_LOG
  }
}

// Launch an file explorer window on a certain path
void Explore(const std::string& path)
{
  wxString wxPath = StrToWxStr(path);
#ifndef _WIN32
  // Default to file
  if (!wxPath.Contains("://"))
  {
    wxPath = "file://" + wxPath;
  }
#endif

#ifdef __WXGTK__
  wxPath.Replace(" ", "\\ ");
#endif

  if (!::wxLaunchDefaultBrowser(wxPath))
  {
    // WARN_LOG
  }
}

void ShowErrorDialog(const wxString& error_msg)
{
  wxMessageBox(error_msg, _("Error"), wxOK | wxICON_ERROR);
}

wxBitmap CreateDisabledButtonBitmap(const wxBitmap& original)
{
  wxImage image = original.ConvertToImage();
  return wxBitmap(image.ConvertToDisabled(240), wxBITMAP_SCREEN_DEPTH, original.GetScaleFactor());
}

void AddToolbarButton(wxToolBar* toolbar, int toolID, const wxString& label, const wxBitmap& bitmap,
                      const wxString& shortHelp)
{
  // Must explicitly set the disabled button bitmap because wxWidgets
  // incorrectly desaturates it instead of lightening it.
  toolbar->AddTool(toolID, label, bitmap, WxUtils::CreateDisabledButtonBitmap(bitmap),
                   wxITEM_NORMAL, shortHelp);
}

wxIconBundle GetDolphinIconBundle()
{
  static wxIconBundle s_bundle;
  if (!s_bundle.IsEmpty())
    return s_bundle;

#ifdef _WIN32

  // Convert the Windows ICO file into a wxIconBundle by tearing it apart into each individual
  // sub-icon using the Win32 API. This is necessary because WX uses its own wxIcons internally
  // which (unlike QIcon in Qt) only contain 1 image per icon, hence why wxIconBundle exists.
  HINSTANCE dolphin = GetModuleHandleW(nullptr);
  for (int size : {16, 32, 48, 256})
  {
    // Extract resource from embedded DolphinWX.rc
    HANDLE win32_icon =
        LoadImageW(dolphin, L"\"DOLPHIN\"", IMAGE_ICON, size, size, LR_CREATEDIBSECTION);
    if (win32_icon && win32_icon != INVALID_HANDLE_VALUE)
    {
      wxIcon icon;
      icon.CreateFromHICON(reinterpret_cast<HICON>(win32_icon));
      s_bundle.AddIcon(icon);
    }
  }

#else

  for (const char* fname : {"Dolphin.png", "dolphin_logo.png", "dolphin_logo@2x.png"})
  {
    wxImage image{StrToWxStr(File::GetSysDirectory() + RESOURCES_DIR DIR_SEP + fname),
                  wxBITMAP_TYPE_PNG};
    if (image.IsOk())
    {
      wxIcon icon;
      icon.CopyFromBitmap(image);
      s_bundle.AddIcon(icon);
    }
  }

#endif

  return s_bundle;
}

wxRect GetVirtualScreenGeometry()
{
  wxRect geometry;
  for (unsigned int i = 0, end = wxDisplay::GetCount(); i < end; ++i)
    geometry.Union(wxDisplay(i).GetGeometry());
  return geometry;
}

void SetWindowSizeAndFitToScreen(wxTopLevelWindow* tlw, wxPoint pos, wxSize size,
                                 wxSize default_size)
{
  if (tlw->IsMaximized())
    return;

  // NOTE: Positions can be negative and still be valid. Coordinates are relative to the
  //   primary monitor so if the primary monitor is in the middle then (-1000, 10) is a
  //   valid position on the monitor to the left of the primary. (This does not apply to
  //   sizes obviously)
  wxRect screen_geometry;
  wxRect window_geometry{pos, size};

  if (wxDisplay::GetCount() > 1)
    screen_geometry = GetVirtualScreenGeometry();
  else
    screen_geometry = wxDisplay(0).GetClientArea();

  // Initialize the default size if it is wxDefaultSize or otherwise negative.
  default_size.DecTo(screen_geometry.GetSize());
  default_size.IncTo(tlw->GetMinSize());
  if (!default_size.IsFullySpecified())
    default_size.SetDefaults(wxDisplay(0).GetClientArea().GetSize() / 2);

  // If the position we're given doesn't make sense then go with the current position.
  // (Assuming the window was created with wxDefaultPosition then this should be reasonable)
  if (pos.x - screen_geometry.GetLeft() < -1000 || pos.y - screen_geometry.GetTop() < -1000 ||
      pos.x - screen_geometry.GetRight() > 1000 || pos.y - screen_geometry.GetBottom() > 1000)
  {
    window_geometry.SetPosition(tlw->GetPosition());
  }

  // If the window is bigger than all monitors combined, or negative (uninitialized) then reset it.
  if (window_geometry.IsEmpty() || window_geometry.GetWidth() > screen_geometry.GetWidth() ||
      window_geometry.GetHeight() > screen_geometry.GetHeight())
  {
    window_geometry.SetSize(default_size);
  }

  // Check if the window entirely lives on a single monitor without spanning.
  // If the window does not span multiple screens then we should constrain it within that
  // single monitor instead of the entire virtual desktop space.
  // The benefit to doing this is that we can account for the OS X menu bar and Windows task
  // bar which are treated as invisible when only looking at the virtual desktop instead of
  // an individual screen.
  if (wxDisplay::GetCount() > 1)
  {
    // SPECIAL CASE: If the window is entirely outside the visible area of the desktop then we
    //   put it back on the primary (zero) monitor.
    wxRect monitor_intersection{window_geometry};
    int the_monitor = 0;
    if (!monitor_intersection.Intersect(screen_geometry).IsEmpty())
    {
      std::array<int, 4> monitors{{wxDisplay::GetFromPoint(monitor_intersection.GetTopLeft()),
                                   wxDisplay::GetFromPoint(monitor_intersection.GetTopRight()),
                                   wxDisplay::GetFromPoint(monitor_intersection.GetBottomLeft()),
                                   wxDisplay::GetFromPoint(monitor_intersection.GetBottomRight())}};
      the_monitor = wxNOT_FOUND;
      bool intersected = false;
      for (int one_monitor : monitors)
      {
        if (one_monitor == the_monitor || one_monitor == wxNOT_FOUND)
          continue;
        if (the_monitor != wxNOT_FOUND)
        {
          // The window is spanning multiple screens.
          the_monitor = wxNOT_FOUND;
          break;
        }
        the_monitor = one_monitor;
        intersected = true;
      }
      // If we get wxNOT_FOUND for all corners then there are holes in the virtual desktop and the
      // entire window is lost in one. (e.g. 3 monitors in an 'L', window in top-right)
      if (!intersected)
        the_monitor = 0;
    }
    if (the_monitor != wxNOT_FOUND)
    {
      // We'll only use the client area of this monitor if the window will actually fit.
      // (It may not fit if the window is spilling off the edge so it isn't entirely visible)
      wxRect client_area{wxDisplay(the_monitor).GetClientArea()};
      if (client_area.GetWidth() >= window_geometry.GetWidth() &&
          client_area.GetHeight() >= window_geometry.GetHeight())
      {
        screen_geometry = client_area;
      }
    }
  }

  // The window SHOULD be small enough to fit on the screen, but it might be spilling off an edge
  // so we'll snap it to the nearest edge as necessary.
  if (!screen_geometry.Contains(window_geometry))
  {
    // NOTE: The order is important here, if the window *is* too big to fit then it will snap to
    //   the top-left corner.
    int spill_x = std::max(0, window_geometry.GetRight() - screen_geometry.GetRight());
    int spill_y = std::max(0, window_geometry.GetBottom() - screen_geometry.GetBottom());
    window_geometry.Offset(-spill_x, -spill_y);
    if (window_geometry.GetTop() < screen_geometry.GetTop())
      window_geometry.SetTop(screen_geometry.GetTop());
    if (window_geometry.GetLeft() < screen_geometry.GetLeft())
      window_geometry.SetLeft(screen_geometry.GetLeft());
  }

  tlw->SetSize(window_geometry, wxSIZE_ALLOW_MINUS_ONE);
}

wxSizer* GiveMinSize(wxWindow* window, const wxSize& min_size)
{
  wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL);
  int flags = wxEXPAND;
  // On Windows comboboxes will misrender when stretched vertically.
  if (wxDynamicCast(window, wxChoice) || wxDynamicCast(window, wxComboBox) ||
      wxDynamicCast(window, wxComboCtrl))
    flags = wxALIGN_CENTER_VERTICAL;
  sizer->Add(window, 1, flags);
  sizer->SetMinSize(min_size);
  return sizer;
}

wxSizer* GiveMinSizeDIP(wxWindow* window, const wxSize& min_size)
{
  return GiveMinSize(window, window->FromDIP(min_size));
}

wxSize GetTextWidgetMinSize(const wxControl* control, const wxString& value)
{
  return control->GetSizeFromTextSize(control->GetTextExtent(value));
}

wxSize GetTextWidgetMinSize(const wxControl* control, unsigned int value)
{
  return GetTextWidgetMinSize(control, wxString::Format("%u", value));
}

wxSize GetTextWidgetMinSize(const wxControl* control, int value)
{
  return GetTextWidgetMinSize(control, wxString::Format("%d", value));
}

wxSize GetTextWidgetMinSize(const wxSpinCtrl* spinner)
{
  wxSize size = GetTextWidgetMinSize(spinner, spinner->GetMin());
  size.IncTo(GetTextWidgetMinSize(spinner, spinner->GetMax()));
  return size;
}

static wxImage LoadScaledImage(const std::string& file_path, const wxWindow* context,
                               const wxSize& output_size, const wxRect& usable_rect, LSIFlags flags,
                               const wxColour& fill_color)
{
  std::string fpath, fname, fext;
  SplitPath(file_path, &fpath, &fname, &fext);

  const double window_scale_factor = context->GetContentScaleFactor();
  // Compute the total scale factor from the ratio of DIPs to window pixels (FromDIP) and
  // window pixels to framebuffer pixels (GetContentScaleFactor).
  // NOTE: Usually only one of these is meaningful:
  //   - On Windows/GTK2: content_scale = 1.0, FromDIP = 96DPI -> Screen DPI
  //   - On Mac OS X: content_scale = screen_dpi / 96, FromDIP = 96DPI -> 96DPI (no-op)
  // [The 1024 is arbitrarily large to minimise rounding error, it has no significance]
  const double scale_factor = (context->FromDIP(1024) / 1024.0) * window_scale_factor;

  // We search for files on quarter ratios of DIPs to framebuffer pixels.
  // By default, the algorithm prefers to find an exact or bigger size then downscale if
  // needed but will resort to upscaling if a bigger image cannot be found.
  // E.g. A basic retina screen on Mac OS X has a scale_factor of 2.0, so we would look for
  // @2x, @2.25x, @2.5x, @2.75x, @3x, @1.75x, @1.5x, @1.25x, @1x, then give up.
  // (At 125% on Windows the search is @1.25, @1.5, @1.75, @2, @2.25, @1)
  // If flags does not include LSI_SCALE_DOWN (i.e. we would be forced to crop big
  // images instead of scaling them) then we will only accept smaller sizes, i.e.
  // @2x, @1.75, @1.5, @1.25, @1, then give up.
  // NOTE: We do a lot of exact comparisons against floating point here but it's fine
  //   because the numbers involved are all powers of 2 so can be represented exactly.
  wxImage image;
  double selected_image_scale = 1;
  {
    auto image_check = [&](double scale) -> bool {
      std::string path = fpath + fname + StringFromFormat("@%gx", scale) + fext;
      if (!File::Exists(path))
      {
        // Special Case: @1x may not have a suffix at all.
        if (scale != 1.0 || !File::Exists(file_path))
          return false;
        path = file_path;
      }
      if (!image.LoadFile(StrToWxStr(path), wxBITMAP_TYPE_ANY))
        return false;
      selected_image_scale = scale;
      return true;
    };
    const bool prefer_smaller = !(flags & LSI_SCALE_DOWN);
    const double scale_factor_quarter =
        prefer_smaller ? std::floor(scale_factor * 4) / 4 : std::ceil(scale_factor * 4) / 4;
    // Search for bigger sizes first (preferred)
    if (!prefer_smaller)
    {
      // We search within a 'circle' of the exact match limited by scale=1.0.
      // i.e. scale_factor = 1.5, radius = 0.5; scale = 2.5, radius = 1.5.
      // The minimum radius is 1.0.
      double limit = std::max(scale_factor_quarter * 2 - 1, scale_factor_quarter + 1);
      for (double quarter = scale_factor_quarter; quarter <= limit; quarter += 0.25)
      {
        if (image_check(quarter))
          break;
      }
    }
    // If we didn't hit a bigger size then we'll fallback to looking for smaller ones
    if (!image.IsOk())
    {
      double quarter = scale_factor_quarter;
      if (!prefer_smaller)  // So we don't recheck the exact match
        quarter -= 0.25;
      for (; quarter >= 1.0; quarter -= 0.25)
      {
        if (image_check(quarter))
          break;
      }
    }
  }

  // The file apparently does not exist so we give up. Create a white square placeholder instead.
  if (!image.IsOk())
  {
    wxLogError("Could not find resource: %s", StrToWxStr(file_path));
    image.Create(1, 1, false);
    image.Clear(0xFF);
  }

  return ScaleImage(image, selected_image_scale, window_scale_factor, output_size, usable_rect,
                    flags, fill_color);
}

wxBitmap LoadScaledBitmap(const std::string& file_path, const wxWindow* context,
                          const wxSize& output_size, const wxRect& usable_rect, LSIFlags flags,
                          const wxColour& fill_color)
{
  return wxBitmap(LoadScaledImage(file_path, context, output_size, usable_rect, flags, fill_color),
                  wxBITMAP_SCREEN_DEPTH, context->GetContentScaleFactor());
}

wxBitmap LoadScaledResourceBitmap(const std::string& name, const wxWindow* context,
                                  const wxSize& output_size, const wxRect& usable_rect,
                                  LSIFlags flags, const wxColour& fill_color)
{
  std::string path = File::GetSysDirectory() + RESOURCES_DIR DIR_SEP + name + ".png";
  return LoadScaledBitmap(path, context, output_size, usable_rect, flags, fill_color);
}

wxBitmap LoadScaledThemeBitmap(const std::string& name, const wxWindow* context,
                               const wxSize& output_size, const wxRect& usable_rect, LSIFlags flags,
                               const wxColour& fill_color)
{
  std::string path = File::GetThemeDir(SConfig::GetInstance().theme_name) + name + ".png";
  return LoadScaledBitmap(path, context, output_size, usable_rect, flags, fill_color);
}

wxBitmap ScaleImageToBitmap(const wxImage& image, const wxWindow* context,
                            const wxSize& output_size, const wxRect& usable_rect, LSIFlags flags,
                            const wxColour& fill_color)
{
  double scale_factor = context->GetContentScaleFactor();
  return wxBitmap(ScaleImage(image, 1.0, scale_factor, output_size, usable_rect, flags, fill_color),
                  wxBITMAP_SCREEN_DEPTH, scale_factor);
}

wxBitmap ScaleImageToBitmap(const wxImage& image, const wxWindow* context, double source_scale,
                            LSIFlags flags, const wxColour& fill_color)
{
  double scale_factor = context->GetContentScaleFactor();
  return wxBitmap(ScaleImage(image, source_scale, scale_factor, wxDefaultSize, wxDefaultSize, flags,
                             fill_color),
                  wxBITMAP_SCREEN_DEPTH, scale_factor);
}

wxImage ScaleImage(wxImage image, double source_scale_factor, double content_scale_factor,
                   wxSize output_size, wxRect usable_rect, LSIFlags flags,
                   const wxColour& fill_color)
{
  if (!image.IsOk())
  {
    wxFAIL_MSG("WxUtils::ScaleImage expects a valid image.");
    return image;
  }

  if (content_scale_factor != 1.0)
  {
    output_size *= content_scale_factor;
    usable_rect.SetPosition(usable_rect.GetPosition() * content_scale_factor);
    usable_rect.SetSize(usable_rect.GetSize() * content_scale_factor);
  }

  // Fix the output size if it's unset.
  wxSize img_size = image.GetSize();
  if (output_size.GetWidth() < 1)
    output_size.SetWidth(
        static_cast<int>(img_size.GetWidth() * (content_scale_factor / source_scale_factor)));
  if (output_size.GetHeight() < 1)
    output_size.SetHeight(
        static_cast<int>(img_size.GetHeight() * (content_scale_factor / source_scale_factor)));

  // Fix the usable rect. If it's empty then the whole canvas is usable.
  if (usable_rect.IsEmpty())
  {
    // Constructs a temp wxRect 0,0->output_size then move assigns it.
    usable_rect = output_size;
  }
  else if (!usable_rect.Intersects(output_size))
  {
    wxFAIL_MSG("Usable Zone Rectangle is not inside the canvas. Check the output size is correct.");
    image.Create(1, 1, false);
    image.SetRGB(0, 0, fill_color.Red(), fill_color.Green(), fill_color.Blue());
    if (fill_color.Alpha() == wxALPHA_TRANSPARENT)
      image.SetMaskColour(fill_color.Red(), fill_color.Green(), fill_color.Blue());
    usable_rect = output_size;
  }

  // Step 1: Scale the image
  if ((flags & LSI_SCALE) != LSI_SCALE_NONE)
  {
    if (flags & LSI_SCALE_NO_ASPECT)
    {
      // Stretch scale without preserving the aspect ratio.
      bool scale_width = (img_size.GetWidth() > usable_rect.GetWidth() && flags & LSI_SCALE_DOWN) ||
                         (img_size.GetWidth() < usable_rect.GetWidth() && flags & LSI_SCALE_UP);
      bool scale_height =
          (img_size.GetHeight() > usable_rect.GetHeight() && flags & LSI_SCALE_DOWN) ||
          (img_size.GetHeight() < usable_rect.GetHeight() && flags & LSI_SCALE_UP);
      if (scale_width || scale_height)
      {
        // NOTE: Using BICUBIC instead of HIGH because it's the same internally
        //   except that downscaling uses a box filter with awful obvious aliasing
        //   for non-integral scale factors.
        image.Rescale(scale_width ? usable_rect.GetWidth() : img_size.GetWidth(),
                      scale_height ? usable_rect.GetHeight() : img_size.GetHeight(),
                      wxIMAGE_QUALITY_BICUBIC);
      }
    }
    else
    {
      // Scale while preserving the aspect ratio.
      double scale = std::min(static_cast<double>(usable_rect.GetWidth()) / img_size.GetWidth(),
                              static_cast<double>(usable_rect.GetHeight()) / img_size.GetHeight());
      int target_width = static_cast<int>(img_size.GetWidth() * scale);
      int target_height = static_cast<int>(img_size.GetHeight() * scale);
      // Bilinear produces sharper images when upscaling, bicubic tends to smear/blur sharp edges.
      if (scale > 1.0 && flags & LSI_SCALE_UP)
        image.Rescale(target_width, target_height, wxIMAGE_QUALITY_BILINEAR);
      else if (scale < 1.0 && flags & LSI_SCALE_DOWN)
        image.Rescale(target_width, target_height, wxIMAGE_QUALITY_BICUBIC);
    }
    img_size = image.GetSize();
  }

  // Step 2: Resize the canvas to match the output size.
  // NOTE: If NOT using LSI_SCALE_DOWN then this will implicitly crop the image
  if (img_size != output_size || usable_rect.GetPosition() != wxPoint())
  {
    wxPoint base = usable_rect.GetPosition();
    if (flags & LSI_ALIGN_HCENTER)
      base.x += (usable_rect.GetWidth() - img_size.GetWidth()) / 2;
    else if (flags & LSI_ALIGN_RIGHT)
      base.x += usable_rect.GetWidth() - img_size.GetWidth();
    if (flags & LSI_ALIGN_VCENTER)
      base.y += (usable_rect.GetHeight() - img_size.GetHeight()) / 2;
    else if (flags & LSI_ALIGN_BOTTOM)
      base.y += usable_rect.GetHeight() - img_size.GetHeight();

    int r = -1, g = -1, b = -1;
    if (fill_color.Alpha() != wxALPHA_TRANSPARENT)
    {
      r = fill_color.Red();
      g = fill_color.Green();
      b = fill_color.Blue();
    }
    image.Resize(output_size, base, r, g, b);
  }

  return image;
}

}  // namespace

std::string WxStrToStr(const wxString& str)
{
  return str.ToUTF8().data();
}

wxString StrToWxStr(const std::string& str)
{
  // return wxString::FromUTF8Unchecked(str.c_str());
  return wxString::FromUTF8(str.c_str());
}