import Event from './Event'
import Dom from './Dom'
import State from './State'

const CHECK_EVENT_PATTERN = /^(click|mouse(down|up|move|enter|leave)|touch(start|move|end)|key(down|up|press)|contextmenu|change|input)/ig;
const CHECK_LOAD_PATTERN = /^load (.*)/ig;
const EVENT_SAPARATOR = ' '
const META_KEYS = ['Control', 'Shift', 'Alt', 'Meta'];

export default class EventMachin {

  constructor() { 
    this.state = new State(this);
    this.refs = {} 

    this.childComponents = this.components()
  }

  /**
   * 자식으로 사용할 컴포넌트를 생성해준다. 
   * 생성 시점에 $store 객체가 자동으로 공유된다. 
   * 모든 데이타는 $store 기준으로 작성한다. 
   */
  newChildComponents () {
    const childKeys = Object.keys(this.childComponents)
    childKeys.forEach(key => {
      const Component = this.childComponents[key]

      this[key] = new Component(this);
    })
  }

  /**
   * 부모가 정의한 template 과  그 안에서 동작하는 자식 컴포넌트들을 다 합쳐서 
   * 최종 element 를 만들어준다. 
   * 
   * 그리고 자동으로 load 되어질게 있으면 로드 해준다. 
   */
  render () {
    // 1. 나의 template 을 만들어내고  
    this.$el = this.parseTemplate(this.template())
    this.refs.$el = this.$el;         

    // 개별 객체 셋팅하고 
    this.parseTarget()

    // 데이타 로드 하고 
    this.load()    

    this.afterRender()

  }

  afterRender() { }
 
  /**
   * 자식 컴포넌트로 사용될 객체 정의 
   */
  components () {
    return {} 
  }

  /**
   * Class 기반으로 $el 을 생성하기 위해서 
   * 선언형으로 html 템플릿을 정의한다. 
   * 
   * @param {*} html 
   */
  parseTemplate (html) {
    const $el = new Dom("div").html(html).firstChild()

    // ref element 정리 
    var refs = $el.findAll('[ref]');

    [...refs].forEach(node => {
      const name = node.getAttribute('ref')
      this.refs[name] = new Dom(node);
    })

    return $el; 
  }

  /**
   * target 으로 지정된 자식 컴포넌트를 대체해준다.
   */
  parseTarget () {
    const $el = this.$el; 
    const targets = $el.findAll('[target]');

    [...targets].forEach(node => {
      const targetComponentName = node.getAttribute('target')
      const refName = node.getAttribute('ref') || targetComponentName

      var Component = this.childComponents[targetComponentName]
      var instance = new Component(this);
      this[refName] = instance
      this.refs[refName] = instance.$el

      if (instance) {
        instance.render()
        var $parent = new Dom(node.parentNode)
        $parent.replace(node, instance.$el.el)                
      }
    })
  }

  // load function이 정의된 객체는 load 를 실행해준다. 
  load () {
    
    this.filterProps(CHECK_LOAD_PATTERN).forEach(callbackName => {
      const elName = callbackName.split('load ')[1]

      if (this.refs[elName]) { 
        this.refs[elName].html(this.parseTemplate(this[callbackName].call(this)))
      }
    })
  }

  // 기본 템플릿 지정 
  template () {
    return '<div></div>';
  }

  initialize() {

  }

  /**
   * 이벤트를 초기화한다. 
   */
  initializeEvent () { 
    this.initializeEventMachin();

    // 자식 이벤트도 같이 초기화 한다. 
    // 그래서 이 메소드는 부모에서 한번만 불려도 된다. 
    Object.keys(this.childComponents).forEach(key => {
      if (this[key]) this[key].initializeEvent()
    })

  }

  /**
   * 자원을 해제한다. 
   * 이것도 역시 자식 컴포넌트까지 제어하기 때문에 가장 최상위 부모에서 한번만 호출되도 된다. 
   */
  destroy() {
    this.destroyEventMachin();
    // this.refs = {} 

    Object.keys(this.childComponents).forEach(key => {
      if (this[key]) this[key].destroy()
    })

  }

  destroyEventMachin () {
    this.removeEventAll();
  }

  initializeEventMachin () {
    this.filterProps(CHECK_EVENT_PATTERN).forEach(this.parseEvent.bind(this));
  }

  /**
   * property 수집하기 
   * 상위 클래스의 모든 property 를 수집해서 리턴한다. 
   */
  collectProps () {

    if (!this.collapsedProps) {
      var p = this.__proto__ 
      var results = [] 
      do {
        results.push(...Object.getOwnPropertyNames(p))
        p  = p.__proto__;
      } while( p );

      this.collapsedProps = results
    }

    return this.collapsedProps; 
  }

  filterProps (pattern) {
    return this.collectProps().filter(key => {
      return key.match(pattern);
    });
  }

  parseEvent (key) {
    let arr = key.split(EVENT_SAPARATOR) ;

    this.bindingEvent(arr,this[key].bind(this));
  }

  getDefaultDomElement (dom) {
    let el; 

    if (dom) {
      el = this.refs[dom] || this[dom] || window[dom]; 
    } else {
      el = this.el || this.$el || this.$root; 
    }

    if (el instanceof Dom) {
      return el.getElement();
    }

    return el;
  }

  getDefaultEventObject (eventName) {
    let arr = eventName.split('.');
    const realEventName = arr.shift();

    const isControl = arr.includes('Control');
    const isShift =  arr.includes('Shift');
    const isAlt = arr.includes('Alt');
    const isMeta =  arr.includes('Meta');

    arr = arr.filter((code) => {
      return META_KEYS.includes(code) === false;
    });
    
    const checkMethodList = arr.filter(code => {
      return !!this[code];
    });
    
    arr = arr.filter(code => {
      return checkMethodList.includes(code) === false; 
    }).map(code => {
      return code.toLowerCase() 
    });

    return {
      eventName : realEventName,
      isControl,
      isShift,
      isAlt,
      isMeta,
      codes : arr,
      checkMethodList: checkMethodList
    }
  }

  bindingEvent ([ eventName, dom, ...delegate], callback) {
    dom = this.getDefaultDomElement(dom);
    let eventObject = this.getDefaultEventObject(eventName);

    eventObject.dom = dom;
    eventObject.delegate = delegate.join(EVENT_SAPARATOR);

    this.addEvent(eventObject, callback);
  }

  matchPath(el, selector) {
    if (el) {
      if (el.matches(selector)) { return el; }
      return this.matchPath(el.parentElement, selector);
    }
    return null;
  }

  getBindings () {

    if (!this._bindings) {
      this.initBindings();
    }

    return this._bindings;
  }

  addBinding (obj) {
    this.getBindings().push(obj);
  }

  initBindings() {
    this._bindings = [];
  }

  checkEventType (e, eventObject ) {
    var onlyControl = eventObject.isControl ? e.ctrlKey : true;
    var onlyShift = eventObject.isShift ? e.shiftKey  : true; 
    var onlyAlt = eventObject.isAlt ? e.altKey : true; 
    var onlyMeta = eventObject.isMeta ? e.metaKey : true; 

    var hasKeyCode = true; 
    if (eventObject.codes.length) {
      hasKeyCode = eventObject.codes.includes(e.code.toLowerCase()) || eventObject.codes.includes(e.key.toLowerCase());
    }

    var isAllCheck = true;  
    if (eventObject.checkMethodList.length) {  // 체크 메소드들은 모든 메소드를 다 적용해야한다. 
      isAllCheck = eventObject.checkMethodList.every(method => {
        return this[method].call(this, e);
      });
    }

    return (onlyControl && onlyAlt && onlyShift && onlyMeta && hasKeyCode && isAllCheck);
  }

  makeCallback ( eventObject, callback) {
    if (eventObject.delegate) {
      return (e) => {
        e.xy = Event.posXY(e);
        if (this.checkEventType(e, eventObject)) {
          const delegateTarget = this.matchPath(e.target || e.srcElement, eventObject.delegate);
  
          if (delegateTarget) { // delegate target 이 있는 경우만 callback 실행 
            e.delegateTarget = delegateTarget;
            e.$delegateTarget = new Dom(delegateTarget);
            return callback(e);
          } 
        }

      }
    }  else {
      return (e) => {
        e.xy = Event.posXY(e);
        if (this.checkEventType(e, eventObject)) { 
          return callback(e);
        }
      }
    }
  }

  addEvent(eventObject, callback) {
    eventObject.callback = this.makeCallback(eventObject, callback)
    this.addBinding(eventObject);

    var options = true; 
    if (eventObject.eventName === 'touchstart') {
      options = { passive : true }
    }
      
    Event.addEvent(eventObject.dom, eventObject.eventName, eventObject.callback, options)
  }

  removeEventAll () {
    this.getBindings().forEach(obj => {
      this.removeEvent(obj);
    });
    this.initBindings();
  }

  removeEvent({eventName, dom, callback}) {
    Event.removeEvent(dom, eventName, callback);
  }
}
