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

fcf.module({
  name: "fcf:NFSQL/Projections.js",
  dependencies: ["fcf:NFSQL/NDetails/SingleParser.js", "fcf:NFSQL/NDetails/Tools.js"],
  module: function(SingleParser, fsqlTools) {
    fcf.prepareObject(fcf, "NFSQL");
    fcf.addException("ERROR_PROJECTION_UNKNOWN_JOIN_FIELD", "Incorrect '${{projection}}$' projection is specified in the join property for the ${{field}}$ field");

    fcf.NFSQL.Projections = function() {
      var self = this;
      this._descs           = {};
      this._translations    = {};
      // Нужен только для загрузки файлов как флаг
      this._reloadDirs      = {};
      this._pendingExtends  = {};
      this._pendingJoin     = {};
      this._mirrors         = {};

      this.transformation = function(a_transformation) {
        var projections = this.clone();
        var projectionsMap = projections._descs;
        for(var alias in projectionsMap) {
          var isFirstTransformation = true;
          for(var transformationName in a_transformation) {
            if (!projectionsMap[alias].transformation)
              continue;
            if (!projectionsMap[alias].transformation[transformationName])
              continue;
            if (isFirstTransformation)
              projectionsMap[alias] = fcf.append(true, {}, projectionsMap[alias]);
            isFirstTransformation = false;

            var transformationItems = fcf.tokenizeObject(projectionsMap[alias].transformation[transformationName], a_transformation[transformationName]);
            if (!Array.isArray(transformationItems))
              continue;

            for(var i = 0; i < transformationItems.length; ++i) {
              var item = transformationItems[i];
              if (!item.source)
                continue;
              var ptr = fcf.resolveEx(projectionsMap[alias], item.source, true);
              ptr.object[ptr.key] = item.value;
            }
          }
        }
        return projections;
      }

      this.loadFromDirectory = function(a_path, a_recursion) {
        a_path = fcf.getPath(a_path);
        if (!a_recursion)
          this._reloadDirs[a_path] = true;

        var  files = libFS.readdirSync(a_path);
        for(var key in files) {
          var fpath = a_path + "/" + files[key];
          try {
            var isDir;
            try {
              var stats = libFS.statSync(fpath);
              isDir = stats.isDirectory();
            } catch(e){
              continue;
            }
            if (isDir) {
              this.loadFromDirectory(fpath, a_recursion);
            } else {
              if (fpath.length < 12)
                continue;
              if (fpath.substr(-11) != ".projection")
                continue;
              this.loadFromFiles(fpath);
            }
          } catch(e) {
            fcf.log.err("", "Can't load projection file '" + fpath + "': (" + e.toString() + ")");
            throw e;
          }
        }
      }

      this.loadFromFiles = function(a_paths) {
        let self = this;

        if (typeof a_paths == "string")
          a_paths = [a_paths];

        let projections = {};
        fcf.each(a_paths, (a_key, a_path)=>{
          a_path = libPath.resolve(fcf.getPath(a_path));
          let raw = libFS.readFileSync(a_path, 'utf8');
          let obj = eval("(" + raw + ")");
          if (obj.enable === false) {
            if (a_path.indexOf("___fcftranslate___") == -1)
              fcf.NDetails.eventChannel.send("watch_file", {file: a_path});
            return;
          }
          let proj = self.appendProjectionStruct(obj);
          projections[proj.alias] = proj;

          if (a_path.indexOf("___fcftranslate___") == -1)
            fcf.NDetails.eventChannel.send("watch_file", {file: a_path});
        })
      }

      this._reloadDirectories = function(){
        try {
          for(var dirPath in this._reloadDirs)
            this.loadFromDirectory(dirPath);
        } catch(e){
          fcf.log.err("FCF", e);
        }
        if (fcf.application.getConfiguration().projectionReloadingTimeout)
          setTimeout(function(){ self._reloadDirectories(); }, fcf.application.getConfiguration().projectionReloadingTimeout);
      }

      this.clone = function() {
        var projections = new fcf.NFSQL.Projections();
        fcf.append(projections._descs, this._descs);
        return projections;
      }

      this.appendProjectionStruct = function(a_objProjection) {
        if (a_objProjection.enable === false)
          return;
        a_objProjection = fcf.append({}, a_objProjection);
        a_objProjection.fields = fcf.append([], a_objProjection.fields);

        if (a_objProjection.extends){
          a_objProjection = this._extends(a_objProjection);
          if (!a_objProjection)
            return;
        }

        a_objProjection.externalAccess = fcf.append({read: true, edit: true, add: true, delete: true}, a_objProjection.externalAccess);

        a_objProjection.singleTitle = a_objProjection.singleTitle ? a_objProjection.singleTitle : "";

        let translate = false;
        for(var i = 0; i < a_objProjection.fields.length; ++i) {
          var f = a_objProjection.fields[i];
          f.selfProjection = a_objProjection.alias;
          if (!("field" in f))
            f.field = f.alias;
          if (!("alias" in f))
            f.alias = f.field;
          if (!("title" in f) && !f.join)
            f.title = f.alias;
          let filter = fcf.getFilter(f);
          translate |= filter ? !!filter.isTranslateField(f) : false;

          if (f.alias === a_objProjection.key) {
            f.notNull = true;
            f.unique  = true;
            f.notEdit = true;
          }
        }

        if (translate)
          a_objProjection.translate = true;

        if (!('connectionGroup' in a_objProjection))
          a_objProjection.connectionGroup = "default";


        this._descs[a_objProjection.alias] = this._createAdaptProjection(a_objProjection);
        this._processJoinFields(a_objProjection.alias);

        this._continueProcessingPendingExtends();
        this._processJoinFields();
        this.recalculate();

        let projection = this._descs[a_objProjection.alias];
        fcf.each(projection.join, (a_key, a_join)=>{
          if (a_join.attach !== "mirror")
            return;
          if (!this._mirrors[a_join.from])
            this._mirrors[a_join.from] = [];
          if (fcf.find(this._mirrors[a_join.from], projection.alias) !== undefined)
            return;
          this._mirrors[a_join.from].push(projection.alias);
        });

        return this._descs[a_objProjection.alias];
      }

      this.get = function(a_alias) {
        let lang = fcf.getContext() ? fcf.getContext().language : undefined;
        if (!(lang in this._translations))
          lang = fcf.getSystemVariable("fcf:defaultLanguage");
        if (!(lang in this._translations))
          lang = "en";
        return this._translations[lang] ? this._translations[lang][a_alias] : undefined;
      }

      this.getProjections = function() {
        let lang = fcf.getContext() ? fcf.getContext().language : undefined;
        if (!(lang in this._translations))
          lang = fcf.getSystemVariable("fcf:defaultLanguage");
        if (!(lang in this._translations))
          lang = "en";
        return this._translations[lang];
      }

      this.recalculate = function() {
        let self = this;
        this._translations = {};
        let languages = fcf.getSystemVariable("fcf:languages");
        if (fcf.empty(languages))
          languages = {en: "English"};

        fcf.each(languages, (a_lang)=>{
          self._translations[a_lang] = fcf.translate(self._descs, a_lang)
        });

        for(let alias in this._descs) {
          let translate = false;
          let projection  = this._descs[alias];
          for(let fieldIndex in projection.fields){
            let field = projection.fields[fieldIndex];
            let filter = fcf.getFilter(field);
            if (!filter)
              continue;
            translate |= !!filter.isTranslateField(field);
            if (translate)
              break;
          }

          if (translate) {
            if (!projection.translate)
              projection.translate = true;
            fcf.each(languages, (a_lang) => {
              if (self._translations[a_lang] && self._translations[a_lang][projection.alias])
                if (!self._translations[a_lang][projection.alias].translate)
                  projection.translate = true;
            });
          }
        }
      }

      this.checkProjections = function() {  /* throw */
        fcf.each(this._descs, (a_key, a_projection)=>{

          if (fcf.empty(a_projection.alias))
            throw new fcf.Exception("ERROR_UNKNOWN_FIELD_TYPE", {param: "alias", projection: a_projection.alias})

          if (fcf.empty(a_projection.table))
            throw new fcf.Exception("ERROR_UNKNOWN_FIELD_TYPE", {param: "table", projection: a_projection.alias})

          if (fcf.empty(a_projection.title))
            throw new fcf.Exception("ERROR_UNKNOWN_FIELD_TYPE", {param: "title", projection: a_projection.alias})

          if (fcf.empty(a_projection.key))
            throw new fcf.Exception("ERROR_UNKNOWN_FIELD_TYPE", {param: "key", projection: a_projection.alias})

          if (!fcf.empty(a_projection.join)){
            fcf.each(a_projection.join, (a_key, a_join)=>{
              if (a_join.as)
              if (!fcf.getProjection(a_join.from))
                throw new fcf.Exception("ERROR_UNKNOWN_PROJECTION_IN_JOIN", {joinProjection: a_join.from, projection: a_projection.alias});
              if (fcf.empty(a_join.on))
                throw new fcf.Exception("ERROR_PROJECTION_UNSET_ON", {projection: a_projection.alias});
            })
          }

          if (fcf.empty(a_projection.fields))
            throw new fcf.Exception("ERROR_UNKNOWN_FIELD_TYPE", {param: "fields", projection: a_projection.alias})
          // fcf.each(a_projection.join, (a_key, a_field)=>{
          //   if (fcf.empty(a_field.alias))
          //     throw new fcf.Exception("ERROR_PROJECTION_UNSET_ALIAS", {projection: a_projection.alias})
          //   if (fcf.empty(a_field.type))
          //     throw new fcf.Exception("ERROR_PROJECTION_UNSET_TYPE",  {projection: a_projection.alias})
          //   let filter = fcf.getFilter(a_field);
          //   if (filter)
          //     throw new fcf.Exception("ERROR_UNKNOWN_FIELD_TYPE",  {param: a_field.alias, projection: a_projection.alias})
          //   filter.checkStructure(a_projection, a_field)
          // });

        });
      }

      this.getMirrors = function(a_alias){
        return this._mirrors[a_alias] ? this._mirrors[a_alias] : [];
      }

      this._processJoinFields = function(a_projection){
        var projections = a_projection ? [a_projection] : this._pendingJoin;

        // fill referencing flag and set property "as"
        let singleParser = new SingleParser();
        fcf.each(projections, function(keyProjection, projectionAlias){
          var projection = self._descs[projectionAlias];
          fcf.each(projection.join, function(a_key, a_join){
            if (!a_join.as)
              a_join.as = a_join.from;

            if ("referencing" in a_join)
              return;

            let whereBlock = typeof a_join.on == "string" ? singleParser.parseWhere(a_join.on) : a_join.on;
            let referencing = true;
            fsqlTools.eachFieldBlockWhere({}, whereBlock, (a_info)=>{
              let joinProjectionAlias = a_info.field.from;
              if (a_join.as && joinProjectionAlias == a_join.as)
                joinProjectionAlias = a_join.from;
              if (!self._descs[joinProjectionAlias])
                return;
              if (projectionAlias == joinProjectionAlias)
                return;
              let proj = self._descs[joinProjectionAlias];
              if (proj.key == a_info.field.field)
                referencing = false;
            })

            a_join.referencing = referencing;
          });
        });

        function clResolveField(a_projection, a_field){
          if (!a_field.join)
            return true;
          let from  = a_field.realjoin ? a_field.realjoin.from : a_field.join.from;
          let field = a_field.realjoin ? a_field.realjoin.field : a_field.join.field;
          let join  = a_projection.join[fcf.find(a_projection.join, (k,v)=>{ return v.as == from;})];
          if (!join){
            delete a_field.realjoin;
            return false;
          }
          let joinProjection = self._descs[join.from];
          if (!joinProjection){
            delete a_field.realjoin;
            return false;
          }
          let joinField = joinProjection.fieldsMap[field];
          if (!joinField){
            delete a_field.realjoin;
            return false;
          }

          if (!a_field.realjoin){
            a_field.realjoin = {
              from:  from,
              field: field,
            };
          }

          let result = true;

          if (joinField.join) {
            a_field.realjoin.from  += "@" + joinField.join.from;
            a_field.realjoin.field = joinField.join.field;
            result = clResolveField(a_projection, a_field);
          }

          if (!result) {
            delete a_field.realjoin;
            return false;
          }

          let newfield = fcf.append({}, joinField, a_field);
          fcf.append(a_field, newfield);

          return result;
        }

        function clProcessBlocks(a_blocks, a_defaultProjectionAlias, a_selfJoin, a_processedJoinOriginAs, a_processedJoin) {
          function clProcessArg(a_arg) {
            if (!a_arg.from || a_arg.from == a_defaultProjectionAlias){
              a_arg.from = a_selfJoin.as;
            } else if (a_arg.from == a_processedJoinOriginAs){
              a_arg.from = a_processedJoin.as;
            }
          }
          fcf.each(a_blocks, (a_key, a_block)=>{
            if (a_block.type === "block") {
              clProcessBlocks(a_block, a_selfJoin, a_processedJoin);
            } else {
              fcf.each(a_block.args, (a_key, a_arg)=>{
                if (typeof a_arg == "object" && a_arg.field)
                  clProcessArg(a_arg);
              });
            }
          })
        }

        function clResolveJoins(a_projection, a_joinIndex){
          let join = a_projection.join[a_joinIndex];
          if (join.inner)
            return true;
          let joinProjection = self._descs[join.from];
          if (!joinProjection)
            return false;
          let result = true;
          fcf.each(joinProjection.join, (a_key, a_join)=>{
            if (a_join.inner)
              return;
            let newjoin = {
              inner:  true,
              from:   a_join.from,
              as:     join.as + "@" + a_join.as,
              attach: "none",
              on:     !Array.isArray(a_join.on) ? singleParser.parseWhere(a_join.on) : fcf.append(true, [], a_join.on)
            };
            clProcessBlocks(newjoin.on, joinProjection.alias, join, a_join.as, newjoin);
            a_projection.join.push(newjoin);
            clResolveJoins(a_projection, a_projection.join.length-1);
          });
          return result;
        }

        fcf.each(projections, (keyProjection, projectionAlias) => {
          let projection = self._descs[projectionAlias];
          let completed = true;
          fcf.each(projection.join, (a_index)=>{
            completed &= clResolveJoins(projection, a_index);
          });
          if (completed) {
            fcf.each(projection.fields, (a_index, a_field)=>{
              completed &= clResolveField(projection, a_field);
            });
          }
          if (completed)
            delete self._pendingJoin[projectionAlias];
          else 
            self._pendingJoin[projectionAlias] = projectionAlias;
        });

      }

      this._continueProcessingPendingExtends = function() {
        var numberProcessed = undefined;
        do {
          numberProcessed = 0;
          var rmkeys = [];

          for(var k in this._pendingExtends) {
            var projection = this._extends(this._pendingExtends);
            if (!projection)
              continue;

            rmkeys.push(k);
            this._descs[projection.alias] = this._createAdaptProjection(projection);
            this._processJoinFields(projection.alias);
            this._processJoinFields();
            ++numberProcessed;
          }

          for(var i = 0; i < rmkeys.length; ++i) {
            delete this._pendingExtends[rmkeys[i]];
          }
        } while(numberProcessed != 0)
      }

      this._extends = function(a_objProjection){
        var parent = this._descs[a_objProjection.extends];
        if (!parent){
          this._pendingExtends[a_objProjection.alias] = a_objProjection;
          return;
        }
        var expandableProjection = fcf.append(true, {}, parent);
        delete expandableProjection.fieldsMap;
        fcf.append(expandableProjection, a_objProjection);

        expandableProjection.fields = fcf.append(true, [], parent.fields);
        var expandableFields = expandableProjection.fields;
        var selfFields   = a_objProjection.fields;

        for (var i = 0; i < selfFields.length; ++i) {
          var key = fcf.find(expandableFields, function(k,v) { return v.alias == selfFields[i].alias});
          if (key === undefined){
            expandableFields.push(selfFields[i]);
          } else {
            if (!selfFields[i].extends)
              expandableFields[key] = selfFields[i];
            else
              fcf.append(expandableFields[key], selfFields[i]);
          }
        }

        return expandableProjection;
      }



      this._createAdaptProjection = function(a_objProjection) {
        var adaptProjection = fcf.append({}, a_objProjection);

        adaptProjection.fieldsMap = {};
        for(var key in adaptProjection.fields) {
          adaptProjection.fieldsMap[adaptProjection.fields[key].alias] = adaptProjection.fields[key];
        }
        return adaptProjection;
      }

      if (fcf.application && fcf.application.getConfiguration() && !fcf.application.getConfiguration().fileСaching)
        setTimeout(function(){ self._reloadDirectories(); }, 2000);

    };

    return fcf.NFSQL.Projections;
  }
});
