/**
 * node.js - base abstract node for blessed
 * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
 * https://github.com/chjj/blessed
 */

/**
 * Modules
 */

var EventEmitter = require('../events').EventEmitter;

/**
 * Interfaces
 */

interface NodeOptions {
  screen?: any;
  parent?: NodeInterface;
  children?: NodeInterface[];
  [key: string]: any;
}

interface NodeScreen {
  focused: any;
  clickable: any[];
  keyable: any[];
  rewindFocus(): void;
  total?: number;
  global?: any;
  instances?: any[];
}

interface NodeData {
  [key: string]: any;
}

interface NodeInterface extends EventEmitter {
  type: string;
  options: NodeOptions;
  screen: NodeScreen;
  parent: NodeInterface | null;
  children: NodeInterface[];
  $: NodeData;
  _: NodeData;
  data: NodeData;
  uid: number;
  index: number;
  detached: boolean;
  destroyed?: boolean;
  
  // Methods
  insert(element: NodeInterface, i: number): void;
  prepend(element: NodeInterface): void;
  append(element: NodeInterface): void;
  insertBefore(element: NodeInterface, other: NodeInterface): void;
  insertAfter(element: NodeInterface, other: NodeInterface): void;
  remove(element: NodeInterface): void;
  detach(): void;
  free(): void;
  destroy(): void;
  forDescendants(iter: (el: NodeInterface) => void, s?: boolean): void;
  forAncestors(iter: (el: NodeInterface) => void, s?: boolean): void;
  collectDescendants(s?: boolean): NodeInterface[];
  collectAncestors(s?: boolean): NodeInterface[];
  emitDescendants(...args: any[]): void;
  emitAncestors(...args: any[]): void;
  hasDescendant(target: NodeInterface): boolean;
  hasAncestor(target: NodeInterface): boolean;
  get(name: string, value?: any): any;
  set(name: string, value: any): any;
  clearPos?(): void;
  emit(event: string, ...args: any[]): boolean;
}

/**
 * Node
 */

function Node(this: NodeInterface, options?: NodeOptions) {
  var self = this;
  var Screen = require('./screen');

  if (!(this instanceof Node)) {
    return new Node(options);
  }

  EventEmitter.call(this);

  options = options || {};
  this.options = options;

  this.screen = this.screen || options.screen;

  if (!this.screen) {
    if (this.type === 'screen') {
      this.screen = this;
    } else if (Screen.total === 1) {
      this.screen = Screen.global;
    } else if (options.parent) {
      this.screen = options.parent;
      while (this.screen && this.screen.type !== 'screen') {
        this.screen = this.screen.parent;
      }
    } else if (Screen.total) {
      // This _should_ work in most cases as long as the element is appended
      // synchronously after the screen's creation. Throw error if not.
      this.screen = Screen.instances[Screen.instances.length - 1];
      process.nextTick(function() {
        if (!self.parent) {
          throw new Error('Element (' + self.type + ')'
            + ' was not appended synchronously after the'
            + ' screen\'s creation. Please set a `parent`'
            + ' or `screen` option in the element\'s constructor'
            + ' if you are going to use multiple screens and'
            + ' append the element later.');
        }
      });
    } else {
      throw new Error('No active screen.');
    }
  }

  this.parent = options.parent || null;
  this.children = [];
  this.$ = this._ = this.data = {};
  this.uid = Node.uid++;
  this.index = this.index != null ? this.index : -1;

  if (this.type !== 'screen') {
    this.detached = true;
  }

  if (this.parent) {
    this.parent.append(this);
  }

  (options.children || []).forEach(this.append.bind(this));
}

Node.uid = 0;

Node.prototype.__proto__ = EventEmitter.prototype;

Node.prototype.type = 'node';

Node.prototype.insert = function(this: NodeInterface, element: NodeInterface, i: number): void {
  var self = this;

  if (element.screen && element.screen !== this.screen) {
    throw new Error('Cannot switch a node\'s screen.');
  }

  element.detach();
  element.parent = this;
  element.screen = this.screen;

  if (i === 0) {
    this.children.unshift(element);
  } else if (i === this.children.length) {
    this.children.push(element);
  } else {
    this.children.splice(i, 0, element);
  }

  element.emit('reparent', this);
  this.emit('adopt', element);

  (function emit(el) {
    var n = el.detached !== self.detached;
    el.detached = self.detached;
    if (n) el.emit('attach');
    el.children.forEach(emit);
  })(element);

  if (!this.screen.focused) {
    this.screen.focused = element;
  }
};

Node.prototype.prepend = function(this: NodeInterface, element: NodeInterface): void {
  this.insert(element, 0);
};

Node.prototype.append = function(this: NodeInterface, element: NodeInterface): void {
  this.insert(element, this.children.length);
};

Node.prototype.insertBefore = function(this: NodeInterface, element: NodeInterface, other: NodeInterface): void {
  var i = this.children.indexOf(other);
  if (~i) this.insert(element, i);
};

Node.prototype.insertAfter = function(this: NodeInterface, element: NodeInterface, other: NodeInterface): void {
  var i = this.children.indexOf(other);
  if (~i) this.insert(element, i + 1);
};

Node.prototype.remove = function(this: NodeInterface, element: NodeInterface): void {
  if (element.parent !== this) return;

  var i = this.children.indexOf(element);
  if (!~i) return;

  element.clearPos();

  element.parent = null;

  this.children.splice(i, 1);

  i = this.screen.clickable.indexOf(element);
  if (~i) this.screen.clickable.splice(i, 1);
  i = this.screen.keyable.indexOf(element);
  if (~i) this.screen.keyable.splice(i, 1);

  element.emit('reparent', null);
  this.emit('remove', element);

  (function emit(el) {
    var n = el.detached !== true;
    el.detached = true;
    if (n) el.emit('detach');
    el.children.forEach(emit);
  })(element);

  if (this.screen.focused === element) {
    this.screen.rewindFocus();
  }
};

Node.prototype.detach = function(this: NodeInterface): void {
  if (this.parent) this.parent.remove(this);
};

Node.prototype.free = function(this: NodeInterface): void {
  return;
};

Node.prototype.destroy = function(this: NodeInterface): void {
  this.detach();
  this.forDescendants(function(el: NodeInterface) {
    el.free();
    el.destroyed = true;
    el.emit('destroy');
  }, this);
};

Node.prototype.forDescendants = function(this: NodeInterface, iter: (el: NodeInterface) => void, s?: boolean): void {
  if (s) iter(this);
  this.children.forEach(function emit(el: NodeInterface) {
    iter(el);
    el.children.forEach(emit);
  });
};

Node.prototype.forAncestors = function(this: NodeInterface, iter: (el: NodeInterface) => void, s?: boolean): void {
  var el: NodeInterface | null = this;
  if (s) iter(this);
  while (el = el.parent) {
    iter(el);
  }
};

Node.prototype.collectDescendants = function(this: NodeInterface, s?: boolean): NodeInterface[] {
  var out: NodeInterface[] = [];
  this.forDescendants(function(el: NodeInterface) {
    out.push(el);
  }, s);
  return out;
};

Node.prototype.collectAncestors = function(this: NodeInterface, s?: boolean): NodeInterface[] {
  var out: NodeInterface[] = [];
  this.forAncestors(function(el: NodeInterface) {
    out.push(el);
  }, s);
  return out;
};

Node.prototype.emitDescendants = function() {
  var args = Array.prototype.slice(arguments)
    , iter;

  if (typeof args[args.length - 1] === 'function') {
    iter = args.pop();
  }

  return this.forDescendants(function(el) {
    if (iter) iter(el);
    el.emit.apply(el, args);
  }, true);
};

Node.prototype.emitAncestors = function() {
  var args = Array.prototype.slice(arguments)
    , iter;

  if (typeof args[args.length - 1] === 'function') {
    iter = args.pop();
  }

  return this.forAncestors(function(el) {
    if (iter) iter(el);
    el.emit.apply(el, args);
  }, true);
};

Node.prototype.hasDescendant = function(target) {
  return (function find(el) {
    for (var i = 0; i < el.children.length; i++) {
      if (el.children[i] === target) {
        return true;
      }
      if (find(el.children[i]) === true) {
        return true;
      }
    }
    return false;
  })(this);
};

Node.prototype.hasAncestor = function(target) {
  var el = this;
  while (el = el.parent) {
    if (el === target) return true;
  }
  return false;
};

Node.prototype.get = function(this: NodeInterface, name: string, value?: any): any {
  if (this.data.hasOwnProperty(name)) {
    return this.data[name];
  }
  return value;
};

Node.prototype.set = function(this: NodeInterface, name: string, value: any): any {
  return this.data[name] = value;
};

/**
 * Expose
 */

module.exports = Node;
