import * as utility from './utility';
import * as event from './event';
import { IHandler, IEvents, IEvent } from './event/registry';

/* -----------------------------------
 *
 * IMeta
 *
 * -------------------------------- */

export interface IMeta {
   owner?: Instance;
}

/* -----------------------------------
 *
 * Instance
 *
 * -------------------------------- */

export class Instance {
   [index: number]: HTMLElement;

   public length: number;
   public events: IEvents;
   private meta: IMeta;

   public constructor(qry: string | HTMLElement, ctx?: Element, meta?: IMeta) {
      let els: any;

      this.meta = meta || {};
      this.events = event.registry();

      if (typeof qry === 'string') {
         els = utility.query(qry, ctx ? ctx : document);
      } else {
         els = qry;
      }

      if (!els) return this;

      if (els.nodeType === 1 || els === window) {
         this[0] = els;
         this.length = 1;
      } else {
         for (
            let len = (this.length = els.length);
            len--;
            this[len] = els[len]
         );
      }
   }

   public get(key: number) {
      return this[key];
   }

   public first() {
      return new Instance(this[0]);
   }

   public find(qry: string) {
      return new Instance(qry, this[0], { owner: this });
   }

   public closest(qry: string) {
      const match = document.querySelectorAll(qry);

      let el = this[0];
      let i;

      do {
         i = match.length;

         while (--i >= 0 && match.item(i) !== el) {
            // noop
         }
      } while (i < 0 && (el = el.parentElement));

      return new Instance(el);
   }

   public each(cb: (el: HTMLElement) => void) {
      for (let i = 0, len = this.length; i < len; ) {
         if (cb.call(this, this[i], i++) === false) {
            break;
         }
      }

      return this;
   }

   public css(obj: { [index: string]: string }) {
      this.each(el => {
         for (const key of Object.keys(obj)) {
            const val = obj[key];

            el.style.setProperty(key, val);
         }
      });

      return this;
   }

   public attr(obj: { [index: string]: string } | string) {
      if (typeof obj === 'string') {
         const value = this[0].getAttribute(obj);

         return value;
      }

      this.each(el => {
         for (const key of Object.keys(obj)) {
            const val = obj[key];

            el.setAttribute(key, val);
         }
      });
   }

   public hasClass(str: string) {
      let result = false;

      this.each(el => {
         result = utility.hasClass(el, str);
      });

      return result;
   }

   public addClass(str: string) {
      this.each(el => {
         const state = utility.hasClass(el, str);

         if (!state) {
            el.className += ' ' + str;
         }
      });

      return this;
   }

   public removeClass(str: string) {
      this.each(el => {
         const state = utility.hasClass(el, str);

         if (state) {
            const reg = new RegExp('(\\s|^)' + str + '(\\s|$)');
            const val = el.className.replace(reg, ' ').trim();

            el.className = val.replace(/\s{2,}/g, ' ');
         }
      });

      return this;
   }

   public toggleClass(str: string) {
      if (this.hasClass(str)) {
         this.removeClass(str);
      } else {
         this.addClass(str);
      }

      return this;
   }

   public on(ev: string, op1: string | IHandler, op2?: IHandler) {
      const { events } = this;

      const direct = typeof op1 === 'function' && op2 === undefined;
      const delegate = typeof op1 === 'string' && typeof op2 === 'function';

      this.each(el => {
         let cb = null;

         if (direct) {
            cb = event.direct(op1 as IHandler);
         }

         if (delegate) {
            cb = event.delegate(el, op1 as string, op2);
         }

         if (cb) {
            el.addEventListener(ev, cb, true);

            events.add({
               type: ev,
               handler: cb,
            });
         } else {
            throw new Error('TSDom.on: Invalid Arguments');
         }
      });

      return this;
   }

   public off(ev: string) {
      const { events } = this;

      this.each(el => {
         const items: IEvent[] = events.find(ev);

         items.forEach(item => {
            if (item !== undefined) {
               el.removeEventListener(ev, item.handler, true);
            }
         });
      });

      events.remove(ev);

      return this;
   }

   public val(val?: string) {
      const item = this.get(0) as HTMLInputElement;

      if (item.value === undefined) {
         return null;
      }

      if (val === undefined) {
         return item.value;
      }

      item.value = val;

      return val;
   }

   public text(val?: string) {
      const item = this.get(0);

      if (!item) {
         return null;
      }

      if (val === undefined) {
         return item.innerText;
      }

      this.each(el => {
         el.innerHTML = val;
      });

      return val;
   }

   public data(key: string, val?: string) {
      const item = this.get(0);

      if (!item) {
         return null;
      }

      if (val === undefined) {
         return item.getAttribute(`data-${key}`);
      }

      this.each(el => {
         el.setAttribute(`data-${key}`, val);
      });

      return val;
   }

   public html(val?: string) {
      const item = this.get(0);

      if (!item) {
         return null;
      }

      if (val === undefined) {
         return item.innerHTML;
      }

      this.each(el => {
         el.innerHTML = val;
      });

      return val;
   }

   public append(item: string | Node | HTMLElement) {
      this.each(el => {
         if (typeof item === 'string') {
            return el.insertAdjacentHTML('beforeend', item);
         }

         el.appendChild(item);
      });

      return this;
   }

   public prepend(item: string | Node | HTMLElement) {
      this.each(el => {
         if (typeof item === 'string') {
            return el.insertAdjacentHTML('afterbegin', item);
         }

         el.insertBefore(item, el.firstChild);
      });

      return this;
   }

   public empty() {
      this.each(el => {
         while (el.firstChild) {
            el.removeChild(el.firstChild);
         }
      });

      return this;
   }

   public remove() {
      this.each(el => {
         el.parentNode.removeChild(el);
      });
   }

   public toArray() {
      const array: HTMLElement[] = [];

      this.each(el => {
         array.push(el);
      });

      return array;
   }
}

/* -----------------------------------
 *
 * Namespace
 *
 * -------------------------------- */

export declare namespace TSDom {
   export type Init = (
      qry: string | HTMLElement,
      ctx?: Element,
      meta?: IMeta
   ) => Instance;

   export class Object extends Instance {}
}

/* -----------------------------------
 *
 * Constructor
 *
 * -------------------------------- */

export default (qry: string | HTMLElement, ctx?: HTMLElement) =>
   new Instance(qry, ctx);
