const isEqual = function(original, fn) {
  return function(val) {
    fn(val===original);
  }
};
const isContain = function(original, fn) {
  return function(val) {
    fn((val||[]).indexOf(original)>-1);
  }
};
const Store = function(cfg) {
    Observable.call(this);
    this.events = new Observable();
    this._props = cfg || {};
  },
  isObject = function(obj) {
    // null is not an object. treat Date as primitive
    return typeof obj === 'object' && obj !== null && !(obj instanceof Date);
  };
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, originalLength;
    if( isObject(val) ){
      if(Array.isArray(val)){

        if(!Array.isArray(pointer[key])){
          pointer[ key ] = [];
          list.push([path.concat(key).join( '.' ), pointer[ key ]])
        }
        originalLength = pointer[ key ].length;
      }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 ];
        }
      }
      if(Array.isArray(val)){
        if(originalLength !== val.length){
          recursiveSet( path.concat( key ), pointer[ key ], 'length', val.length, list );
          list.push([path.concat(key).join( '.' ), pointer[ key ]]);
        }
      }
    }else{
      if( val !== pointer[ key ] ){
        pointer[ key ] = val;
        list.push( [ path.concat(key).join( '.' ), pointer[ key ] ] );
      }

    }
  };
var StoreParent = function(parent, key, item) {
  this.parent = parent;
  this.key = key;
  this.item = item;
};
StoreParent.prototype = {
  getKey: function() {
    if(this.item){
      var idx = this.parent._props.indexOf(this.item);
      if(idx > -1)
        return idx;
      console.error('try to write to not existed item');
    }else{
      return this.key;
    }
  }
};
Store.prototype = {
  parent: null,
  setParent: function(parent, key, item){
    this.parent = new StoreParent(parent, key, item);
  },
  item: function(key, item){
    let subStore = new Store(item?item:key);
    subStore.setParent(this, key, item);
    return subStore;
  },
  array: function(key){
    let arrayStore = new ArrayStore(this.get(key))
    arrayStore.setParent(this, key)
    return arrayStore;
  },
  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);
  },
  clear: function() {
    this._props = {};
    this.events.fire('change', null, null);
  },
  /*
  key: String, val: any
  key: {k: v, ...}
   */
  set: function(key, val, changeList) {
    var isChangeList = changeList !== void 0;
    changeList = changeList || [];

    var type = typeof key;

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

    var _key;
    if(type === 'string') {
      _key = key.split('.');
    }else if(type === 'number'){
      _key = [key];

    }

    this._set(
      _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, prefix) {
    prefix = prefix || '';
    for( let i = changeList.length; i; ){
      const changeListElement = changeList[ --i ];
      var key = changeListElement[0], val = changeListElement[1],
        fullKey = prefix+(key===''?'':(prefix===''?'':'.')+key);
      this.events.fire('change', fullKey, changeListElement[1]);
      this.fire(fullKey, changeListElement[1]);

    }
    if(this.parent){
      if(prefix) {
        this.parent.parent._notify(changeList, this.parent.getKey() + '.' + prefix);
      }else{
        this.parent.parent._notify(changeList, this.parent.getKey());

      }
    }
  },
  get: function(key, returnLastStore) {
    if(key === void 0)
      return this._props;

    let type = typeof key;
    if(type === 'string') {
      key = key.split('.');
    }
    if(type === 'number') {
      key = [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, suppresFirstCall) {
    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 ))
        }
      }
      !suppresFirstCall && wrap();
      return function() {
        for( var i = 0, _i = uns.length; i < _i; i++ ){
          uns[ i ]();
        }
      };
    }else{
      un = this.on( key, fn );
      !suppresFirstCall && 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;
  },
  contain: function(key, val, fn) {
    const wrap = isContain(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)});
    }
  },
  valContains: function(key, val) {
    const me = this;
    return function backwardCallback(update) {
      me.contain(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 (k, fn) {
    if(typeof k === 'function'){
      fn = k;
      return this.un = this.store.sub(this.key, fn);
    }else{
      return this.un = this.store.sub(
        (Array.isArray(k)?k:[k]).map(k=>this.key+'.'+k), fn);
    }

  },
  set: function (val) {
    this.store.set(this.key, val);
  },
  get: function() {
    return this.store.get(this.key);
  },
  bind: function(key) {
    return new StoreBinding(this.store, this.key+'.'+key);
  }
};
Store.prototype = Object.assign(new Observable, Store.prototype);

Store.StoreBinding = StoreBinding;

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;
  },
  binding: function() {
    var x = new StoreBinding();
    x.get = ()=> this.get();
    x.set = (a)=> this.set(a);
    x.sub = (fn)=> this.hook(fn);
    return x;
  },
  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) {
    if(!(this instanceof Hook))
      return new Hook(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()); }
  }),
  Number: new HookFactory({
    setter: function(val) { return val|0; }
  })
};
Store.HookPrototype = HookPrototype;



const ArrayStore = function (cfg) {
  Store.call(this, cfg);
};
ArrayStore.prototype = {
  push: function (val) {
    var changeList = [];
    changeList.push(['', this._props])

    this.set(this.getLength(), val, changeList);
    this._notify(changeList);
    return this;
  },
  pop: function () {
    var changeList = [];
    changeList.push(['', this._props]);
    var out = this.get(this.getLength()-1);
    this.set(this.getLength()-1, void 0, changeList);
    this._props.pop();
    this._notify(changeList);
    return out;
  },
  remove: function(item) {
    var idx = this._props.indexOf(item);
    if(idx>-1){
      var changeList = [];
      changeList.push(['', this._props]);
      this._props.splice(idx,1);
      //this.set(this.getLength()-1, void 0, changeList);
      this._notify(changeList);
    }
    return this;
  },
  slice: function () {},
  splice: function () {},
  getLength: function () {
    return this._props.length;
  },
  update: function() {
    var changeList = [];
    changeList.push(['', this._props]);
    this._notify(changeList);
    return this;
  },
  '~destructor': function() {
    this._listeners = this.events = this._props = null;
  }
};

ArrayStore.prototype = Object.assign(new Store(), ArrayStore.prototype);


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