fcf.module({
  name: "fcf:NFSQL/NFilter/ExternalRef.js",
  dependencies: ["fcf:NFSQL/NFilter/Filter.js", "fcf:NFSQL/NFunction/Function.js"],
  module: function(Filter, Function) {
    var NFilter = fcf.prepareObject(fcf, "NFSQL.NFilter");
    var NFunction = fcf.prepareObject(fcf, "NFSQL.NFunction");

    fcf.addException("ERROR_FSQL_WHERE_OBJECT_FULL", "An object '${{field}}$' field cannot be used in an WHERE block without specifying a subfield");
    fcf.addException("ERROR_FSQL_OBJECT_INCORRECT_SUBFIELD", "The '${{subfield}}$' subfield specified in the request is not valid");

    NFilter.ExternalRef = class ExternalRef extends Filter {

      constructor(){
        super({type: "externalRef"});
      }

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

      processOutputField(a_taskInfo, a_info){
        var fieldInfo = a_taskInfo.getFieldInfo(a_info.field)
        var mode      = a_info.field.mode;
        var fieldProjection = a_taskInfo.getSourceProjectionField(a_info.field);
        var refProjection = a_taskInfo.projections[fieldInfo.projection];
        var selfLinkField = undefined;
        var refLinkField  = undefined;
        fcf.each(refProjection.fields, function(a_key, a_field){
          if (selfLinkField == undefined && a_field.type == "singleRef" && a_field.projection ==  fieldProjection.alias)
            selfLinkField = a_field;
          else if (refLinkField == undefined && a_field.type == "singleRef" && a_field.projection != fieldProjection.alias)
            refLinkField = a_field;
        });


        a_info.field.from  = a_taskInfo.getQueryTableByField(a_info.field);
        a_info.field.field = selfLinkField.refField;
        var as             = a_taskInfo.setAutoAs(a_info.field);

        a_taskInfo.postActions.then((a_res, a_act) =>{
          var query = {
            type: "select",
            from: refLinkField.projection,
            fields: [],
            join:[{
              join: "left",
              from: refProjection.alias,
              as:   "ref",
              on:   [{ type: "=", args: [{field: refLinkField.refField, mode: "value"}, {from: "ref", field: refLinkField.alias, mode: "value" }]}],
            }],
            where: [],
          }
          fcf.each(a_taskInfo.getResult(), function(a_key, a_record){
            query.where.push({
              logic: "or",
              type: "=",
              args: [
                { field: selfLinkField.alias, from: "ref"},
                { value: a_record[as]},
              ],
            });
          });
          if (mode !== "short"){
            fcf.each(a_taskInfo.projections[refLinkField.projection].fields, function(a_key, a_field){
              query.fields.push({field: a_field.alias});
            });
          }
          query.fields.push({field: selfLinkField.alias, as: "@__join__selfref", from: "ref", mode: "value"});
          query.fields.push({function: "key",   as: "@key"});
          query.fields.push({function: "title", as: "@title"});

          var queryOptions = fcf.append({}, a_taskInfo.options, {query: query});
          a_taskInfo.storage.query(queryOptions, function(a_error, a_records){
            if (a_error){
              a_act.error(a_error);
              return;
            }
            var map = {};
            fcf.each(a_records[0], function(a_key, a_record){
              if (!map[a_record["@__join__selfref"]])
                map[a_record["@__join__selfref"]] = [];
              map[a_record["@__join__selfref"]].push(a_record);
              delete a_record["@__join__selfref"];
            });

            var result = a_taskInfo.getResult();
            fcf.each(result, function(a_key, a_record){
              a_record[as] = a_record[as] in map ? map[a_record[as]] : [];
            });

            a_act.complete();
          });
        });
      }

      processWhereField(a_taskInfo, a_info){
        if (fcf.empty(a_info.field.path))
          throw new fcf.Exception("ERROR_FSQL_WHERE_OBJECT_FULL", {field: a_info.field.field});

        var fieldProjection = a_taskInfo.getSourceProjectionField(a_info.field);
        var fieldInfo       = a_taskInfo.getFieldInfo(a_info.field);
        var selfFieldTable  = a_taskInfo.getQueryTableByField(a_info.field);
        var refProjection = a_taskInfo.projections[fieldInfo.projection];
        var selfLinkField = undefined;
        var entityLinkField  = undefined;
        fcf.each(refProjection.fields, function(a_key, a_field){
          if (selfLinkField == undefined && a_field.type == "singleRef" && a_field.projection ==  fieldProjection.alias)
            selfLinkField = a_field;
          else if (entityLinkField == undefined && a_field.type == "singleRef" && a_field.projection != fieldProjection.alias)
            entityLinkField = a_field;
        });
        var entityProjection = a_taskInfo.projections[entityLinkField.projection];

        var refJoinAs = "externreffield@ref@" +
                        (a_info.field.from ? a_info.field.from + "@" : "") +
                        a_info.field.field;
        var entityJoinAs = "externreffield@entity@" +
                        (a_info.field.from ? a_info.field.from + "@" : "") +
                        a_info.field.field;

        if (fcf.find(a_taskInfo.query.join, function(k, v){ return v.as == refJoinAs}) === undefined){
          a_taskInfo.query.join.push({
            from: refProjection.alias,
            as: refJoinAs,
            on: [{ type: "=", args: [{from: selfFieldTable, field: fieldProjection.fieldsMap[selfLinkField.refField].field}, {from: refJoinAs, field: selfLinkField.field}]  }]
          });
          a_taskInfo.aliases[refJoinAs] = refProjection.alias;
        }

        if (fcf.find(a_taskInfo.query.join, function(k, v){ return v.as == entityJoinAs}) === undefined){
          a_taskInfo.query.join.push({
            from: entityLinkField.projection,
            as: entityJoinAs,
            on: [{ type: "=", args: [{from: refJoinAs, field: entityLinkField.field}, {from: entityJoinAs, field: entityProjection.fieldsMap[entityLinkField.refField].field}]  }]
          });
          a_taskInfo.aliases[entityJoinAs] = entityLinkField.projection;
        }


        var subField = a_info.field.path[0];
        if (subField == "@key") {
          var func = NFunction.getFunction("key");
          a_info.function = a_info.field;
          a_info.function.function = "key";
          a_info.function.from     = entityJoinAs;
          delete a_info.field.field;
          delete a_info.field.path;
          delete a_info.field.mode;
          func.processFunction(a_taskInfo, a_info);
        } else if (subField == "@title") {
          var func = NFunction.getFunction("title");
          a_info.function = a_info.field;
          a_info.function.function = "title";
          a_info.function.from     = entityJoinAs;
          delete a_info.field.field;
          delete a_info.field.path;
          delete a_info.field.mode;
          func.processFunction(a_taskInfo, a_info);
        } else {
          var subFieldInfo = entityProjection.fieldsMap[subField];
          if (!subFieldInfo)
            throw new fcf.Exception("ERROR_FSQL_OBJECT_INCORRECT_SUBFIELD", {subfield: subField});
          var filter = fcf.getFilter(subFieldInfo.type);
          a_info.field.field = subField;
          a_info.field.from = entityJoinAs;
          delete a_info.field.path;
          delete a_info.field.mode;
          filter.processWhereField(a_taskInfo, a_info);
        }
      }

      _loadUpdateRecords(a_taskInfo, a_info, a_cb){
        var fieldInfo = a_taskInfo.getFieldInfo(a_info.field);
        if (fieldInfo.realjoin && fieldInfo.realjoin.from){
          var fieldProjection = a_taskInfo.getSourceProjectionField(a_info.field);
          var fieldInfo       = a_taskInfo.getFieldInfo(a_info.field);
          var selfFieldTable  = a_taskInfo.getQueryTableByField(a_info.field);
          var refProjection = a_taskInfo.projections[fieldInfo.projection];
          var selfLinkField = undefined;
          var entityLinkField  = undefined;
          fcf.each(refProjection.fields, function(a_key, a_field){
            if (selfLinkField == undefined && a_field.type == "singleRef" && a_field.projection ==  fieldProjection.alias)
              selfLinkField = a_field;
            else if (entityLinkField == undefined && a_field.type == "singleRef" && a_field.projection != fieldProjection.alias)
              entityLinkField = a_field;
          });

          var queryOptions = fcf.append({}, a_taskInfo.options, {
            query: {
              type: "select",
              fields: [{
                from:  fieldInfo.realjoin.from,
                field: selfLinkField.refField,
                mode: "value"
              }],
              from: a_taskInfo.originQuery.from,
              where: a_taskInfo.originQuery.where,
            }
          });
          a_taskInfo.storage.query(queryOptions, function(a_error, a_records){
            a_cb(a_error, a_records[0])
          });
        } else {
          a_taskInfo.loadRecords(function(a_records) { a_cb(undefined, a_records); });
        }
      }

      processUpdateField(a_taskInfo, a_info){
        let self = this;
        a_taskInfo.query.values.splice(a_info.key, 1);
        a_taskInfo.postActions.then(function(a_res, a_act){
          self._loadUpdateRecords(a_taskInfo, a_info, function(a_error, a_records){
            if (a_error) {
              a_act.error(a_error);
              return;
            }
            var fieldProjection = a_taskInfo.getSourceProjectionField(a_info.field);
            var fieldInfo       = a_taskInfo.getFieldInfo(a_info.field);
            var selfFieldTable  = a_taskInfo.getQueryTableByField(a_info.field);
            var refProjection = a_taskInfo.projections[fieldInfo.projection];
            var selfLinkField = undefined;
            var entityLinkField  = undefined;
            fcf.each(refProjection.fields, function(a_key, a_field){
              if (selfLinkField == undefined && a_field.type == "singleRef" && a_field.projection ==  fieldProjection.alias)
                selfLinkField = a_field;
              else if (entityLinkField == undefined && a_field.type == "singleRef" && a_field.projection != fieldProjection.alias)
                entityLinkField = a_field;
            });

            if (fcf.empty(a_records)){
              a_act.complete();
              return;
            }

            var queryOptions = fcf.append({}, a_taskInfo.options, {
              query: {
                type: "delete",
                from: refProjection.alias,
                where: []
              }
            });
            fcf.each(a_records, function(a_key, a_record){
              queryOptions.query.where.push({
                logic: "or",
                type: "=",
                args: [
                  {field: selfLinkField.alias},
                  {value: a_record[selfLinkField.refField] },
                ],
              })
            });
            fcf.actions()
            .catch((a_error)=>{
              a_act.error(a_error);
            })
            .then(function(a_res, a_subact){
              a_taskInfo.storage.query(queryOptions, function(a_error, a_records){
                if (a_error){
                  a_subact.error(a_error);
                  return;
                }
                a_subact.complete();
              });
            })
            .each(a_records, function(a_value, a_record, a_res, a_subact){
              var refValues = Array.isArray(a_info.field.value) ? a_info.field.value : [a_info.field.value];
              fcf.actions()
              .catch((a_error)=>{
                a_subact.error(a_error);
              })
              .each(refValues, function(a_key, a_refValue, a_res, a_refsact){
                var selfValue = a_record[selfLinkField.refField];
                var refValue = typeof a_refValue === "object" ? a_refValue[entityLinkField.refField]
                                                              : a_refValue;
                var queryOptions = fcf.append({}, a_taskInfo.options, {
                  query: {
                    type: "insert",
                    from: refProjection.alias,
                    values: [
                      { field: selfLinkField.alias, value: selfValue},
                      { field: entityLinkField.alias, value: refValue},
                    ]
                  }
                });
                a_taskInfo.storage.query(queryOptions, function(a_error, a_value){
                  if(a_error){
                    a_refsact.error(a_error);
                    return;
                  }
                  a_refsact.complete();
                });
              })
              .then(function(){
                a_subact.complete();
              });

            })
            .then(function(){
              a_act.complete();
            });

          });
        });

      }

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

        a_taskInfo.postActions.then(function(a_res, a_act){
          a_taskInfo.loadRecords(function(a_records){
            var fieldProjection  = a_taskInfo.getSourceProjectionField(a_info.field);
            var fieldInfo        = a_taskInfo.getFieldInfo(a_info.field);
            var selfFieldTable   = a_taskInfo.getQueryTableByField(a_info.field);
            var refProjection    = a_taskInfo.projections[fieldInfo.projection];
            var selfLinkField    = undefined;
            var entityLinkField  = undefined;
            fcf.each(refProjection.fields, function(a_key, a_field){
              if (selfLinkField == undefined && a_field.type == "singleRef" && a_field.projection ==  fieldProjection.alias)
                selfLinkField = a_field;
              else if (entityLinkField == undefined && a_field.type == "singleRef" && a_field.projection != fieldProjection.alias)
                entityLinkField = a_field;
            });

            var queries = [];
            var values = Array.isArray(a_info.field.value) ? a_info.field.value : [a_info.field.value];
            fcf.actions()
            .catch((a_error)=>{
              a_act.error(a_error);
            })
            .each(values, function(a_key, a_value, a_res){
              var value = typeof a_value != "object" ? a_value
                                                     : a_value[entityLinkField.refField];
              var queryOptions = fcf.append({}, a_taskInfo.options, {
                query: {
                  type: "insert",
                  from: refProjection.alias,
                  values: [
                    { field: selfLinkField.alias, value: a_records[0][selfLinkField.refField]},
                    { field: entityLinkField.alias, value: value},
                  ]
                }
              });
              return a_taskInfo.storage.query(queryOptions, function(a_error, a_value){ });
            })
            .then(function() {
              a_taskInfo.resetRecords();
              a_act.complete();
            });
          })
        })
      }

      async processDeleteField(a_taskInfo, a_info){
        let records         = await a_taskInfo.loadRecords();
        var fieldProjection = a_taskInfo.getSourceProjectionField(a_info.field);
        var fieldInfo       = a_taskInfo.getFieldInfo(a_info.field);
        var selfFieldTable  = a_taskInfo.getQueryTableByField(a_info.field);
        var refProjection = a_taskInfo.projections[fieldInfo.projection];
        var selfLinkField = undefined;
        var entityLinkField  = undefined;
        fcf.each(refProjection.fields, function(a_key, a_field){
          if (selfLinkField == undefined && a_field.type == "singleRef" && a_field.projection ==  fieldProjection.alias)
            selfLinkField = a_field;
          else if (entityLinkField == undefined && a_field.type == "singleRef" && a_field.projection != fieldProjection.alias)
            entityLinkField = a_field;
        });

        if (fcf.empty(records))
          return;

        var queryOptions = fcf.append({}, a_taskInfo.options, {
          query: {
            type: "delete",
            from: refProjection.alias,
            where: []
          }
        });
        fcf.each(records, function(a_key, a_record){
          queryOptions.query.where.push({
            logic: "or",
            type: "=",
            args: [
              {field: selfLinkField.alias},
              {value: a_record[selfLinkField.refField] },
            ],
          })
        });
        await a_taskInfo.storage.query(queryOptions);
      }

    }

    return NFilter.ExternalRef;
  }
});
