/**
 ******************************************************************************
 * Xenia : Xbox 360 Emulator Research Project                                 *
 ******************************************************************************
 * Copyright 2022 Ben Vanik. All rights reserved.                             *
 * Released under the BSD license - see LICENSE in the root for more details. *
 ******************************************************************************
 */

#include "xenia/ui/window.h"

#include <algorithm>
#include <iterator>

#include "third_party/imgui/imgui.h"
#include "xenia/base/assert.h"
#include "xenia/base/clock.h"
#include "xenia/base/logging.h"
#include "xenia/ui/imgui_drawer.h"
#include "xenia/ui/presenter.h"

namespace xe {
namespace ui {

Window::Window(WindowedAppContext& app_context, const std::string_view title,
               uint32_t desired_logical_width, uint32_t desired_logical_height)
    : app_context_(app_context),
      title_(title),
      desired_logical_width_(desired_logical_width),
      desired_logical_height_(desired_logical_height) {}

Window::~Window() {
  // In case the implementation didn't need to call EnterDestructor. Though
  // that was likely a mistake, so placing an assertion.
  assert_true(phase_ == Phase::kDeleting);
  EnterDestructor();

  if (presenter_) {
    Presenter* old_presenter = presenter_;
    // Null the pointer to prevent an infinite loop between
    // SetWindowSurfaceFromUIThread and SetPresenter calling each other.
    presenter_ = nullptr;
    old_presenter->SetWindowSurfaceFromUIThread(nullptr, nullptr);
    presenter_surface_.reset();
  }

  // Right before destruction has finished, after which no interaction with
  // *this can be done, notify the destruction receivers that the window is
  // being destroyed and that *this is not accessible anymore.
  while (innermost_destruction_receiver_) {
    innermost_destruction_receiver_->window_ = nullptr;
    innermost_destruction_receiver_ =
        innermost_destruction_receiver_->outer_receiver_;
  }
}

void Window::AddListener(WindowListener* listener) {
  assert_not_null(listener);
  // Check if already added.
  if (std::find(listeners_.cbegin(), listeners_.cend(), listener) !=
      listeners_.cend()) {
    return;
  }
  listeners_.push_back(listener);
}

void Window::RemoveListener(WindowListener* listener) {
  assert_not_null(listener);
  auto it = std::find(listeners_.cbegin(), listeners_.cend(), listener);
  if (it == listeners_.cend()) {
    return;
  }
  // Actualize the next listener indices after the erasure from the vector.
  ListenerIterationContext* iteration_context =
      innermost_listener_iteration_context_;
  if (iteration_context) {
    size_t existing_index = size_t(std::distance(listeners_.cbegin(), it));
    while (iteration_context) {
      if (iteration_context->next_index > existing_index) {
        --iteration_context->next_index;
      }
      iteration_context = iteration_context->outer_context;
    }
  }
  listeners_.erase(it);
}

void Window::AddInputListener(WindowInputListener* listener, size_t z_order) {
  assert_not_null(listener);
  // Check if already added.
  for (auto it_existing = input_listeners_.rbegin();
       it_existing != input_listeners_.rend(); ++it_existing) {
    if (it_existing->second != listener) {
      continue;
    }
    if (it_existing->first == z_order) {
      return;
    }
    // If removing the listener that is the next in a current listener loop,
    // skip it (in a multimap, only one element iterator is invalidated).
    InputListenerIterationContext* iteration_context =
        innermost_input_listener_iteration_context_;
    while (iteration_context) {
      if (iteration_context->next_iterator == it_existing) {
        ++iteration_context->next_iterator;
      }
      iteration_context = iteration_context->outer_context;
    }
    input_listeners_.erase(std::prev(it_existing.base()));
    // FIXME(Triang3l): Changing the Z order of an existing listener while
    // already executing the listeners may cause listeners to be called twice if
    // the Z order is lowered from one that has already been processed to one
    // below the current one. Because nested listener calls are supported, a
    // single last call index can't be associated with a listener to skip it if
    // calling twice for the same event (a "not equal to" comparison of the call
    // indices will result in the skipping being cancelled in the outer loop if
    // an inner one is done, a "greater than" comparison will cause the inner
    // loop to effectively terminate all outer ones).
  }
  auto it_new = std::prev(
      std::make_reverse_iterator(input_listeners_.emplace(z_order, listener)));
  // If adding to layers in between the currently being processed ones (from
  // highest to lowest) and the previously next, make sure the new listener is
  // executed too. Execution within one layer, however, happens in the reverse
  // order of addition, so if adding to the Z layer currently being processed,
  // the new listener must not be executed within the loop. But, if adding to
  // the next Z layer after the current one, it must be executed immediately.
  {
    InputListenerIterationContext* iteration_context =
        innermost_input_listener_iteration_context_;
    while (iteration_context) {
      if (z_order < iteration_context->current_z_order &&
          (iteration_context->next_iterator == input_listeners_.crend() ||
           z_order >= iteration_context->next_iterator->first)) {
        iteration_context->next_iterator = it_new;
      }
      iteration_context = iteration_context->outer_context;
    }
  }
}

void Window::RemoveInputListener(WindowInputListener* listener) {
  assert_not_null(listener);
  for (auto it_existing = input_listeners_.rbegin();
       it_existing != input_listeners_.rend(); ++it_existing) {
    if (it_existing->second != listener) {
      continue;
    }
    // If removing the listener that is the next in a current listener loop,
    // skip it (in a multimap, only one element iterator is invalidated).
    InputListenerIterationContext* iteration_context =
        innermost_input_listener_iteration_context_;
    while (iteration_context) {
      if (iteration_context->next_iterator == it_existing) {
        ++iteration_context->next_iterator;
      }
      iteration_context = iteration_context->outer_context;
    }
    input_listeners_.erase(std::prev(it_existing.base()));
    return;
  }
}

bool Window::Open() {
  if (phase_ != Phase::kClosedOpenable) {
    return true;
  }

  // For consistency of the behavior of OpenImpl and the initial On*Update
  // that should be called as a result of it, reset the actual state to its
  // defaults for a closed window.
  // Note that this is performed in Open, not after closing, because there's
  // only one entry point for opening a window, while closing may be done
  // different ways - by actually closing, by destroying, or by failing to call
  // OpenImpl - instead of performing this reset in every possible close case,
  // just returning these defaults from the actual state getters if
  // HasActualState is false.
  actual_physical_width_ = 0;
  actual_physical_height_ = 0;
  has_focus_ = false;
  phase_ = Phase::kOpening;
  bool platform_open_result = OpenImpl();
  if (!platform_open_result) {
    phase_ = Phase::kClosedOpenable;
    return false;
  }
  if (phase_ != Phase::kOpening) {
    // The window was closed mid-opening.
    return true;
  }
  phase_ = Phase::kOpen;

  // Call the listeners (OnOpened with all the new state so the listeners are
  // aware that they can start interacting with the open Window, and after that,
  // in case certain listeners don't handle OnOpened, but rather, only need the
  // more granular callbacks, make sure those callbacks receive the new state
  // too) for the actual state of the new window (that may be different than the
  // desired, depending on how the platform has interpreted the desired state).
  {
    MonitorUpdateEvent e(this, true, true);
    OnMonitorUpdate(e);
  }
  {
    UISetupEvent e(this, true);
    WindowDestructionReceiver destruction_receiver(this);
    SendEventToListeners([&e](auto listener) { listener->OnOpened(e); },
                         destruction_receiver);
    if (destruction_receiver.IsWindowDestroyedOrListenersUncallable()) {
      return true;
    }
    SendEventToListeners([&e](auto listener) { listener->OnDpiChanged(e); },
                         destruction_receiver);
    if (destruction_receiver.IsWindowDestroyedOrListenersUncallable()) {
      return true;
    }
    SendEventToListeners([&e](auto listener) { listener->OnResize(e); },
                         destruction_receiver);
    if (destruction_receiver.IsWindowDestroyedOrListenersUncallable()) {
      return true;
    }
    if (HasFocus()) {
      SendEventToListeners([&e](auto listener) { listener->OnGotFocus(e); },
                           destruction_receiver);
      if (destruction_receiver.IsWindowDestroyedOrListenersUncallable()) {
        return true;
      }
    }
  }

  // May now try to create a valid surface (though the window may be in a
  // minimized state without the possibility of creating a surface, but that
  // will be resolved by the implementation), after OnDpiChanged and OnResize so
  // nothing related to painting will be making wrong assumptions about the
  // size.
  OnSurfaceChanged(true);

  return true;
}

void Window::SetFullscreen(bool new_fullscreen) {
  if (fullscreen_ == new_fullscreen) {
    return;
  }
  fullscreen_ = new_fullscreen;
  if (!CanApplyState()) {
    return;
  }
  WindowDestructionReceiver destruction_receiver(this);
  ApplyNewFullscreen();
  if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) {
    return;
  }
}

void Window::SetTitle(const std::string_view new_title) {
  if (title_ == new_title) {
    return;
  }
  title_ = new_title;
  if (!CanApplyState()) {
    return;
  }
  WindowDestructionReceiver destruction_receiver(this);
  ApplyNewTitle();
  if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) {
    return;
  }
}

void Window::SetIcon(const void* buffer, size_t size) {
  bool reset = !buffer || !size;
  WindowDestructionReceiver destruction_receiver(this);
  LoadAndApplyIcon(reset ? nullptr : buffer, reset ? 0 : size, CanApplyState());
  if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) {
    return;
  }
}

void Window::SetMainMenu(std::unique_ptr<MenuItem> new_main_menu) {
  // The primary reason for this comparison (of two unique pointers) is
  // nullptr == nullptr.
  if (main_menu_ == new_main_menu) {
    return;
  }
  // Keep the old menu object existing while it's still being detached from
  // the platform window.
  std::unique_ptr<MenuItem> old_main_menu = std::move(main_menu_);
  main_menu_ = std::move(new_main_menu);
  if (!CanApplyState()) {
    return;
  }
  WindowDestructionReceiver destruction_receiver(this);
  ApplyNewMainMenu(old_main_menu.get());
  if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) {
    return;
  }
}

void Window::CompleteMainMenuItemsUpdate() {
  if (!main_menu_ || !CanApplyState()) {
    return;
  }
  WindowDestructionReceiver destruction_receiver(this);
  CompleteMainMenuItemsUpdateImpl();
  if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) {
    return;
  }
}

void Window::SetMainMenuEnabled(bool enabled) {
  if (!main_menu_) {
    return;
  }
  // Not comparing to the actual enabled state, it's a part of the MenuItem, not
  // the Window.
  // In case enabling (or even disabling) causes menu-related events (like
  // pressing) that may execute callbacks potentially destroying the Window via
  // the outer architecture.
  WindowDestructionReceiver destruction_receiver(this);
  main_menu_->SetEnabled(enabled);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
  // Modifying the state of the items, notify the implementation so it makes the
  // displaying of the main menu consistent.
  CompleteMainMenuItemsUpdate();
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::CaptureMouse() {
  ++mouse_capture_request_count_;
  if (!CanApplyState()) {
    return;
  }
  WindowDestructionReceiver destruction_receiver(this);
  // Call even if capturing while the mouse is already assumed to be captured,
  // in case something has released it in the OS.
  ApplyNewMouseCapture();
  if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) {
    return;
  }
}

void Window::ReleaseMouse() {
  assert_not_zero(mouse_capture_request_count_);
  if (!mouse_capture_request_count_) {
    return;
  }
  if (--mouse_capture_request_count_) {
    return;
  }
  if (!CanApplyState()) {
    return;
  }
  WindowDestructionReceiver destruction_receiver(this);
  ApplyNewMouseRelease();
  if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) {
    return;
  }
}

void Window::SetCursorVisibility(CursorVisibility new_cursor_visibility) {
  if (cursor_visibility_ == new_cursor_visibility) {
    return;
  }
  CursorVisibility old_cursor_visibility = cursor_visibility_;
  cursor_visibility_ = new_cursor_visibility;
  if (!CanApplyState()) {
    return;
  }
  WindowDestructionReceiver destruction_receiver(this);
  ApplyNewCursorVisibility(old_cursor_visibility);
  if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) {
    return;
  }
}

void Window::Focus() {
  if (!CanApplyState() || has_focus_) {
    return;
  }
  WindowDestructionReceiver destruction_receiver(this);
  FocusImpl();
  if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) {
    return;
  }
}

void Window::SetPresenter(Presenter* presenter) {
  if (presenter_ == presenter) {
    return;
  }
  if (presenter_) {
    presenter_->SetWindowSurfaceFromUIThread(nullptr, nullptr);
    presenter_surface_.reset();
  }
  presenter_ = presenter;
  if (presenter_) {
    presenter_surface_ = CreateSurface(presenter_->GetSupportedSurfaceTypes());
    presenter_->SetWindowSurfaceFromUIThread(this, presenter_surface_.get());
  }
}

void Window::OnSurfaceChanged(bool new_surface_potentially_exists) {
  if (!presenter_) {
    return;
  }

  // Detach the presenter from the old surface before attaching to the new one.
  if (presenter_surface_) {
    presenter_->SetWindowSurfaceFromUIThread(this, nullptr);
    presenter_surface_.reset();
  }

  if (!new_surface_potentially_exists) {
    return;
  }

  presenter_surface_ = CreateSurface(presenter_->GetSupportedSurfaceTypes());
  if (presenter_surface_) {
    presenter_->SetWindowSurfaceFromUIThread(this, presenter_surface_.get());
  }
}

void Window::OnBeforeClose(WindowDestructionReceiver& destruction_receiver) {
  // Because events are not sent from closed windows, and to make sure the
  // window isn't closed while its surface is still attached to the presenter,
  // this must be called before doing what constitutes closing in the platform
  // implementation, not after.

  bool was_open = phase_ == Phase::kOpen;
  bool was_open_or_opening = was_open || phase_ == Phase::kOpening;
  assert_true(was_open_or_opening);
  if (!was_open_or_opening) {
    return;
  }

  if (was_open) {
    phase_ = Phase::kOpenBeforeClosing;

    // If the window was focused, notify the listeners that focus is being lost
    // because the window is being closed (the implementation usually wouldn't
    // be sending any events to the listeners after OnClosing even if the OS
    // actually sends the focus loss event as part of the closing process).
    OnFocusUpdate(false, destruction_receiver);
    if (destruction_receiver.IsWindowDestroyed()) {
      return;
    }

    {
      UIEvent e(this);
      SendEventToListeners([&e](auto listener) { listener->OnClosing(e); },
                           destruction_receiver);
      if (destruction_receiver.IsWindowDestroyed()) {
        return;
      }
    }

    // Disconnect from the surface without connecting to the new one (after the
    // listeners so they don't try to reconnect afterwards).
    OnSurfaceChanged(false);
  }

  phase_ = Phase::kClosing;
}

void Window::OnAfterClose() {
  assert_true(phase_ == Phase::kClosing);
  if (phase_ != Phase::kClosing) {
    return;
  }
  phase_ = (innermost_listener_iteration_context_ ||
            innermost_input_listener_iteration_context_)
               ? Phase::kClosedLeavingListeners
               : Phase::kClosedOpenable;
}

void Window::OnDpiChanged(UISetupEvent& e,
                          WindowDestructionReceiver& destruction_receiver) {
  SendEventToListeners([&e](auto listener) { listener->OnDpiChanged(e); },
                       destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::OnMonitorUpdate(MonitorUpdateEvent& e) {
  if (presenter_surface_) {
    presenter_->OnSurfaceMonitorUpdateFromUIThread(
        e.old_monitor_potentially_disconnected());
  }
}

bool Window::OnActualSizeUpdate(
    uint32_t new_physical_width, uint32_t new_physical_height,
    WindowDestructionReceiver& destruction_receiver) {
  if (actual_physical_width_ == new_physical_width &&
      actual_physical_height_ == new_physical_height) {
    return false;
  }
  actual_physical_width_ = new_physical_width;
  actual_physical_height_ = new_physical_height;
  // The listeners may reference the presenter, update the presenter first.
  if (presenter_surface_) {
    presenter_->OnSurfaceResizeFromUIThread();
  }
  UISetupEvent e(this);
  SendEventToListeners([&e](auto listener) { listener->OnResize(e); },
                       destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return true;
  }
  return true;
}

void Window::OnFocusUpdate(bool new_has_focus,
                           WindowDestructionReceiver& destruction_receiver) {
  if (has_focus_ == new_has_focus) {
    return;
  }
  has_focus_ = new_has_focus;
  UISetupEvent e(this);
  if (has_focus_) {
    SendEventToListeners([&e](auto listener) { listener->OnGotFocus(e); },
                         destruction_receiver);
  } else {
    SendEventToListeners([&e](auto listener) { listener->OnLostFocus(e); },
                         destruction_receiver);
  }
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::OnPaint(bool force_paint) {
  if (is_painting_) {
    return;
  }
  is_painting_ = true;
  if (presenter_surface_) {
    presenter_->PaintFromUIThread(force_paint);
  }
  is_painting_ = false;
}

void Window::OnFileDrop(FileDropEvent& e,
                        WindowDestructionReceiver& destruction_receiver) {
  SendEventToListeners([&e](auto listener) { listener->OnFileDrop(e); },
                       destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::OnKeyDown(KeyEvent& e,
                       WindowDestructionReceiver& destruction_receiver) {
  PropagateEventThroughInputListeners(
      [&e](auto listener) {
        listener->OnKeyDown(e);
        return e.is_handled();
      },
      destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::OnKeyUp(KeyEvent& e,
                     WindowDestructionReceiver& destruction_receiver) {
  PropagateEventThroughInputListeners(
      [&e](auto listener) {
        listener->OnKeyUp(e);
        return e.is_handled();
      },
      destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::OnKeyChar(KeyEvent& e,
                       WindowDestructionReceiver& destruction_receiver) {
  PropagateEventThroughInputListeners(
      [&e](auto listener) {
        listener->OnKeyChar(e);
        return e.is_handled();
      },
      destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::OnMouseDown(MouseEvent& e,
                         WindowDestructionReceiver& destruction_receiver) {
  PropagateEventThroughInputListeners(
      [&e](auto listener) {
        listener->OnMouseDown(e);
        return e.is_handled();
      },
      destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::OnMouseMove(MouseEvent& e,
                         WindowDestructionReceiver& destruction_receiver) {
  PropagateEventThroughInputListeners(
      [&e](auto listener) {
        listener->OnMouseMove(e);
        return e.is_handled();
      },
      destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::OnMouseUp(MouseEvent& e,
                       WindowDestructionReceiver& destruction_receiver) {
  PropagateEventThroughInputListeners(
      [&e](auto listener) {
        listener->OnMouseUp(e);
        return e.is_handled();
      },
      destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::OnMouseWheel(MouseEvent& e,
                          WindowDestructionReceiver& destruction_receiver) {
  PropagateEventThroughInputListeners(
      [&e](auto listener) {
        listener->OnMouseWheel(e);
        return e.is_handled();
      },
      destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::OnTouchEvent(TouchEvent& e,
                          WindowDestructionReceiver& destruction_receiver) {
  PropagateEventThroughInputListeners(
      [&e](auto listener) {
        listener->OnTouchEvent(e);
        return e.is_handled();
      },
      destruction_receiver);
  if (destruction_receiver.IsWindowDestroyed()) {
    return;
  }
}

void Window::SendEventToListeners(
    std::function<void(WindowListener*)> fn,
    WindowDestructionReceiver& destruction_receiver) {
  if (!CanSendEventsToListeners()) {
    return;
  }
  ListenerIterationContext iteration_context(
      innermost_listener_iteration_context_);
  innermost_listener_iteration_context_ = &iteration_context;
  while (iteration_context.next_index < listeners_.size()) {
    // iteration_context.next_index may be changed during the execution of the
    // listener if the list of the listeners is modified by it - don't assume
    // that after the call iteration_context.next_index will be the same as
    // iteration_context.next_index + 1 before it.
    fn(listeners_[iteration_context.next_index++]);
    if (destruction_receiver.IsWindowDestroyed()) {
      // The window was destroyed by the listener, can't access anything in
      // *this anymore, including innermost_listener_iteration_context_ which
      // has to be left in an indeterminate state.
      return;
    }
    if (!CanSendEventsToListeners()) {
      // The listener has put the window in a state in which the window can't
      // send events anymore.
      break;
    }
  }
  assert_true(innermost_listener_iteration_context_ == &iteration_context);
  innermost_listener_iteration_context_ =
      innermost_listener_iteration_context_->outer_context;
  if (phase_ == Phase::kClosedLeavingListeners &&
      !innermost_listener_iteration_context_ &&
      !innermost_input_listener_iteration_context_) {
    phase_ = Phase::kClosedOpenable;
  }
}

void Window::PropagateEventThroughInputListeners(
    std::function<bool(WindowInputListener*)> fn,
    WindowDestructionReceiver& destruction_receiver) {
  if (!CanSendEventsToListeners()) {
    return;
  }
  InputListenerIterationContext iteration_context(
      innermost_input_listener_iteration_context_, input_listeners_.crbegin());
  innermost_input_listener_iteration_context_ = &iteration_context;
  while (iteration_context.next_iterator != input_listeners_.crend()) {
    // The current iterator may be invalidated, and
    // iteration_context.next_iterator may be changed, during the execution of
    // the listener if the list of the listeners is modified by it - don't
    // assume that after the call iteration_context.next_iterator will be the
    // same as std::next(iteration_context.next_iterator) before it.
    iteration_context.current_z_order = iteration_context.next_iterator->first;
    bool event_handled = fn((iteration_context.next_iterator++)->second);
    if (destruction_receiver.IsWindowDestroyed()) {
      // The window was destroyed by the listener, can't access anything in
      // *this anymore, including innermost_listener_iteration_context_ which
      // has to be left in an indeterminate state.
      return;
    }
    if (event_handled) {
      break;
    }
    if (!CanSendEventsToListeners()) {
      // The listener has put the window in a state in which the window can't
      // send events anymore.
      break;
    }
  }
  assert_true(innermost_input_listener_iteration_context_ ==
              &iteration_context);
  innermost_input_listener_iteration_context_ =
      innermost_input_listener_iteration_context_->outer_context;
  if (phase_ == Phase::kClosedLeavingListeners &&
      !innermost_listener_iteration_context_ &&
      !innermost_input_listener_iteration_context_) {
    phase_ = Phase::kClosedOpenable;
  }
}

}  // namespace ui
}  // namespace xe
