import { BrowserWindow, ipcMain, Menu, app, dialog } from 'electron';
import * as path from 'path';
import * as URL from 'url';

import { Subscription, PartialObserver } from 'rxjs';

import { Emitter } from '@overflow/core/emitter';

import { encodePathAsUrl } from '@overflow/core/path';
import { registerWindowStateChangedEvents } from '../commons/model/window-state';
import { URLActionType } from '@overflow/core/parse-app-url';
import { now } from '@overflow/core/now';

import { MenuEvent } from '../commons/type';
import { LaunchState } from '../commons/model';
import { menuFromElectronMenu } from '../commons/model/app-menu';

let windowStateKeeper: any | null = null;

export class AppWindow {
  private window: Electron.BrowserWindow;
  private emitter = new Emitter();

  private _loadTime: number | null = null;
  private _rendererReadyTime: number | null = null;

  private minWidth = 960;
  private minHeight = 660;

  public constructor() {
    if (!windowStateKeeper) {
      // `electron-window-state` requires Electron's `screen` module, which can
      // only be required after the app has emitted `ready`. So require it
      // lazily.
      windowStateKeeper = require('electron-window-state');
    }

    const savedWindowState = windowStateKeeper({
      defaultWidth: this.minWidth,
      defaultHeight: this.minHeight,
    });

    const windowOptions: Electron.BrowserWindowConstructorOptions = {
      x: savedWindowState.x,
      y: savedWindowState.y,
      width: savedWindowState.width,
      height: savedWindowState.height,
      minWidth: this.minWidth,
      minHeight: this.minHeight,
      show: false,
      // This fixes subpixel aliasing on Windows
      // See https://github.com/atom/atom/commit/683bef5b9d133cb194b476938c77cc07fd05b972
      backgroundColor: '#fff',
      webPreferences: {
        // Disable auxclick event
        // See https://developers.google.com/web/updates/2016/10/auxclick
        disableBlinkFeatures: 'Auxclick',
        // Enable, among other things, the ResizeObserver
        experimentalFeatures: true,
      },
      acceptFirstMouse: true,
    };

    if (__DARWIN__) {
      windowOptions.titleBarStyle = 'hidden';
    } else if (__WIN32__) {
      windowOptions.frame = false;
    } else if (__LINUX__) {
      windowOptions.icon = path.join(__dirname, 'static', 'icon-logo.png');
    }

    this.window = new BrowserWindow(windowOptions);
    savedWindowState.manage(this.window);

    let quitting = false;
    app.on('before-quit', () => {
      quitting = true;
    });

    ipcMain.on('will-quit', (event: Electron.IpcMessageEvent) => {
      quitting = true;
      event.returnValue = true;
    });

    // on macOS, when the user closes the window we really just hide it. This
    // lets us activate quickly and keep all our interesting logic in the
    // renderer.
    if (__DARWIN__) {
      this.window.on('close', e => {
        if (!quitting) {
          e.preventDefault();
          Menu.sendActionToFirstResponder('hide:');
        }
      });
    }
  }

  public load() {
    let startLoad = 0;
    // We only listen for the first of the loading events to avoid a bug in
    // Electron/Chromium where they can sometimes fire more than once. See
    // See
    // https://github.com/desktop/desktop/pull/513#issuecomment-253028277. This
    // shouldn't really matter as in production builds loading _should_ only
    // happen once.
    this.window.webContents.once('did-start-loading', () => {
      this._rendererReadyTime = null;
      this._loadTime = null;

      startLoad = now();
    });

    this.window.webContents.once('did-finish-load', () => {
      if (process.env.NODE_ENV === 'development') {
        this.window.webContents.openDevTools();
      }

      this._loadTime = now() - startLoad;

      this.maybeEmitDidLoad();
    });

    this.window.webContents.on('did-finish-load', () => {
      this.window.webContents.setVisualZoomLevelLimits(1, 1);
    });

    this.window.webContents.on('did-fail-load', () => {
      this.window.webContents.openDevTools();
      this.window.show();
    });

    // TODO: This should be scoped by the window.
    ipcMain.once(
      'renderer-ready',
      (event: Electron.IpcMessageEvent, readyTime: number) => {
        this._rendererReadyTime = readyTime;

        this.maybeEmitDidLoad();
      }
    );

    this.window.on('focus', () => this.window.webContents.send('focus'));
    this.window.on('blur', () => this.window.webContents.send('blur'));

    registerWindowStateChangedEvents(this.window);
    // this.window.loadURL(encodePathAsUrl(__dirname, 'index.html'));

    const indexUrl = URL.format({
      pathname: path.join('//localhost:4200'),
      protocol: 'http:',
      slashes: true
    });
    this.window.loadURL(indexUrl);
  }

  /**
   * Emit the `onDidLoad` event if the page has loaded and the renderer has
   * signalled that it's ready.
   */
  private maybeEmitDidLoad() {
    if (!this.rendererLoaded) {
      return;
    }

    this.emitter.emit('did-load');
  }

  /** Is the page loaded and has the renderer signalled it's ready? */
  private get rendererLoaded(): boolean {
    return !!this.loadTime && !!this.rendererReadyTime;
  }

  public onClose(fn: () => void) {
    this.window.on('closed', fn);
  }

  /**
   * Register a function to call when the window is done loading. At that point
   * the page has loaded and the renderer has signalled that it is ready.
   */
  public onDidLoad(fn: (data?: any) => void): Subscription {
    return this.emitter.listen('did-load', fn);
  }

  public isMinimized() {
    return this.window.isMinimized();
  }

  /** Is the window currently visible? */
  public isVisible() {
    return this.window.isVisible();
  }

  public restore() {
    this.window.restore();
  }

  public focus() {
    this.window.focus();
  }

  /** Show the window. */
  public show() {
    this.window.show();
  }

  /** Send the menu event to the renderer. */
  public sendMenuEvent(name: MenuEvent) {
    this.show();

    this.window.webContents.send('menu-event', { name });
  }

  /** Send the URL action to the renderer. */
  public sendURLAction(action: URLActionType) {
    this.show();

    this.window.webContents.send('url-action', { action });
  }

  /** Send the app launch timing stats to the renderer. */
  public sendLaunchTimingStats(state: LaunchState) {
    this.window.webContents.send('launch-timing-stats', { state });
  }

  /** Send the app menu to the renderer. */
  public sendAppMenu() {
    const appMenu = Menu.getApplicationMenu();
    if (appMenu) {
      const menu = menuFromElectronMenu(appMenu);
      this.window.webContents.send('app-menu', { menu });
    }
  }

  /** Send a certificate error to the renderer. */
  public sendCertificateError(
    certificate: Electron.Certificate,
    error: string,
    url: string
  ) {
    this.window.webContents.send('certificate-error', {
      certificate,
      error,
      url,
    });
  }

  public showCertificateTrustDialog(
    certificate: Electron.Certificate,
    message: string
  ) {
    // The Electron type definitions don't include `showCertificateTrustDialog`
    // yet.
    const d = dialog as any;
    d.showCertificateTrustDialog(
      this.window,
      { certificate, message },
      () => { }
    );
  }

  /** Report the exception to the renderer. */
  public sendException(error: Error) {
    // `Error` can't be JSONified so it doesn't transport nicely over IPC. So
    // we'll just manually copy the properties we care about.
    const friendlyError = {
      stack: error.stack,
      message: error.message,
      name: error.name,
    };
    this.window.webContents.send('main-process-exception', friendlyError);
  }

  /**
   * Get the time (in milliseconds) spent loading the page.
   *
   * This will be `null` until `onDidLoad` is called.
   */
  public get loadTime(): number | null {
    return this._loadTime;
  }

  /**
   * Get the time (in milliseconds) elapsed from the renderer being loaded to it
   * signaling it was ready.
   *
   * This will be `null` until `onDidLoad` is called.
   */
  public get rendererReadyTime(): number | null {
    return this._rendererReadyTime;
  }

  public destroy() {
    this.window.destroy();
  }
}