import * as path from 'path';
import * as url from 'url';
import * as fse from 'fs-extra';

import { AnimationQueue } from '../utils/animation-queue';
import {
  ElectronNotificationOptions,
  DefaultElectronNotificationOptions,
} from '../models/electron-notification-options';
import { screen, BrowserWindow, ipcMain, IpcMainEvent, shell } from 'electron';
import { ElectronNotification } from '../models/electron-notification';
import { ElectronNotificationEventType } from '../types/event.type';
import { Channel } from '../types/channel.type';
import { ElectronWebContentsChannel } from '@ucap-webmessenger/electron-core';

const onClickElectronNotification = 'onClickElectronNotification';
const onCloseElectronNotification = 'onCloseElectronNotification';

interface ENPoint {
  x: number;
  y: number;
}

interface ENDimension {
  width: number;
  height: number;
}

export class ElectronNotificationService {
  private animationQueue: AnimationQueue;
  private customOptions: ElectronNotificationOptions;
  private nextInsertPosition: ENPoint;
  private totalDimension: ENDimension;
  private firstPosition: ENPoint;
  private lowerRightCornerPosition: ENPoint;
  private maxVisibleNotifications: number;
  private activeNotifications: BrowserWindow[];
  private inactiveWindows: BrowserWindow[];
  private notificationQueue: ElectronNotification[];
  private closedNotifications: Map<number, boolean>;
  private latestId: number;
  private templateUrl: string;

  constructor(options?: ElectronNotificationOptions) {
    this.customOptions = {
      ...DefaultElectronNotificationOptions,
    };
    if (!!options) {
      this.customOptions = {
        ...this.customOptions,
        ...options,
      };
    }

    this.setup();
    this.setupEvents();
  }

  set options(options: ElectronNotificationOptions) {
    if (!!options) {
      this.customOptions = {
        ...this.customOptions,
        ...options,
      };
    }
    this.calcDimensions();
  }

  get options(): ElectronNotificationOptions {
    return this.customOptions;
  }

  set templatePath(templatePath: string) {
    if (!!templatePath) {
      this.customOptions.templatePath = templatePath;
      this.updateTemplatePath();
    }
  }

  get templatePath(): string {
    if (!this.templateUrl) {
      this.updateTemplatePath();
    }
    return this.templateUrl;
  }

  notify(notification: ElectronNotification): number {
    notification.id = this.latestId++;
    this.animationQueue.push({
      context: this,
      func: this.showNotification,
      args: [notification],
    });
    return notification.id;
  }

  dispose(): void {
    this.animationQueue.clear();
    this.activeNotifications.forEach(window => window.close());
    this.inactiveWindows.forEach(window => window.close());
  }

  closeAll(): void {
    this.animationQueue.clear();
    this.activeNotifications.forEach(window => window.close());
    this.inactiveWindows.forEach(window => window.close());

    this.setup();
  }

  private setup(): void {
    this.nextInsertPosition = { x: 0, y: 0 };
    this.totalDimension = { width: 0, height: 0 };
    this.firstPosition = { x: 0, y: 0 };
    this.activeNotifications = [];
    this.inactiveWindows = [];
    this.notificationQueue = [];
    this.closedNotifications = new Map();
    this.latestId = 0;

    this.animationQueue = new AnimationQueue();

    const display = screen.getPrimaryDisplay();

    this.lowerRightCornerPosition = {
      x: display.bounds.x + display.workArea.x + display.workAreaSize.width,
      y: display.bounds.y + display.workArea.y + display.workAreaSize.height,
    };

    this.calcDimensions();

    this.maxVisibleNotifications = Math.floor(
      display.workAreaSize.height / this.totalDimension.height
    );

    this.maxVisibleNotifications =
      7 < this.maxVisibleNotifications ? 7 : this.maxVisibleNotifications;
  }

  private setupEvents(): void {
    const self = this;
    ipcMain.on(
      Channel.close,
      (
        event: IpcMainEvent,
        windowId: number,
        notification: ElectronNotification
      ) => {
        const onClose = self.buildCloseNotification(
          BrowserWindow.fromId(windowId),
          notification
        );
        self.buildCloseNotificationSafely(onClose)('close');
      }
    );

    ipcMain.on(
      Channel.click,
      (
        event: IpcMainEvent,
        windowId: number,
        notification: ElectronNotification
      ) => {
        if (!!notification.url) {
          shell.openExternal(notification.url);
        }
        const notificationWindow = BrowserWindow.fromId(windowId);

        if (
          notificationWindow &&
          notificationWindow[onClickElectronNotification]
        ) {
          const onClose = self.buildCloseNotification(
            BrowserWindow.fromId(windowId),
            notification
          );
          notificationWindow[onClickElectronNotification]({
            type: ElectronNotificationEventType.Click,
            id: notification.id,
            close: self.buildCloseNotificationSafely(onClose),
          });
          delete notificationWindow[onClickElectronNotification];
        }
      }
    );
  }

  private calcDimensions() {
    this.totalDimension = {
      width: this.customOptions.width + this.customOptions.padding,
      height: this.customOptions.height + this.customOptions.padding,
    };

    this.firstPosition = {
      x: this.lowerRightCornerPosition.x - this.totalDimension.width,
      y: this.lowerRightCornerPosition.y - this.totalDimension.height,
    };

    this.nextInsertPosition = {
      x: this.firstPosition.x,
      y: this.firstPosition.y,
    };
  }

  private calcInsertPosition() {
    if (this.activeNotifications.length < this.maxVisibleNotifications) {
      this.nextInsertPosition.y =
        this.lowerRightCornerPosition.y -
        this.totalDimension.height * (this.activeNotifications.length + 1);
    }
  }

  private updateTemplatePath() {
    try {
      fse.statSync(this.customOptions.templatePath).isFile();

      this.templateUrl = url.format({
        pathname: this.customOptions.templatePath,
        protocol: 'file:',
        slashes: true,
      });
    } catch (e) {
      console.log(
        'electron-notify: Could not find template ("' +
          this.customOptions.templatePath +
          '").'
      );
      console.log(
        'electron-notify: To use a different template you need to correct the config.templatePath or simply adapt config.htmlTemplate'
      );
    }
  }

  private showNotification(notification: ElectronNotification): Promise<any> {
    const self = this;
    return new Promise<any>((resolve, reject) => {
      if (this.activeNotifications.length < this.maxVisibleNotifications) {
        self.getWindow().then(notificationWindow => {
          self.calcInsertPosition();
          notificationWindow.setPosition(
            self.nextInsertPosition.x,
            self.nextInsertPosition.y
          );
          self.activeNotifications.push(notificationWindow);

          const displayTime = !!notification.displayTime
            ? notification.displayTime
            : self.customOptions.displayTime;
          let timeoutId: any;
          const onClose = self.buildCloseNotification(
            notificationWindow,
            notification,
            () => timeoutId
          );
          const onCloseNotificationSafely = self.buildCloseNotificationSafely(
            onClose
          );
          timeoutId = setTimeout(() => {
            if (notificationWindow.isDestroyed()) {
              return;
            }
            onCloseNotificationSafely('timeout');
          }, displayTime);

          if (!!notification.onShow) {
            notification.onShow({
              type: ElectronNotificationEventType.Show,
              id: notification.id,
              close: onCloseNotificationSafely,
            });
          }

          if (!!notification.onClick) {
            notificationWindow[onClickElectronNotification] =
              notification.onClick;
          } else {
            delete notificationWindow[onClickElectronNotification];
          }

          if (!!notification.onClose) {
            notificationWindow[onCloseElectronNotification] =
              notification.onClose;
          } else {
            delete notificationWindow[onCloseElectronNotification];
          }

          notificationWindow.webContents.send(
            Channel.browserWindowSetContents,
            notification
          );
          notificationWindow.showInactive();
          resolve(notificationWindow);
        });
      } else {
        self.notificationQueue.push(notification);
        resolve();
      }
    });
  }

  private buildCloseNotification(
    notificationWindow: BrowserWindow,
    notification: ElectronNotification,
    timeoutIdFunc?: () => number
  ) {
    const self = this;
    return (e: ElectronNotificationEventType): Promise<void> => {
      if (notificationWindow.isDestroyed()) {
        return;
      }

      if (self.closedNotifications.has(notification.id)) {
        self.closedNotifications.delete(notification.id);
        return new Promise<void>(resolve => {
          resolve();
        });
      } else {
        self.closedNotifications.set(notification.id, true);
      }

      if (!!notificationWindow[onCloseElectronNotification]) {
        notificationWindow[onCloseElectronNotification]({
          type: e,
          id: notification.id,
        });
        delete notificationWindow[onCloseElectronNotification];
      }

      notificationWindow.webContents.send(Channel.reset);

      if (!!timeoutIdFunc) {
        clearTimeout(timeoutIdFunc());
      }
      const i = self.activeNotifications.indexOf(notificationWindow);
      self.activeNotifications.splice(i, 1);
      self.inactiveWindows.push(notificationWindow);

      notificationWindow.hide();
      self.checkForQueuedNotifications();

      return self.moveOneDown(i);
    };
  }

  private buildCloseNotificationSafely(
    onClose: (e: ElectronNotificationEventType) => any
  ) {
    const self = this;
    return (reason: any) => {
      if (!reason) {
        reason = 'closedByAPI';
      }
      self.animationQueue.push({
        context: self,
        func: onClose,
        args: [reason],
      });
    };
  }

  private checkForQueuedNotifications(): void {
    if (
      0 < this.notificationQueue.length &&
      this.activeNotifications.length < this.maxVisibleNotifications
    ) {
      this.animationQueue.push({
        context: this,
        func: this.showNotification,
        args: [this.notificationQueue.shift()],
      });
    }
  }

  private getWindow(): Promise<BrowserWindow> {
    const slef = this;
    return new Promise<BrowserWindow>((resolve, reject) => {
      if (0 < slef.inactiveWindows.length) {
        resolve(slef.inactiveWindows.pop());
      } else {
        const windowProperties = slef.customOptions.defaultWindow;
        windowProperties.width = slef.customOptions.width;
        windowProperties.height = slef.customOptions.height;

        const notificationWindow = new BrowserWindow(windowProperties);
        notificationWindow.setVisibleOnAllWorkspaces(true);
        notificationWindow.loadURL(slef.templatePath);
        notificationWindow.webContents.on(
          ElectronWebContentsChannel.DidFinishLoad,
          () => {
            // Done
            notificationWindow.webContents.send(
              Channel.loadConfig,
              slef.customOptions
            );
            resolve(notificationWindow);
          }
        );
        notificationWindow.webContents.on(
          ElectronWebContentsChannel.DevtoolsOpened,
          () => {
            notificationWindow.webContents.closeDevTools();
          }
        );
      }
    });
  }

  private moveOneDown(startPos: number): Promise<void> {
    const self = this;
    return new Promise<void>(async (resolve, reject) => {
      if (startPos >= self.activeNotifications.length || -1 === startPos) {
        resolve();
        return;
      }

      const aryNotificationPos: number[] = [];
      for (let i = startPos; i < self.activeNotifications.length; i++) {
        aryNotificationPos.push(i);
      }

      await Promise.all(
        aryNotificationPos.map(async index => {
          await self.moveNotificationAnimation(index);
        })
      );
      resolve();
    });
  }

  private moveNotificationAnimation(index: number): Promise<void> {
    const self = this;
    return new Promise<void>((resolve, reject) => {
      const notificationWindow = self.activeNotifications[index];
      const newY =
        self.lowerRightCornerPosition.y -
        self.totalDimension.height * (index + 1);
      const startY = notificationWindow.getPosition()[1];
      const step = (newY - startY) / self.customOptions.animationSteps;
      let curStep = 1;
      const animationInterval = setInterval(() => {
        // Abort condition
        if (curStep === self.customOptions.animationSteps) {
          notificationWindow.setPosition(self.firstPosition.x, newY);
          clearInterval(animationInterval);
          return resolve();
        }
        // Move one step down
        notificationWindow.setPosition(
          self.firstPosition.x,
          Math.trunc(startY + curStep * step)
        );
        curStep++;
      }, self.customOptions.animationStepMs);
    });
  }
}
