const { spawn } = require('child_process');
const kill = require('tree-kill');
const path = require('path');
const fs = require('fs');
const ini = require('ini');
const { ipcMain } = require('electron')
const { app } = require('electron')

const isWin = /^win/.test(process.platform);

let runningControllers = {};

exports.getLocalControllerPath = async () => {
  let binary = isWin ? 'gns3server.exe': 'gns3server';
  return findBinary('exe.', binary);
}

exports.getUbridgePath = async () => {
  let binary = isWin ? 'ubridge.exe': 'ubridge';
  return findBinary('ubridge', binary);
}

exports.startLocalController = async (controller) => {
  return await run(controller, {
    logStdout: true
  });
}

exports.stopLocalController = async (controller) => {
  return await stop(controller.name);
}

exports.getRunningControllers = () => {
  return Object.keys(runningControllers);
}

exports.stopAllLocalControllers = async () => {
  return await stopAll();
}

async function findBinary(binaryDirectory, filename) {
  const lookupDirectories = [
    __dirname,
    path.dirname(app.getPath('exe'))
  ];

  for(var directory of lookupDirectories) {
    const serverPath = await findBinaryInDirectory(directory, binaryDirectory, filename);
    if(serverPath !== undefined) {
      return serverPath;
    }
  }
}

async function findBinaryInDirectory(baseDirectory, binaryDirectory, filename) {
  const distDirectory = path.join(baseDirectory, 'dist');

  if (!fs.existsSync(distDirectory)) {
    return;
  }

  const files = fs.readdirSync(distDirectory);

  let binaryPath = null;

  files.forEach((directory) => {
    if(directory.startsWith(binaryDirectory)) {
      binaryPath = path.join(baseDirectory, 'dist', directory, filename);
    }
  });

  if(binaryPath !== null && fs.existsSync(binaryPath)) {
    return binaryPath;
  }

  return;
}


function getControllerArguments(controller, overrides, configPath) {
  let controllerArguments = [];
  if(controller.host) {
    controllerArguments.push('--host');
    controllerArguments.push(controller.host);
  }
  if(controller.port) {
    controllerArguments.push('--port');
    controllerArguments.push(controller.port);
  }

  controllerArguments.push('--local');

  if(configPath) {
    controllerArguments.push('--config');
    controllerArguments.push(configPath);
  }

  return controllerArguments;
}

function getChannelForController(controller) {
  return `local-controller-run-${controller.name}`;
}

function notifyStatus(status) {
  ipcMain.emit('local-controller-status-events', status);
}

function filterOutput(line) {
  const index = line.search('CRITICAL');
  if(index > -1) {
    return {
      isCritical: true,
      errorMessage: line.substr(index)
    };
  }
  return {
    isCritical: false
  }
}

async function stopAll() {
  for(var controllerName in runningControllers) {
    let result, error = await stop(controllerName);
  }
  console.log(`Stopped all controllers`);
}

async function stop(controllerName) {
  let pid = undefined;

  const runningController = runningControllers[controllerName];

  if(runningController !== undefined && runningController.process) {
    pid = runningController.process.pid;
  }

  console.log(`Stopping '${controllerName}' with PID='${pid}'`);

  const stopped = new Promise((resolve, reject) => {
    if(pid === undefined) {
      resolve(`Controller '${controllerName} is already stopped`);
      delete runningControllers[controllerName];
      return;
    }

    kill(pid, (error) => {
      if(error) {
        console.error(`Error occured during stopping '${controllerName}' with PID='${pid}'`);
        reject(error);
      }
      else {
        delete runningControllers[controllerName];
        console.log(`Stopped '${controllerName}' with PID='${pid}'`);
        resolve(`Stopped '${controllerName}' with PID='${pid}'`);

        notifyStatus({
          controllerName: controllerName,
          status: 'stopped',
          message: `Controller '${controllerName}' stopped'`
        });
      }
    });
  });

  return stopped;
}

async function getIniFile(controller) {
  return path.join(app.getPath('userData'), `gns3_controller_${controller.id}.ini`);
}

async function configure(configPath, controller) {
  if(!fs.existsSync(configPath)) {
    fs.closeSync(fs.openSync(configPath, 'w'));
    console.log(`Configuration file '${configPath}' has been created.`);
  }

  var config = ini.parse(fs.readFileSync(configPath, 'utf-8'));

  if(controller.path) {
    config.path = controller.path;
  }
  if(controller.host) {
    config.host = controller.host;
  }
  if(controller.port) {
    config.port = controller.port;
  }
  if(controller.ubridge_path) {
    config.ubridge_path = controller.ubridge_path;
  }

  fs.writeFileSync(configPath, ini.stringify(config, { section: 'Controller' }));
}

async function setPATHEnv() {
  const vpcsLookup = [
    path.join(__dirname, 'dist', 'vpcs'),
    path.join(path.dirname(app.getPath('exe')), 'dist', 'vpcs')
  ];

  const dynamipsLookup = [
    path.join(__dirname, 'dist', 'dynamips'),
    path.join(path.dirname(app.getPath('exe')), 'dist', 'dynamips')
  ];

  // prevent adding duplicates
  let extra = [
    ...vpcsLookup,
    ...dynamipsLookup
  ].filter((dir) => {
    return process.env.PATH.indexOf(dir) < 0;
  });
  extra.push(process.env.PATH);
  process.env.PATH = extra.join(";");
}

async function run(controller, options) {
  if(!options) {
    options = {};
  }

  const logStdout = options.logStdout || false;
  const logSterr = options.logSterr || false;

  console.log(`Configuring`);

  const configPath = await getIniFile(controller);
  await configure(configPath, controller);

  console.log(`Setting up PATH`);
  await setPATHEnv();

  console.log(`Running '${controller.path}'`);

  let controllerProcess = spawn(controller.path, getControllerArguments(controller, {}, configPath));

  notifyStatus({
    controllerName: controller.name,
    status: 'started',
    message: `Controller '${controller.name}' started'`
  });

  runningControllers[controller.name] = {
    process: controllerProcess
  };

  controllerProcess.stdout.on('data', function(data) {
    const line = data.toString();
    const { isCritical, errorMessage } = filterOutput(line);
    if(isCritical) {
      notifyStatus({
        controllerName: controller.name,
        status: 'stderr',
        message: `Controller reported error: '${errorMessage}`
      });
    }

    if(logStdout) {
      console.log(data.toString());
    }
  });

  controllerProcess.stderr.on('data', function(data) {
    if(logSterr) {
      console.log(data.toString());
    }
  });

  controllerProcess.on('exit', (code, signal) => {
    notifyStatus({
      controllerName: controller.name,
      status: 'errored',
      message: `controller '${controller.name}' has exited with status='${code}'`
    });
  });

  controllerProcess.on('error', (err) => {
    notifyStatus({
      controllerName: controller.name,
      status: 'errored',
      message: `Controller errored: '${err}`
    });
  });

}

async function main() {
  await run({
    name: 'my-local',
    path: 'c:\\Program Files\\GNS3\\gns3server.EXE',
    port: 3080
  }, {
    logStdout: true
  });
}

if(ipcMain) {
  ipcMain.on('local-controller-run', async function (event, controller) {
    const responseChannel = getChannelForController();
    await run(controller);
    event.sender.send(responseChannel, {
      success: true
    });
  });
}

if (require.main === module) {
  process.on('SIGINT', function() {
    console.log("Caught interrupt signal");
    stopAll();
  });

  process.on('unhandledRejection', (reason, promise) => {
    console.log(`UnhandledRejection occured '${reason}'`);
    process.exit(1);
  });

  main();
}