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);
  },

  /*
  key: String, val: any
  key: {k: v, ...}
   */
  set: function(key, val, changeList) {
    var isChangeList = changeList !== void 0;
    changeList = changeList || [];


    if(typeof key === 'object'){
      for(let k in key){
        this.set(k, key[k], changeList);
      }
      this._notify(changeList);
      return this;
    }

    this._set(
        typeof key === 'string' ? key.split('.') : key,
        val,
        this._props,
        changeList
    );
    if(!isChangeList)
      this._notify(changeList);

    return this;
  },
  reSet: function(key, val, changeList) {
    this._props = {};
    if(key===void 0)
      return this;
    return this.set(key, val, changeList);
  },
  _notify: function(changeList) {
    for( let i = changeList.length; i; ){
      const changeListElement = changeList[ --i ];
      this.fire('change', changeListElement[0], changeListElement[1]);
      this.fire(changeListElement[0], changeListElement[1]);
    }
  },
  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) {
    var un;
    if(Array.isArray(key)){
      var wrap = () => fn.apply(this, key.map(key=>key instanceof StoreBinding ? key.get() : this.get(key)));
      var uns = [];
      for( var i = 0, _i = key.length; i < _i; i++ ){
        if(key[i] instanceof StoreBinding){
          uns.push(key[i].sub(wrap));
        }else{
          uns.push(this.on( key[ i ], wrap ))
        }
      }
      wrap();
      return function() {
        for( var i = 0, _i = uns.length; i < _i; i++ ){
          uns[ i ]();
        }
      };
    }else{
      un = this.on( key, fn );
      fn( this.get( key ) );
      return un;
    }

  },
  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)});
    }
  },
  valEqualOnly: function(key, val) {
    const me = this;
    return function backwardCallback(update) {
      me.equal(key, val, compareResult => {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);
  },
  get: function() {
    return this.store.get(this.key);
  }
};
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
        let hook = val => {
          // update item corresponding value and check condition
          vals[ i ] = val;
          check();
        };
        if(args[i] instanceof HookPrototype){
          args[i].hook(hook)
        }else{
          args[ i ]( hook )
        }
      }

    };

  };
};

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.ELSE = function(){if(!( this instanceof Store.ELSE))return new Store.ELSE()};
Store.IF = function(cfg, children){
  var holders = {true: [], false: []},
    holder = holders.true;
  for( var i = 0, _i = children.length; i < _i; i++ ){
    var child = children[ i ];
    if(child instanceof Store.ELSE){
      holder = holders.false;
    }else{
      holder.push(child);
    }
  }
  holders.true.length === 0 && (holders.true = null);
  holders.false.length === 0 && (holders.false = null);

  return function( update ){
    if( 'condition' in cfg )
      var hook = function( cond ){
        update( cond ? holders.true : holders.false );
      };
      if(cfg.condition instanceof HookPrototype){
        cfg.condition.hook( hook );
      }else if(typeof cfg.condition === 'function'){
        cfg.condition( hook );
      }else{
        // TODO other hooklikes
        hook(cfg.condition)
      }
  }
};
Store.NOT = Store.AGGREGATE(function(values, length) {
  return !values[0];
});
Store.debounce = function(fn, dt) {
  var timeout = false, args, scope,
    realCall = function() {
      timeout = false;
      fn.apply(scope, args)
    };
  return function(){
    args = [].slice.call(arguments);
    scope = this;
    if(!timeout){
      timeout = setTimeout(realCall, dt);
    }
  }
};
var HookPrototype = function() {};
HookPrototype.prototype = {
  setter: function(val) { return val; },
  //getter: function(val) { return val; },
  set: function(val) {
    val = this.setter(val);
    var oldVal = this.get();
    if(!this.equal(oldVal, val)){
      this.data = val;
      this._emit(oldVal, val);
    }
  },
  equal: function(oldVal, newVal){
    return newVal === oldVal;
  },
  get: function() {
    return this.data;
  },

  hook: function(fn) { this.subscribers.push(fn); fn(this.get()); },
  _emit: function(oldVal, val) {
    var subscribers = this.subscribers;
    for( var i = 0, _i = subscribers.length; i < _i; i++ ){
      subscribers[ i ](val);
    }
  }
};
var HookFactory = function(accessor) {
  var Hook = function(cfg) {
    this.data = {};
    this.subscribers = [];
    this.set(cfg);
  };
  Hook.prototype = Object.assign( new HookPrototype(), accessor );

  return Hook;
};

Store.Value = {
  Boolean: new HookFactory({
    setter: function(val) { return !!val; },
    toggle: function() { this.set(!this.get()); }
  })
};
Store.HookPrototype = HookPrototype;

typeof module === 'object' && (module.exports = Store);
(typeof window === 'object') && (window.Store = Store);