fcf.module({
  name: "fcf:NFSQL/NFilter/BackRef.js",
  dependencies: ["fcf:NFSQL/NFilter/Filter.js", "fcf:NFSQL/NFilter/Errors.js"],
  module: function(Filter, Errors){
    var Namespace = fcf.prepareObject(fcf, "packages/fcf/NFSQL/NFilter");

    class BackRef extends Filter{
      constructor(){
        super({type: "backRef"})
        this.comparisons = {
          "*":    [],
        };
      }

      getRealFields(a_projection, a_fieldAlias){
        return [];
      }

      isTranslateField(a_fieldDescription) {
        let projection  = fcf.application.getProjections().get(a_fieldDescription.projection);
        return projection ? !!projection.translate : false;
      }

      processOutputField(a_taskInfo, a_info){
        let fieldInfo       = a_taskInfo.getFieldInfo(a_info.field)
        let fieldProjection = a_taskInfo.getProjectionField(a_info.field)
        let refProjection   = a_taskInfo.projections[fieldInfo.projection];
        let refFieldInfo    = refProjection.fieldsMap[fieldInfo.refField];
        let originField     = fcf.append({}, a_info.field);
        let from            = originField.from ? originField.from : a_taskInfo.query.from;

        let as              = a_taskInfo.setAutoAs(a_info.field);
        a_info.field.field  = refFieldInfo.refField;
        a_info.field.field  = a_taskInfo.getQueryField(a_info.field);
        a_info.field.from   = a_taskInfo.getQueryTableByField(originField);

        a_taskInfo.query.fields.push({
          as:    as + "@key",
          field: fieldProjection.fieldsMap[fieldProjection.key].field,
          from:  a_info.field.from
        });

        a_taskInfo.postActions.then(() =>{
          let query = {
            type:   "select",
            from:   refProjection.alias,
            fields: [{field: refFieldInfo.alias, path: [refFieldInfo.refField], as: "@backref@attach@key"}],
            where:  [],
            language: a_taskInfo.query.language,
            defaultLanguage: a_taskInfo.query.defaultLanguage,
          };

          if (!a_taskInfo.options.backrefRecursion)
            a_taskInfo.options.backrefRecursion = {};

          let looping = false;

          if (fcf.empty(a_taskInfo.result))
            return;

          fcf.each(a_taskInfo.result, function(a_key, a_record){
            let ownerKey = a_record[as + "@key"];
            let protectionLoopingKey = ownerKey + " -> "+ from + " -> " + originField.field + "->" + as + " -> "+ a_record[as];

            if (!a_taskInfo.options.backrefRecursion[protectionLoopingKey])
              a_taskInfo.options.backrefRecursion[protectionLoopingKey] = true;
            else
              looping = true;
            query.where.push({logic: "or", type: "=", args:[{field: refFieldInfo.alias}, {value: a_record[as]}]});
            delete a_record[as + "@key"];
          });

          if (!fcf.empty(a_info.field.path)) {
            if (a_info.field.path[0] == "@key") {
              query.fields.push({function: "key", as: "@key"});
            } else if (a_info.field.path[0] == "@title") {
              query.fields.push({function: "title", as: "@title"});
            } else {
              query.fields.push({field: a_info.field.path[0]})
            }
          } else if (looping || a_info.field.mode == "short"){
            query.fields.push({function: "title", as: "@title"});
          } else {
            query.fields.push({field: "*"})
          }

          let options = fcf.append({}, a_taskInfo.options, {query: query});
          return a_taskInfo.storage.query(options)
          .then((a_records)=>{
            let map = {};
            for(let i = 0; i < a_records[0].length; ++i) {
              if (!map[a_records[0][i]["@backref@attach@key"]])
                map[a_records[0][i]["@backref@attach@key"]] = [];
              map[a_records[0][i]["@backref@attach@key"]].push(a_records[0][i]);
              delete a_records[0][i]["@backref@attach@key"];

              if (looping){
                for (let key in a_records[0][i]){
                  if (key != "@key" && key != "@title"){
                    delete a_records[0][i][key];
                  }
                }
              }
            }

            for(let i = 0; i < a_taskInfo.result.length; ++i){
              a_taskInfo.result[i][as] = map[a_taskInfo.result[i][as]];
              if (!a_taskInfo.result[i][as])
                a_taskInfo.result[i][as] = [];
            }
          })

        });
      }

      processWhereField(a_taskInfo, a_info){
        let fieldInfo       = a_taskInfo.getFieldInfo(a_info.field)
        let fieldProjection = a_taskInfo.getProjectionField(a_info.field)
        let refProjection   = a_taskInfo.projections[fieldInfo.projection];
        let refFieldInfo    = refProjection.fieldsMap[fieldInfo.refField];

        let originField     = fcf.append({}, a_info.field);
        let from            = originField.from ? originField.from : a_taskInfo.query.from;
        let joinAs          = from + "@" + fieldInfo.alias + "@join";

        if(fcf.find(a_taskInfo.query.join, (a_key, a_join)=>{ return a_join.as == joinAs; }) === undefined ){
          a_taskInfo.query.join.push({
            from: refProjection.table,
            as:   joinAs,
            on:   [{type: "=", args:[{ from: fieldProjection.table, field: fieldProjection.fieldsMap[refFieldInfo.refField].field },
                                     { from: joinAs, field: refFieldInfo.field }
                                    ]}],
            processed: true,
          });
          a_taskInfo.aliases[joinAs] = fieldInfo.projection;
        }

        if (!fcf.empty(a_info.field.path)){
          if (a_info.field.path[0] == "@key"){
            let subField = a_info.field.path[0];
            a_info.function = a_info.field;
            delete a_info.field;
            delete a_info.function.field;
            a_info.function.function = "key";
            a_info.function.from  = joinAs;
            let func = fcf.NFSQL.NFunction.getFunction("key");
            func.processFunction(a_taskInfo, a_info);
          } else if (a_info.field.path[0] == "@title") {
            let subField = a_info.field.path[0];
            a_info.function = a_info.field;
            delete a_info.field;
            delete a_info.function.field;
            a_info.function.function = "title";
            a_info.function.from  = joinAs;
            let func = fcf.NFSQL.NFunction.getFunction("title");
            func.processFunction(a_taskInfo, a_info);
          } else {
            let subField = a_info.field.path[0];
            a_info.field.field = subField;
            a_info.field.from  = joinAs;
            a_info.field.path  = [];
            let filter = fcf.getFilter(refProjection.fieldsMap[subField].type);
            filter.processWhereField(a_taskInfo, a_info);
          }
        } else {

        }
      }

      async processInsertField(a_taskInfo, a_info){
        if (!a_taskInfo.options.generalInformation.backRefInfo) 
          a_taskInfo.options.generalInformation.backRefInfo = {};

        let fieldInfo       = a_taskInfo.getFieldInfo(a_info.field)
        let fieldProjection = a_taskInfo.getProjectionField(a_info.field)
        let refProjection   = a_taskInfo.projections[fieldInfo.projection];
        let refFieldInfo    = refProjection.fieldsMap[fieldInfo.refField];

        a_taskInfo.query.values.splice(a_info.key, 1);

        a_taskInfo.postActions.then(async ()=>{
          let insertRecords = await a_taskInfo.loadRecords();
          insertRecords = insertRecords[0];
          let refValue = insertRecords[refFieldInfo.refField];
          let insertQueries = [];


          fcf.each(a_info.field.value, (a_key, a_values)=>{
            let query = {
              type: "insert",
              from: refProjection.alias,
              values: [{field: refFieldInfo.alias, value: refValue}],
              language: a_taskInfo.query.language,
              defaultLanguage: a_taskInfo.query.defaultLanguage,
              recorduuid: a_info.field.value[a_key]["@recorduuid"],
            };
            fcf.each(a_values, (a_fieldAlias, a_fieldValue)=>{
              if (!refProjection.fieldsMap[a_fieldAlias])
                return;
              if (refFieldInfo.alias == a_fieldAlias)
                return;
              query.values.push({field: a_fieldAlias, value: a_fieldValue});
            })
            insertQueries.push(query);
          });

          if (!insertQueries.length)
            return;

          let options = fcf.append({}, a_taskInfo.options, {query: insertQueries});
          let keyRecords = await a_taskInfo.storage.query(options);
          fcf.each(keyRecords, (a_key, a_records)=>{
            fcf.each(a_records, (k, a_record)=>{
              if(!insertQueries[a_key].recorduuid)
                return;
              if (!a_taskInfo.options.generalInformation.backRefInfo[refProjection.alias]) a_taskInfo.options.generalInformation.backRefInfo[refProjection.alias] = {};
              a_taskInfo.options.generalInformation.backRefInfo[refProjection.alias][insertQueries[a_key].recorduuid] = a_record["@key"];
            });
          });
        });
      }

      async processDeleteField(a_taskInfo, a_info) {
        let fieldInfo       = a_taskInfo.getFieldInfo(a_info.field)
        let fieldProjection = a_taskInfo.getProjectionField(a_info.field)
        let refProjection   = a_taskInfo.projections[fieldInfo.projection];
        let refFieldInfo    = refProjection.fieldsMap[fieldInfo.refField];
        let removeQuery      = {
          type:   "delete",
          from:   refProjection.alias,
          where: []
        };

        let deleteRecords = await a_taskInfo.loadRecords();
        fcf.each(deleteRecords, (a_key, a_record)=>{
          fcf.each(a_record[fieldInfo.alias], (a_key, a_subrecord)=>{
            removeQuery.where.push({ logic: "or", type: "=", args: [{ function: "key" }, {value: a_subrecord["@key"] }] });
          });
        });

        if (removeQuery.where.length == 0)
          return;

        let options = fcf.append({}, a_taskInfo.options, {query: removeQuery});
        return a_taskInfo.storage.query(options);
      }

      async processUpdateField(a_taskInfo, a_info){
        if (!a_taskInfo.options.generalInformation.backRefInfo)
          a_taskInfo.options.generalInformation.backRefInfo = {};

        a_taskInfo.query.values.splice(a_info.key, 1);

        let fieldInfo       = a_taskInfo.getFieldInfo(a_info.field)
        let fieldProjection = a_taskInfo.getProjectionField(a_info.field)
        let refProjection   = a_taskInfo.projections[fieldInfo.projection];
        let refFieldInfo    = refProjection.fieldsMap[fieldInfo.refField];

        let records = await a_taskInfo.loadRecords();

        // update records
        let updateQueries = [];
        fcf.each(a_info.field.value, (a_key, a_values)=>{
          let updateValues = [];

          if (a_taskInfo.options.generalInformation.backRefInfo && 
              a_taskInfo.options.generalInformation.backRefInfo[refProjection.alias] && 
              a_taskInfo.options.generalInformation.backRefInfo[refProjection.alias][a_values["@recorduuid"]]){
            a_values["@key"] = a_taskInfo.options.generalInformation.backRefInfo[refProjection.alias][a_values["@recorduuid"]];
          }

          if (!a_values["@key"])
            return;

          fcf.each(a_values, (a_fieldAlias, a_value)=>{
            if (a_fieldAlias == "@key")
              return;
            if (!(a_fieldAlias in refProjection.fieldsMap))
              return;
            if (refProjection.fieldsMap[a_fieldAlias].notEdit)
              return;
            updateValues.push({field: a_fieldAlias, value: a_value})
          });

          if (fcf.empty(updateValues))
            return;

          updateQueries.push({
            type:   "update",
            from:   refProjection.alias,
            values: updateValues,
            where:  [{type: "=", args: [{function: "key"}, {value: a_values["@key"]}]}],
            language: a_taskInfo.query.language,
            defaultLanguage: a_taskInfo.query.defaultLanguage,
          });
        });
        if (!fcf.empty(updateQueries)){
          let options = fcf.append({}, a_taskInfo.options, {query: updateQueries});
          await a_taskInfo.storage.query(options);
        }


        // insert records
        let insertValues  = [];
        fcf.each(a_info.field.value, (a_key, a_values)=>{
          if (!!a_values["@key"])
            return;
          let recorduuid = a_values["@recorduuid"];
          insertValues.push([]);
          insertValues[insertValues.length-1].key = a_key;
          fcf.each(a_values, (a_fieldAlias, a_value)=>{
            if (!(a_fieldAlias in refProjection.fieldsMap))
              return;
            if (refProjection.fieldsMap[a_fieldAlias].notEdit)
              return;
            if (refFieldInfo.alias == a_fieldAlias)
              return;
            insertValues[insertValues.length-1].push({field: a_fieldAlias, value: a_value})
          });
        });

        let insertQueries = [];
        let recorduuids = [];
        fcf.each(records, (a_key, a_record)=>{
          fcf.each(insertValues, (a_key, a_values)=>{
            insertQueries.push({
              type: "insert",
              from: refProjection.alias,
              values: fcf.append([], a_values),
              language: a_taskInfo.query.language,
              defaultLanguage: a_taskInfo.query.defaultLanguage,
              recorduuid: a_info.field.value[a_values.key]["@recorduuid"],
            })
            insertQueries[insertQueries.length-1].values.push({
              field: refFieldInfo.alias,
              value: a_record[refFieldInfo.refField],
            })
          });
        });


        let options = fcf.append({}, a_taskInfo.options, {query: insertQueries});
        let insertRecords = await a_taskInfo.storage.query(options);
        fcf.each(insertRecords, (a_key, a_records)=>{
          if(!insertQueries[a_key].recorduuid)
            return;
          if (!a_taskInfo.options.generalInformation.backRefInfo[refProjection.alias]) a_taskInfo.options.generalInformation.backRefInfo[refProjection.alias] = {};
          a_taskInfo.options.generalInformation.backRefInfo[refProjection.alias][insertQueries[a_key].recorduuid] = a_records[0]["@key"];
        });
      

        // remove old records
        let existsKeys = fcf.map(records, (k,v)=>{ return fcf.map(v[fieldInfo.alias], (k, v)=>{ return [v["@key"], v["@key"]] }); });
        let updateKeys = fcf.map(a_info.field.value, (k, v)=>{ return v["@key"] ? [v["@key"],v["@key"]] : [] });
        let deleteKeys = fcf.filter(existsKeys, (k, v)=>{ return !(v in updateKeys); });
        if (!fcf.empty(deleteKeys)) {
          a_taskInfo.postActions.then(async ()=>{
            let removeQuery = {
              type: "delete",
              from: refProjection.alias,
              where: [],
            };
            fcf.each(deleteKeys, (key)=>{
              removeQuery.where.push({logic: "or", type: "=", args: [{function: "key"}, {value: key}]});
            });
            let options = fcf.append({}, a_taskInfo.options, {query: removeQuery});
            return await a_taskInfo.storage.query(options);
          });
        }

      }


    };

    Namespace.BackRef = BackRef;

    return Namespace.BackRef;
  }
});
