const libFS     = require('fs');
const libPath   = require('path');
const libUtil   = require('util');

fcf.module({
  name: "fcf:NSystem/NPackage/Package.js",
  dependencies: ["fcf:NSystem/NPackage/PackageHandler.js"],
  module: function(PackageHandler){
    var NPackage = fcf.prepareObject(fcf, "NSystem/NPackage");

    function versionParse(a_str) {
      if (!a_str)
        return [];
      let src = a_str.split(".");
      let arr = [];
      for(let i = 0; i < src.length; ++i) {
        arr.push(parseInt(src[i]));
        if (isNaN(arr[arr.length-1]))
          arr[arr.length-1] = 0;
        let suffix = src[i].split("-");
        if (suffix[1]){
          let items = [...suffix[1].match("([a-zA-Z]*)([0-9]*)")];
          if (items[1])
            arr.push(items[1]);
          if (items[2] && !isNaN(parseInt(items[2])))
            arr.push(parseInt(items[2]));
        }
      }
      return arr;
    }

    function versionCmp(a_left, a_right){
      let left = versionParse(a_left);
      let right = versionParse(a_right);
      let s = fcf.min(left.length, right.length);
      for(let i = 0; i < s; ++i){
        if (left[i] > right[i])
          return 1;
        else if (left[i] < right[i])
          return -1;
      }
      if (left.length > right.length){
        let z = true;
        for(let i = s; i < left.length; ++i)
          if (left[i])
            z = false;
        if (!z)
          return 1;
      } else if (right.length > left.length){
        let z = true;
        for(let i = s; i < right.length; ++i)
          if (right[i])
            z = false;
        if (!z)
          return -1;
      }
      return 0;
    }

    NPackage.Package = class PackageClass {
      constructor(a_name, a_directory, a_configuration){
        this._name           = a_name;
        this._directory      = a_directory;
        this._configuration  = a_configuration;
        this._updateFlag     = false;
        this._handler        = undefined;
        this._handlerFile    = "package.js";
      }

      async initialize(){
        if (this._name)
          fcf.log.log("FCF", `Initialize package ${this._name} ...`);
        await this._setConfiguration();
        await this._setPublic();
        await this._setRoutes();
        await this._setFSQLFilters();
        await this._setFSQLFunctions();
        await this._appendProjections();
        await this._preparePackage();
        await this._appendSystemVariables();
        await this._initializePackage();
      }

      getName() {
        return this._name;
      }

      getUpdateFlag(){
        return this._updateFlag;
      }

      isTheme() {
        return false;
      }

      async _setConfiguration() {
        if (!fcf.empty(this._name))
          fcf.application.getConfiguration().appendConfiguration(this._configuration);
      }

      async _setFSQLFilters(){
        await fcf.each(this._configuration.filters, async (a_type, a_modulePath)=>{
          let modules = await fcf.require([a_modulePath]);
          fcf.NDetails._filters[a_type] = new modules[0]();;
        })
      }

      async _setFSQLFunctions(){
        await fcf.each(this._configuration.functions, async (a_name, a_modulePath)=>{
          let modules = await fcf.require([a_modulePath]);
          fcf.NFSQL.NFunction.setFunction(a_name, modules[0]);

        })
      }

      async _appendProjections(){
        await fcf.each(this._configuration.projectionDirectories, async (a_key, a_directory)=>{
          await fcf.application.getProjections().loadFromDirectory(a_directory);
        })
        if (!fcf.empty(this._configuration.projections))
          await fcf.application.getProjections().loadFromFiles(this._configuration.projections);
      }

      async _appendSystemVariables(){
        if (fcf.application.getConfiguration().disableSys)
          return;
        await fcf.each(this._configuration.systemVariables, async (a_name, a_info) => {
          let packageName  = a_name.split(":")[0];
          let variableName = a_name.split(":")[1];
          let record = (await fcf.application.getStorage().query(
                            "SELECT name FROM ___fcf___variables WHERE package=${1} AND name=${2}",
                            [packageName, variableName]
                            )
                        )[0][0];
          if (!!record)
            return;
          fcf.log.log("FCF", `Install system variable ${packageName}:${variableName}`);
          let value = a_info.value && typeof a_info.value === "object" ? JSON.stringify(a_info.value) : a_info.value;
          await fcf.application.getStorage().query(
                            "INSERT INTO ___fcf___variables (package, name, description, value) VALUES (${1}, ${2}, ${3}, ${4})",
                            [packageName, variableName, a_info.description, value]
                            );
        });
      }

      async _setPublic(){
        let self = this;
        fcf.each(this._configuration.public, (a_key, a_info)=>{
          let relativeUrl;
          let path;

          if (typeof a_info === "object"){
            relativeUrl = a_info.relativeUrl;
            path        = a_info.path;
          } else {
            relativeUrl = a_info;
            path        = a_info;
          }

          if (fcf.empty(relativeUrl))
            relativeUrl = path.split(":")[0];
          if (fcf.empty(path))
            path = relativeUrl;

          var sourcePath = "";
          if (path.indexOf("@") !== -1){
            sourcePath = path;
          } else  if (path.indexOf(":") === -1){
            sourcePath = self._name + ":" + path;
          } else {
            sourcePath = path;
          }

          let shortPath = path.split(":")[0];
          let isDirectory;
          try {
            isDirectory = libFS.lstatSync(fcf.getPath(sourcePath)).isDirectory();
          } catch(e){
            throw new Error("Incorrect configuration. Failed public path [" + sourcePath + "] for module " + self._name +":" + e);
          }

          fcf.application.getRouter().append([{
              uri: isDirectory ? "fcfpackages/" + self._name + "/" + shortPath + "/*" :
                                 "fcfpackages/" + self._name + "/" + shortPath,
              controller: "fcf:NServer/NControllers/File.js",
              source: sourcePath,
            }]);
        })
      }

      async _setRoutes(){
        if (Array.isArray(this._configuration.routes) && !fcf.empty(this._configuration.routes))
          fcf.application.getRouter().append(this._configuration.routes);

        // Append translations path
        if (this._name) {
          fcf.application.getRouter().append([{
              uri: "fcfpackages/" + this._name + "/translations/*",
              controller: "fcf:NServer/NControllers/File.js",
              source: this._name + ":translations",
            }]);
        }

      }

      async _preparePackage(){
        try {
          let handlerFileExists = await libUtil.promisify(libFS.exists)(fcf.getPath(`${this._name}:${this._handlerFile}`));
          if (handlerFileExists) {
            let modules = await fcf.requireEx([`${this._name}:${this._handlerFile}`], {showError: false});
            if (modules[0])
              throw modules[0];
            this._handler = new modules[1]({configuration: this._configuration});
          } else {
            this._handler = new PackageHandler({configuration: this._configuration});
          }

        } catch (error){
          throw error;
        }

        await this._handler.prepare();
      }

      async _initializePackage(){
        try {
          let handlerFileExists = await libUtil.promisify(libFS.exists)(fcf.getPath(`${this._name}:${this._handlerFile}`));
          if (handlerFileExists) {
            let modules = await fcf.requireEx([`${this._name}:${this._handlerFile}`], {showError: false});
            if (modules[0])
              throw modules[0];
            this._handler = new modules[1]({configuration: this._configuration});
          } else {
            this._handler = new PackageHandler({configuration: this._configuration});
          }

        } catch (error){
          throw error;
        }

        if (!fcf.application.getConfiguration().disableSys) {
          let installModules = fcf.application.getSystemVariable("fcf:installModules");
          if (!Array.isArray(installModules) || fcf.empty(installModules))
            installModules = [];

          if (fcf.find(installModules, this._name) == undefined) {
            if (this._name)
              fcf.log.log("FCF", `Install package ${this._name} ...`);
            else
              fcf.log.log("FCF", `Install application ...`);
            await this._handler.install();
            installModules.push(this._name);
            fcf.application.setSystemVariable("fcf:installModules", installModules);
          }

          let currentVersions = fcf.application.getSystemVariable("fcf:packageVersions");
          let currentVersion  = currentVersions[this._name];
          this._updateFlag = currentVersion != this._configuration.version;
          let updateFileExists = await libUtil.promisify(libFS.exists)(fcf.getPath(libPath.join(this._directory, "update.js")));
          if (updateFileExists) {
            let [UpdateHandler] = await fcf.require(this._name + ":update.js");
            let updateHandler = new UpdateHandler();
            let methods = Object.getOwnPropertyNames(Object.getPrototypeOf(updateHandler));
            if (versionCmp(this._configuration.version, currentVersion) > 0) {
              for(let i = 0; i < methods.length; ++i){
                if (methods[i].indexOf(".") == -1)
                  continue;
                let cmpNewVersion = versionCmp(methods[i], this._configuration.version);
                if (versionCmp(methods[i], currentVersion) > 0 && (cmpNewVersion < 0 || cmpNewVersion == 0)){
                  fcf.log.log("FCF", `Performing an update "${methods[i]}" for "${this._name ? this._name : "Application"}" ...`)
                  await updateHandler[methods[i]]();
                }
              }
            }
          }

          if (versionCmp(this._configuration.version, currentVersion) != 0) {
            currentVersions[this._name] = this._configuration.version;
            fcf.application.setSystemVariable("fcf:packageVersions", currentVersions);
          }
        }

        await this._handler.initialize();
      }
    };

    return NPackage.Package;
  }
});