/* -*- 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/UIApplication.h>
#import <UIKit/UIScreen.h>
#import <UIKit/UIWindow.h>

#include "mozilla/Components.h"
#include "nsIObserverService.h"
#include "gfxPlatform.h"
#include "nsAppShell.h"
#include "nsCOMPtr.h"
#include "nsDirectoryServiceDefs.h"
#include "nsObjCExceptions.h"
#include "nsString.h"
#include "nsIAppStartup.h"
#include "nsIRollupListener.h"
#include "nsIWidget.h"
#include "nsThreadUtils.h"
#include "nsMemoryPressure.h"
#include "nsServiceManagerUtils.h"
#include "mozilla/widget/ScreenManager.h"
#include "ScreenHelperUIKit.h"
#include "mozilla/Hal.h"
#include "HeadlessScreenHelper.h"
#include "nsWindow.h"

using namespace mozilla;
using namespace mozilla::widget;

nsAppShell* nsAppShell::gAppShell = NULL;

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

// AppShellDelegate
//
// Acts as a delegate for the UIApplication

@interface AppShellDelegate : NSObject <UIApplicationDelegate> {
}
@property(strong, nonatomic) UIWindow* window;
@end

@implementation AppShellDelegate

- (BOOL)application:(UIApplication*)application
    didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
  ALOG("[AppShellDelegate application:didFinishLaunchingWithOptions:]");

  return YES;
}

- (void)applicationWillTerminate:(UIApplication*)application {
  ALOG("[AppShellDelegate applicationWillTerminate:]");
  nsAppShell::gAppShell->WillTerminate();
}

- (void)applicationDidBecomeActive:(UIApplication*)application {
  ALOG("[AppShellDelegate applicationDidBecomeActive:]");
}

- (void)applicationWillResignActive:(UIApplication*)application {
  ALOG("[AppShellDelegate applicationWillResignActive:]");
}

- (void)applicationDidReceiveMemoryWarning:(UIApplication*)application {
  ALOG("[AppShellDelegate applicationDidReceiveMemoryWarning:]");
  NS_NotifyOfMemoryPressure(MemoryPressureState::LowMemory);
}
@end

// nsAppShell implementation

NS_IMETHODIMP
nsAppShell::ResumeNative(void) { return nsBaseAppShell::ResumeNative(); }

nsAppShell::nsAppShell()
    : mAutoreleasePool(NULL),
      mDelegate(NULL),
      mCFRunLoop(NULL),
      mCFRunLoopSource(NULL),
      mRunningEventLoop(false),
      mTerminated(false),
      mNotifiedWillTerminate(false) {
  gAppShell = this;
}

nsAppShell::~nsAppShell() {
  if (mAutoreleasePool) {
    [mAutoreleasePool release];
    mAutoreleasePool = NULL;
  }

  if (mCFRunLoop) {
    if (mCFRunLoopSource) {
      ::CFRunLoopRemoveSource(mCFRunLoop, mCFRunLoopSource,
                              kCFRunLoopCommonModes);
      ::CFRelease(mCFRunLoopSource);
    }
    ::CFRelease(mCFRunLoop);
  }

  gAppShell = NULL;
}

// Init
//
// public
nsresult nsAppShell::Init() {
  mAutoreleasePool = [[NSAutoreleasePool alloc] init];

  // Add a CFRunLoopSource to the main native run loop.  The source is
  // responsible for interrupting the run loop when Gecko events are ready.

  mCFRunLoop = [[NSRunLoop currentRunLoop] getCFRunLoop];
  NS_ENSURE_STATE(mCFRunLoop);
  ::CFRetain(mCFRunLoop);

  CFRunLoopSourceContext context;
  bzero(&context, sizeof(context));
  // context.version = 0;
  context.info = this;
  context.perform = ProcessGeckoEvents;

  mCFRunLoopSource = ::CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
  NS_ENSURE_STATE(mCFRunLoopSource);

  ::CFRunLoopAddSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes);

  hal::Init();

  if (XRE_IsParentProcess()) {
    ScreenManager& screenManager = ScreenManager::GetSingleton();

    if (gfxPlatform::IsHeadless()) {
      screenManager.SetHelper(mozilla::MakeUnique<HeadlessScreenHelper>());
    } else {
      screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperUIKit>());
    }
  }

  nsresult rv = nsBaseAppShell::Init();

  nsCOMPtr<nsIObserverService> obsServ =
      mozilla::services::GetObserverService();
  if (obsServ) {
    obsServ->AddObserver(this, "profile-after-change", false);
  }

  return rv;
}

NS_IMETHODIMP nsAppShell::Observe(nsISupports* aSubject, const char* aTopic,
                                  const char16_t* aData) {
  bool removeObserver = false;
  if (!strcmp(aTopic, "profile-after-change")) {
    // Gecko on iOS follows the iOS app model where it never stops until it is
    // killed by the system or told explicitly to quit. Therefore, we should
    // *not* exit Gecko when there is no window or the last window is closed.
    // nsIAppStartup::Quit will still force Gecko to exit.
    nsCOMPtr<nsIAppStartup> appStartup = components::AppStartup::Service();
    if (appStartup) {
      appStartup->EnterLastWindowClosingSurvivalArea();
    }
    removeObserver = true;
  } else {
    return nsBaseAppShell::Observe(aSubject, aTopic, aData);
  }

  if (removeObserver) {
    nsCOMPtr<nsIObserverService> obsServ =
        mozilla::services::GetObserverService();
    if (obsServ) {
      obsServ->RemoveObserver(this, aTopic);
    }
  }
  return NS_OK;
}

// ProcessGeckoEvents
//
// The "perform" target of mCFRunLoop, called when mCFRunLoopSource is
// signalled from ScheduleNativeEventCallback.
//
// protected static
void nsAppShell::ProcessGeckoEvents(void* aInfo) {
  nsAppShell* self = static_cast<nsAppShell*>(aInfo);
  if (self->mRunningEventLoop) {
    self->mRunningEventLoop = false;
  }
  self->NativeEventCallback();
  self->Release();
}

// WillTerminate
//
// public
void nsAppShell::WillTerminate() {
  mNotifiedWillTerminate = true;
  if (mTerminated) return;
  mTerminated = true;
  // We won't get another chance to process events
  NS_ProcessPendingEvents(NS_GetCurrentThread());

  // Unless we call nsBaseAppShell::Exit() here, it might not get called
  // at all.
  nsBaseAppShell::Exit();
}

// ScheduleNativeEventCallback
//
// protected virtual
void nsAppShell::ScheduleNativeEventCallback() {
  if (mTerminated) return;

  NS_ADDREF_THIS();

  // This will invoke ProcessGeckoEvents on the main thread.
  ::CFRunLoopSourceSignal(mCFRunLoopSource);
  ::CFRunLoopWakeUp(mCFRunLoop);
}

// ProcessNextNativeEvent
//
// protected virtual
bool nsAppShell::ProcessNextNativeEvent(bool aMayWait) {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if (mTerminated) return false;

  bool wasRunningEventLoop = mRunningEventLoop;
  mRunningEventLoop = aMayWait;
  NSString* currentMode = nil;
  NSDate* waitUntil = nil;
  if (aMayWait) waitUntil = [NSDate distantFuture];
  NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];

  do {
    currentMode = [currentRunLoop currentMode];
    if (!currentMode) currentMode = NSDefaultRunLoopMode;

    if (aMayWait) {
      [currentRunLoop runMode:currentMode beforeDate:waitUntil];
    } else {
      [currentRunLoop acceptInputForMode:currentMode beforeDate:waitUntil];
    }
  } while (mRunningEventLoop);

  mRunningEventLoop = wasRunningEventLoop;

  NS_OBJC_END_TRY_IGNORE_BLOCK;

  return false;
}

// Run
//
// public
NS_IMETHODIMP
nsAppShell::Run(void) {
  ALOG("nsAppShell::Run");

  nsresult rv = NS_OK;
  if (XRE_UseNativeEventProcessing()) {
    char argv[1][4] = {"app"};
    UIApplicationMain(1, (char**)argv, nil, @"AppShellDelegate");
    // UIApplicationMain doesn't exit. :-(
  } else {
    rv = nsBaseAppShell::Run();
  }

  return rv;
}

NS_IMETHODIMP
nsAppShell::Exit(void) {
  if (mTerminated) return NS_OK;

  mTerminated = true;
  return nsBaseAppShell::Exit();
}
