// Modules to control application life and create native browser window

const {app, BrowserWindow, Tray, nativeImage, Menu, dialog} = require('electron');
const {ipcMain} = require('electron');
const Constants = require('../src/constants');
const path = require('path');
const url = require('url');
require('electron-debug/index')();
const os = require('os');
const helpers = require('../src/helpers');
const fs = require('fs');
const unzip = require('unzipper');
const AutoLaunch = require('auto-launch');

require.extensions['.sh'] = function (module, filename) {
  module.exports = fs.readFileSync(filename, 'utf8');
};
const detectScript = require('./detect_linux.sh');
const installScript = require('./update_linux.sh');

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let trayContextMenu;
let mainWindow;
let mainWindowTray;
let mountedData = {};
let mountedLocations = [];
let expectedUnmount = {};
let launchHidden = false;
let firstMountCheck = true;
let manualMountDetection = {};

let isQuiting = false;

app.on('before-quit', function () {
  isQuiting = true;
});

function closeApplication() {
  app.quit();
}

function setWindowVisibility(show) {
  if (show) {
    mainWindow.show();
    if (os.platform() === 'darwin') {
      app.dock.show();
    }

    if (mainWindow.isMinimized()) {
      mainWindow.restore();
    }
    mainWindow.focus();
  } else {
    mainWindow.hide();
    if (os.platform() === 'darwin') {
      app.dock.hide();
    }
  }

  if (trayContextMenu && mainWindowTray) {
    trayContextMenu.items[0].checked = show;
    mainWindowTray.setContextMenu(trayContextMenu)
  }
}

function createWindow() {
  loadUiSettings();

  let extra = {};
  if (os.platform() === 'linux') {
    extra = {
      icon: path.join(__dirname, '../', 'icon.png'),
    }
  }

  // Create the browser window.
  const height = (process.env.ELECTRON_START_URL || (os.platform() === 'darwin') ? 294 : 274) - ((os.platform() === 'win32') ? 0 : 20);
  mainWindow = new BrowserWindow({
    width: 428 + ((os.platform() === 'win32') ? 0 : (os.platform() === 'darwin') ? 150 : 160),
    height: height,
    fullscreen: false,
    resizable: false,
    show: !launchHidden,
    title: 'Repertory UI',
    ...extra,
    webPreferences: {
      webSecurity: !process.env.ELECTRON_START_URL
    }
  });

  if ((os.platform() === 'darwin') && launchHidden) {
    app.dock.hide();
  }

  // and load the index.html of the app.
  const startUrl = process.env.ELECTRON_START_URL || url.format({
    pathname: path.join(__dirname, '../build/index.html'),
    protocol: 'file:',
    slashes: true
  });

  mainWindow.on('close', function (event) {
    if (!isQuiting) {
      event.preventDefault();
      if (mainWindow.isVisible()) {
        setWindowVisibility(false);
      }
      event.returnValue = false;
    }
  });

  mainWindow.on('closed', function () {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;

    // Unmount all items
    for (const i in mountedLocations) {
      const data = mountedData[mountedLocations[i]];
      helpers.stopMountProcessSync(data.Version, data.Provider);
    }

    mountedLocations = [];
    mountedData = {};
  });

  const appPath = (os.platform() === 'win32') ? path.resolve(path.join(app.getAppPath(), '..\\..\\repertory-ui.exe')) :
    (os.platform() === 'darwin') ? path.resolve(path.join(path.dirname(app.getAppPath()), '../MacOS/repertory-ui')) :
    process.env.APPIMAGE;

  const autoLauncher = new AutoLaunch({
    name: 'Repertory UI',
    path: appPath,
  });

  trayContextMenu = Menu.buildFromTemplate([
    {
      label: 'Visible', type: 'checkbox', click(item) {
        setWindowVisibility(item.checked);
      },
      checked: !launchHidden,
    },
    {
      label: 'Auto-start', type: 'checkbox', click(item) {
        if (item.checked) {
          autoLauncher.enable();
        } else {
          autoLauncher.disable();
        }
      }
    },
    {
      type: 'separator'
    },
    {
      label: 'Launch Hidden', type: 'checkbox', click(item) {
        launchHidden = !!item.checked;
        saveUiSettings();
      },
      checked: launchHidden,
    },
    {
      type: 'separator'
    },
    {
      label: 'Exit and Unmount', click() {
        closeApplication();
      }
    }
  ]);

  const image = nativeImage.createFromPath(path.join(__dirname, (os.platform() === 'darwin') ? '../build/logo_mac.png' : '../build/logo.png'));
  mainWindowTray = new Tray(image);

  autoLauncher
    .isEnabled()
    .then((enabled) => {
      trayContextMenu.items[1].checked = enabled;
      mainWindowTray.setToolTip('Repertory UI');
      mainWindowTray.setContextMenu(trayContextMenu)
    })
    .catch(() => {
      closeApplication();
    });

  mainWindow.loadURL(startUrl);
}

const instanceLock = app.requestSingleInstanceLock();
if (!instanceLock) {
  closeApplication();
} else {
  app.on('second-instance', () => {
    if (mainWindow) {
      setWindowVisibility(true);
    }
  });

  app.on('ready', createWindow);

  app.on('window-all-closed', () => {
    closeApplication();
  });
}

const clearManualMountDetection = (provider) => {
  if (manualMountDetection[provider]) {
    clearInterval(manualMountDetection[provider]);
    delete manualMountDetection[provider];
  }
};

const loadUiSettings = () => {
  const settingFile = path.join(helpers.resolvePath(Constants.DATA_LOCATIONS[os.platform()]), 'ui.json');
  try {
    if (fs.statSync(settingFile).isFile()) {
      const settings = JSON.parse(fs.readFileSync(settingFile, 'utf8'));
      launchHidden = settings.launch_hidden;
    }
  } catch (e) {
  }
};

const monitorMount = (sender, provider, version, pid, location) => {
  manualMountDetection[provider] = setInterval(() => {
    helpers
      .detectRepertoryMounts(version)
      .then(result => {
        if (result[provider].PID !== pid) {
          if (result[provider].PID === -1) {
            clearManualMountDetection(provider);
            sender.send(Constants.IPC_Unmount_Drive_Reply, {
              data: {
                Expected: expectedUnmount[provider],
                Location: location,
                Provider: provider,
                Error: Error(provider + ' Unmounted').toString(),
                Success: false,
              }
            });
          } else {
            pid = result[provider].PID;
          }
        }
      })
      .catch(e => {
        console.log(e);
      });
  },6000);
};

const saveUiSettings = () => {
  const settingFile = path.join(helpers.getDataDirectory(), 'ui.json');
  try {
    fs.writeFileSync(settingFile, JSON.stringify({
      launch_hidden: launchHidden,
    }), 'utf-8');
  } catch (e) {
  }
};

const standardIPCReply = (event, channel, data, error) => {
  if (mainWindow) {
    event.sender.send(channel, {
      data: {
        ...data,
        Error: error instanceof Error ? error.toString() : error,
        Success: !error,
      }
    });
  }
};

ipcMain.on(Constants.IPC_Browse_Directory + '_sync', (event, data) => {
  dialog.showOpenDialog(mainWindow, {
    defaultPath: data.Location,
    properties: ['openDirectory'],
    title: data.Title,
  }, (filePaths) => {
    if (filePaths && (filePaths.length > 0)) {
      event.returnValue = filePaths[0];
    } else {
      event.returnValue = '';
    }
  });
});

ipcMain.on(Constants.IPC_Check_Daemon_Version, (event, data) => {
  helpers
    .checkDaemonVersion(data.Version, data.Provider)
    .then(code => {
      standardIPCReply(event, Constants.IPC_Check_Daemon_Version_Reply, {
        Valid: (code === 0),
        Code: code,
      });
    })
    .catch(e => {
      standardIPCReply(event, Constants.IPC_Check_Daemon_Version_Reply, {
        Valid: false,
      }, e);
    });
});

ipcMain.on(Constants.IPC_Check_Daemon_Version + '_sync', (event, data) => {
  helpers
  .checkDaemonVersion(data.Version, data.Provider)
  .then(code => {
    event.returnValue = {
      data: {
        Success: true,
        Valid: (code === 0),
        Code: code,
      },
    };
  })
  .catch(e => {
    event.returnValue = {
      data: {
        Error: e.toString(),
        Success: false,
        Valid: false
      },
    };
  });
});

ipcMain.on(Constants.IPC_Check_Dependency_Installed, (event, data) => {
  try {
    const exists = fs.lstatSync(data.File).isFile();
    standardIPCReply(event, Constants.IPC_Check_Dependency_Installed_Reply, {
      data: {
        Exists: exists,
      },
    });
  } catch (e) {
    standardIPCReply(event, Constants.IPC_Check_Dependency_Installed_Reply, {
      data : {
        Exists: false,
      },
    });
  }
});

ipcMain.on(Constants.IPC_Check_Dependency_Installed + '_sync', (event, data) => {
  try {
    const ls = fs.lstatSync(data.File);
    event.returnValue = {
      data: {
        Exists: ls.isFile() || ls.isSymbolicLink(),
      },
    };
  } catch (e) {
    event.returnValue = {
      data: {
        Exists: false
      },
    };
  }
});

ipcMain.on(Constants.IPC_Check_Installed, (event, data) => {
  const destination = path.join(helpers.getDataDirectory(), data.Version);
  helpers
  .getMissingDependencies(data.Dependencies)
  .then((dependencies) => {
    let exists = false;
    try {
      exists = fs.existsSync(destination) && fs.lstatSync(destination).isDirectory();
    } catch (e) {
    }
    standardIPCReply(event, Constants.IPC_Check_Installed_Reply, {
      Dependencies: dependencies,
      Exists: exists,
      Version: data.Version,
    });
  }).catch(error => {
    standardIPCReply(event, Constants.IPC_Check_Installed_Reply, {
      Dependencies: [],
      Version: data.Version,
    }, error);
  });
});

ipcMain.on(Constants.IPC_Check_Mount_Location + '_sync', (event, data) => {
  let response = {
    Success: true,
    Error: ''
  };

  try {
    if (fs.existsSync(data.Location) && fs.statSync(data.Location).isDirectory()) {
      if (fs.readdirSync(data.Location).length !== 0) {
        response.Success = false;
        response.Error = 'Directory not empty: ' + data.Location;
      }
    } else {
      response.Success = false;
      response.Error = 'Directory not found: ' + data.Location;
    }
  } catch (e) {
    response.Success = false;
    response.Error = e.toString();
  }
  event.returnValue = response;
});

ipcMain.on(Constants.IPC_Delete_File, (event, data) => {
  try {
    if (fs.existsSync(data.FilePath)) {
      fs.unlinkSync(data.FilePath);
    }
  } catch (e) {
  }
});

ipcMain.on(Constants.IPC_Delete_File + '_sync', (event, data) => {
  try {
    if (fs.existsSync(data.FilePath)) {
      fs.unlinkSync(data.FilePath);
    }
    event.returnValue = {
      data: true,
    };
  } catch (e) {
    event.returnValue = {
      data: false,
    };
  }
});

ipcMain.on(Constants.IPC_Detect_Mounts, (event, data) => {
  let driveLetters = {};
  for (const provider of Constants.PROVIDER_LIST) {
    driveLetters[provider] = [];
  }

  const grabDriveLetters = (locations) => {
    for (let i = 'c'.charCodeAt(0); i <= 'z'.charCodeAt(0); i++) {
      const drive = (String.fromCharCode(i) + ':').toUpperCase();
      let driveInUse;
      if (Object.keys(locations).length > 0) {
        for (const provider of Constants.PROVIDER_LIST) {
          driveInUse = locations[provider].startsWith(drive);
          if (driveInUse)
            break;
        }
      }
      if (!driveInUse) {
        try {
          if (!fs.existsSync(drive)) {
            for (const provider of Constants.PROVIDER_LIST) {
              driveLetters[provider].push(drive);
            }
          }
        } catch (e) {
        }
      }
    }

    if (Object.keys(locations).length > 0) {
      for (const provider of Constants.PROVIDER_LIST) {
        if (locations[provider].length > 0) {
          if (!driveLetters[provider].find((driveLetter) => {
            return driveLetter === locations[provider];
          })) {
            driveLetters[provider].push(locations[provider]);
          }
        }
      }
    }
  };

  const setImage = (locations) => {
    let driveInUse;
    if (Object.keys(locations).length > 0) {
      for (const provider of Constants.PROVIDER_LIST) {
        driveInUse = locations[provider].length > 0;
        if (driveInUse)
          break;
      }
    }

    let image;
    if (driveInUse) {
      image = nativeImage.createFromPath(path.join(__dirname, os.platform() === 'darwin' ? '../build/logo_both_mac.png' : '../build/logo_both.png'));
    } else {
      image = nativeImage.createFromPath(path.join(__dirname, os.platform() === 'darwin' ? '../build/logo_mac.png' : '../build/logo.png'));
    }

    mainWindowTray.setImage(image);
  };
  
  helpers
    .detectRepertoryMounts(data.Version)
    .then((results) => {
      let storageData = {};
      let locations = {};
      for (const provider of Constants.PROVIDER_LIST) {
        storageData[provider] = results[provider] ? results[provider] : {
          Active: false,
          Location: '',
          PID: -1,
        };
        locations[provider] = storageData[provider].Location;

        if (storageData[provider].PID !== -1) {
          expectedUnmount[provider] = false;
          if (firstMountCheck) {
            monitorMount(event.sender, provider, data.Version, storageData[provider].PID, storageData[provider].Location);
          }
        }
      }

      if (os.platform() === 'win32') {
        grabDriveLetters(locations);
      }

      setImage(locations);
      if (firstMountCheck) {
        firstMountCheck = false;
      }
      standardIPCReply(event, Constants.IPC_Detect_Mounts_Reply, {
        DriveLetters: driveLetters,
        Locations: locations,
      });
    })
    .catch(error => {
      if (os.platform() === 'win32') {
        grabDriveLetters({});
      }
      setImage({});
      standardIPCReply(event, Constants.IPC_Detect_Mounts_Reply, {
        DriveLetters: driveLetters,
      }, error);
    });
});

ipcMain.on(Constants.IPC_Download_File, (event, data) => {
  const destination = path.join(helpers.getDataDirectory(), data.Filename);
  helpers.downloadFile(data.URL, destination, (progress) => {
    standardIPCReply(event, Constants.IPC_Download_File_Progress, {
      Destination: destination,
      Progress: progress,
      URL: data.URL,
    });
  }, error => {
    standardIPCReply(event, Constants.IPC_Download_File_Complete, {
      Destination: destination,
      URL: data.URL,
    }, error);
  });
});

ipcMain.on(Constants.IPC_Extract_Release, (event, data) => {
  const destination = path.join(helpers.getDataDirectory(), data.Version);
  helpers.mkDirByPathSync(destination);

  const stream = fs.createReadStream(data.Source);
  stream
    .pipe(unzip.Extract({ path: destination }))
    .on('error', error => {
      try {
        helpers.removeDirectoryRecursively(destination);
      } catch (e) {
      }
      stream.close();
      standardIPCReply(event, Constants.IPC_Extract_Release_Complete, {
        Source: data.Source,
      }, error);
    })
    .on('finish', () => {
      stream.close();
      if (os.platform() !== 'win32') {
        helpers
          .executeAndWait("chmod +x \"" + path.join(destination, 'repertory') + "\"")
          .then(() => {
            standardIPCReply(event, Constants.IPC_Extract_Release_Complete, {
              Source: data.Source,
            });
          })
          .catch(error => {
            standardIPCReply(event, Constants.IPC_Extract_Release_Complete, {
              Source: data.Source,
            }, error);
        })
      } else {
        standardIPCReply(event, Constants.IPC_Extract_Release_Complete, {
          Source: data.Source,
        });
      }
    });
});

ipcMain.on(Constants.IPC_Get_Config, (event, data) => {
  helpers
    .getConfig(data.Version, data.Provider)
    .then((data) => {
      if (data.Code === 0) {
        standardIPCReply(event, Constants.IPC_Get_Config_Reply, {
          Config: data.Data,
        });
      } else {
        standardIPCReply(event, Constants.IPC_Get_Config_Reply, {}, data.Code);
      }
    })
    .catch(error => {
      standardIPCReply(event, Constants.IPC_Get_Config_Reply, {}, error);
    });
});

ipcMain.on(Constants.IPC_Get_Config_Template, (event, data) => {
  helpers
    .getConfigTemplate(data.Version, data.Provider)
    .then((data) => {
      standardIPCReply(event, Constants.IPC_Get_Config_Template_Reply, {
        Template: data,
      });
    })
    .catch(error => {
      standardIPCReply(event, Constants.IPC_Get_Config_Template_Reply, {}, error);
    });
});

ipcMain.on(Constants.IPC_Get_Platform, (event) => {
  const platform = os.platform();
  if (platform === 'linux') {
    const scriptFile = path.join(os.tmpdir(), 'repertory_detect_linux.sh');
    fs.writeFileSync(scriptFile, detectScript);
    helpers
      .executeScript(scriptFile)
      .then(data => {
        event.sender.send(Constants.IPC_Get_Platform_Reply, {
          AppPlatform: data.replace(/(\r\n|\n|\r)/gm,""),
          Platform: platform,
        });
      })
      .catch(() => {
        event.sender.send(Constants.IPC_Get_Platform_Reply, {
          AppPlatform: platform,
          Platform: platform,
        });
      });
  } else {
    event.sender.send(Constants.IPC_Get_Platform_Reply, {
      AppPlatform: platform,
      Platform: platform,
    });
  }
});

ipcMain.on(Constants.IPC_Get_State, event => {
  helpers.mkDirByPathSync(helpers.getDataDirectory());
  const configFile = path.join(helpers.getDataDirectory(), 'settings.json');
  if (fs.existsSync(configFile)) {
    event.sender.send(Constants.IPC_Get_State_Reply, {
      data: JSON.parse(fs.readFileSync(configFile, 'utf8')),
    });
  } else {
    event.sender.send(Constants.IPC_Get_State_Reply, {
      data: null,
    });
  }
});

ipcMain.on(Constants.IPC_Install_Dependency, (event, data) => {
  if (data.Source.toLowerCase().endsWith('.dmg')) {
    helpers
      .executeAsync('open', ['-a', 'Finder', '-W', data.Source])
      .then(() => {
        standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, {
          Source: data.Source,
          URL: data.URL,
        });
      })
      .catch(error=> {
        standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, {
          Source: data.Source,
          URL: data.URL,
        }, error);
      });
  } else {
    helpers
    .executeAndWait(data.Source)
    .then(() => {
      standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, {
        Source: data.Source,
        URL: data.URL,
      });
    })
    .catch(error => {
      standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, {
        Source: data.Source,
        URL: data.URL,
      }, error);
    });
  }
});

ipcMain.on(Constants.IPC_Install_Upgrade, (event, data) => {
  let allowSkipVerification = true;

  let tempSig;
  let tempPub;
  const cleanupFiles = () => {
    try {
      if (tempSig) {
        fs.unlinkSync(tempSig);
      }
      if (tempPub) {
        fs.unlinkSync(tempPub);
      }
    } catch (e) {
    }
  };

  const errorHandler = err => {
    cleanupFiles();
    standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, {
      AllowSkipVerification: allowSkipVerification,
      Source: data.Source,
    }, err);
  };

  // TODO Enable verification in 1.0.4
  const hasSignature = false;//!data.SkipVerification && data.Signature && (data.Signature.length > 0);
  const hasHash = false;//!data.SkipVerification && data.Sha256 && (data.Sha256.length > 0);
  if (hasSignature) {
    try {
      const files = helpers.createSignatureFiles(data.Signature, Constants.DEV_PUBLIC_KEY);
      tempPub = files.PublicKeyFile;
      tempSig = files.SignatureFile;
    } catch (e) {
      errorHandler(e);
      return;
    }
  }

  let command;
  let args;
  const platform = os.platform();
  if (platform === 'win32') {
    command = data.Source;
  } else if (platform === 'darwin') {
    command = 'open';
    args = ['-a', 'Finder', data.Source];
  } else if (platform === 'linux') {
    try {
      const execPath = path.join(os.tmpdir(), 'install_linux.sh');
      fs.writeFileSync(execPath, installScript);
      fs.chmodSync(execPath, '750');
      command = execPath;
      args = [data.Source];
    } catch (e) {
      errorHandler(e);
    }
  } else {
    errorHandler(Error('Platform not supported: ' + os.platform()));
  }

  if (command) {
    const executeInstall = () => {
      helpers
        .executeAsync(command, args)
        .then(() => {
          cleanupFiles();
          closeApplication();
        })
        .catch(error => {
          errorHandler(error);
        });
    };

    if (hasSignature) {
      helpers
        .verifySignature(data.Source, tempSig, tempPub)
        .then(() => {
          executeInstall();
        })
        .catch(() => {
          errorHandler(Error('Failed to verify installation package signature'));
        });
    } else if (hasHash) {
      helpers
        .verifyHash(data.Source, data.Sha256)
        .then(()=> {
          executeInstall();
        })
        .catch(() => {
          errorHandler(Error('Failed to verify installation package hash'));
        });
    } else {
      executeInstall();
    }
  } else {
    errorHandler(Error('Unsupported upgrade: ' + data.Source));
  }
});

ipcMain.on(Constants.IPC_Mount_Drive, (event, data) => {
  expectedUnmount[data.Provider] = false;
  
  if (mountedLocations.indexOf(data.Location) !== -1) {
    console.log(data.Provider + ' already mounted: ' + data.Location);
  } else {
    mountedLocations.push(data.Location);
    mountedData[data.Location] = {
      Version: data.Version,
      Provider: data.Provider,
    };
    const errorHandler = (pid, error) => {
      if (mountedLocations.indexOf(data.Location) !== -1) {
        mountedLocations.splice(mountedLocations.indexOf(data.Location), 1);
        delete mountedData[data.Location];
      }

      standardIPCReply(event, Constants.IPC_Unmount_Drive_Reply, {
        Expected: expectedUnmount[data.Provider],
        Location: data.Location,
        Provider: data.Provider,
      }, error || Error(data.Provider + ' Unmounted'));
    };
    helpers
      .executeMount(data.Version, data.Provider, data.Location, data.NoConsoleSupported, (error, pid) => {
        errorHandler(pid, error);
      })
      .then(() => {
        standardIPCReply(event, Constants.IPC_Mount_Drive_Reply, {
          Provider: data.Provider,
        });
      })
      .catch(error => {
        errorHandler(-1, error);
      });
  }
});

ipcMain.on(Constants.IPC_Save_State, (event, data) => {
  helpers.mkDirByPathSync(helpers.getDataDirectory());
  const configFile = path.join(helpers.getDataDirectory(), 'settings.json');
  fs.writeFileSync(configFile, JSON.stringify(data.State), 'utf8');
});

ipcMain.on(Constants.IPC_Set_Config_Values, (event, data) => {
  const setConfigValue = (i) => {
    if (i < data.Items.length) {
      helpers
        .setConfigValue(data.Items[i].Name, data.Items[i].Value, data.Provider, data.Version)
        .then(() => {
          setConfigValue(++i);
        })
        .catch(() => {
          setConfigValue(++i);
        });
    } else {
      standardIPCReply(event, Constants.IPC_Set_Config_Values_Reply, {});
    }
  };
  setConfigValue(0);
});

ipcMain.on(Constants.IPC_Shutdown, () => {
  closeApplication();
});

ipcMain.on(Constants.IPC_Show_Window, () => {
  setWindowVisibility(true);
});

ipcMain.on(Constants.IPC_Show_Window + '_sync', event => {
  setWindowVisibility(true);
  event.returnValue = true;
});

ipcMain.on(Constants.IPC_Unmount_Drive, (event, data) => {
  clearManualMountDetection(data.Provider);

  expectedUnmount[data.Provider] = true;
  helpers
    .stopMountProcess(data.Version, data.Provider)
    .then((result)=> {
      console.log(result);
    })
    .catch((e) => {
      console.log(e);
    });
});
