/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#import <UIKit/UIEvent.h>
#import <UIKit/UIKit.h>
#import <UIKit/UIGraphics.h>
#import <UIKit/UIInterface.h>
#import <UIKit/UIScreen.h>
#import <UIKit/UITapGestureRecognizer.h>
#import <UIKit/UITouch.h>
#import <UIKit/UIView.h>
#import <UIKit/UIViewController.h>
#import <UIKit/UIWindow.h>
#import <QuartzCore/QuartzCore.h>

#include <algorithm>

#include "nsWindow.h"
#include "nsAppShell.h"
#include "nsIAppWindow.h"
#include "nsIWindowWatcher.h"
#ifdef ACCESSIBILITY
#  include "nsAccessibilityService.h"
#  include "mozilla/a11y/LocalAccessible.h"
#endif

#include "nsWidgetsCID.h"
#include "nsGfxCIID.h"

#include "gfxPlatform.h"
#include "gfxQuartzSurface.h"
#include "gfxUtils.h"
#include "gfxImageSurface.h"
#include "gfxContext.h"
#include "nsObjCExceptions.h"
#include "nsQueryObject.h"
#include "nsRegion.h"
#include "nsTArray.h"
#include "TextInputHandler.h"
#include "UIKitUtils.h"

#include "mozilla/BasicEvents.h"
#include "mozilla/EventForwards.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/TouchEvents.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/gfx/Logging.h"
#include "mozilla/widget/GeckoViewSupport.h"
#ifdef ACCESSIBILITY
#  include "mozilla/a11y/MUIRootAccessibleProtocol.h"
#endif

using namespace mozilla;
using namespace mozilla::gfx;
using namespace mozilla::layers;
using namespace mozilla::widget;
using mozilla::dom::Touch;
using mozilla::widget::UIKitUtils;

#define ALOG(args...)    \
  fprintf(stderr, args); \
  fprintf(stderr, "\n")

static LayoutDeviceIntPoint UIKitPointsToDevPixels(CGPoint aPoint,
                                                   CGFloat aBackingScale) {
  return LayoutDeviceIntPoint(NSToIntRound(aPoint.x * aBackingScale),
                              NSToIntRound(aPoint.y * aBackingScale));
}

static CGRect DevPixelsToUIKitPoints(const LayoutDeviceIntRect& aRect,
                                     CGFloat aBackingScale) {
  return CGRectMake((CGFloat)aRect.x / aBackingScale,
                    (CGFloat)aRect.y / aBackingScale,
                    (CGFloat)aRect.width / aBackingScale,
                    (CGFloat)aRect.height / aBackingScale);
}

// Used to retain a Cocoa object for the remainder of a method's execution.
class nsAutoRetainUIKitObject {
 public:
  explicit nsAutoRetainUIKitObject(id anObject) { mObject = [anObject retain]; }
  ~nsAutoRetainUIKitObject() { [mObject release]; }

 private:
  id mObject;  // [STRONG]
};

#ifdef ACCESSIBILITY
@interface ChildView : UIView <UIKeyInput, MUIRootAccessibleProtocol> {
#else
@interface ChildView : UIView <UIKeyInput> {
#endif
 @public
  nsWindow* mGeckoChild;  // weak ref
  BOOL mWaitingForPaint;
  NSMapTable<UITouch*, NSNumber*>* mTouches;
  int mNextTouchID;
}
// sets up our view, attaching it to its owning gecko view
- (id)initWithFrame:(CGRect)inFrame geckoChild:(nsWindow*)inChild;
// Our Gecko child was Destroy()ed
- (void)widgetDestroyed;
// Tear down this ChildView
- (void)delayedTearDown;
- (void)sendMouseEvent:(EventMessage)aType
                 point:(LayoutDeviceIntPoint)aPoint
                widget:(nsWindow*)aWindow;
- (void)handleTap:(UITapGestureRecognizer*)sender;
- (BOOL)isUsingMainThreadOpenGL;
- (void)drawUsingOpenGL;
- (void)drawUsingOpenGLCallback;
- (void)sendTouchEvent:(EventMessage)aType
               touches:(NSSet*)aTouches
                widget:(nsWindow*)aWindow;
// Event handling (UIResponder)
- (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event;
- (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event;
- (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event;
- (void)touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event;

- (void)activateWindow:(NSNotification*)notification;
- (void)deactivateWindow:(NSNotification*)notification;

#ifdef ACCESSIBILITY
// MUIRootAccessible
- (BOOL)hasRepresentedView;
- (id)representedView;

// MUIAccessible
- (BOOL)isAccessibilityElement;
- (NSString*)accessibilityLabel;
- (CGRect)accessibilityFrame;
- (NSString*)accessibilityValue;
- (uint64_t)accessibilityTraits;
- (NSInteger)accessibilityElementCount;
- (nullable id)accessibilityElementAtIndex:(NSInteger)index;
- (NSInteger)indexOfAccessibilityElement:(id)element;
- (NSArray* _Nullable)accessibilityElements;
- (UIAccessibilityContainerType)accessibilityContainerType;
#endif

@end

@implementation ChildView
+ (Class)layerClass {
  return [CAEAGLLayer class];
}

- (id)initWithFrame:(CGRect)inFrame geckoChild:(nsWindow*)inChild {
  self.multipleTouchEnabled = YES;
  if ((self = [super initWithFrame:inFrame])) {
    mGeckoChild = inChild;
  }
  ALOG("[ChildView[%p] initWithFrame:] (mGeckoChild = %p)", (void*)self,
       (void*)mGeckoChild);
  self.opaque = YES;
  self.alpha = 1.0;

  UITapGestureRecognizer* tapRecognizer =
      [[UITapGestureRecognizer alloc] initWithTarget:self
                                              action:@selector(handleTap:)];
  tapRecognizer.numberOfTapsRequired = 1;
  [self addGestureRecognizer:tapRecognizer];

  mTouches = [[NSMapTable alloc] init];
  mNextTouchID = 0;

  // This is managed with weak references by the notification center so that we
  // do not need to call removeObserver.
  // https://developer.apple.com/documentation/foundation/nsnotificationcenter/1415360-addobserver#discussion
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(activateWindow:)
             name:UIWindowDidBecomeKeyNotification
           object:nil];
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(deactivateWindow:)
             name:UIWindowDidResignKeyNotification
           object:nil];

  return self;
}

- (void)widgetDestroyed {
  mGeckoChild = nullptr;
  [mTouches release];
}

- (void)delayedTearDown {
  [self removeFromSuperview];
  [self release];
}

- (void)activateWindow:(NSNotification*)notification {
  ALOG("[[ChildView[%p] activateWindow]", (void*)self);

  if (!mGeckoChild) {
    return;
  }

  if (nsIWidgetListener* listener = mGeckoChild->GetWidgetListener()) {
    listener->WindowActivated();
  }
}

- (void)deactivateWindow:(NSNotification*)notification {
  ALOG("[[ChildView[%p] deactivateWindow]", (void*)self);

  if (!mGeckoChild) {
    return;
  }

  if (nsIWidgetListener* listener = mGeckoChild->GetWidgetListener()) {
    listener->WindowDeactivated();
  }
}

- (void)sendMouseEvent:(EventMessage)aType
                 point:(LayoutDeviceIntPoint)aPoint
                widget:(nsWindow*)aWindow {
  MOZ_DIAGNOSTIC_ASSERT(
      aType != eContextMenu,
      "eContextMenu event may need to be dispatched as WidgetPointerEvent");
  WidgetMouseEvent event(true, aType, aWindow, WidgetMouseEvent::eReal);

  event.mRefPoint = aPoint;
  event.mClickCount = 1;
  event.mButton = MouseButton::ePrimary;
  event.mInputSource = mozilla::dom::MouseEvent_Binding::MOZ_SOURCE_UNKNOWN;

  nsEventStatus status;
  aWindow->DispatchEvent(&event, status);
}

- (void)handleTap:(UITapGestureRecognizer*)sender {
  if (sender.state == UIGestureRecognizerStateEnded) {
    ALOG("[ChildView[%p] handleTap]", self);
    LayoutDeviceIntPoint lp = UIKitPointsToDevPixels(
        [sender locationInView:self], [self contentScaleFactor]);
    [self sendMouseEvent:eMouseMove point:lp widget:mGeckoChild];
    [self sendMouseEvent:eMouseDown point:lp widget:mGeckoChild];
    [self sendMouseEvent:eMouseUp point:lp widget:mGeckoChild];
  }
}

- (void)sendTouchEvent:(EventMessage)aType
               touches:(NSSet*)aTouches
                widget:(nsWindow*)aWindow {
  WidgetTouchEvent event(true, aType, aWindow);
  // XXX: I think nativeEvent.timestamp * 1000 is probably usable here but
  // I don't care that much right now.
  event.mTouches.SetCapacity(aTouches.count);
  for (UITouch* touch in aTouches) {
    LayoutDeviceIntPoint loc = UIKitPointsToDevPixels(
        [touch locationInView:self], [self contentScaleFactor]);
    LayoutDeviceIntPoint radius = UIKitPointsToDevPixels(
        CGPointMake([touch majorRadius], [touch majorRadius]),
        [self contentScaleFactor]);
    NSNumber* value = [mTouches objectForKey:touch];
    if (value == nil) {
      // This shouldn't happen.
      NS_ASSERTION(false, "Got a touch that we didn't know about");
      continue;
    }
    int id = [value intValue];
    RefPtr<Touch> t = new Touch(id, loc, radius, 0.0f, 1.0f);
    event.mRefPoint = loc;
    event.mTouches.AppendElement(t);
  }
  aWindow->DispatchInputEvent(&event);
}

- (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
  ALOG("[ChildView[%p] touchesBegan", self);
  if (!mGeckoChild) return;

  for (UITouch* touch : touches) {
    [mTouches setObject:[NSNumber numberWithInt:mNextTouchID] forKey:touch];
    mNextTouchID++;
  }
  [self sendTouchEvent:eTouchStart
               touches:[event allTouches]
                widget:mGeckoChild];
}

- (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
  ALOG("[ChildView[%p] touchesCancelled", self);
  [self sendTouchEvent:eTouchCancel touches:touches widget:mGeckoChild];
  for (UITouch* touch : touches) {
    [mTouches removeObjectForKey:touch];
  }
  if (mTouches.count == 0) {
    mNextTouchID = 0;
  }
}

- (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
  ALOG("[ChildView[%p] touchesEnded", self);
  if (!mGeckoChild) return;

  [self sendTouchEvent:eTouchEnd touches:touches widget:mGeckoChild];
  for (UITouch* touch : touches) {
    [mTouches removeObjectForKey:touch];
  }
  if (mTouches.count == 0) {
    mNextTouchID = 0;
  }
}

- (void)touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
  ALOG("[ChildView[%p] touchesMoved", self);
  if (!mGeckoChild) return;

  [self sendTouchEvent:eTouchMove
               touches:[event allTouches]
                widget:mGeckoChild];
}

- (BOOL)canBecomeFirstResponder {
  if (!mGeckoChild) {
    return NO;
  }

  if (mGeckoChild->IsVirtualKeyboardDisabled()) {
    return NO;
  }
  return YES;
}

- (void)setNeedsDisplayInRect:(CGRect)aRect {
  if ([self isUsingMainThreadOpenGL]) {
    // Draw without calling drawRect. This prevent us from
    // needing to access the normal window buffer surface unnecessarily, so we
    // waste less time synchronizing the two surfaces.
    if (!mWaitingForPaint) {
      mWaitingForPaint = YES;
      // Use NSRunLoopCommonModes instead of the default NSDefaultRunLoopMode
      // so that the timer also fires while a native menu is open.
      [self performSelector:@selector(drawUsingOpenGLCallback)
                 withObject:nil
                 afterDelay:0
                    inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
    }
  }
}

- (BOOL)isUsingMainThreadOpenGL {
  if (!mGeckoChild || ![self window]) return NO;

  return NO;
}

- (void)drawUsingOpenGL {
  ALOG("drawUsingOpenGL");
  AUTO_PROFILER_LABEL("ChildView::drawUsingOpenGL", OTHER);

  if (!mGeckoChild->IsVisible()) return;

  mWaitingForPaint = NO;

  LayoutDeviceIntRect geckoBounds = mGeckoChild->GetBounds();
  LayoutDeviceIntRegion region(geckoBounds);

  mGeckoChild->PaintWindow(region);
}

// Called asynchronously after setNeedsDisplay in order to avoid entering the
// normal drawing machinery.
- (void)drawUsingOpenGLCallback {
  if (mWaitingForPaint) {
    [self drawUsingOpenGL];
  }
}

// The display system has told us that a portion of our view is dirty. Tell
// gecko to paint it
- (void)drawRect:(CGRect)aRect {
  CGContextRef cgContext = UIGraphicsGetCurrentContext();
  [self drawRect:aRect inContext:cgContext];
}

- (void)drawRect:(CGRect)aRect inContext:(CGContextRef)aContext {
#ifdef DEBUG_UPDATE
  LayoutDeviceIntRect geckoBounds = mGeckoChild->GetBounds();

  fprintf(stderr,
          "---- Update[%p][%p] [%f %f %f %f] cgc: %p\n  gecko bounds: [%d %d "
          "%d %d]\n",
          self, mGeckoChild, aRect.origin.x, aRect.origin.y, aRect.size.width,
          aRect.size.height, aContext, geckoBounds.x, geckoBounds.y,
          geckoBounds.width, geckoBounds.height);

  CGAffineTransform xform = CGContextGetCTM(aContext);
  fprintf(stderr, "  xform in: [%f %f %f %f %f %f]\n", xform.a, xform.b,
          xform.c, xform.d, xform.tx, xform.ty);
#endif

  if (true) {
    // For Gecko-initiated repaints in OpenGL mode, drawUsingOpenGL is
    // directly called from a delayed perform callback - without going through
    // drawRect.
    // Paints that come through here are triggered by something that Cocoa
    // controls, for example by window resizing or window focus changes.

    // Do GL composition and return.
    [self drawUsingOpenGL];
    return;
  }
  AUTO_PROFILER_LABEL("ChildView::drawRect", OTHER);

  // The CGContext that drawRect supplies us with comes with a transform that
  // scales one user space unit to one Cocoa point, which can consist of
  // multiple dev pixels. But Gecko expects its supplied context to be scaled
  // to device pixels, so we need to reverse the scaling.
  double scale = mGeckoChild->BackingScaleFactor();
  CGContextSaveGState(aContext);
  CGContextScaleCTM(aContext, 1.0 / scale, 1.0 / scale);

  CGSize viewSize = [self bounds].size;
  gfx::IntSize backingSize(NSToIntRound(viewSize.width * scale),
                           NSToIntRound(viewSize.height * scale));

  CGContextSaveGState(aContext);

  LayoutDeviceIntRegion region =
      LayoutDeviceIntRect(NSToIntRound(aRect.origin.x * scale),
                          NSToIntRound(aRect.origin.y * scale),
                          NSToIntRound(aRect.size.width * scale),
                          NSToIntRound(aRect.size.height * scale));

  // Create Cairo objects.
  RefPtr<gfxQuartzSurface> targetSurface;

  UniquePtr<gfxContext> targetContext;
  if (gfxPlatform::GetPlatform()->SupportsAzureContentForType(
          gfx::BackendType::CAIRO)) {
    // This is dead code unless you mess with prefs, but keep it around for
    // debugging.
    targetSurface = new gfxQuartzSurface(aContext, backingSize);
    RefPtr<gfx::DrawTarget> dt =
        gfxPlatform::CreateDrawTargetForSurface(targetSurface, backingSize);
    if (!dt || !dt->IsValid()) {
      gfxDevCrash(mozilla::gfx::LogReason::InvalidContext)
          << "Window context problem 2 " << backingSize;
      return;
    }
    targetContext = gfxContext::CreateOrNull(dt);
  } else {
    MOZ_ASSERT_UNREACHABLE("COREGRAPHICS is the only supported backend");
  }
  MOZ_ASSERT(targetContext);  // already checked for valid draw targets above

  // Set up the clip region.
  targetContext->NewPath();
  for (auto iter = region.RectIter(); !iter.Done(); iter.Next()) {
    const LayoutDeviceIntRect& r = iter.Get();
    targetContext->Rectangle(gfxRect(r.x, r.y, r.width, r.height));
  }
  targetContext->Clip();

  // nsAutoRetainCocoaObject kungFuDeathGrip(self);
  bool painted = false;
  targetContext = nullptr;
  targetSurface = nullptr;

  CGContextRestoreGState(aContext);

  // Undo the scale transform so that from now on the context is in
  // CocoaPoints again.
  CGContextRestoreGState(aContext);
  if (!painted && [self isOpaque]) {
    // Gecko refused to draw, but we've claimed to be opaque, so we have to
    // draw something--fill with white.
    CGContextSetRGBFillColor(aContext, 1, 1, 1, 1);
    CGContextFillRect(aContext, aRect);
  }

#ifdef DEBUG_UPDATE
  fprintf(stderr, "---- update done ----\n");

#  if 0
  CGContextSetRGBStrokeColor (aContext,
                            ((((unsigned long)self) & 0xff)) / 255.0,
                            ((((unsigned long)self) & 0xff00) >> 8) / 255.0,
                            ((((unsigned long)self) & 0xff0000) >> 16) / 255.0,
                            0.5);
#  endif
  CGContextSetRGBStrokeColor(aContext, 1, 0, 0, 0.8);
  CGContextSetLineWidth(aContext, 4.0);
  CGContextStrokeRect(aContext, aRect);
#endif
}

// UIKeyInput

- (void)insertText:(NSString*)text {
  if (!mGeckoChild || mGeckoChild->Destroyed()) {
    return;
  }
  widget::TextInputHandler* textInputHandler =
      mGeckoChild->GetTextInputHandler();
  if (!textInputHandler) {
    return;
  }
  textInputHandler->InsertText(text);
}

- (void)deleteBackward {
  if (!mGeckoChild || mGeckoChild->Destroyed()) {
    return;
  }
  widget::TextInputHandler* textInputHandler =
      mGeckoChild->GetTextInputHandler();
  if (!textInputHandler) {
    return;
  }
  textInputHandler->HandleCommand(Command::DeleteCharBackward);
}

- (BOOL)hasText {
  if (!mGeckoChild || mGeckoChild->Destroyed()) {
    return NO;
  }
  widget::InputContext context = mGeckoChild->GetInputContext();
  if (context.mIMEState.mEnabled == IMEEnabled::Disabled) {
    return NO;
  }
  return YES;
}

// UITextInputTraits

- (UIKeyboardType)keyboardType {
  if (!mGeckoChild || mGeckoChild->Destroyed()) {
    return UIKeyboardTypeDefault;
  }
  return UIKitUtils::GetUIKeyboardType(mGeckoChild->GetInputContext());
}

- (UIReturnKeyType)returnKeyType {
  if (!mGeckoChild || mGeckoChild->Destroyed()) {
    return UIReturnKeyDefault;
  }
  return UIKitUtils::GetUIReturnKeyType(mGeckoChild->GetInputContext());
}

- (UITextAutocapitalizationType)autocapitalizationType {
  if (!mGeckoChild || mGeckoChild->Destroyed()) {
    return UITextAutocapitalizationTypeNone;
  }
  return UIKitUtils::GetUITextAutocapitalizationType(
      mGeckoChild->GetInputContext());
}

- (UITextAutocorrectionType)autocorrectionType {
  if (!mGeckoChild || mGeckoChild->Destroyed()) {
    return UITextAutocorrectionTypeDefault;
  }

  return UIKitUtils::GetUITextAutocorrectionType(
      mGeckoChild->GetInputContext());
}

- (BOOL)isSecureTextEntry {
  if (!mGeckoChild || mGeckoChild->Destroyed()) {
    return NO;
  }
  if (mGeckoChild->GetInputContext().IsPasswordEditor()) {
    return YES;
  }
  return NO;
}

#ifdef ACCESSIBILITY
// MUIRootAccessible

- (id<MUIRootAccessibleProtocol>)accessible {
  if (!mGeckoChild) return nil;

  id<MUIRootAccessibleProtocol> nativeAccessible = nil;

  // nsAutoRetainCocoaObject kungFuDeathGrip(self);
  RefPtr<nsWindow> geckoChild(mGeckoChild);
  RefPtr<a11y::LocalAccessible> accessible = geckoChild->GetRootAccessible();
  if (!accessible) return nil;

  accessible->GetNativeInterface((void**)&nativeAccessible);

  return nativeAccessible;
}

- (BOOL)hasRepresentedView {
  return YES;
}

- (id)representedView {
  return self;
}

- (BOOL)isAccessibilityElement {
  if (!mozilla::a11y::ShouldA11yBeEnabled()) {
    return [super isAccessibilityElement];
  }

  return [[self accessible] isAccessibilityElement];
}

- (NSString*)accessibilityLabel {
  if (!mozilla::a11y::ShouldA11yBeEnabled()) {
    return [super accessibilityLabel];
  }

  return [[self accessible] accessibilityLabel];
}

- (CGRect)accessibilityFrame {
  // Use the UIView implementation here. We rely on the position of this
  // frame to place gecko bounds in the right offset.
  return [super accessibilityFrame];
}

- (NSString*)accessibilityValue {
  if (!mozilla::a11y::ShouldA11yBeEnabled()) {
    return [super accessibilityValue];
  }

  return [[self accessible] accessibilityValue];
}

- (uint64_t)accessibilityTraits {
  if (!mozilla::a11y::ShouldA11yBeEnabled()) {
    return [super accessibilityTraits];
  }

  return [[self accessible] accessibilityTraits];
}

- (NSInteger)accessibilityElementCount {
  if (!mozilla::a11y::ShouldA11yBeEnabled()) {
    return [super accessibilityElementCount];
  }

  return [[self accessible] accessibilityElementCount];
}

- (nullable id)accessibilityElementAtIndex:(NSInteger)index {
  if (!mozilla::a11y::ShouldA11yBeEnabled()) {
    return [super accessibilityElementAtIndex:index];
  }

  return [[self accessible] accessibilityElementAtIndex:index];
}

- (NSInteger)indexOfAccessibilityElement:(id)element {
  if (!mozilla::a11y::ShouldA11yBeEnabled()) {
    return [super indexOfAccessibilityElement:element];
  }

  return [[self accessible] indexOfAccessibilityElement:element];
}

- (NSArray* _Nullable)accessibilityElements {
  if (!mozilla::a11y::ShouldA11yBeEnabled()) {
    return [super accessibilityElements];
  }

  return [[self accessible] accessibilityElements];
}

- (UIAccessibilityContainerType)accessibilityContainerType {
  if (!mozilla::a11y::ShouldA11yBeEnabled()) {
    return [super accessibilityContainerType];
  }

  return [[self accessible] accessibilityContainerType];
}
#endif

@end

NS_IMPL_ISUPPORTS_INHERITED(nsWindow, nsBaseWidget, nsWindow);

nsWindow::nsWindow()
    : mNativeView(nullptr),
      mVisible(false),
      mSizeMode(nsSizeMode_Normal),
      mParent(nullptr) {}

nsWindow::~nsWindow() {
  [mNativeView widgetDestroyed];  // Safe if mNativeView is nil.
  TearDownView();                 // Safe if called twice.
}

void nsWindow::TearDownView() {
  if (!mNativeView) return;

  [mNativeView performSelectorOnMainThread:@selector(delayedTearDown)
                                withObject:nil
                             waitUntilDone:false];
  mNativeView = nil;
}

bool nsWindow::IsTopLevel() {
  return mWindowType == WindowType::TopLevel ||
         mWindowType == WindowType::Dialog ||
         mWindowType == WindowType::Invisible;
}

//
// nsIWidget
//

nsresult nsWindow::Create(nsIWidget* aParent, const LayoutDeviceIntRect& aRect,
                          widget::InitData* aInitData) {
  ALOG("nsWindow[%p]::Create %p [%d %d %d %d]", (void*)this, (void*)aParent,
       aRect.x, aRect.y, aRect.width, aRect.height);
  nsWindow* parent = (nsWindow*)aParent;

  mBounds = aRect;

  ALOG("nsWindow[%p]::Create bounds: %d %d %d %d", (void*)this, mBounds.x,
       mBounds.y, mBounds.width, mBounds.height);

  // Set defaults which can be overriden from aInitData in BaseCreate
  mWindowType = WindowType::TopLevel;
  mBorderStyle = BorderStyle::Default;

  nsBaseWidget::BaseCreate(aParent, aInitData);

  NS_ASSERTION(IsTopLevel() || parent,
               "non top level window doesn't have a parent!");

  mNativeView = [[ChildView alloc]
      initWithFrame:DevPixelsToUIKitPoints(mBounds, BackingScaleFactor())
         geckoChild:this];
  mNativeView.hidden = YES;

  if (parent) {
    parent->mChildren.AppendElement(this);
    mParent = parent;
  }

  if (parent && parent->mNativeView) {
    [parent->mNativeView addSubview:mNativeView];
  } else if (nsAppShell::gWindow) {
    [nsAppShell::gWindow.rootViewController.view addSubview:mNativeView];
  } else {
    [nsAppShell::gTopLevelViews addObject:mNativeView];
  }

  mTextInputHandler = new widget::TextInputHandler(this);

  return NS_OK;
}

void nsWindow::Destroy() {
  for (uint32_t i = 0; i < mChildren.Length(); ++i) {
    // why do we still have children?
    mChildren[i]->ClearParent();
  }

  if (mParent) mParent->mChildren.RemoveElement(this);

  if (mTextInputHandler) {
    mTextInputHandler->OnDestroyed();
  }
  mTextInputHandler = nullptr;

  [mNativeView widgetDestroyed];

  nsCOMPtr<nsIWidget> kungFuDeathGrip(this);

  nsBaseWidget::Destroy();

  // ReportDestroyEvent();

  TearDownView();

  nsBaseWidget::OnDestroy();
}

void nsWindow::Show(bool aState) {
  if (aState != mVisible) {
    mNativeView.hidden = aState ? NO : YES;
    if (aState) {
      UIView* parentView = mParent
                               ? mParent->mNativeView
                               : nsAppShell::gWindow.rootViewController.view;
      [parentView bringSubviewToFront:mNativeView];
      [mNativeView setNeedsDisplay];
    }
    mVisible = aState;
  }
}

void nsWindow::Move(double aX, double aY) {
  if (!mNativeView || (mBounds.x == aX && mBounds.y == aY)) return;

  // XXX: handle this
  // The point we have is in Gecko coordinates (origin top-left). Convert
  // it to Cocoa ones (origin bottom-left).
  mBounds.x = aX;
  mBounds.y = aY;

  mNativeView.frame = DevPixelsToUIKitPoints(mBounds, BackingScaleFactor());

  if (mVisible) [mNativeView setNeedsDisplay];

  ReportMoveEvent();
}

void nsWindow::Resize(double aX, double aY, double aWidth, double aHeight,
                      bool aRepaint) {
  BOOL isMoving = (mBounds.x != aX || mBounds.y != aY);
  BOOL isResizing = (mBounds.width != aWidth || mBounds.height != aHeight);
  if (!mNativeView || (!isMoving && !isResizing)) return;

  if (isMoving) {
    mBounds.x = aX;
    mBounds.y = aY;
  }
  if (isResizing) {
    mBounds.width = aWidth;
    mBounds.height = aHeight;
  }

  [mNativeView setFrame:DevPixelsToUIKitPoints(mBounds, BackingScaleFactor())];

  if (mVisible && aRepaint) [mNativeView setNeedsDisplay];

  if (isMoving) ReportMoveEvent();

  if (isResizing) ReportSizeEvent();
}

void nsWindow::Resize(double aWidth, double aHeight, bool aRepaint) {
  if (!mNativeView || (mBounds.width == aWidth && mBounds.height == aHeight))
    return;

  mBounds.width = aWidth;
  mBounds.height = aHeight;

  [mNativeView setFrame:DevPixelsToUIKitPoints(mBounds, BackingScaleFactor())];

  if (mVisible && aRepaint) [mNativeView setNeedsDisplay];

  ReportSizeEvent();
}

void nsWindow::SetSizeMode(nsSizeMode aMode) {
  if (aMode == static_cast<int32_t>(mSizeMode)) {
    return;
  }

  mSizeMode = static_cast<nsSizeMode>(aMode);
  if (aMode == nsSizeMode_Maximized || aMode == nsSizeMode_Fullscreen) {
    // Resize to fill screen
    nsBaseWidget::InfallibleMakeFullScreen(true);
  }
  ReportSizeModeEvent(aMode);
}

void nsWindow::Invalidate(const LayoutDeviceIntRect& aRect) {
  if (!mNativeView || !mVisible) return;

  [mNativeView setNeedsLayout];
  [mNativeView setNeedsDisplayInRect:DevPixelsToUIKitPoints(
                                         mBounds, BackingScaleFactor())];
}

void nsWindow::SetFocus(Raise, mozilla::dom::CallerType) {
  [[mNativeView window] makeKeyWindow];
  [mNativeView becomeFirstResponder];
}

void nsWindow::WillPaintWindow() {
  if (mWidgetListener) {
    mWidgetListener->WillPaintWindow(this);
  }
}

bool nsWindow::PaintWindow(LayoutDeviceIntRegion aRegion) {
  if (!mWidgetListener) return false;

  bool returnValue = false;
  returnValue = mWidgetListener->PaintWindow(this, aRegion);

  if (mWidgetListener) {
    mWidgetListener->DidPaintWindow();
  }

  return returnValue;
}

void nsWindow::ReportMoveEvent() { NotifyWindowMoved(mBounds.x, mBounds.y); }

void nsWindow::ReportSizeModeEvent(nsSizeMode aMode) {
  if (mWidgetListener) {
    // This is terrible.
    nsSizeMode theMode;
    switch (aMode) {
      case nsSizeMode_Maximized:
        theMode = nsSizeMode_Maximized;
        break;
      case nsSizeMode_Fullscreen:
        theMode = nsSizeMode_Fullscreen;
        break;
      default:
        return;
    }
    mWidgetListener->SizeModeChanged(theMode);
  }
}

void nsWindow::ReportSizeEvent() {
  LayoutDeviceIntRect innerBounds = GetClientBounds();

  if (mWidgetListener) {
    mWidgetListener->WindowResized(this, innerBounds.width, innerBounds.height);
  }

  if (mAttachedWidgetListener) {
    mAttachedWidgetListener->WindowResized(this, innerBounds.width,
                                           innerBounds.height);
  }
}

LayoutDeviceIntRect nsWindow::GetScreenBounds() {
  return LayoutDeviceIntRect(WidgetToScreenOffset(), mBounds.Size());
}

LayoutDeviceIntPoint nsWindow::WidgetToScreenOffset() {
  LayoutDeviceIntPoint offset(0, 0);
  if (mParent) {
    offset = mParent->WidgetToScreenOffset();
  }

  CGPoint temp = [mNativeView convertPoint:temp toView:nil];

  if (!mParent && nsAppShell::gWindow) {
    // convert to screen coords
    temp = [nsAppShell::gWindow convertPoint:temp toWindow:nil];
  }

  offset.x += static_cast<int32_t>(temp.x);
  offset.y += static_cast<int32_t>(temp.y);

  return offset;
}

nsresult nsWindow::DispatchEvent(mozilla::WidgetGUIEvent* aEvent,
                                 nsEventStatus& aStatus) {
  aStatus = nsEventStatus_eIgnore;
  nsCOMPtr<nsIWidget> kungFuDeathGrip(aEvent->mWidget);
  mozilla::Unused << kungFuDeathGrip;  // Not used within this function

  if (mAttachedWidgetListener) {
    aStatus = mAttachedWidgetListener->HandleEvent(aEvent, mUseAttachedEvents);
  } else if (mWidgetListener) {
    aStatus = mWidgetListener->HandleEvent(aEvent, mUseAttachedEvents);
  }

  return NS_OK;
}

void nsWindow::SetInputContext(const InputContext& aContext,
                               const InputContextAction& aAction) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  const bool changingEnabledState =
      aContext.IsInputAttributeChanged(mInputContext);

  mInputContext = aContext;

  if (IsVirtualKeyboardDisabled()) {
    [mNativeView resignFirstResponder];
    return;
  }

  [mNativeView becomeFirstResponder];

  if (aAction.UserMightRequestOpenVKB() || changingEnabledState) {
    // TODO(m_kato):
    // It is unnecessary to call reloadInputViews with changingEnabledState if
    // virtual keyboard is disappeared.
    [mNativeView reloadInputViews];
  }

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

widget::InputContext nsWindow::GetInputContext() {
  if (!mTextInputHandler) {
    InputContext context;
    context.mIMEState.mEnabled = IMEEnabled::Disabled;
    context.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
    return context;
  }
  return mInputContext;
}

widget::TextEventDispatcherListener*
nsWindow::GetNativeTextEventDispatcherListener() {
  return mTextInputHandler;
}

bool nsWindow::IsVirtualKeyboardDisabled() const {
  return mInputContext.mIMEState.mEnabled == IMEEnabled::Disabled ||
         mInputContext.mHTMLInputMode.EqualsLiteral("none");
}

void nsWindow::SetBackgroundColor(const nscolor& aColor) {
  mNativeView.backgroundColor = [UIColor colorWithRed:NS_GET_R(aColor)
                                                green:NS_GET_G(aColor)
                                                 blue:NS_GET_B(aColor)
                                                alpha:NS_GET_A(aColor)];
}

void* nsWindow::GetNativeData(uint32_t aDataType) {
  void* retVal = nullptr;

  switch (aDataType) {
    case NS_NATIVE_WIDGET:
      retVal = (void*)mNativeView;
      break;

    case NS_NATIVE_WINDOW:
      retVal = [mNativeView window];
      break;

    case NS_NATIVE_GRAPHIC:
      NS_ERROR("Requesting NS_NATIVE_GRAPHIC on a UIKit child view!");
      break;

    case NS_NATIVE_OFFSETX:
      retVal = 0;
      break;

    case NS_NATIVE_OFFSETY:
      retVal = 0;
      break;

    case NS_RAW_NATIVE_IME_CONTEXT:
      retVal = GetPseudoIMEContext();
      if (retVal) {
        break;
      }
      retVal = NS_ONLY_ONE_NATIVE_IME_CONTEXT;
      break;
  }

  return retVal;
}

CGFloat nsWindow::BackingScaleFactor() {
  if (mNativeView) {
    return [mNativeView contentScaleFactor];
  }
  return [UIScreen mainScreen].scale;
}

int32_t nsWindow::RoundsWidgetCoordinatesTo() {
  if (BackingScaleFactor() == 2.0) {
    return 2;
  }
  return 1;
}

EventDispatcher* nsWindow::GetEventDispatcher() const {
  if (mIOSView) {
    return mIOSView->mEventDispatcher;
  }
  return nullptr;
}

already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() {
  nsCOMPtr<nsIWidget> window = new nsWindow();
  return window.forget();
}

already_AddRefed<nsIWidget> nsIWidget::CreateChildWindow() {
  nsCOMPtr<nsIWidget> window = new nsWindow();
  return window.forget();
}

/* static */
already_AddRefed<nsWindow> nsWindow::From(nsPIDOMWindowOuter* aDOMWindow) {
  nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(aDOMWindow);
  return From(widget);
}

/* static */
already_AddRefed<nsWindow> nsWindow::From(nsIWidget* aWidget) {
  RefPtr<nsWindow> window = do_QueryObject(aWidget);
  return window.forget();
}

NS_IMPL_ISUPPORTS(IOSView, nsIGeckoViewEventDispatcher, nsIGeckoViewView)

IOSView::~IOSView() { [mInitData release]; }

nsresult IOSView::GetInitData(JSContext* aCx,
                              JS::MutableHandle<JS::Value> aOut) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

@interface GeckoViewWindowImpl : NSObject <GeckoViewWindow> {
 @public
  RefPtr<nsWindow> mWindow;
  nsCOMPtr<nsPIDOMWindowOuter> mOuterWindow;
}

@end

@implementation GeckoViewWindowImpl
- (UIView*)view {
  return mWindow ? (UIView*)mWindow->GetNativeData(NS_NATIVE_WIDGET) : nil;
}

- (void)close {
  if (mWindow) {
    if (IOSView* iosView = mWindow->GetIOSView()) {
      iosView->mEventDispatcher->Detach();
    }
    mWindow = nullptr;
  }

  if (mOuterWindow) {
    mOuterWindow->ForceClose();
    mOuterWindow = nullptr;
  }
}
@end

id<GeckoViewWindow> GeckoViewOpenWindow(NSString* aId,
                                        id<SwiftEventDispatcher> aDispatcher,
                                        id aInitData, bool aPrivateMode) {
  MOZ_ASSERT(NS_IsMainThread());

  AUTO_PROFILER_LABEL("GeckoViewOpenWindows", OTHER);

  nsCOMPtr<nsIWindowWatcher> ww = do_GetService(NS_WINDOWWATCHER_CONTRACTID);
  MOZ_RELEASE_ASSERT(ww);

  nsAutoCString url;
  nsresult rv = Preferences::GetCString("toolkit.defaultChromeURI", url);
  if (NS_FAILED(rv)) {
    url = "chrome://geckoview/content/geckoview.xhtml"_ns;
  }

  // Prepare an nsIGeckoViewView to pass as argument to the window.
  RefPtr<IOSView> iosView = new IOSView();
  iosView->mEventDispatcher->Attach(aDispatcher);
  iosView->mInitData = [aInitData retain];

  nsAutoCString chromeFlags("chrome,dialog=0,remote,resizable,scrollbars");
  if (aPrivateMode) {
    chromeFlags += ",private";
  }

  nsCOMPtr<mozIDOMWindowProxy> domWindow;
  ww->OpenWindow(
      nullptr, url,
      nsDependentCString([aId UTF8String],
                         [aId lengthOfBytesUsingEncoding:NSUTF8StringEncoding]),
      chromeFlags, iosView, getter_AddRefs(domWindow));
  MOZ_RELEASE_ASSERT(domWindow);

  nsCOMPtr<nsPIDOMWindowOuter> pdomWindow = nsPIDOMWindowOuter::From(domWindow);
  const RefPtr<nsWindow> window = nsWindow::From(pdomWindow);
  MOZ_ASSERT(window);

  window->SetIOSView(iosView.forget());

  if (nsIWidgetListener* widgetListener = window->GetWidgetListener()) {
    nsCOMPtr<nsIAppWindow> appWindow(widgetListener->GetAppWindow());
    if (appWindow) {
      // Our window is not intrinsically sized, so tell AppWindow to
      // not set a size for us.
      appWindow->SetIntrinsicallySized(false);
    }
  }

  GeckoViewWindowImpl* gvWindow = [[GeckoViewWindowImpl alloc] init];
  gvWindow->mOuterWindow = pdomWindow;
  gvWindow->mWindow = window;
  return [gvWindow autorelease];
}
