import * as path from 'path';
import * as url from 'url';

import { app, BrowserWindow, screen, ipcMain, IpcMainEvent } from 'electron';
import windowStateKeeper from 'electron-window-state';
import { EventEmitter } from 'events';

import { now } from '../util/now';
import { registerWindowStateChangedEvents } from '../lib/window-state';
import {
  ElectronAppChannel,
  ElectronBrowserWindowChannel,
  ElectronWebContentsChannel
} from '@ucap-webmessenger/electron-core';

export class AppWindow {
  private window: BrowserWindow | null = null;

  private eventEmitter = new EventEmitter();

  // tslint:disable-next-line: variable-name
  private _loadTime: number | null = null;
  // tslint:disable-next-line: variable-name
  private _rendererReadyTime: number | null = null;

  private minWidth = 960;
  private minHeight = 660;

  public constructor(private appIconPath: string) {
    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,
      center: true,
      // 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,
        nodeIntegration: true
      },
      acceptFirstMouse: true,
      icon: this.appIconPath
    };

    if (__DARWIN__) {
      windowOptions.titleBarStyle = 'hidden';
    } else if (__WIN32__) {
      windowOptions.frame = false;
    } else if (__LINUX__) {
    }

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

    let quitting = false;
    app.on(ElectronAppChannel.BeforeQuit, () => {
      quitting = true;
    });

    ipcMain.on(ElectronAppChannel.WillQuit, (event: IpcMainEvent) => {
      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(ElectronBrowserWindowChannel.Close, e => {
        if (!quitting) {
          e.preventDefault();
        }
      });
    } else if (__WIN32__) {
      this.window.on(ElectronBrowserWindowChannel.Minimize, e => {
        if (!quitting) {
          e.preventDefault();
          this.window.hide();
        }
      });
      this.window.on(ElectronBrowserWindowChannel.Close, e => {
        if (!quitting) {
          e.preventDefault();
          this.window.hide();
        }
      });
    }

    if (__WIN32__) {
      // workaround for known issue with fullscreen-ing the app and restoring
      // is that some Chromium API reports the incorrect bounds, so that it
      // will leave a small space at the top of the screen on every other
      // maximize
      //
      // adapted from https://github.com/electron/electron/issues/12971#issuecomment-403956396
      //
      // can be tidied up once https://github.com/electron/electron/issues/12971
      // has been confirmed as resolved
      this.window.once(ElectronBrowserWindowChannel.ReadyToShow, () => {
        this.window.on(ElectronBrowserWindowChannel.Unmaximize, () => {
          setTimeout(() => {
            const bounds = this.window.getBounds();
            bounds.width += 1;
            this.window.setBounds(bounds);
            bounds.width -= 1;
            this.window.setBounds(bounds);
          }, 5);
        });
      });
    }
  }

  public load(): void {
    let startLoad = 0;

    this.window.webContents.once(
      ElectronWebContentsChannel.DidStartLoading,
      () => {
        this._rendererReadyTime = null;
        this._loadTime = null;

        startLoad = now();
      }
    );

    this.window.webContents.once(
      ElectronWebContentsChannel.DidFinishLoad,
      () => {
        this.window.webContents.setVisualZoomLevelLimits(1, 1);

        if (process.env.NODE_ENV === 'development') {
          this.window.webContents.openDevTools();
        }

        this._loadTime = now() - startLoad;
      }
    );

    this.window.webContents.on(ElectronWebContentsChannel.DidFailLoad, () => {
      this.window.webContents.openDevTools();
      this.window.show();
    });

    registerWindowStateChangedEvents(this.window);

    if (__DEV__) {
      this.window.loadURL('http://localhost:4200');
    } else {
      this.window.loadURL(
        url.format({
          pathname: path.join(
            __dirname,
            '..',
            'ucap-webmessenger-app/index.html'
          ),
          protocol: 'file:',
          slashes: true
        })
      );
    }
  }

  /** 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(ElectronBrowserWindowChannel.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: () => void): EventEmitter {
    return this.eventEmitter.on('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();
  }

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

  /**
   * 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();
  }

  public get browserWindow(): BrowserWindow | null {
    return this.window;
  }
}
