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/DBBuilder.js",
                 "fcf:NFSQL/NDetails/Tools.js"],
  module: function(SingleParser, DBBuilder, 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.loadFromDirectory = async function(a_path, a_recursion, a_disableLoad) {
        a_path = fcf.getPath(a_path);
        if (!a_recursion)
          this._reloadDirs[a_path] = true;

        let projectionPaths = [];

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

        if (!a_disableLoad)
          await this.loadFromFiles(projectionPaths);

        return projectionPaths;
      }

      this.loadFromFiles = async function(a_paths, a_disableDBBuild) {
        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 = undefined;
          try {
            obj = eval("(" + raw + ")");
          } catch(error){
            throw new fcf.Exception("ERROR_INVALID_PROJECTION_FORMAT", {projection: a_path}, error);
          }
          if (obj.enable === false) {
            if (a_path.indexOf("___fcftranslate___") == -1)
              fcf.NDetails.eventChannel.send("watch_file", {file: a_path});
            return;
          }
          projections[obj.alias] = obj;

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

        await self.appendProjectionStructs(projections, a_disableDBBuild);
      }

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

      this.appendProjectionStructs = async function(a_objProjections, a_disableDBBuild) {
        let self = this;
        let waitingProjections = this._getWaitingProjections(fcf.array(a_objProjections, (a_key, a_proj)=>{ return a_proj.alias; }));

        await fcf.asyncEach(a_objProjections, async (a_key, a_projection)=>{
          await self.appendProjectionStruct(a_projection, true);
        });

        if (!a_disableDBBuild) {
          let preparedProjections = this._getPreparedProjections(waitingProjections);
          if (!fcf.empty(preparedProjections)){
            let dbBuilder = new DBBuilder(preparedProjections);
            await dbBuilder.build();
          }
          await this._postBuild();
        }
      }

      this.appendProjectionStruct = async function(a_objProjection, a_disableDBBuild) {
        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;
        }

        let waitingProjections = this._getWaitingProjections([a_objProjection.alias]);

        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.retranslate();

        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);
        });

        if (!a_disableDBBuild) {
          let preparedProjections = this._getPreparedProjections(waitingProjections);
          if (!fcf.empty(preparedProjections)){
            let dbBuilder = new DBBuilder(preparedProjections);
            await dbBuilder.build();
          }
          await this._postBuild();
        }

        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.application.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.application.getSystemVariable("fcf:defaultLanguage");
        if (!(lang in this._translations))
          lang = "en";
        return this._translations[lang];
      }

      this.retranslate = function() {
        let self = this;
        this._translations = {};
        let languages = fcf.application.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.application.getProjections().get(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._getWaitingProjections = function(a_addedProjections){
        let result = Array.isArray(a_addedProjections) ? fcf.append([], a_addedProjections) : [];
        fcf.each(this._pendingExtends, (a_alias)=>{
          if (fcf.find(result, a_alias) === undefined)
            result.push(a_alias);
        })
        fcf.each(this._pendingJoin, (a_alias)=>{
          if (fcf.find(result, a_alias) === undefined)
            result.push(a_alias);
        })
        return result;
      }

      this._getPreparedProjections = function(a_waitingProjections) {
        let result                = {};
        let curWaitingProjections = this._getWaitingProjections();
        fcf.each(a_waitingProjections, (a_key, a_alias) => {
          if (fcf.find(curWaitingProjections, a_alias) === undefined)
            result[a_alias] = this._descs[a_alias];
        });
        return result;
      }

      this._postBuild = async function () {
        let waiting1 = this._getWaitingProjections();
        if (fcf.empty(waiting1))
          return;
        this._continueProcessingPendingExtends();
        this._processJoinFields();
        this.retranslate();
        let waiting2 = this._getWaitingProjections();
        if (waiting1.length == waiting2.length)
          return;

        let preparedProjections = this._getPreparedProjections(waiting1);
        if (!fcf.empty(preparedProjections)) {
          let dbBuilder = new DBBuilder(preparedProjections);
          await dbBuilder.build();
        }

        this._postBuild();
      }

      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;
      }

    };

    return fcf.NFSQL.Projections;
  }
});
