module.exports = (function() {

  /**
   * Logic for translating a given Objective-C "type" encoding into a node-ffi
   * type.
   *
   * ### References:
   *
   *   * [Apple "Type Encoding" Docs](http://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html)
   *   * [node-ffi "Type List"](https://github.com/rbranson/node-ffi/wiki/Node-FFI-Tutorial#wiki-type-list)
   */


  /**
   * Module dependencies.
   */

  /**
   * Internal module that parses *"struct type encodings"* and turns them into
   * proper node-ffi `Struct` instances.
   */

  /*!
   * Module dependencies.
   */
  var Struct = require('ref-struct')
    , assert = require('assert')
    , test = /^\{.*?\}$/
    , structCache = {}
    , knownStructs = {}

  /**
   * Tests if the given arg is a `Struct` constructor or a string type encoding
   * describing a struct (then true), otherwise false.
   *
   * @api private
   */

  function isStruct (type) {
    return !!type.__isStructType__ || test.test(type)
  }

  /**
   * Returns the struct constructor function for the given struct name or type.
   *
   *     {CGPoint="x"d"y"d}
   *
   * @api private
   */

  function getStruct (type) {
    // First check if a regular name was passed in
    var rtn = structCache[type];

    if (rtn) return rtn;

    // If the struct type name has already been created, return that one
    var name = parseStructName(type);

    rtn = structCache[name];
    if (rtn) return rtn;

    if(knownStructs[name]) type = knownStructs[name];
    // Next parse the type structure
    var parsed = parseStruct(type);
    // Otherwise we need to create a new Struct constructor
    var props = [];
    parsed.props.forEach(function (prop) {
      props.push([ map(prop[1]), prop[0] ])
    });
    var z;
    try { z = Struct(props); } catch(e) { }
    return structCache[parsed.name] = z;
  }

  /**
   * Extracts only the name of the given struct type encoding string.
   *
   * @api private
   */

  function parseStructName (struct) {
    var s = struct.substring(1, struct.length-1)
      , equalIndex = s.indexOf('=')
    if (~equalIndex)
      s = s.substring(0, equalIndex)
    return s
  }

  /**
   * Parses a struct type string into an Object with a `name` String and
   * a `props` Array (entries are a type string, or another parsed struct object)
   *
   * @api private
   */

  function parseStruct (struct) {
    var s = struct.substring(1, struct.length-1)
      , equalIndex = s.indexOf('=')
      , rtn = {
          name: s.substring(0, equalIndex)
        , props: []
      }
    s = s.substring(equalIndex+1);
    
    var curProp = [], numBrackets = 0, entries = [];

    for (var i=0; i < s.length; i++) {
      var cur = s[i];
      switch (cur) {
        case '"':
          if (numBrackets > 0)
            curProp.push(cur);
          else {  
            entries.push(curProp.join(''));
            curProp = [];
            numBrackets = 0;
          }
          break;
        case '{':
        case '[':
        case '(':
          numBrackets++;
          curProp.push(cur);
          break;
        case '}':
        case ']':
        case ')':
          numBrackets--;
          curProp.push(cur);
          break;
        default:
          curProp.push(cur);
          break;
      }
    }
    entries.push(curProp.join(''));
    curProp = [];
    numBrackets = 0;

    for (var i=1; i < entries.length; i+=2) {
      rtn.props.push([entries[i], entries[i+1]]);
    }
    return rtn
  }


  /**
   * A map of Objective-C type encodings to node-ffi types.
   *
   * @api private
   */

  var typeEncodings = {
      'c': 'char'
    , 'i': 'int32'
    , 's': 'short'
    , 'l': 'long'
    , 'q': 'longlong'
    , 'C': 'uchar'
    , 'I': 'uint32'
    , 'S': 'ushort'
    , 'L': 'ulong'
    , 'Q': 'ulonglong'
    , 'f': 'float'
    , 'd': 'double'
    , 'B': 'int8'
    , 'v': 'void'
    , '*': 'string'   // String
    , '@': 'pointer'  // id
    , '#': 'pointer'  // Class
    , ':': 'pointer'  // SEL
    , '?': 'pointer'  // Unknown, used for function pointers
  }

  var DELIMS = Object.keys(typeEncodings)

  /**
   * A map of the additional type info for some ObjC methods.
   *
   * @api private
   */

  var methodEncodings = {
      'r': 'const'
    , 'n': 'in'
    , 'N': 'inout'
    , 'o': 'out'
    , 'O': 'bycopy'
    , 'R': 'byref'
    , 'V': 'oneway'
  }

  /**
   * Used to remove and method encodings present on the type.
   * NodObjC does not use them...
   */

  var methodEncodingsTest = new RegExp('^(' + Object.keys(methodEncodings).join('|') + ')')

  /**
   * Maps a single Obj-C 'type' into a valid node-ffi type.
   * This mapping logic is kind of a mess...
   */

  function map (type) {
    if (!type) throw new Error('got falsey "type" to map ('+type+'). this should NOT happen!');
    if (type.type) type = type.type;
    if (isStruct(type)) return getStruct(type);
    type = type.replace(methodEncodingsTest, '')
    // if the first letter is a ^ then it's a "pointer" type
    if (type[0] === '^') return 'pointer'
    // now we can try matching from the typeEncodings map
    var rtn = typeEncodings[type]
    if (rtn) return rtn
    // last shot... try the last char? this may be a bad idea...
    rtn = typeEncodings[type[type.length-1]]
    if (rtn) return rtn
    // couldn't find the type. throw a descriptive error as to why:
    if (type[0] == '[') return 'pointer';
      //throw new Error('Array types not yet supported: ' + type)
    if (type[0] == '(') return 'pointer';
      //throw new Error('Union types not yet supported: ' + type)
    if (type[0] == 'b') return 'pointer';
      //throw new Error('Bit field types not yet supported: ' + type)
    throw new Error('Could not convert type: ' + type)
  }

  /**
   * Accepts an Array of ObjC return type and argument types (i.e. the result of
   * parse() below), and returns a new Array with the values mapped to valid ffi
   * types.
   */

  function mapArray (types) {
    return types.map(function (type) {
      return Array.isArray(type) ? mapArray(type) : map(type)
    })
  }

  /**
   * This normalizes or finds objects (id's) and for the type returns back an
   * objective-c type of '@' or '#'
   */
   function normalizeObjects(types) {
    var ret = types[0].type ? types[0].type : types[0];
    var args = types[1].map(function(item) { return item.type ? item.type : item; });
    return [ret, args];
   }

  /**
   * Parses a "types string" (i.e. `'v@:'`) and returns a "types Array", where the
   * return type is the first array value, and an Array of argument types is the
   * array second value.
   */

  function parse (types) {
    if (typeof types === 'string') {
      var rtn = []
        , cur = []
        , len = types.length
        , depth = 0
      for (var i=0; i<len; i++) {
        var c = types[i]

        if (depth || !/(\d)/.test(c)) {
          cur.push(c)
        }

        if (c == '{' || c == '[' || c == '(') {
          depth++
        } else if (c == '}' || c == ']' || c == ')') {
          depth--
          if (!depth)
            add()
        } else if (~DELIMS.indexOf(c) && !depth) {
          add()
        }
      }
      function add () {
        rtn.push(cur.join(''))
        cur = []
        depth = 0
      }
      assert.equal(rtn[1], '@', '_self argument expected as first arg: ' + types)
      assert.equal(rtn[2], ':', 'SEL argument expected as second arg: ' + types)
      return [ rtn[0], rtn.slice(1) ]
    } else if (Array.isArray(types)) {
      return normalizeObjects(types);
    } else { 
      var args = types.args
      assert.equal(args[0], '@', '_self argument expected as first arg: ' + types)
      assert.equal(args[1], ':', 'SEL argument expected as second arg: ' + types)
      return [ types.retval, args ]
    }
  }

  /**
   * @private
   * This registers the internal type maps for objc->ffi on an object
   * that's passed in.
   */
  function registerTypes(onto) {
    for(var i=0; i < DELIMS.length; i++) {
      if(DELIMS[i] != 'pointer') {
        onto[typeEncodings[DELIMS[i]]] = DELIMS[i];
      }
    }
  }

  /**
   * Module exports.
   */

  var ex = {};
  ex.registerTypes = registerTypes;
  ex.map = map;
  ex.mapArray = mapArray;
  ex.parse = parse;
  ex.typeEncodings = typeEncodings;
  ex.methodEncodings = methodEncodings;
  ex.isStruct = isStruct;
  ex.getStruct = getStruct;
  ex.parseStructName = parseStructName;
  ex.parseStruct = parseStruct;
  ex.knownStructs = knownStructs;
  return ex;
})();