fcf.module({
  name: "fcf:NFSQL/NDetails/NConnections/MemoryConnection.js",
  dependencies: ["fcf:NFSQL/Projections.js", "fcf:NFSQL/NDetails/Errors.js"],
  module: function(Projections, Errors) {
    var NConnections = fcf.prepareObject(fcf, "NFSQL.NDetails.NConnections");

    fcf.addException("ERROR_MEMDB_TABLE_NOT_EXISTS",      "The specified table '${{table}}$' does not exist");
    fcf.addException("ERROR_MEMDB_DUPLICATE_PRIMARY_KEY", "Duplicate primary key of table '${{table}}$'");
    fcf.addException("ERROR_MEMDB_NOIMP",                 "There is no implementation for this operation");

    NConnections.MemoryConnection = function(a_options) {
      var self = this;
      /**
      * @fn void constructor(a_options)
      *   object                  tables
      *   fcf::NFSQL::Projections projections
      **/
      a_options = fcf.append(
        {
          tables: {},
          projections: new Projections(),
        },
        a_options);
      this._tables = {};
      this._keys   = {};
      this._keysQueue = {};
      this._projections = a_options.projections;

      this.getType = function(){
        return "memory";
      }

      this.setTable = function(a_projection, a_table, a_cb) {
        a_table = Array.isArray(a_table) ? a_table : [];
        var projections = {};
        var tables = {};

        projections[a_projection.alias] = a_projection;
        tables[a_projection.table] = a_table;
        this._setTablesWithRawProjections(projections, tables);
        if (a_cb)
          a_cb();
      }

      this.query = function(a_queryObject, a_options, a_cb) {
        if (a_queryObject.type == "select")
          this._processSelect(a_queryObject, a_options, a_cb);
        else if (a_queryObject.type == "insert")
          this._processInsert(a_queryObject, a_options, a_cb);
        else if (a_queryObject.type == "update")
          this._processUpdate(a_queryObject, a_options, a_cb);
        else if (a_queryObject.type == "delete")
          this._processDelete(a_queryObject, a_options, a_cb);
        else if (a_cb)
          a_cb(new fcf.Exception("ERROR_MEMDB_NOIMP"));
      }

      this.connect = function() {
      }

      this.disconnect = function(){
      }

      this.destroy = function() {
      }

      this._processUpdate = function(a_queryObject, a_options, a_cb) {
        var from        = a_queryObject.from;
        var projection  = this._projections.get(a_queryObject.from);
        if (!projection){
          if (a_cb)
            a_cb(new fcf.Exception("ERROR_MEMDB_TABLE_NOT_EXISTS", {table: table}));
          return;
        }
        var table       = projection.table;
        var tables      = this._tables[table];
        if (tables === undefined){
          if (a_cb)
            a_cb(new fcf.Exception("ERROR_MEMDB_TABLE_NOT_EXISTS", {table: table}));
          return;
        }

        var selectQuery = {
          type: "select",
          fields: [ { field: projection.key } ],
          from: a_queryObject.from,
          where: a_queryObject.where,
        };

        this._syncProcessSelect(selectQuery, a_options, function(a_error, a_records){
          if (a_error){
            if (a_cb)
              a_cb(a_error);
            return;
          }

          for(var recIndex = 0; recIndex < a_records.length; ++recIndex){
            var key = a_records[recIndex][projection.key];
            for(var valIndex = 0; valIndex < a_queryObject.values.length; ++valIndex){
              var val = a_queryObject.values[valIndex];
              tables[key][val.field] = val.value
            }
          }

          if (a_cb)
            a_cb(undefined, undefined);
        });

      }

      this._processDelete = function(a_queryObject, a_options, a_cb) {
        var from        = a_queryObject.from;
        var projection  = this._projections.get(a_queryObject.from);
        if (!projection){
          if (a_cb)
            a_cb(new fcf.Exception("ERROR_MEMDB_TABLE_NOT_EXISTS", {table: table}));
          return;
        }
        var table       = projection.table;
        var tables      = this._tables[table];
        if (tables === undefined){
          if (a_cb)
            a_cb(new fcf.Exception("ERROR_MEMDB_TABLE_NOT_EXISTS", {table: table}));
          return;
        }
        var keys     = this._keys[table];

        var selectQuery = {
          type: "select",
          fields: [ { field: projection.key } ],
          from: a_queryObject.from,
          where: a_queryObject.where,
        };

        this._syncProcessSelect(selectQuery, a_options, function(a_error, a_records){
          if (a_error){
            if (a_cb)
              a_cb(a_error);
            return;
          }

          for(var recIndex = 0; recIndex < a_records.length; ++recIndex){
            var key = a_records[recIndex][projection.key];
            delete tables[key];
            var keyIndex = fcf.find(keys, function(k, v) { return key == v[table].key; } );
            keys.splice(keyIndex, 1);
          }

          if (a_cb)
            a_cb(undefined, undefined);
        });

      }


      this._processInsert = function(a_queryObject, a_options, a_cb) {
        var from        = a_queryObject.from;
        var projection  = this._projections.get(a_queryObject.from);
        if (!projection){
          if (a_cb)
            a_cb(new fcf.Exception("ERROR_MEMDB_TABLE_NOT_EXISTS", {table: table}));
          return;
        }
        var table       = projection.table;
        var records     = this._keys[table];
        var tables      = this._tables[table];

        if (records === undefined || tables === undefined){
          if (a_cb)
            a_cb(new fcf.Exception("ERROR_MEMDB_TABLE_NOT_EXISTS", {table: table}));
          return;
        }

        var values           = fcf.append([], a_queryObject.values);
        var keyFoundPosition = fcf.find(values, function(k, v){ return v.field == projection.key });
        if (keyFoundPosition === undefined){
          var keyValue = this._nextKeyQueue(table);
          values.push({field: projection.key, value: keyValue});
        } else {
          var keyValue        = values[keyFoundPosition].value;
          var foundPrimaryKey = keyValue in this._tables[table];
          if (foundPrimaryKey !== undefined){
            if (a_cb)
              a_cb(new fcf.Exception("ERROR_MEMDB_DUPLICATE_PRIMARY_KEY", {table: table}));
            return;
          }
          this._setMaxKeyQueue(table, values[keyFoundPosition].value);
        }

        var record = {};
        for(var i = 0; i < values.length; ++i)
          record[values[i].field] = values[i].value;

        this._tables[table][keyValue] = record;
        var objKeys = {};
        objKeys[table] = {key: keyValue};
        this._keys[table].push(objKeys);

        if (a_cb)
          a_cb(undefined, [{"@key": keyValue}]);
      }

      this._processSelect = function(a_queryObject, a_options, a_cb) {
        this._syncProcessSelect(a_queryObject, a_options, a_cb);
      }

      this._syncProcessSelect = function(a_queryObject, a_options, a_cb) {
        var from        = a_queryObject.from;
        var projection  = this._projections.get(a_queryObject.from);
        if (!projection){
          if (a_cb)
            a_cb(new fcf.Exception("ERROR_MEMDB_TABLE_NOT_EXISTS", {table: table}));
          return;
        }
        var table       = projection.table;
        var records = this._keys[table];
        var tables = this._tables[table];

        if (records === undefined || tables === undefined){
          if (a_cb)
            a_cb(new fcf.Exception("ERROR_MEMDB_TABLE_NOT_EXISTS", {table: table}));
          return;
        }

        var aliases = {};
        aliases[table] = table;
        records = this._processJoins(table, records, a_queryObject, aliases);
        records = this._screeningCopy(table, records, a_queryObject.where);
        records = this._processLimit(a_queryObject, records);
        var rows = this._buildResponce(a_queryObject, records, aliases);
            rows = this._sort(a_queryObject, rows);
        if (a_cb)
          a_cb(undefined, rows);
      }

      this._nextKeyQueue = function(a_tableName) {
        if (a_tableName in this._keysQueue){
          ++this._keysQueue[a_tableName];
        } else {
          this._keysQueue[a_tableName] = 1;
        }
        return this._keysQueue[a_tableName];
      }

      this._setMaxKeyQueue = function(a_tableName, a_value) {
        if (a_tableName in  this._keysQueue){
          if (a_value > this._keysQueue[a_tableName])
            this._keysQueue[a_tableName] = a_value;
        } else {
          this._keysQueue[a_tableName] = a_value;
        }
      }

      this._setTables = function(a_projections, a_tables){
        this._setTablesWithRawProjections(a_projections ? a_projections.getProjections() : {}, a_tables)
      }

      this._setTablesWithRawProjections = function(a_projections, a_tables){
        fcf.foreach(a_projections, function(k, v){
          self._projections.appendProjectionStruct(v);
        });

        for(var name in a_tables) {
          var key = this._projections.get(name).key;
          this._tables[name] = {};
          this._keys[name] = [];
          for (var recordIndex = 0; recordIndex < a_tables[name].length; ++recordIndex) {
            var record = a_tables[name][recordIndex];
            this._tables[name][ record[key] ] = record;
            var objKeys = {};
            objKeys[name] = {key: record[key]};
            this._keys[name].push(objKeys);
          }
        }
      }

      this._setTables(a_options.projections, a_options.tables);

      this._processJoins = function(a_from, a_records, a_query, a_aliases) {
        var records = a_records;
        if (!fcf.empty(a_query.join)) {
          for (var i = 0; i < a_query.join.length; ++i) {
            records = this._processJoin(a_from, a_records, a_query, a_query.join[i], a_aliases);
          }
        }
        return records;
      }

      this._processJoin = function(a_from, a_records, a_query, a_join, a_aliases) {
        var records = [];
        var joinrecords = this._keys[a_join.from];
        var joinas = a_join.as ? a_join.as : a_join.from;
        a_aliases[joinas] = a_join.from;

        var codeWhereFunc = this._createJSWhereFunc(a_from, a_records, a_join.on, a_join.from, joinas);
        eval(codeWhereFunc);

        for (var recIndex = 0; recIndex < a_records.length; ++recIndex) {
          var isEmpty = true;
          for (var joinIndex = 0; joinIndex < joinrecords.length; ++joinIndex) {
            if (wherefunc(this._tables, a_records[recIndex], joinrecords[joinIndex])) {
              isEmpty = false;
              var joinrec = {};
              joinrec[joinas] = fcf.first(joinrecords[joinIndex]);
              var record = fcf.append(true, {}, a_records[recIndex], joinrec);
              records.push(record);
            }
          }
          if (isEmpty) {
            var record = fcf.append(true, {}, a_records[recIndex]);
            records.push(record);
          }
        }
        return records;
      }

      this._sort = function(a_queryObject, a_rows) {
        var result = [];
        var order = a_queryObject.order;
        if (Array.isArray(a_rows) && Array.isArray(order)){
          result = a_rows.sort(
            function(a_left, a_right) {
              for (var i = 0; i < order.length; ++i) {
                if (!(order[i].field in a_left))
                  continue;
                if (typeof a_left[order[i].field] === "object")
                  continue;
                var o = order[i].order == "desc" ? "desc" : "asc";
                if (o == "asc" ) {
                  if (a_left[order[i].field] < a_right[order[i].field])
                    return -1;
                  else if (a_left[order[i].field] > a_right[order[i].field])
                    return 1;
                } else {
                  if (a_left[order[i].field] < a_right[order[i].field])
                    return 1;
                  else if (a_left[order[i].field] > a_right[order[i].field])
                    return -1;
                }
              }
              return 0;
            }
          );
          return result;
        }
        return a_rows;
      }


      this._processLimit = function(a_query, a_records) {
        if (a_query.limit === 0)
          return [];
        if (!a_query.offset && !a_query.limit)
          return a_records;
        var beg = a_query.offset ? a_query.offset : 0;
        var end = a_query.limit  ? a_query.limit + beg: a_records.length;
        return a_records.slice(beg, end);
      }

      this._buildResponce = function(a_query, a_records, a_aliases) {
        var optionsex = {
          grouped: false
        }
        var rows = [];
        for(var i = 0; i < a_records.length; ++i) {
          var row = {};
          for(var fieldIndex = 0; fieldIndex < a_query.fields.length; ++fieldIndex) {
            var value = this._buildArg(a_query.fields[fieldIndex], a_query, a_records[i], a_records, a_aliases, optionsex);
            var from  = a_query.fields[fieldIndex].from                               ? a_query.fields[fieldIndex].from : a_query.from;
            var as    = a_query.fields[fieldIndex].as                                 ? a_query.fields[fieldIndex].as :
                        a_query.fields[fieldIndex].field && from != a_query.from      ? from + "." + a_query.fields[fieldIndex].field :
                        a_query.fields[fieldIndex].function                           ? a_query.fields[fieldIndex].function + "()" :
                                                                                        a_query.fields[fieldIndex].field;
            row[as] = value;
          }
          rows.push(row);
          if (optionsex.grouped)
            break;
        }

        return rows;
      }

      this._buildArg = function(a_argInfo, a_query, a_record, a_records, a_aliases, a_optionsex) {
        if (a_argInfo.field) {
          var from      = a_argInfo.from ? a_argInfo.from : a_query.from;
          var key       = a_record[from].key;
          var tableName = a_aliases[from] ? a_aliases[from] : from;
          var rec       = this._tables[tableName][key];
          var value     = rec[a_argInfo.field];
        } else if (a_argInfo.value) {
          var value     = a_argInfo.value;
        } else if (a_argInfo.function) {
          var value = this._buildFunc(a_argInfo, a_query, a_record, a_records, a_aliases, a_optionsex);
        }
        return value;
      }

      this._buildFunc = function(a_argInfo, a_query, a_record, a_records, a_aliases, a_optionsex) {
        var args = [];
        if (Array.isArray(a_argInfo.args) && !fcf.empty(a_argInfo.args))
          for(var i = 0; i < a_argInfo.args.length; ++i)
            args.push(this._buildArg(a_argInfo.args[i], a_query, a_record, a_records, a_aliases, a_optionsex));

        var func = a_argInfo.function.toLowerCase();
        if (func == "concat") {
          var value = "";
          for(var i = 0; i < args.length; ++i)
            value += args[i];
          return value;
        } else if (func == "count") {
          a_optionsex.grouped = true;
          var value = a_records.length;
          return value;
        } else {
          throw new fcf.Exception("ERROR_NFSQL_UNKNOWN_FUNCTION", {function: a_argInfo.function});
        }
      }

      this._screeningCopy = function(a_from, a_keys, a_where) {
        var result = [];
        var codeWhereFunc = this._createJSWhereFunc(a_from, a_keys, a_where);
        eval(codeWhereFunc);
        for (var rowIndex = 0; rowIndex < a_keys.length; ++rowIndex) {
          if (wherefunc(this._tables, a_keys[rowIndex]))
            result.push(a_keys[rowIndex]);
        }
        return result;
      }

      this._createJSWhereFunc = function(a_from, a_keys, a_where, a_joinFrom, a_joinAs) {
        return "function wherefunc(a_tables, a_record, a_joinrecord) { return " + this._createJSWhere(a_from, a_keys, a_where, a_joinFrom, a_joinAs) + "}";
      }

      this._createJSWhere = function(a_from, a_keys, a_where, a_joinFrom, a_joinAs) {
        a_where = Array.isArray(a_where) ? a_where : [];

        function processArg(a_arg){
          if ("value" in a_arg) {
            return typeof a_arg.value == "string" ? "\"" + fcf.escapeQuotes(a_arg.value) + "\""
                                                  : a_arg.value;
          } else if ("field" in a_arg) {
            var from = a_arg.from ? a_arg.from : a_from;
            if (a_joinAs == from){
              return "a_tables[\"" + fcf.escapeQuotes(a_joinFrom) + "\"][fcf.first(a_joinrecord).key][[\"" + fcf.escapeQuotes(a_arg.field) + "\"]]";
            } else {
              return "a_tables[\"" + fcf.escapeQuotes(from) + "\"][a_record[\"" + fcf.escapeQuotes(from) + "\"].key][[\"" + fcf.escapeQuotes(a_arg.field) + "\"]]";
            }
          }
        }

        var code = "";
        for (var i = 0; i < a_where.length; ++i) {
          if (i != 0)
            code +=  typeof a_where[i].logic !== "string" || a_where[i].logic.toLowerCase() !== "or" ? " && " : " || ";
          if (a_where[i].type == "block") {
            code += "(";
            code += this._createJSWhere(a_from, a_keys, a_where[i].args, a_joinFrom, a_joinAs);
            code += ")";
          } else if (a_where[i].type == "=") {
            code += processArg(a_where[i].args[0]);
            code += " == ";
            code += processArg(a_where[i].args[1]);
          } else if (a_where[i].type == "<>") {
            code += processArg(a_where[i].args[0]);
            code += " != ";
            code += processArg(a_where[i].args[1]);
          } else if (a_where[i].type == "<") {
            code += processArg(a_where[i].args[0]);
            code += " < ";
            code += processArg(a_where[i].args[1]);
          } else if (a_where[i].type == ">") {
            code += processArg(a_where[i].args[0]);
            code += " > ";
            code += processArg(a_where[i].args[1]);
          } else if (a_where[i].type == "<=") {
            code += processArg(a_where[i].args[0]);
            code += " <= ";
            code += processArg(a_where[i].args[1]);
          } else if (a_where[i].type == ">=") {
            code += processArg(a_where[i].args[0]);
            code += " >= ";
            code += processArg(a_where[i].args[1]);
          }
        }



        return fcf.empty(code) ? "true" : code;
      }

    }

    return NConnections.MemoryConnection;
  }
});
