const isEqual = function(original, fn) {
  return function(val) {
    fn(val===original);
  }
};
const Store = function(cfg) {
  Observable.call(this);
  this._props = cfg || {};
},
  isObject = function(obj) {
    return typeof obj === 'object' && obj !== null;
  };
const recursiveWalk = function(path, pointer, key, list) {
    if(typeof pointer[key] === 'object'){
      for(let subKey in pointer[key]){
        if(typeof pointer[key][subKey] === 'object'){
          list.push( [ path.concat( key, subKey ).join('.'), void 0 ] );
        }
        recursiveWalk(path.concat(key), pointer[key], subKey, list);
      }
    }else{
      list.push( [ path.concat( key ).join('.'), void 0 ] );
    }
  },
  recursiveSet = function(path, pointer, key, val, list){
    let subKey,i;
    if( isObject(val) ){
      if(Array.isArray(val)){
        if(!Array.isArray(pointer[key])){
          pointer[ key ] = [];
          list.push([path.concat(key).join( '.' ), pointer[ key ]])
        }
      }else{
        if(!isObject(pointer[ key ])){
          pointer[ key ] = {};
          list.push([path.concat(key).join( '.' ), pointer[ key ]])
        }
      }

      for(subKey in val){
        recursiveSet( path.concat( key ), pointer[ key ], subKey, val[subKey], list );
      }

      for( subKey in pointer[ key ] ){
        if( !val.hasOwnProperty( subKey ) ){
          // remove
          recursiveWalk( path.concat( key ), pointer[key], subKey, list );
          delete pointer[ key ][ subKey ];
        }
      }
    }else{
      if( val !== pointer[ key ] ){
        pointer[ key ] = val;
        list.push( [ path.concat(key).join( '.' ), pointer[ key ] ] );
      }

    }
  };
Store.prototype = {
  experimental: false,
  _set: function(keys, val, pointer, changeList) {
    changeList = changeList || [];
    let parent, i, _i;
    for(i=0, _i = keys.length - 1; i < _i; i++){
      let key = keys[i];
      parent = pointer;

      if(key in pointer){
        pointer = parent[key];
        if(!isObject(pointer)){
          pointer = parent[key] = {};
          changeList.push([keys.slice(0, i).join( '.' ), parent[key]]);
        }
      }else{
        pointer = parent[key] = {};
        changeList.push([keys.slice(0, i+1).join( '.' ), parent[key]]);
      }

    }

    let key = keys[i];
    recursiveSet(keys.slice(0, keys.length - 1), pointer, key, val, changeList);
  },

  set: function(key, val) {
    let changeList = [];
    this._set(
        typeof key === 'string' ? key.split('.') : key,
        val,
        this._props,
        changeList
    );
    for( let i = changeList.length; i; ){
      const changeListElement = changeList[ --i ];
      this.fire('change', changeListElement[0], changeListElement[1]);
      this.fire(changeListElement[0], changeListElement[1]);
    }
    return;
  },
  get: function(key, returnLastStore) {
    key = typeof key === 'string' ? key.split('.') : key;

    let ref = this,
        lastStore = ref, i, _i;

    for( i = 0, _i = key.length; i < _i; i++ ){
      if( ref instanceof Store ){
        ref = ref._props[key[i]];
        lastStore = ref;
      }else{
        ref = ref[key[i]];
      }

      if( !ref && i < key.length - 1 )
        return void 0;
    }

    if( returnLastStore )
      return { lastQObject: lastStore, result: ref };

    return ref;
  },

  sub: function(key, fn) {
    if(Array.isArray(key)){
      var wrap = () => fn.apply(this, key.map(key=>this.get(key)));

      for( var i = 0, _i = key.length; i < _i; i++ ){
        this.on(key[i], wrap)
      }
      wrap();
    }else{
      this.on( key, fn )
      fn( this.get( key ) );
    }
    return this;
  },
  equal: function(key, val, fn) {
    const wrap = isEqual(val, fn);
    this.on(key, wrap);
    wrap(this.get(key));
    return this;
  },
  bind: function (key) {
    return new StoreBinding(this, key);
  },
  val: function(key) {
    const me = this;
    return function backwardCallback(update) {
      me.sub(key, val => update(val));
    }
  },
  valEqual: function(key, val) {
    const me = this;
    return function backwardCallback(update) {
      me.equal(key, val, compareResult => update(compareResult));
    }
  },
  valTrue: function(key) {
    return this.valEqual(key, true);
  },
  valFalse: function(key) {
    return this.valEqual(key, false);
  }
};
const StoreBinding = function(store, key){
  this.store = store;
  this.key = key;
};
StoreBinding.prototype = {
  sub: function (fn) {
    this.store.sub(this.key, fn);
  },
  set: function (val) {
    this.store.set(this.key, val);
  }
};
Store.prototype = Object.assign(new Observable, Store.prototype);

Store.AGGREGATE = function(fn) {
  return function(...args) {
    let composite, _i = args.length,
      vals = new Array(_i);
    return function(update) {
      let check = ()=>{
        let result = fn(vals, _i);

        if(composite !== result)
          update(composite = result);
      };
      for(let i = 0; i < _i; i++){
        // set backward callback
        args[i]( val => {
          // update item corresponding value and check condition
          vals[i] = val;
          check();
        })
      }

    };

  };
};

Store.AND = Store.AGGREGATE(function(values, length) {
  let result = true;
  for(let i = 0; i < length; i++){
    result = result && values[i];
  }
  return result;
});

Store.OR = Store.AGGREGATE(function(values, length) {
  let result = false;
  for(let i = 0; i < length; i++){
    result = result || values[i];
  }
  return result;
});
Store.IF = function(cfg, children){
  return function( update ){
    if( 'condition' in cfg )
      cfg.condition( function( cond ){
        update( cond ? children : null );
      } );
  }
};
Store.NOT = Store.AGGREGATE(function(values, length) {
  return !values[0];
});
typeof module === 'object' && (module.exports = Store);
