fcf.module({
  name: "fcf:NFSQL/NDetails/DBBuilder.js",
  dependencies: ["fcf:NFSQL/Parser.js"],
  module: function(Parser){
    let Namespace = fcf.prepareObject(fcf, "packages/fcf/NFSQL/NDetails");
    let stProcessedTables = {};
    let stProcessedFields = {};
    let stProcessedUnique = {};
    let stProcessedJoinTables = {};

    class DBBuilder {
      constructor(a_projectionsStruct){
        this._projections = fcf.filter(a_projectionsStruct, (a_key, a_projection)=>{ return !!a_projection.dbSync});
      }

      build(){
        if (fcf.application.getConfiguration().disableSys)
          return fcf.actions();

        let self = this;
        return fcf.actions()
        .then(()=>{
          return self._createDBs();
        })
        .then(()=>{
          return self._updateFields();
        })
        .then(()=>{
          return self._updateUnique();
        })
        .then(()=>{
          return self._autoFillJoinTables();
        })
        .then(()=>{
          return self.completionInitialization();
        })
        .catch((a_error)=>{
          fcf.log.err("FCF:FSQL BUILDER", a_error);
        })
      }

      completionInitialization(){
        let self = this;
        return fcf.actions()
        .then(async ()=>{
          let map  = await fcf.application.loadSystemVariable("fcf:autoFillJoinTables");
          fcf.each(self._projections, (a_key, a_projection)=>{
            fcf.each(a_projection.join, (a_key, a_joinInfo)=>{
              if (a_joinInfo.attach == "mirror"){
                if (!map[a_joinInfo.from])
                  map[a_joinInfo.from] = {};
                map[a_joinInfo.from][a_projection.alias] = true;
              }
            });
          })
          return fcf.application.setSystemVariable("fcf:autoFillJoinTables", map);
        })
      }

      _updateUnique() {
        let self = this;
        return fcf.actions()
        .each(this._projections, (a_key, a_projection)=>{
          if (a_projection.alias in stProcessedUnique)
            return;
          stProcessedUnique[a_projection.alias] = a_projection.alias;
          let connection = fcf.application.getStorage().getStorage().getConnection(a_projection.connection);

          return connection.updateUnique(a_projection);
        })
      }

      _createDBs() {
        let self = this;
        return fcf.actions()
        .each(self._projections, (a_key, a_projection)=>{
          if (!a_projection.dbSync){
            return;
          }

          if (a_projection.alias in stProcessedTables)
            return;
          stProcessedTables[a_projection.alias] = a_projection.alias;

          let connection = fcf.application.getStorage().getStorage().getConnection(a_projection.connection);
          return connection.checkTable(a_projection.table)
          .then((a_tableExist)=>{
            if (!a_tableExist)
              return connection.createTable(a_projection.table);
          })
        })
      }    

      _updateFields() {
        let self = this;
        return fcf.actions()
        .each(self._projections, (a_key, a_projection, a_res, a_act)=>{
          if (a_projection.alias in stProcessedFields){
            a_act.complete();
            return;
          }
          stProcessedFields[a_projection.alias] = a_projection.alias;

          if (!a_projection.dbSync){
            a_act.complete();
            return;
          }

          let connection = fcf.application.getStorage().getStorage().getConnection(a_projection.connection);
          connection.getTableFields(a_projection.table)
          .then((a_tableFields)=>{
            let projFields = [];
            let mapFilters = {};
            fcf.each(a_projection.fields, (a_key, a_field)=>{
              if (a_field.join)
                return;
              let filter = fcf.getFilter(a_field);
              let prjRealFields = filter.getRealFields(a_projection, a_field.alias);
              fcf.each(prjRealFields, (a_key, a_field)=>{
                mapFilters[a_field.field] = filter;
              })
              fcf.append(projFields, prjRealFields);
            });

            let actions = fcf.actions();

            fcf.each(projFields, (a_key, a_projField)=>{
              let indexTableField = fcf.find(a_tableFields, (a_key, a_tableField) => { return a_tableField.field == a_projField.field });
              if (indexTableField === undefined){
                actions.then(connection.createField(a_projection, a_projField));
              } else if (!mapFilters[a_projField.field].equalRealFields(a_projField, a_tableFields[indexTableField])) {
                actions.then(connection.modifyField(a_projection, a_projField));
              }
            })

            actions.then(()=>{
              a_act.complete()
            })
            .catch((e)=>{
              a_act.error(e)
            })
          })
          .catch((e)=>{
            a_act.error(e)
          })
        })
      }

      _autoFillJoinTables() {
        let self = this;
        let map  = {};
        fcf.each(this._projections, (a_key, a_projection)=>{
          fcf.each(a_projection.join, (a_key, a_joinInfo)=>{
            if (a_joinInfo.attach == "mirror") {
              if (!map[a_joinInfo.from])
                map[a_joinInfo.from] = {};
              let as = a_joinInfo.as ? a_joinInfo.as : a_joinInfo.from;
              if (!map[a_joinInfo.from][as])
                map[a_joinInfo.from][as] = {};
              map[a_joinInfo.from][as][a_projection.alias] = true;
            }
          });
        })
        let list = [];
        return fcf.actions()
        .then(()=>{
          return fcf.application.loadSystemVariable("fcf:autoFillJoinTables");
        })
        .then((a_autoFillJoinTables)=>{
          for(let lowerProjectionAlias in map) {
            for(let lowerJoinAlias in map[lowerProjectionAlias]) {
              for(let upperProjectionAlias in map[lowerProjectionAlias][lowerJoinAlias]) {
                let key = lowerProjectionAlias + "->-->-->-" + lowerJoinAlias + "->-->-->-" + upperProjectionAlias;
                if (!(lowerProjectionAlias in a_autoFillJoinTables) || !(upperProjectionAlias in a_autoFillJoinTables[lowerProjectionAlias])) {
                  if (!(key in stProcessedJoinTables)){
                    stProcessedJoinTables[key] = true;
                    list.push({lower: lowerProjectionAlias, lowerAlias: lowerJoinAlias, upper: upperProjectionAlias});
                  }
                }
              }
            }
          }
        })
        .each(()=>{ return list; }, (a_key, a_value)=>{
          return self._autoFillJoinTable(a_value.lower, a_value.lowerAlias, a_value.upper);
        })
      }

      _autoFillJoinTable(a_lowProjectionAlias, a_lowJoinAlias, a_upperProjectionAlias) {
        fcf.log.log("FCF", "Creating records in table " + a_upperProjectionAlias + " based on table " + a_lowProjectionAlias + " (join: " + a_lowJoinAlias + ")");
        let upperProjection = fcf.application.getProjections().get(a_upperProjectionAlias);
        let join            = upperProjection.join[fcf.find(upperProjection.join, (a_key, a_join) => { return a_join.from ==  a_lowProjectionAlias; } )];
        let where           = typeof join.on === "string" ? (new Parser()).parseWhere(join.on ,[]) : join.on; 
        let links           = {};
        function clFillLinks(a_where){
          fcf.each(a_where, (a_key, a_part)=>{
            if (a_part.type == "="){
              if ((a_part.args[0].field && a_part.args[0].from === a_upperProjectionAlias) || !a_part.args[0].from){
                if (a_part.args[1].field) {
                  links[a_part.args[0].field] = a_part.args[1].field;
                }
              } else if ((a_part.args[1].field && a_part.args[1].from === a_upperProjectionAlias) || !a_part.args[1].from){
                if (a_part.args[0].field) {
                  links[a_part.args[1].field] = a_part.args[0].field;
                }
              }
            } else if (a_part.type == "block"){
              clFillLinks(a_part.args);
            }
          })
        }
        clFillLinks(where);

        return fcf.actions()
        .then(()=>{
          return fcf.application.getStorage().query({
            type:   "select",
            from:   a_upperProjectionAlias,
            fields: fcf.array(links, (a_upperField)=>{ return {field: a_upperField}; }),
          },
          {roles: ["root"]});
        })
        .then((a_upperRecords)=>{
          return fcf.application.getStorage().query({
            type:   "select",
            from:   a_lowProjectionAlias,
            fields: fcf.array(links, (a_upperField, a_lowField)=>{ return {field: a_lowField}; }),
          }, {roles: ["root"]})
          .then((a_lowRecords)=>{
            let lower  = fcf.map(a_lowRecords[0], (a_key, a_record)=>{ 
                            let key = "";
                            let first = true;
                            for (let upperField in links) {
                              if (!first)
                                key += "^&*(->7892akl";
                              first = true;
                              key += a_record[links[upperField]];
                            }
                            return [key, a_record];
                          });
            let upper = fcf.map(a_upperRecords[0], (a_key, a_record)=>{ 
                            let key = "";
                            let first = true;
                            for (let upperField in links) {
                              if (!first)
                                key += "^&*(->7892akl";
                              first = true;
                              key += a_record[upperField];
                            }
                            return [key, a_record];
                          });
            let queries = [];
            for(let key in lower){
              if (key in upper)
                continue;
              queries.push({
                type: "insert",
                from: a_upperProjectionAlias,
                values: [],
                disableJoins: [a_lowJoinAlias],
              });
              for (let upperField in links) {
                queries[queries.length-1].values.push({
                  field: upperField,
                  value: lower[key][links[upperField]]
                });
              }
            }
            return fcf.application.getStorage().query(queries, {roles: ["root"]});
          })
        })
      }

    };

    Namespace.DBBuilder = DBBuilder;

    return Namespace.DBBuilder;
  }
});
