text: 'use strict';
  
  (() => {
    const isMac = process.platform === 'darwin';
    const defaultFetchIntervalSec = 600;
    const notieDisplaySec = 1.5;
    const colorIconFilename64 = 'redmine_icon_color_64.png';
    const blackIconFilename24 = 'redmine_icon_black_24.png';
    const blackIconFilename24Notification = 'redmine_icon_black_24_notification.png';
    const colorIconFilename24 = 'redmine_icon_color_24.png';
    const colorIconFilename24Notification = 'redmine_icon_color_24_notification.png';
    const fetchMode = Object.freeze({time: 'TIME', date: 'DATE'});
  
    const {remote} = require('electron');
    const {app, dialog, shell, Menu, Tray} = remote;
    const fs = require('fs');
    const notie = require('notie');
  
    const appName = app.getName();
    const appCopyright = 'Copyright (c) 2015-2019 emsk';
  
    let appDir = `${__dirname}.unpacked`; // Production
    try {
      fs.statSync(appDir);
    } catch (err) {
      appDir = __dirname; // Development
    }
  
    let nodeNotifier = require('node-notifier');
    if (isMac) {
      nodeNotifier = new nodeNotifier.NotificationCenter({
        customPath: `${appDir}/custom/terminal-notifier.app/Contents/MacOS/terminal-notifier`
      });
    } else {
      nodeNotifier = new nodeNotifier.WindowsToaster({
        customPath: `${appDir}/custom/SnoreToast.exe`
      });
    }
  
    const appIconFilePath = isMac ? null : `${appDir}/images/${colorIconFilename64}`;
  
    let notifierScreen = null;
  
    /**
     * Class to check updated issues.
     */
    class RedmineNotifier {
      /**
       * Initialize the RedmineNotifier object.
       * @constructor
       * @param {number} index - Index of the object.
       */
      constructor(index) {
        this._newFlag = false;
        this._index = index;
        this._lastExecutionTime = null;
        this._settings = null;
        this._fetchTimer = null;
        this._fetchMode = null;
        this._mostRecentIssueId = null;
      }
  
      /**
       * Set flag of whether the object is new.
       * @param {boolean} newFlag - true if the object is new.
       * @return {Object} Current object.
       */
      setNewFlag(newFlag) {
        this._newFlag = newFlag;
        return this;
      }
  
      /**
       * Display the settings on the screen.
       * @return {Object} Current object.
       */
      displaySettings() {
        document.getElementById('url').value = this._settings.url;
        document.getElementById('api-key').value = this._settings.apiKey;
        document.getElementById('project-id').value = this._settings.projectId;
        document.getElementById('fetch-interval-sec').value = this._settings.fetchIntervalSec;
        return this;
      }
  
      /**
       * Get the settings from the screen.
       * @return {Object} Settings.
       */
      getPageSettings() {
        return {
          url: document.getElementById('url').value,
          apiKey: document.getElementById('api-key').value,
          projectId: document.getElementById('project-id').value,
          fetchIntervalSec: document.getElementById('fetch-interval-sec').value
        };
      }
  
      /**
       * Read the settings from the screen.
       * @return {Object} Current object.
       */
      readScreenSettings() {
        this._settings = this.getPageSettings();
        return this;
      }
  
      /**
       * Read the settings from the localStorage.
       * @return {Object} Current object.
       */
      readStoredSettings() {
        this._settings = {
          url: localStorage.getItem(`url${this._index}`),
          apiKey: localStorage.getItem(`apiKey${this._index}`),
          projectId: localStorage.getItem(`projectId${this._index}`),
          fetchIntervalSec: localStorage.getItem(`fetchIntervalSec${this._index}`)
        };
  
        return this;
      }
  
      /**
       * Get the setting from the localStorage.
       * @param {string} key - Setting key.
       * @return {string} Setting value.
       */
      getStoredSetting(key) {
        return localStorage.getItem(`${key}${this._index}`);
      }
  
      /**
       * Update the stored last execution time.
       * @return {Object} Current object.
       */
      updateLastExecutionTime() {
        this._lastExecutionTime = (new Date()).toISOString().replace(/\.\d+Z$/, 'Z');
        localStorage.setItem(`lastExecutionTime${this._index}`, this._lastExecutionTime);
        return this;
      }
  
      /**
       * Update the stored settings.
       * @return {Object} Current object.
       */
      updateSettings() {
        localStorage.setItem(`url${this._index}`, this._settings.url);
        localStorage.setItem(`apiKey${this._index}`, this._settings.apiKey);
        localStorage.setItem(`projectId${this._index}`, this._settings.projectId);
        localStorage.setItem(`fetchIntervalSec${this._index}`, this._settings.fetchIntervalSec);
        return this;
      }
  
      /**
       * Delete the settings.
       * @return {Object} Current object.
       */
      deleteStoredSettings() {
        localStorage.removeItem(`url${this._index}`);
        localStorage.removeItem(`apiKey${this._index}`);
        localStorage.removeItem(`projectId${this._index}`);
        localStorage.removeItem(`fetchIntervalSec${this._index}`);
        return this;
      }
  
      /**
       * Validate the settings.
       * @return {boolean} true if valid.
       */
      validateSettings() {
        if (this._settings.url && this._settings.apiKey) {
          return true;
        }
        notie.alert({
          type: 'error',
          text: 'Please enter required fields.',
          time: notieDisplaySec
        });
        return false;
      }
  
      /**
       * Initialize the fetch function.
       * @return {Object} Current object.
       */
      initFetch() {
        const intervalMsec = 1000 * (this._settings.fetchIntervalSec || defaultFetchIntervalSec);
  
        clearInterval(this._fetchTimer);
  
        this._fetchTimer = setInterval(() => {
          this.fetch(this._fetchMode || fetchMode.time);
        }, intervalMsec);
  
        return this;
      }
  
      /**
       * Fetch updated issues by using Redmine REST API.
       * @param {string} mode - Time or date.
       * @return {Object} Current object.
       */
      fetch(mode) {
        const xhr = new XMLHttpRequest();
  
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4) {
            this.handleResponseFetch(mode, xhr.status, xhr.responseText);
          }
        };
  
        xhr.open('GET', `${this._settings.url}/issues.json${this.getRequestParams(mode, this._settings.projectId)}`);
        xhr.setRequestHeader('X-Redmine-API-Key', this._settings.apiKey);
        xhr.send();
  
        return this;
      }
  
      /**
       * Handle the response for the fetch.
       * @param {string} mode - Time or date.
       * @param {number} status - Response status.
       * @param {string} responseText - Response text.
       * @return {Object} Current object.
       */
      handleResponseFetch(mode, status, responseText) {
        if (mode === fetchMode.time) {
          if (status === 200) {
            const response = JSON.parse(responseText);
            this.notify(response.issues, this.isOverPage(response))
              .updateLastExecutionTime();
          } else if (status === 422) {
            // Retry with date mode if Redmine API doesn't accept time format
            this._fetchMode = fetchMode.date;
            this.fetch(fetchMode.date);
          }
        } else {
          if (status === 200) {
            const response = JSON.parse(responseText);
            this.notify(this.pickIssues(response.issues), this.isOverPage(response));
          }
  
          this.updateLastExecutionTime();
        }
  
        return this;
      }
  
      /**
       * Check whether issues over 1 page.
       * @param {Object} response - Response.
       * @return {boolean} true if over 1 page.
       */
      isOverPage(response) {
        return response.total_count > response.limit;
      }
  
      /**
       * Get issues which were updated after last execution time.
       * @param {string} responseIssues - Response issues.
       * @return {Object[]} Processed issues.
       */
      pickIssues(responseIssues) {
        const lastExecutionTime = new Date(this._lastExecutionTime).getTime();
  
        const issues = responseIssues.filter(issue => {
          const updatedTime = new Date(issue.updated_on).getTime();
          return updatedTime >= lastExecutionTime;
        });
  
        return issues;
      }
  
      /**
       * Test the connection to the Redmine.
       * @param {string} mode - Time or date.
       * @return {Object} Current object.
       */
      testConnection(mode) {
        const xhr = new XMLHttpRequest();
        const pageSettings = this.getPageSettings();
  
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4) {
            this.handleResponseTestConnection(mode, xhr.status);
          }
        };
  
        xhr.open('GET', `${pageSettings.url}/issues.json${this.getRequestParams(mode, pageSettings.projectId)}`);
        xhr.setRequestHeader('X-Redmine-API-Key', pageSettings.apiKey);
        xhr.send();
  
        return this;
      }
  
      /**
       * Handle the response for the test connection.
       * @param {string} mode - Time or date.
       * @param {number} status - Response status.
       * @return {Object} Current object.
       */
      handleResponseTestConnection(mode, status) {
        if (status === 200) {
          notie.alert({
            type: 'success',
            text: 'Connection succeeded.',
            time: notieDisplaySec
          });
          return this;
        }
  
        // Retry with date mode if Redmine API doesn't accept time format
        if (mode === fetchMode.time && status === 422) {
          this.testConnection(fetchMode.date);
          return this;
        }
  
        notie.alert({
          type: 'error',
          text: 'Connection failed.',
          time: notieDisplaySec
        });
  
        return this;
      }
  
      /**
       * Get the request parameters.
       * @param {string} mode - Time or date.
       * @param {string} projectId - Project ID (a numeric value, not a project identifier).
       * @return {string} Request parameters.
       */
      getRequestParams(mode, projectId) {
        const params = [
          `updated_on=%3E%3D${this.getLastExecutionTime(mode)}`,
          'status_id=*',
          'sort=updated_on:desc',
          'limit=100'
        ];
  
        if (typeof projectId === 'string' && projectId !== '') {
          params.unshift(`project_id=${projectId}`);
        }
  
        return `?${params.join('&')}`;
      }
  
      /**
       * Get last execution time by mode.
       * @param {string} mode - Time or date.
       * @return {string} Last execution time.
       */
      getLastExecutionTime(mode) {
        if (mode === fetchMode.time) {
          return this._lastExecutionTime;
        }
        return this._lastExecutionTime.replace(/T.*/, ''); // Date
      }
  
      /**
       * Send the desktop notification.
       * @param {Object} issues - All of updated issues.
       * @param {boolean} isOverPage - true if over 1 page.
       * @return {Object} Current object.
       */
      notify(issues, isOverPage) {
        const issueCount = issues.length;
  
        if (issueCount === 0) {
          return this;
        }
  
        this._mostRecentIssueId = issues[0].id;
        notifierScreen.setNotificationIcon(this._index);
  
        // Display the latest issue's subject only
        nodeNotifier.notify({
          title: this.buildNotificationTitle(issueCount, isOverPage),
          message: issues[0].subject,
          wait: true
        });
  
        nodeNotifier.removeAllListeners();
  
        nodeNotifier.once('click', () => {
          shell.openExternal(`${this._settings.url}/issues/${this._mostRecentIssueId}`);
          notifierScreen.setNormalIcon();
          nodeNotifier.removeAllListeners();
        });
  
        nodeNotifier.once('timeout', () => {
          nodeNotifier.removeAllListeners();
        });
  
        return this;
      }
  
      /**
       * Build a notification title.
       * @param {number} issueCount - Count of issues.
       * @param {boolean} isOverPage - true if over 1 page.
       * @return {string} Notification title.
       */
      buildNotificationTitle(issueCount, isOverPage) {
        let title = `(${issueCount}`;
        if (isOverPage) {
          title += '+';
        }
        title += ') Redmine Notifier';
  
        return title;
      }
    }
  
    /**
     * Class to handle settings screen.
     */
    class RedmineNotifierScreen {
      /**
       * Initialize the RedmineNotifierScreen object.
       * @constructor
       */
      constructor() {
        this._notifiers = null;
        this._currentNotifierIndex = null;
        this._tray = null;
        this._contextMenu = null;
        this._mostRecentNotifierIndex = null;
  
        if (isMac) {
          this._iconFilePath = `${__dirname}/images/${blackIconFilename24}`;
          this._notificationIconFilePath = `${__dirname}/images/${blackIconFilename24Notification}`;
        } else {
          this._iconFilePath = `${__dirname}/images/${colorIconFilename24}`;
          this._notificationIconFilePath = `${__dirname}/images/${colorIconFilename24Notification}`;
        }
      }
  
      /**
       * Initialize the RedmineNotifier objects.
       * @param {RedmineNotifier[]} notifiers - RedmineNotifier objects.
       * @return {Object} Current object.
       */
      initNotifiers(notifiers) {
        this._notifiers = notifiers;
        this._currentNotifierIndex = Number(localStorage.getItem('lastDisplayedNotifierIndex'));
  
        const notifier = this._notifiers[this._currentNotifierIndex];
        notifier.displaySettings();
  
        return this;
      }
  
      /**
       * Initialize the application menu and context menu.
       * @return {Object} Current object.
       */
      initMenu() {
        const appMenu = Menu.buildFromTemplate([
          {
            label: 'Edit',
            submenu: [
              {role: 'undo'},
              {role: 'redo'},
              {role: 'cut'},
              {role: 'copy'},
              {role: 'paste'},
              {role: 'selectall'}
            ]
          }
        ]);
  
        let aboutMenuItem;
        if (isMac) {
          aboutMenuItem = {role: 'about'};
        } else {
          aboutMenuItem = {
            label: `About ${appName}`,
            click: () => {
              dialog.showMessageBox({
                title: `About ${appName}`,
                message: `${appName} ${app.getVersion()}`,
                detail: appCopyright,
                icon: appIconFilePath,
                buttons: []
              });
            }
          };
        }
  
        this._contextMenu = Menu.buildFromTemplate([
          {
            label: 'Open Most Recent Issue in Browser',
            click: () => {
              const notifier = this._notifiers[this._mostRecentNotifierIndex];
              shell.openExternal(`${notifier._settings.url}/issues/${notifier._mostRecentIssueId}`);
              notifierScreen.setNormalIcon();
            },
            enabled: false
          },
          {
            label: 'Preferences',
            click: () => {
              remote.getCurrentWindow().show();
            }
          },
          {
            type: 'separator'
          },
          aboutMenuItem,
          {
            type: 'separator'
          },
          {
            label: 'Quit',
            click: () => {
              const notifier = this._notifiers[this._currentNotifierIndex];
              if (!notifier._newFlag) {
                this.updateLastDisplayedNotifierIndex();
              }
  
              remote.app.quit();
            }
          }
        ]);
  
        Menu.setApplicationMenu(appMenu);
  
        this._tray = new Tray(this._iconFilePath);
        this._tray.setContextMenu(this._contextMenu);
  
        return this;
      }
  
      /**
       * Initialize the event listeners.
       * @return {Object} Current object.
       */
      initEventListener() {
        document.getElementById('save-button').addEventListener('click', () => {
          const notifier = this._notifiers[this._currentNotifierIndex];
          notifier.readScreenSettings();
  
          if (notifier.validateSettings()) {
            notifier.initFetch()
              .updateSettings();
            this.updateNotifierCount();
  
            if (notifier._newFlag) {
              this.updateLastDisplayedNotifierIndex();
              notifier.setNewFlag(false);
            }
  
            notie.alert({
              type: 'success',
              text: 'Settings have been saved.',
              time: notieDisplaySec
            });
          } else {
            notifier.readStoredSettings();
          }
        });
  
        document.getElementById('close-button').addEventListener('click', () => {
          const notifier = this._notifiers[this._currentNotifierIndex];
          notifier.readStoredSettings()
            .displaySettings();
          remote.getCurrentWindow().hide();
        });
  
        document.getElementById('test-connection-button').addEventListener('click', () => {
          const notifier = this._notifiers[this._currentNotifierIndex];
          notifier.testConnection(fetchMode.time);
        });
  
        document.getElementById('new-url-button').addEventListener('click', () => {
          const lastNotifier = this._notifiers[this._notifiers.length - 1];
          if (lastNotifier._settings.url === null) {
            return;
          }
  
          this._currentNotifierIndex = this._notifiers.length;
          this.addNotifier(this._currentNotifierIndex)
            .displaySettings();
        });
  
        document.getElementById('other-urls-button').addEventListener('click', () => {
          this.openURLMenu();
  
          if (this._notifiers.length === 0) {
            this.addNotifier(0);
          }
        });
  
        document.getElementById('delete-button').addEventListener('click', () => {
          notie.confirm({
            text: 'Are you sure you want to delete this setting?',
            cancelText: 'No',
            submitCallback: () => {
              this.deleteCurrentNotifierSettings()
                .resetAllSettings()
                .updateNotifierCount()
                .displaySettingsAfterDelete();
  
              notie.alert({
                type: 'success',
                text: 'Settings have been deleted.',
                time: notieDisplaySec
              });
            }
          });
        });
  
        return this;
      }
  
      /**
       * Display the default settings on the screen.
       * @return {Object} Current object.
       */
      displayDefaultSettings() {
        document.getElementById('default-fetch-interval-sec').innerHTML = defaultFetchIntervalSec;
        return this;
      }
  
      /**
       * Update the stored count of RedmineNotifier objects.
       * @return {Object} Current object.
       */
      updateNotifierCount() {
        localStorage.setItem('notifierCount', this._notifiers.length);
        return this;
      }
  
      /**
       * Update the stored index of last displayed RedmineNotifier object.
       * @return {Object} Current object.
       */
      updateLastDisplayedNotifierIndex() {
        localStorage.setItem('lastDisplayedNotifierIndex', this._currentNotifierIndex);
        return this;
      }
  
      /**
       * Add a RedmineNotifier object.
       * @param {number} index - Index of the object.
       * @return {Object} The RedmineNotifier object.
       */
      addNotifier(index) {
        const notifier = new RedmineNotifier(index);
        notifier.updateLastExecutionTime()
          .readStoredSettings()
          .setNewFlag(true);
        this._notifiers.push(notifier);
        return notifier;
      }
  
      /**
       * Select valid RedmineNotifier objects.
       * @return {RedmineNotifier[]} Valid RedmineNotifier objects.
       */
      selectValidNotifiers() {
        return this._notifiers.filter(notifier => {
          return notifier._settings.url !== null;
        });
      }
  
      /**
       * Open the URL menu.
       * @return {Object} Current object.
       */
      openURLMenu() {
        const choices = [];
  
        const notifiers = this.selectValidNotifiers();
        notifiers.forEach((notifier, index) => {
          choices.push({
            text: notifier.getStoredSetting('url'),
            handler: () => {
              this._currentNotifierIndex = index;
              this.updateLastDisplayedNotifierIndex();
              notifier.readStoredSettings()
                .displaySettings();
  
              this._notifiers = this.selectValidNotifiers();
            }
          });
        });
  
        notie.select({
          text: 'Stored URLs',
          choices
        });
  
        this.wrapURLMenuItems();
  
        return this;
      }
  
      /**
       * Wrap an HTMLElement around URL menu item elements.
       * @return {Object} Current object.
       */
      wrapURLMenuItems() {
        const selectContainer = document.createElement('div');
        selectContainer.className = 'notie-select-container';
  
        const selectChoices = Array.prototype.slice.call(document.getElementsByClassName('notie-select-choice'));
        const notieContainer = selectChoices[0].parentNode;
  
        selectChoices.forEach(choice => {
          selectContainer.appendChild(choice);
        });
  
        const {cancelButton} = notieContainer.getElementsByClassName('notie-background-neutral notie-button');
        notieContainer.insertBefore(selectContainer, cancelButton);
  
        return this;
      }
  
      /**
       * Delete the settings of current RedmineNotifier object.
       * @return {Object} Current object.
       */
      deleteCurrentNotifierSettings() {
        const notifier = this._notifiers[this._currentNotifierIndex];
        notifier.deleteStoredSettings()
          .readStoredSettings();
        return this;
      }
  
      /**
       * Reset all settings.
       * @return {Object} Current object.
       */
      resetAllSettings() {
        this._notifiers = this.selectValidNotifiers();
  
        localStorage.clear();
  
        this._notifiers.forEach((notifier, index) => {
          notifier._index = index;
          notifier.updateSettings();
        });
  
        return this;
      }
  
      /**
       * Display settings after deleting.
       * @return {Object} Current object.
       */
      displaySettingsAfterDelete() {
        if (this._notifiers.length === 0) {
          // Display the first settings
          this._currentNotifierIndex = 0;
          this.addNotifier(this._currentNotifierIndex);
        } else if (this._notifiers[this._currentNotifierIndex] === undefined) {
          // Display the previous settings
          this._currentNotifierIndex = this._currentNotifierIndex - 1;
        }
  
        const notifier = this._notifiers[this._currentNotifierIndex];
        notifier.displaySettings();
  
        return this;
      }
  
      /**
       * Set normal icon and disable "Open Most Recent Issue in Browser" in context menu.
       * @return {Object} Current object.
       */
      setNormalIcon() {
        this._tray.setImage(this._iconFilePath);
        this._contextMenu.items[0].enabled = false;
        this._mostRecentNotifierIndex = null;
        return this;
      }
  
      /**
       * Set notification icon and enable "Open Most Recent Issue in Browser" in context menu.
       * @param {number} index - Index of the most recent RedmineNotifier object.
       * @return {Object} Current object.
       */
      setNotificationIcon(index) {
        this._tray.setImage(this._notificationIconFilePath);
        this._contextMenu.items[0].enabled = true;
        this._mostRecentNotifierIndex = index;
        return this;
      }
    }
  
    /**
     * Check whether old settings exist.
     * @param {Object} oldSettings - Old settings.
     * @return {boolean} true if old settings exist.
     */
    const hasOldSettings = oldSettings => {
      for (const key in oldSettings) {
        if (Object.prototype.hasOwnProperty.call(oldSettings, key) && oldSettings[key] !== null) {
          return true;
        }
      }
  
      return false;
    };
  
    /**
     * Migrate the settings of Redmine Notifier 0.5.0 or 0.6.0.
     * @return {boolean} true if old settings are migrated.
     */
    const migrateOldSettings = () => {
      const oldSettings = {
        url: localStorage.getItem('url'),
        apiKey: localStorage.getItem('apiKey'),
        projectId: localStorage.getItem('projectId'),
        fetchIntervalSec: localStorage.getItem('fetchIntervalSec'),
        lastExecutionTime: localStorage.getItem('lastExecutionTime')
      };
  
      if (!hasOldSettings(oldSettings)) {
        return false;
      }
  
      // Copy to index 0
      const notifier = new RedmineNotifier(0);
      delete oldSettings.lastExecutionTime; // Strictly speaking, `lastExecutionTime` is not a setting
      notifier._settings = oldSettings;
      notifier.updateSettings();
      notifier.updateLastExecutionTime();
  
      localStorage.setItem('notifierCount', 1);
      localStorage.setItem('lastDisplayedNotifierIndex', 0);
  
      // Delete old settings
      localStorage.removeItem('url');
      localStorage.removeItem('apiKey');
      localStorage.removeItem('projectId');
      localStorage.removeItem('fetchIntervalSec');
      localStorage.removeItem('lastExecutionTime');
  
      return true;
    };
  
    window.addEventListener('load', () => {
      migrateOldSettings();
  
      notie.setOptions({
        classes: {
          selectChoice: 'notie-select-choice'
        }
      });
  
      const notifiers = [];
      const notifierCount = Number(localStorage.getItem('notifierCount'));
  
      for (let i = 0; i < notifierCount; i++) {
        const notifier = new RedmineNotifier(i);
        notifier.updateLastExecutionTime()
          .readStoredSettings();
  
        if (notifier.validateSettings()) {
          notifier.initFetch();
        }
  
        notifiers.push(notifier);
      }
  
      if (notifiers.length === 0) {
        const notifier = new RedmineNotifier(0);
        notifier.updateLastExecutionTime()
          .readStoredSettings();
        notifiers.push(notifier);
      }
  
      notifierScreen = new RedmineNotifierScreen();
      notifierScreen.initNotifiers(notifiers)
        .initMenu()
        .initEventListener()
        .displayDefaultSettings();
    });
  })();
  
  