if (fcf.isServer()) {
  var libPath = require('path');
  var libFS = require('fs');
}

fcf.addException("ERROR_PACKAGE_NOT_FOUND",  "Package '${{1}}$' was not found");


fcf.module({
  name: "fcf:NServer/Package.js",
  dependencies: [ "fcf:NFSQL/NFilter/Filter.js", "fcf:NServer/PackageHandler.js"],
  module: function(Filter, DefaultPackageHandler) {
    var NDetails = fcf.prepareObject(fcf, "NServer.NDetails");

    var firstDBQueryErrorSkip = true;

    NDetails.Package = function(a_settings) {
      /**
      * @constructor
      * @param a_settings
      *         - obejct actions
      *         - string package
      *         - array  sources
      *         - array  extension
      *         - object info optional
      *         - string [optional] modPrefix
      **/

      var self = this;
      this._info = {html: {include: {}}};
      this._packageName = a_settings.package;
      this._dependecies = [];
      if (!fcf.empty(a_settings.info)){
        fcf.append(this._info, a_settings.info);
      }

      this.getInfo = function() {
        return this._info;
      }

      this.getName = function() {
        return this._packageName;
      }

      this.initialize = function() {
        let self = this;
        let packageData = undefined;
        let reverseProjections = [];
        let installModules = [];
        let packageHandler = undefined;
        let clModules = [];
        let clTypes  = [];
        let clFunctionsModules = [];
        let clFunctionsTypes  = [];
        let dirPack = undefined;
        let modName = undefined;
        let packFilePath = undefined;

        if (fcf.application.getPackages()[a_settings.package])
          return fcf.actions();

        return fcf.actions()
        .then(function(a_res, a_act) {
          packFilePath = self._findPackagePath(a_settings.sources, a_settings.package + "." + a_settings.extension)
          if (!packFilePath)
            throw new fcf.Exception("ERROR_PACKAGE_NOT_FOUND", [a_settings.package]);

          dirPack = libPath.dirname(packFilePath);
          modName = a_settings.package;
          if (a_settings.modPrefix)
            modName = a_settings.modPrefix + modName.charAt(0).toUpperCase() + modName.substr(1);
          self._packageName = modName;
          var urlPack = "fcfpackages/" + modName;

          fcf.addModule(modName, dirPack, urlPack);

          fcf.NDetails.eventChannel.send("watch_file", {file: fcf.getPath(packFilePath)});

          fcf.load({
            path: packFilePath,
            onResult: function(a_error, a_data) {
              if (a_error) {
                a_act.error(a_error);
                return;
              }
              packageData = fcf.scriptExecutor.parse(a_data, {}, packFilePath, 0);
              a_act.complete();
            }
          });
        })
        .then(function(a_res, a_act) {
          fcf.actions()
          .catch((a_error)=>{
            a_act.error(a_error);
          })
          .each(packageData.dependencies, function(a_key, a_dependencies, a_res, a_subact) {
              let pack = new NDetails.Package({
                application: a_settings.application,
                configuration: a_settings.configuration,
                package:   a_dependencies,
                sources:   a_settings.sources,
                extension: a_settings.extension,
              });
              fcf.actions()
              .then(()=>{
                return pack.initialize();
              })
              .then(function(){
                a_settings.application.appendPackage(pack);
                a_subact.complete();
              })
              .catch(function(a_error){
                a_subact.error(a_error);
              })
          })
          .then(function(){
            a_act.complete();
          })
        })
        .then(function(a_res, a_act) {
          try {

            self._mergeOptions(packageData);

            if (a_settings.configuration !== false){
              a_settings.configuration.appendConfiguration(packageData, self.isTheme);
            }

            // append  template aliases
            if (typeof packageData.aliases === "object")
              for(k in packageData.aliases)
                fcf.application.appendTemplateAlias(k, packageData.aliases[k]);

            // append  template views
            if (typeof packageData.views === "object")
              for(k in packageData.views)
                fcf.application.appendViewAlias(k, packageData.views[k]);

            // append  template filters
            if (typeof packageData.filters === "object"){
              for(type in packageData.filters) {
                clModules.push(packageData.filters[type]);
                clTypes.push(type);
              }
            }

            // append  template FSQL functions
            if (typeof packageData.functions === "object"){
              for(type in packageData.functions) {
                clFunctionsModules.push(packageData.functions[type]);
                clFunctionsTypes.push(type);
              }
            }

            // Append public paths
            if (Array.isArray(packageData.public)) {
              for (var i = 0; i < packageData.public.length; ++i) {
                let relativeUrl;
                let path;

                if (typeof packageData.public[i] === "object"){
                  relativeUrl = packageData.public[i].relativeUrl;
                  path        = packageData.public[i].path;
                } else {
                  relativeUrl = packageData.public[i];
                  path        = packageData.public[i];
                }

                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 = modName + ":" + 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 " + modName +":" + e);
                }

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

            // append configuration module file to network access
            fcf.application.getRouter().append([{
              uri: "fcfpackages/" + modName + "/" + modName + "." + a_settings.extension,
              controller: "fcf:NServer/NControllers/File.js",
              source: modName + ":" + modName + "." + a_settings.extension,
            }]);

            if (Array.isArray(packageData.publicFiles)) {
              for(var i = 0; i < packageData.publicFiles.length; ++i) {
                if (typeof packageData.publicFiles[i] !== "object")
                  continue;
                var relativeUrl = packageData.publicFiles[i].relativeUrl;
                fcf.application.getRouter().append([{
                    uri: fcf.empty(relativeUrl) ? "fcfpackages/" + modName + "/" + packageData.publicFiles[i].path : "fcfpackages/" + modName + "/" + relativeUrl + "",
                    controller: "fcf:NServer/NControllers/File.js",
                    source: modName + ":" + packageData.publicFiles[i].path,
                  }]);
              }
            }

            if (Array.isArray(packageData.routes)) {
              for(var i = 0; i < packageData.routes.length; ++i) {
                if (typeof packageData.routes[i] !== "object")
                  continue;
                fcf.application.getRouter().append([packageData.routes[i]]);
              }
            }

            let projectionFiles = [];
            if (Array.isArray(packageData.projectionDirectories) && !fcf.empty(packageData.projectionDirectories)) {
              let clReadDirectory = (a_path)=>{
                a_path = a_path.indexOf("/") == 0 ?  a_path :
                         a_path.indexOf(":") == -1 ? self._packageName + ":" + a_path :
                                                     a_path;

                a_path = fcf.getPath(a_path);
                var files = libFS.readdirSync(a_path);
                for(var key in files) {
                  let fpath = a_path + "/" + files[key];
                  let fstat = undefined;
                  try {
                    fstat = libFS.statSync(fpath);
                  } catch(e) {
                    continue;
                  }
                  if (fstat.isDirectory()) {
                    clReadDirectory(fpath);
                  } else {
                    if (fcf.getExtension(fpath) == "projection")
                      projectionFiles.push(fpath);
                  }
                }
              }
              for(let i = 0; i < packageData.projectionDirectories.length; ++i)
                clReadDirectory(packageData.projectionDirectories[i]);
            }

            if (Array.isArray(packageData.projections) && !fcf.empty(packageData.projections)){
              let projections = [];
              fcf.each(packageData.projections, (a_key, a_path)=>{
                a_path = a_path.indexOf("/") == 0 ?  a_path :
                         a_path.indexOf(":") == -1 ? self._packageName + ":" + a_path :
                                                     a_path;
                projections.push(a_path);
              })

              fcf.append(projectionFiles, projections)
            }

            if (!fcf.empty(projectionFiles))
              fcf.application.getProjections().loadFromFiles(projectionFiles);


            fcf.each(fcf.application.getProjections().getProjections(), function(a_alias, a_projection){
              if (a_projection.dbSync)
                reverseProjections.push(a_projection);
            });

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

          } catch(except) {
            a_act.error(except);
            return;
          }

          a_act.complete();
        })

        .then(function(a_res, a_act) {
          if (!fcf.empty(clModules)){
            fcf.require(clModules, function() {
              for(var i = 0; i < clTypes.length; ++i) {
                let constr = arguments[i];
                fcf.NDetails._filters[clTypes[i]] = new constr();
              }
              a_act.complete();
            });
          } else {
            a_act.complete();
          }
        })
        // Append FSQL functions
        .then(function(a_res, a_act) {
          if (!fcf.empty(clFunctionsModules)){
            fcf.require(clFunctionsModules, function() {
              let modules = arguments;
              fcf.each(clFunctionsTypes, function(a_key, a_type){
                fcf.NFSQL.NFunction.setFunction(a_type, modules[a_key]);
              });
              a_act.complete();
            });
          } else {
            a_act.complete();
          }
        })
        // Build DB
        .then((a_res, a_act) => {
          if (fcf.application.getConfiguration().disableUpdateProjection){
            a_act.complete();
            return
          }
          fcf.require(["fcf:NFSQL/NDetails/DBBuilder.js"], (DBBuilder)=>{
            let dbBuilder = new DBBuilder(fcf.application.getStorage(), reverseProjections);
            dbBuilder.build()
            .then(()=>{
              a_act.complete();
            })
            .catch((e)=>{
              a_act.error(e);
            })
          })
        })
        // Read install modules
        .then(function(a_res, a_act) {
          var options = {hideErrors: ["ER_NO_SUCH_TABLE", "ERROR_MEMDB_TABLE_NOT_EXISTS"]};
          firstDBQueryErrorSkip = false;
          fcf.application.selectSystemVariables("fcf:installModules", options, function(a_error, a_variables){
            if (a_error){
              a_act.complete();
              return;  
            }
            installModules = Array.isArray(a_variables["fcf:installModules"]) ? a_variables["fcf:installModules"] : [];
            a_act.complete();
          });
        })
        // Install module
        .then(function(a_res, a_act) {
            var dirPack = libPath.dirname(packFilePath);
            var modHandlerName = modName;
            if (modName == "fcf")
              modHandlerName = "fcfModule"

            var isModuleExist = libFS.existsSync(fcf.getPath(modName + ":" + modName + ".js", true));
            fcf.requireEx(
              [modName + ":" + modHandlerName + ".js"],
              {showError: false},
              function(a_error, PackageHandler) {
                if (isModuleExist && a_error){
                  console.error(a_error, PackageHandler);
                }
                PackageHandler = PackageHandler ? PackageHandler : DefaultPackageHandler;
                packageHandler = new PackageHandler();

                if (!fcf.application.getConfiguration().disableSys && fcf.find(installModules, modName) === undefined){
                  fcf.log.log("FCF", "Installing package " + modName + "...");
                  fcf.actions()
                  .then(()=>{
                    return packageHandler.install();
                  })
                  .then(()=>{
                    fcf.log.log("FCF", "Installing package " + modName + " is completed");
                    installModules.push(modName);
                    fcf.application.updateSystemVariables({"fcf:installModules": installModules}, function(a_error){
                      if (a_error){
                        fcf.log.err("FCF", a_error);
                      }
                      a_act.complete();
                    })
                  })
                  .catch(function(a_error){
                    a_act.error(a_error);
                  });
                } else {
                  a_act.complete();
                }
              }
            );
        })
        // initialize module
        .then(function() {
          if (!packageHandler)
            return;
          return packageHandler.initialize();
        });
      }

      this._mergeOptions = function(a_data) {
        for (var key in a_data) {
          if (key == "public") {
            if (!this._info[key])
              this._info[key] = {};
            fcf.append(this._info[key], a_data[key]);
          } else if (key == "html") {
            if (!a_data[key].include)
              continue;
            if (!this._info[key])
              this._info[key] = {};
            if (!this._info[key].include)
              this._info[key].include = {};
            fcf.append(this._info[key], a_data[key]);
          } else if (key == "aliases") {
            if (!this._info[key])
              this._info[key] = {};
            fcf.append(this._info[key], a_data[key]);
          } else {
            this._info[key] = a_data[key];
          }
        }
      }

      this._findPackagePath = function(a_paths, a_fileName) {
        function clFind(a_path, a_fileName) {
          var statDir = libFS.lstatSync(a_path);
          if (statDir && !statDir.isDirectory())
            return;

          var files = libFS.readdirSync(a_path);

          for(var i = 0; i < files.length; ++i) {
            var filePath = libPath.join(a_path, files[i]);
            var stat = libFS.lstatSync(filePath);
            if (!stat.isDirectory() && a_fileName == files[i])
              return filePath;
          };

          for(var i = 0; i < files.length; ++i) {
            var filePath = libPath.join(a_path, files[i]);
            var stat = libFS.lstatSync(filePath);
            if (stat.isDirectory()) {
              var res = clFind(filePath, a_fileName);
              if (res)
                return res;
            }
          };
        };

        for(var i = a_paths.length - 1; i >= 0; --i) {
          var res = clFind(fcf.getPath(a_paths[i]), a_fileName)
          if (res)
            return res;
        }
      }

    }

    return NDetails.Package;
  }
});
