import {
  app,
  ipcMain,
  IpcMainEvent,
  Tray,
  Menu,
  shell,
  dialog,
  BrowserWindow
} from 'electron';
import path from 'path';
import fse from 'fs-extra';
import semver from 'semver';
import os from 'os';
import ChildProcess from 'child_process';

import AutoLaunch from 'auto-launch';

import { AppWindow } from './app/AppWindow';
import { now } from './util/now';
import { showUncaughtException } from './crash/show-uncaught-exception';

import {
  UpdaterChannel,
  FileChannel,
  ProcessChannel,
  IdleStateChannel,
  NotificationChannel,
  ChatChannel,
  MessengerChannel,
  MessageChannel,
  AppChannel
} from '@ucap-webmessenger/native-electron';
import { ElectronNotificationService } from '@ucap-webmessenger/electron-notification';
import { ElectronUpdateWindowService } from '@ucap-webmessenger/electron-update-window';

import { root } from './util/root';
import { FileUtil } from './lib/file-util';

import { IdleChecker } from './lib/idle-checker';
import {
  NotificationRequest,
  NotificationType,
  NativePathName
} from '@ucap-webmessenger/native';
import { ElectronAppChannel } from '@ucap-webmessenger/electron-core';

import { autoUpdater, CancellationToken } from 'electron-updater';
import log from 'electron-log';

import { RendererUpdater } from './lib/renderer-updater';
import { Storage } from './lib/storage';

const appIconPath = __LINUX__
  ? __DEV__
    ? path.join(
        __dirname,
        '../../',
        'config/build/linux/icon/daesang/',
        '256x256.png'
      )
    : path.join(__dirname, '..', '..', '/assets/icon/', '256x256.png')
  : __DEV__
  ? path.join(
      __dirname,
      '../../',
      'config/build/win/icon/daesang/',
      '16x16.ico'
    )
  : path.join(__dirname, '..', '..', '/assets/icon/', '16x16.ico');

let appWindow: AppWindow | null = null;
let appTray: Tray | null = null;

const launchTime = now();
let readyTime: number | null = null;

type OnDidLoadFn = (window: AppWindow) => void;
let onDidLoadFns: Array<OnDidLoadFn> | null = [];

let preventQuit = false;

let notificationService: ElectronNotificationService | null;
let updateWindowService: ElectronUpdateWindowService | null;
const appStorage: Storage = new Storage();

function handleUncaughtException(error: Error) {
  preventQuit = true;

  // If we haven't got a window we'll assume it's because
  // we've just launched and haven't created it yet.
  // It could also be because we're encountering an unhandled
  // exception on shutdown but that's less likely and since
  // this only affects the presentation of the crash dialog
  // it's a safe assumption to make.
  const isLaunchError = appWindow === null;

  if (appWindow) {
    appWindow.destroy();
    appWindow = null;
  }

  showUncaughtException(isLaunchError, error);
}

function getUptimeInSeconds() {
  return (now() - launchTime) / 1000;
}

process.on('uncaughtException', (error: Error) => {
  // error = withSourceMappedStack(error);
  // reportError(error, getExtraErrorContext());
  handleUncaughtException(error);
});

let isDuplicateInstance = false;
const gotSingleInstanceLock = app.requestSingleInstanceLock();
isDuplicateInstance = !gotSingleInstanceLock;
let idle: IdleChecker | null;
let rendererUpdater: RendererUpdater | undefined;

log.transports.file.level = 'debug';

let autoUpdaterCancellationToken: CancellationToken;
autoUpdater.autoDownload = false;
autoUpdater.logger = log;

const ucapMessengerLauncher = new AutoLaunch({
  name: app.name
});

app.on(ElectronAppChannel.SecondInstance, (event, args, workingDirectory) => {
  // Someone tried to run a second instance, we should focus our window.
  if (appWindow) {
    if (appWindow.isMinimized()) {
      appWindow.restore();
    }

    if (!appWindow.isVisible()) {
      appWindow.show();
    }

    appWindow.focus();
  }
});

if (isDuplicateInstance) {
  app.quit();
}

function createWindow() {
  const window = new AppWindow(appIconPath);

  if (__DEV__) {
    // const {
    //   default: installExtension,
    //   REDUX_DEVTOOLS
    // } = require('electron-devtools-installer');

    import('electron-debug').then(ed => {
      ed.default({ showDevTools: true });
    });

    import('electron-devtools-installer').then(edi => {
      const ChromeLens = {
        id: 'idikgljglpfilbhaboonnpnnincjhjkd',
        electron: '>=1.2.1'
      };

      const extensions = [edi.REDUX_DEVTOOLS, ChromeLens];

      for (const extension of extensions) {
        try {
          edi.default(extension);
        } catch (e) {
          log.error(e);
        }
      }
    });
  }

  window.onClose(() => {
    appWindow = null;
    if (!__DARWIN__ && !preventQuit) {
      app.quit();
    }
  });

  window.onDidLoad(() => {
    if (!appStorage.startupHideWindow) {
      window.show();
    }

    const fns = onDidLoadFns;
    onDidLoadFns = null;
    for (const fn of fns) {
      fn(window);
    }
  });

  window.load();

  appWindow = window;
}

function createTray() {
  appTray = new Tray(appIconPath);

  const contextMenu = Menu.buildFromTemplate([
    {
      label: '로그아웃',
      // accelerator: 'Q',
      // selector: 'terminate:',
      click: () => {
        appWindow.show();
        appWindow.browserWindow.webContents.send(MessengerChannel.Logout);
      }
    },
    {
      label: '설정',
      // accelerator: 'Q',
      // selector: 'terminate:',
      click: () => {
        appWindow.show();
        appWindow.browserWindow.webContents.send(MessengerChannel.ShowSetting);
      }
    },
    { label: '버전', submenu: [{ label: 'Ver. ' + app.getVersion() }] },
    {
      label: '종료',
      // accelerator: 'Q',
      // selector: 'terminate:',
      click: () => {
        // 메신저에 로그아웃 후 종료
        appExit();
      }
    }
  ]);
  appTray.setToolTip('DS Talk');
  appTray.setContextMenu(contextMenu);

  appTray.on('click', () => {
    appWindow.isVisible() ? appWindow.hide() : appWindow.show();
  });
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on(ElectronAppChannel.Ready, () => {
  if (isDuplicateInstance) {
    return;
  }

  readyTime = now() - launchTime;

  createWindow();

  createTray();

  notificationService = new ElectronNotificationService({
    width: 340,
    height: 100,
    padding: 0,
    borderRadius: 0,
    // appIcon: iconPath,
    displayTime: 5000,
    defaultStyleContainer: {},
    defaultStyleAppIcon: { display: 'none' },
    defaultStyleImage: {},
    defaultStyleClose: {},
    defaultStyleText: {}
  });

  notificationService.options.defaultWindow.webPreferences.preload = path.join(
    __dirname,
    'assets/notification/preload.js'
  );

  notificationService.templatePath = path.join(
    __dirname,
    'assets/notification/template.html'
  );

  updateWindowService = new ElectronUpdateWindowService({
    width: 500,
    height: 160,
    frame: false,
    skipTaskbar: true,
    alwaysOnTop: true,
    maximizable: false,
    onReady: () => {},
    onAcceptUpdate: () => {
      log.info('OnAcceptUpdate');
      autoUpdaterCancellationToken = new CancellationToken();
      autoUpdater.downloadUpdate(autoUpdaterCancellationToken);
    },
    onDenyUpdate: () => {
      log.info('OnDenyUpdate');
      updateWindowService.close();
    },
    onCancelDownload: () => {
      autoUpdaterCancellationToken.cancel();
      updateWindowService.close();
    }
  });

  updateWindowService.templatePath = path.join(
    __dirname,
    'assets/update-window/template.html'
  );

  // updateWindowService.show();

  ipcMain.on('uncaught-exception', (event: IpcMainEvent, error: Error) => {
    handleUncaughtException(error);
  });

  ipcMain.on(
    'send-error-report',
    (
      event: IpcMainEvent,
      { error, extra }: { error: Error; extra: { [key: string]: string } }
    ) => {}
  );
});

// Quit when all windows are closed.
app.on(ElectronAppChannel.WindowAllClosed, () => {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on(ElectronAppChannel.Activate, () => {
  onDidLoad(window => {
    window.show();
  });
});

function onDidLoad(fn: OnDidLoadFn) {
  if (onDidLoadFns) {
    onDidLoadFns.push(fn);
  } else {
    if (appWindow) {
      fn(appWindow);
    }
  }
}

ipcMain.on(UpdaterChannel.Apply, (event: IpcMainEvent, ...args: any[]) => {
  // if (__DEV__) {
  //   event.returnValue = false;
  //   return;
  // }

  const ver = args[0];

  if (semver.lt(app.getVersion(), ver)) {
    autoUpdater
      .checkForUpdatesAndNotify()
      .then(result => {})
      .catch(reason => {
        log.error(reason);
      });
  }
});

ipcMain.on(
  MessengerChannel.GetNetworkInfo,
  (event: IpcMainEvent, ...args: any[]) => {
    const interfaces = os.networkInterfaces();
    const addresses: { ip: string; mac: string }[] = [];

    // tslint:disable-next-line: forin
    for (const k in interfaces) {
      // tslint:disable-next-line: forin
      for (const k2 in interfaces[k]) {
        const address = interfaces[k][k2];
        if (address.family === 'IPv4' && !address.internal) {
          addresses.push({ ip: address.address, mac: address.mac });
        }
      }
    }

    event.returnValue = addresses;
  }
);

ipcMain.on(
  MessengerChannel.GetVersionInfo,
  (event: IpcMainEvent, ...args: any[]) => {
    event.returnValue = app.getVersion();
  }
);

ipcMain.on(
  MessengerChannel.ChangeAutoLaunch,
  (event: IpcMainEvent, ...args: any[]) => {
    const isAutoLaunch = args[0] as boolean;

    if (isAutoLaunch) {
      ucapMessengerLauncher
        .enable()
        .then(() => {
          event.returnValue = true;
          log.info('AutoLaunch is enabled');
        })
        .catch(reason => {
          event.returnValue = false;
        });
    } else {
      ucapMessengerLauncher
        .disable()
        .then(() => {
          event.returnValue = true;
          log.info('AutoLaunch is disabled');
        })
        .catch(reason => {
          event.returnValue = false;
        });
    }
  }
);

ipcMain.on(
  MessengerChannel.ChangeStartupHideWindow,
  (event: IpcMainEvent, ...args: any[]) => {
    const isStartupHideWindow = args[0] as boolean;

    appStorage.startupHideWindow = isStartupHideWindow;
    log.info(
      'StartupHideWindow is changed from ',
      !appStorage.startupHideWindow,
      ' to ',
      appStorage.startupHideWindow
    );
    event.returnValue = true;
  }
);

ipcMain.on(
  MessengerChannel.ChangeDownloadPath,
  (event: IpcMainEvent, ...args: any[]) => {
    const downloadPath = args[0] as string;

    if (!!downloadPath && downloadPath.length > 0) {
      appStorage.downloadPath = downloadPath;
      log.info('downloadPath is changed to ', appStorage.downloadPath);

      event.returnValue = appStorage.downloadPath;
    } else {
      event.returnValue = '';
    }
  }
);

ipcMain.on(
  UpdaterChannel.StartCheckInstant,
  (event: IpcMainEvent, ...args: any[]) => {
    // const config = args[0] as UpdateCheckConfig;
    // if (!!rendererUpdater) {
    //   rendererUpdater.stopCheck();
    //   rendererUpdater = null;
    // }
    // rendererUpdater = new RendererUpdater(appWindow.browserWindow, config); // default 10min
    // rendererUpdater.startCheck();
  }
);

ipcMain.on(
  UpdaterChannel.StopCheckInstant,
  (event: IpcMainEvent, ...args: any[]) => {
    // if (!!rendererUpdater) {
    //   rendererUpdater.stopCheck();
    //   rendererUpdater = null;
    // }
  }
);

ipcMain.on(
  UpdaterChannel.ApplyInstant,
  (event: IpcMainEvent, ...args: any[]) => {
    // if (!!rendererUpdater) {
    //   rendererUpdater.apply();
    // }
  }
);

ipcMain.on(FileChannel.ReadFile, (event: IpcMainEvent, ...args: any[]) => {
  const filePath = root(args[0]);
  try {
    fse.readFile(filePath, (err, data) => {
      if (!!err) {
        event.returnValue = err;
        log.error(`File read failed path[${filePath}] err:`, err);
      } else {
        event.returnValue = data;
      }
    });
  } catch (error) {
    log.error(`File read failed path[${filePath}] err:`, error);
    event.returnValue = null;
  }
});

ipcMain.on(
  FileChannel.SaveFile,
  async (event: IpcMainEvent, ...args: any[]) => {
    try {
      const buffer: Buffer = args[0];
      const fileName: string = args[1];
      const mimeType: string = args[2];
      const customSavePath: string = args[3];
      let savePath: string = path.join(
        !!appStorage.downloadPath
          ? appStorage.downloadPath
          : app.getPath('downloads'),
        fileName
      );
      if (!!customSavePath) {
        savePath = customSavePath;
      } else {
        savePath = await FileUtil.uniqueFileName(savePath);
      }

      fse.writeFile(savePath, buffer, err => {
        if (!err) {
          event.returnValue = savePath;
        } else {
          event.returnValue = undefined;
        }
      });
    } catch (error) {
      event.returnValue = undefined;
    }
  }
);

ipcMain.on(
  FileChannel.OpenFolderItem,
  async (event: IpcMainEvent, ...args: any[]) => {
    try {
      let folderItem: string = args[0];
      const make: boolean = args[1];
      if (!folderItem) {
        folderItem = app.getPath('downloads');
      }

      let isSuccess = true;
      if (make) {
        fse.ensureDirSync(folderItem);
      }

      if (isSuccess && fse.existsSync(folderItem)) {
        shell.openItem(folderItem);
      } else {
        isSuccess = false;
      }

      if (isSuccess) {
        event.returnValue = true;
      } else {
        event.returnValue = false;
      }
    } catch (error) {
      event.returnValue = false;
    }
  }
);

ipcMain.on(FileChannel.GetPath, async (event: IpcMainEvent, ...args: any[]) => {
  try {
    const name: NativePathName = args[0];
    event.returnValue = app.getPath(name);
  } catch (error) {
    event.returnValue = undefined;
  }
});

ipcMain.on(
  FileChannel.SelectDirectory,
  (event: IpcMainEvent, ...args: any[]) => {
    dialog
      .showOpenDialog(appWindow.browserWindow, {
        defaultPath: app.getPath('home'),
        properties: ['openDirectory']
      })
      .then(value => {
        event.returnValue = value.filePaths[0];
      })
      .catch(reason => {
        event.returnValue = undefined;
      });
  }
);

ipcMain.on(
  FileChannel.SelectSaveFilePath,
  (event: IpcMainEvent, ...args: any[]) => {
    const defaultPath = args[0];
    const ext = path.extname(defaultPath);
    dialog
      .showSaveDialog({
        defaultPath
      })
      .then(obj => {
        const filePath =
          '' !== ext && '' === path.extname(obj.filePath)
            ? `${obj.filePath}${ext}`
            : obj.filePath;

        event.returnValue = {
          canceled: obj.canceled,
          filePath
        };
      })
      .catch(obj => {
        event.returnValue = undefined;
      });
  }
);

ipcMain.on(ProcessChannel.Execute, (event: IpcMainEvent, ...args: any[]) => {
  try {
    const executableName: string = args[0];
    const binPath = __DEV__
      ? path.join(__dirname, '../../', 'config/build/win/bin/')
      : path.join(__dirname, '..', '..', '..', '/bin/');

    const executablePath = __WIN32__
      ? path.join(binPath, `${executableName}.exe`)
      : path.join(binPath, executableName);

    const childProcess = ChildProcess.spawn(executablePath, [], {
      stdio: ['ignore', 'ignore', 'ignore'],
      detached: true
    });

    event.returnValue = childProcess.pid;
  } catch (error) {
    event.returnValue = undefined;
  }
});

ipcMain.on(
  IdleStateChannel.StartCheck,
  (event: IpcMainEvent, ...args: any[]) => {
    if (!!idle) {
      idle.destoryChecker();
      idle = null;
    }

    idle = new IdleChecker(appWindow.browserWindow); // default 10min
    idle.startChecker();
  }
);
ipcMain.on(
  IdleStateChannel.StopCheck,
  (event: IpcMainEvent, ...args: any[]) => {
    if (!!idle) {
      idle.destoryChecker();
      idle = null;
    }
  }
);

ipcMain.on(
  IdleStateChannel.ChangeLimitTime,
  (event: IpcMainEvent, ...args: any[]) => {
    const limitTime: number = args[0];
    if (!!idle) {
      idle.destoryChecker();
      idle = null;
    }

    idle = new IdleChecker(appWindow.browserWindow);
    idle.resetIdleTime(limitTime);
  }
);

ipcMain.on(
  NotificationChannel.Notify,
  (event: IpcMainEvent, ...args: any[]) => {
    const noti: NotificationRequest = args[0];

    notificationService.notify({
      title: noti.title,
      text: noti.contents,
      image:
        noti.image ||
        path.join(__dirname, 'assets/notification/images/img_nophoto_50.png'),
      sound: noti.useSound
        ? path.join(
            'file://',
            __dirname,
            'assets/notification/sounds/messageAlarm.mp3'
          )
        : '',
      displayTime: !!noti.displayTime ? noti.displayTime : undefined,
      onClick: e => {
        appWindow.browserWindow.flashFrame(false);
        if (noti.type === NotificationType.Event) {
          appWindow.browserWindow.webContents.send(
            ChatChannel.OpenRoom,
            noti.seq
          );
        } else if (noti.type === NotificationType.Message) {
          appWindow.browserWindow.webContents.send(
            MessageChannel.OpenMessage,
            noti.seq
          );
        }
        appWindow.show();
        e.close();
      }
    });

    appWindow.browserWindow.flashFrame(true);
  }
);

ipcMain.on(
  NotificationChannel.CloseAllNotify,
  (event: IpcMainEvent, ...args: any[]) => {
    appWindow.browserWindow.flashFrame(false);
  }
);

ipcMain.on(AppChannel.Exit, (event: IpcMainEvent, ...args: any[]) => {
  appExit();
});

autoUpdater.on('checking-for-update', () => {
  log.info('Checking for update...');
});
autoUpdater.on('update-available', info => {
  log.info(info);
  log.info('Update available.');

  updateWindowService.show({
    latest: info.version,
    installed: app.getVersion()
  });
});
autoUpdater.on('update-not-available', info => {
  log.info('Update not available.');
});
autoUpdater.on('error', err => {
  log.info('Error in auto-updater. ' + err);
});
autoUpdater.on('download-progress', progressObj => {
  let logMessage = 'Download speed: ' + progressObj.bytesPerSecond;
  logMessage = logMessage + ' - Downloaded ' + progressObj.percent + '%';
  logMessage =
    logMessage + ' (' + progressObj.transferred + '/' + progressObj.total + ')';
  log.info(logMessage);

  updateWindowService.setDownloadValue(
    progressObj.transferred,
    progressObj.total
  );
});
autoUpdater.on('update-downloaded', info => {
  log.info('Update downloaded');

  updateWindowService.setDownloadComplete();

  app.removeAllListeners('window-all-closed');
  const browserWindows = BrowserWindow.getAllWindows();

  // https://github.com/electron-userland/electron-builder/issues/1604#issuecomment-372091881
  browserWindows.forEach(browserWindow => {
    browserWindow.removeAllListeners('close');
    browserWindow.removeAllListeners('closed');
  });

  setTimeout(() => {
    autoUpdater.quitAndInstall(true, true);
  }, 2000);
});

function appExit() {
  appWindow = null;
  app.exit();
}
