/* @flow */

import * as t from "../index";

export let VISITOR_KEYS = {};
export let ALIAS_KEYS = {};
export let NODE_FIELDS = {};
export let BUILDER_KEYS = {};

function getType(val) {
  if (Array.isArray(val)) {
    return "array";
  } else if (val === null) {
    return "null";
  } else if (val === undefined) {
    return "undefined";
  } else {
    return typeof val;
  }
}

export function assertEach(callback: Function): Function {
  return function (node, key, val) {
    if (!Array.isArray(val)) return;

    for (let i = 0; i < val.length; i++) {
      callback(node, `${key}[${i}]`, val[i]);
    }
  };
}

export function assertOneOf(...vals): Function {
  function validate(node, key, val) {
    if (vals.indexOf(val) < 0) {
      throw new TypeError(`Property ${key} expected value to be one of ${JSON.stringify(vals)} but got ${JSON.stringify(val)}`);
    }
  }

  validate.oneOf = vals;

  return validate;
}

export function assertNodeType(...types: Array<string>): Function {
  function validate(node, key, val) {
    let valid = false;

    for (let type of types) {
      if (t.is(type, val)) {
        valid = true;
        break;
      }
    }

    if (!valid) {
      throw new TypeError(`Property ${key} of ${node.type} expected node to be of a type ${JSON.stringify(types)} but instead got ${JSON.stringify(val && val.type)}`);
    }
  }

  validate.oneOfNodeTypes = types;

  return validate;
}

export function assertValueType(type: string): Function {
  function validate(node, key, val) {
    let valid = getType(val) === type;

    if (!valid) {
      throw new TypeError(`Property ${key} expected type of ${type} but got ${getType(val)}`);
    }
  }

  validate.type = type;

  return validate;
}

export function chain(...fns: Array<Function>): Function {
  return function (...args) {
    for (let fn of fns) {
      fn(...args);
    }
  };
}

export default function defineType(
  type: string,
  opts: {
    fields?: Object;
    visitor?: Array<string>;
    aliases?: Array<string>;
    builder?: Array<string>;
    inherits?: string;
  } = {},
) {
  let inherits = (opts.inherits && store[opts.inherits]) || {};

  opts.fields  = opts.fields || inherits.fields || {};
  opts.visitor = opts.visitor || inherits.visitor || [];
  opts.aliases = opts.aliases || inherits.aliases || [];
  opts.builder = opts.builder || inherits.builder || opts.visitor || [];

  // ensure all field keys are represented in `fields`
  for (let key of (opts.visitor.concat(opts.builder): Array<string>)) {
    opts.fields[key] = opts.fields[key] || {};
  }

  for (let key in opts.fields) {
    let field = opts.fields[key];

    if (field.default === undefined) {
      field.default = null;
    } else if (!field.validate) {
      field.validate = assertValueType(getType(field.default));
    }
  }

  VISITOR_KEYS[type] = opts.visitor;
  BUILDER_KEYS[type] = opts.builder;
  NODE_FIELDS[type]  = opts.fields;
  ALIAS_KEYS[type]   = opts.aliases;

  store[type] = opts;
}

let store = {};
