fcf.module({
  name: "fcf:NSystem/cache.js",
  dependencies: [],
  module: function(){
    var Namespace = fcf.prepareObject(fcf, "NSystem");

    Namespace.Cache = function(){
      var self = this;
      this._cache       = {};
      this._info        = {};
      this._projections = {};

      this.append = function(a_options) {
        let self = this;

        if ("variant" in a_options && !Array.isArray(a_options.variant))
          a_options.variant = [a_options.variant];

        if (!(a_options.part in this._info))
          this._info[a_options.part] = {};

        if (this._info[a_options.part][a_options.name] && this._info[a_options.part][a_options.name].projections){
          let projections = this._info[a_options.part][a_options.name].projections;
          fcf.each(projections,(a_key, a_projectionName)=>{
            if (this._projections[a_projectionName] && this._projections[a_projectionName][a_options.part] && this._projections[a_projectionName][a_options.part][a_options.name])
              delete this._projections[a_projectionName][a_options.part][a_options.name];
          });
        }

        this._info[a_options.part][a_options.name] = a_options;

        fcf.each(a_options.projections, (a_key, a_projection) => {
          let projections =  typeof a_projection === "object" ? [a_projection.name] : [a_projection];
          fcf.each(projections, (a_key, a_projection) => {
            if (!(a_projection in self._projections)) {
              self._projections[a_projection] = {};
              fcf.application.getEventChannel().on(`fcf_fsql_update_before:${a_projection}`, {global: "global"}, (a_event)=>{
                return Namespace.cache.updateByProjections([a_event.projection]);
              })
              fcf.application.getEventChannel().on(`fcf_fsql_insert_before:${a_projection}`, {global: "global"}, (a_event)=>{
                return Namespace.cache.updateByProjections([a_event.projection]);
              })
              fcf.application.getEventChannel().on(`fcf_fsql_delete_before:${a_projection}`, {global: "global"}, (a_event)=>{
                return Namespace.cache.updateByProjections([a_event.projection]);
              })
            }
            if (!(a_options.part in self._projections[a_projection]))
              self._projections[a_projection][a_options.part] = {};
            self._projections[a_projection][a_options.part][a_options.name] = true;
          });
        });

        if ("value" in a_options) {
          let ptr = this._getCachePtr(a_options.part, a_options.name, true);
          ptr.object[ptr.key] = a_options.value;
        }

      }

      this.update = function(a_part, a_name) {
        if (!(a_part in this._info))
          return fcf.actions();
        if (!(a_name in this._info[a_part]))
          return fcf.actions();
        var inf = this._info[a_part][a_name];

        if (typeof inf.recalculate !== "function"){
          if (a_part in this._cache && a_name in this._cache[a_part])
            delete this._cache[a_part][a_name]
          return fcf.actions();
        }

        var result = inf.recalculate();

        return fcf.actions()
        .then(()=>{
          return result;
        })
        .then((a_data)=>{
          //TODO: This event will be removed
          return fcf.application.getEventChannel().send("fcf_cache_update_before", {part: a_part, variable: a_name, value: a_data})
          .then(()=>{
            return a_data;
          });
        })
        .then((a_data)=>{
          //TODO: need adaption for variant options
          if (!(a_part in self._cache))
            self._cache[a_part] = {};
          self._cache[a_part][a_name] = a_data;
          return a_data;
        })
        .then((a_data)=>{
          //TODO: This event will be removed
          fcf.application.getEventChannel().send("fcf_cache_update_after", {part: a_part, variable: a_name, value: a_data})
          .then(()=>{
          });
        })
      }

      this.updateByProjections = async function (a_projections){
        let self = this;
        await fcf.each(a_projections, async (a_key, a_projection)=>{
          if (!(a_projection in Namespace.cache._projections))
            return;
          await fcf.each(Namespace.cache._projections[a_projection], async (a_part, a_map)=>{
            await fcf.each(a_map, async (a_name, a_map)=>{
              await self.update(a_part, a_name);
            });
          });
        })
      }


      this.updateAll = function(){
        fcf.each(this._info, function(a_partName, a_part){
          fcf.each(a_part, function(a_keyName, a_data){
            self.update(a_partName, a_keyName);
          });
        })
      }

      this.get = function(a_part, a_name){
        let ptr = this._getCachePtr(a_part, a_name, false);
        return ptr ? ptr.object[ptr.key] : undefined;
      }

      this.set = function(a_part, a_name, a_data){
        let ptr = this._getCachePtr(a_part, a_name, true);
        ptr.object[ptr.key] = a_data;
      }

      this._getCachePtr = function(a_part, a_name, a_createEmptyPath){
        if (!this._info[a_part] || !this._info[a_part][a_name])
          return undefined;

        let inf    = this._info[a_part][a_name];
        let result = undefined;
        if (a_createEmptyPath) {
          if (!this._cache[a_part])
            this._cache[a_part] = {};
          result = {
            object: this._cache[a_part],
            key:    a_name,
          }
          fcf.each(inf.variant, (a_key, a_value)=>{
            if (a_value == "roles") {
              let roles    = fcf.getContext().session.user.roles;
              let rolesStr = JSON.stringify(fcf.array(roles, (k)=>{ return k; }).sort());
              if (!result.object[result.key])
                result.object[result.key] = {};
              result.object = result.object[result.key];
              result.key    = rolesStr;
            } else if (a_value.indexOf("context") == 0) {
              if (!result.object[result.key])
                result.object[result.key] = {};
              result.object = result.object[result.key];
              result.key    = fcf.resolve({context: fcf.getContext()}, a_value);
            }
          });
        } else {
          if (!this._cache[a_part])
            return result;
          if (!this._cache[a_part][a_name])
            return result;
          result = {
            object: this._cache[a_part],
            key:    a_name,
          };
          fcf.each(inf.variant, (a_key, a_value) => {
            if (a_value == "roles") {
              let roles    = fcf.getContext().session.user.roles;
              let rolesStr = JSON.stringify(fcf.array(roles, (k)=>{ return k; }).sort());
              result.object = result.object[result.key];
              result.key    = rolesStr;
              if (!(result.key  in result.object)){
                result = undefined;
                return false;
              }
            } else {
              result.object = result.object[result.key];
              result.key    = fcf.resolve({context: fcf.getContext()}, a_value);
              if (!(result.key  in result.object)){
                result = undefined;
                return false;
              }
            }
          });
        }

        return result;
      }


    }

    Namespace.cache = new Namespace.Cache()

    return Namespace.cache;
  }
});
