const { spawn } = require("child_process");
const { v4: uuidv4 } = require("uuid");
const crypto = require("crypto");
const path = require("path");
var fs = require("fs");
const exiftool = require(path.join(
  process.cwd(),
  "node_modules",
  "node-exiftool"
));
const exiftoolBin = require(path.join(
  process.cwd(),
  "node_modules",
  "dist-exiftool"
));
const ep = new exiftool.ExiftoolProcess(exiftoolBin);

let isWindows;
let fileMetaData;

const root = path.join(process.cwd(), "dpc_packer");
const pkgPath = path.join(root, "pkg");
const genPath = path.join(pkgPath, "gen");
const fileNameMsi = "Deployment_Client.msi";
const fileNameExeUpdate = "DeploymentClient.exe";
const fileNameExeUpdater = "ServiceUpdateHelper.exe";
const fileNameDpcHelper = "DeploymentClientHelper.exe";
const fileNameLocalNugetServer = "LocalNugetServer.exe";
const fileExeUpdate = path.join(pkgPath, fileNameExeUpdate);
const fileExeUpdater = path.join(pkgPath, fileNameExeUpdater);
const fileDpcHelper = path.join(pkgPath, fileNameDpcHelper);
const fileLocalNugetServer = path.join(pkgPath, fileNameLocalNugetServer);

async function init() {
  try {
    fileMetaData = await getFileMeta(fileExeUpdate);
    updateMetaOnChange(fileExeUpdate);

    if (!(await exist(genPath))) {
      createDir(genPath);
    }

    if (/^win/i.test(process.platform)) {
      isWindows = true;
    } else {
      isWindows = false;
    }

    console.log("Windows platform:", isWindows);
  } catch (err) {
    console.error(err);
  }
}

function getCurrentMetaVersion() {
  if (
    fileMetaData &&
    fileMetaData.length != 0 &&
    fileMetaData[0].ProductVersion &&
    fileMetaData[0].ProductVersion != ""
  ) {
    return fileMetaData[0].ProductVersion;
  } else {
    return null;
  }
}

async function updateMetaOnChange(file) {
  fs.watchFile(file, async () => {
    if (await exist(file)) {
      fileMetaData = await getFileMeta(file);
    } else {
      fileMetaData = null;
    }
  });
}

async function cleanUp(dir, toSkip) {
  return new Promise(async (resolve, reject) => {
    fs.readdir(dir, async (err, files) => {
      if (err) {
        reject(err);
      } else {
        for (const file of files) {
          if ((toSkip && !file.includes(toSkip)) || !toSkip) {
            await deleteFile(path.join(dir, file));
          }
        }

        resolve();
      }
    });
  });
}

async function createPackage(domainName, domainKey, pairingKey) {
  return new Promise(async (resolve, reject) => {
    let uuid = uuidv4();
    let uniquePath = crypto.randomBytes(20).toString("hex");
    let workingPath = path.join(genPath, uniquePath);

    try {
      await createDir(workingPath);

      await writeFile(path.join(workingPath, "domainName"), domainName);
      await writeFile(path.join(workingPath, "domainKey"), domainKey);
      await writeFile(path.join(workingPath, "pairingKey"), pairingKey);

      let nsiConfig = await readFile(path.join(pkgPath, "config.nsi"));
      nsiConfig = nsiConfig.toString();
      nsiConfig = nsiConfig.replace(
        /@Version/g,
        fileMetaData[0].ProductVersion
      );
      await writeFile(
        path.join(workingPath, "Deployment_Client.nsi"),
        nsiConfig
      );

      let wxsConfig = await readFile(path.join(pkgPath, "config.wxs"));
      wxsConfig = wxsConfig.toString();

      wxsConfig = wxsConfig.replace(
        /@NewVersion/g,
        fileMetaData[0].ProductVersion
      );
      wxsConfig = wxsConfig.replace(/@OldVersion/g, "0.0.0.0");
      wxsConfig = wxsConfig.replace(/@NewGuid/g, uuid);
      wxsConfig = wxsConfig.replace(/@OldGuid/g, uuid);
      wxsConfig = wxsConfig.replace(/@Source/g, "Deployment_Client.exe");
      wxsConfig = wxsConfig.replace(/@Manufacturer/g, "JAHA IT");
      wxsConfig = wxsConfig.replace(/@Name/g, fileNameMsi);

      await writeFile(workingPath + "/Deployment_Client.wxs", wxsConfig);

      let toExecute = getToExecute(workingPath);

      for (let exe of toExecute) {
        try {
          var exitCode;
          if (isWindows) {
            let file = exe.args[0];
            exe.args.splice(0, 1);
            exitCode = await exec(file, exe.args, exe.workingPath);
          } else {
            exitCode = await exec("wine", exe.args, exe.workingPath);
          }

          if (exitCode != 0) {
            reject(
              "Failed to complete " + exe.args[0] + " with code: " + exitCode
            );
            break;
          }
        } catch (err) {
          console.error(err.message);
          reject(err);
          break;
        }
      }

      if (!isWindows) {
        await exec("chmod", ["700", fileNameMsi], workingPath);
      }

      cleanUp(workingPath, ".msi");
      resolve({
        workingPath: workingPath,
        uniquePath: uniquePath,
      });
    } catch (err) {
      reject(err);
    }
  });
}

async function getFileMeta(file) {
  return new Promise((resolve, reject) => {
    ep.open()
      .then(() => ep.readMetadata(file, ["-File:all"]))
      .then((result) => {
        if (!result.error) {
          resolve(result.data);
        } else {
          reject(result.error);
        }
      })
      .then(() => ep.close())
      .catch(() => {
        reject("Unknown error");
      });
  });
}

async function deleteDir(dir) {
  return new Promise(async (resolve, reject) => {
    if (await exist(dir)) {
      fs.rmdir(dir, (err) => {
        if (!err) {
          resolve();
        } else {
          reject("Could not delete " + dir + ": " + err);
        }
      });
    } else {
      resolve();
    }
  });
}

async function deleteFile(file) {
  return new Promise(async (resolve, reject) => {
    if (await exist(file)) {
      fs.unlink(file, (err) => {
        if (!err) {
          resolve();
        } else {
          reject("Could not delete " + file + ": " + err);
        }
      });
    } else {
      resolve();
    }
  });
}

async function readFile(file) {
  return new Promise(async (resolve, reject) => {
    if (await exist(file)) {
      fs.readFile(file, (err, data) => {
        if (!err) {
          resolve(data);
        } else {
          reject("Could not read " + file + ": " + err);
        }
      });
    } else {
      reject(file + " does not exist.");
    }
  });
}

async function writeFile(file, data) {
  return new Promise(async (resolve, reject) => {
    if (!(await exist(file))) {
      fs.writeFile(file, data, (err) => {
        if (!err) {
          resolve();
        } else {
          reject("Could not write to " + file + ": " + err);
        }
      });
    } else {
      reject(file + " already exist.");
    }
  });
}

async function exist(dir) {
  return new Promise((resolve, reject) => {
    fs.stat(dir, (err, stats) => {
      if (stats) {
        resolve(true);
      } else {
        resolve(false);
      }
    });
  });
}

async function createDir(dir) {
  return new Promise(async (resolve, reject) => {
    if (!(await exist(dir))) {
      fs.mkdir(
        dir,
        {
          recursive: true,
        },
        (err) => {
          if (!err) {
            resolve();
          } else {
            reject("Could not create " + dir + ": " + err);
          }
        }
      );
    } else {
      reject(dir + " already exists.");
    }
  });
}

async function exec(file, args, workingPath) {
  return new Promise((resolve, reject) => {
    const app = spawn(file, args, {
      cwd: workingPath,
    });

    app.stdout.on("data", (data) => {
      //console.log(data.toString());
    });

    app.stderr.on("data", (data) => {
      //console.error(data.toString());
    });

    app.on("close", (code) => {
      //console.log(code.toString());
      resolve(code);
    });

    app.on("error", (error) => {
      console.error(error.toString());
      reject(error);
    });
  });
}

function getToExecute(workingPath) {
  return [
    {
      args: [path.join(root, "nsis", "makensis.exe"), "Deployment_Client.nsi"],
      workingPath: workingPath,
    },
    {
      args: [path.join(root, "wix", "candle.exe"), "Deployment_Client.wxs"],
      workingPath: workingPath,
    },
    {
      args: [
        path.join(root, "wix", "light.exe"),
        "-sval",
        "Deployment_Client.wixobj",
      ],
      workingPath: workingPath,
    },
  ];
}

module.exports = {
  root,
  pkgPath,
  genPath,
  fileNameMsi,
  fileNameExeUpdate,
  fileNameExeUpdater,
  fileNameDpcHelper,
  fileNameLocalNugetServer,
  fileExeUpdate,
  fileExeUpdater,
  fileDpcHelper,
  fileLocalNugetServer,
  init,
  getCurrentMetaVersion,
  updateMetaOnChange,
  cleanUp,
  createPackage,
  getFileMeta,
  getToExecute,
  exec,
  getToExecute,
  createDir,
  writeFile,
  exist,
  deleteFile,
  deleteDir,
  getFileMeta,
};
