var JSONY = function () {};

JSONY.prototype.parse = (function () {
  var states = {
    BEFORE: 'before',
    AFTER: 'after',
    INSIDE: 'inside',
    KEY: 'key',
    VALUE: 'value',
    AFTER_VALUE: 'after-value',
    STRING_TYPE: 'string-type',
    NUMBER_TYPE: 'number-type'
  };
  
  var contexts = {
    OBJECT_TYPE: 'object-type',
    ARRAY_TYPE: 'array-type'
  };
  
  // some utils
  var isNumeric = (char) => char.match(/^\d+$/);
  var isAlpha = (char) => char.match(/^[a-zA-Z]$/);
  var isSpace = (char) => char.match(/^\s$/);
  var isDelimiter = (char) => char === ',';
  var isQuote = (char) => char === '"';
    
  /**
   * Main function
   * @param {string} str
   * @param {number} start
   * @param {number} depth
   */
  var parse = (str, start, depth) => {
    var
      state,
      // an object or an array
      context = contexts.OBJECT_TYPE,
      result = {},
      currentKey = '',
      currentValue = '',
      currentArray = [];
      
    var applyKeyValue = () => {
      if (isArrayContext()) result.push(currentValue);
      else result[currentKey] = currentValue;
      currentKey = '';
      currentValue = '';
    };
    
    var setState = (newState) =>  state = newState;
    var switchState = (newState) => { state = newState; index-- };
    
    setState(states.BEFORE);
    
    // some other utils
    var buildResult    = ()     => { return { endIndex: index, result: result }};
    var isArrayContext = ()     => context === contexts.ARRAY_TYPE;
    var isEnding       = (char) => isArrayContext() && char === ']' || char === '}';
    var isLiteralStart = (char) => char === '[' || char === '{';
    var isStart = (char) => {
      if (char === '{') return true;
      
      if (depth && char === '[') {
        context = contexts.ARRAY_TYPE;
        result = [];
        return true;
      }
      return false;
    };
    
    var raise = (char, index) => {
      throw new SyntaxError('Unexpected token: ' + char + ' in position ' + index);
    };
    
    //
    // handlers for each state
    //
    
    var handleStateBefore = (char) => {
      // spaces outside the string literals does not affect the state
      if (isSpace(char)) return;
      else if (!isStart(char)) raise(char, index);
      else setState(states.INSIDE);
    };
    
    var handleStateInside = (char) => {
      if (isSpace(char)) return;
      // end of the object or array literal
      else if (isEnding(char)) setState(states.AFTER);
      else if (isArrayContext()) {
        switchState(states.VALUE);
      } else {
        // expecting alpha character
        if (!isAlpha(char)) raise(char, index);
        else {
          setState(states.KEY);
          currentKey += char;
        }
      }
      
    };
    
    var handleStateKey = (char) => {
      if (isSpace(char)) return;
      else if (char === ':') setState(states.VALUE);
      else if (!isAlpha(char) && !isNumeric(char)) raise(char, index);
      else currentKey += char;
    };
    
    var handleStateValue = (char) => {
      if (isSpace(char)) return;
      // expecting {, [, ", or alphanumeric
      else if (isNumeric(char)) {
        currentValue = parseInt(char);
        setState(states.NUMBER_TYPE);
      } else if (isQuote(char)) {
        currentValue = '';
        setState(states.STRING_TYPE);
      } else if (isLiteralStart(char)) {
        // objects & arrays are handled the same way
        parseResult = parse(str, index, depth + 1);
        currentValue = parseResult.result;
        index = parseResult.endIndex - 1;
        applyKeyValue();
        setState(states.AFTER_VALUE);
      } else raise(char, index);
    };
    
    var handleStateNumberType = (char) => {
      // space means that is the end of the number
      if (isSpace(char)) {
        setState(states.AFTER_VALUE);
        applyKeyValue();
      } else if (isDelimiter(char)) {
        if (isArrayContext()) switchState(states.AFTER_VALUE);
        else setState(states.INSIDE);
        applyKeyValue();
      } else if (isEnding(char)) {
        setState(states.AFTER);
        applyKeyValue();
      } else if (isNumeric(char)) {
        currentValue = currentValue * 10 + parseInt(char);
      } else raise(char, index);
    };
    
    var handleStateStringType = (char, prev) => {
      if (isQuote(char) && prev !== '\\') {
        setState(states.AFTER_VALUE);
        applyKeyValue();
      } else if (char !== '\\') {
        currentValue = currentValue + char;
      }
    };
    
    var handleStateAfterValue = (char) => {
      if (isSpace(char)) return;
      else if (isEnding(char)) setState(states.AFTER);
      else if (isDelimiter(char)) setState(states.INSIDE);
      else raise(char, index);
    };
    
    // plain state machine
    for (var index = start; index < str.length; index++) {
      let char = str[index];
      let prev = str[index - 1];
      
      switch (state) {
        case states.BEFORE:      handleStateBefore(char);            break;
        case states.INSIDE:      handleStateInside(char);            break;
        case states.KEY:         handleStateKey(char);               break;
        case states.VALUE:       handleStateValue(char);             break;
        case states.NUMBER_TYPE: handleStateNumberType(char);        break;
        case states.STRING_TYPE: handleStateStringType(char, prev);  break;
        case states.AFTER_VALUE: handleStateAfterValue(char);        break;
        case states.AFTER:       return buildResult();
      }
    }
    
    if (state !== states.AFTER) throw new SyntaxError('Unexpected end of input');
    
    return buildResult();
  };
    
  return (str) => parse(str, 0, 0).result;
}());


JSONY.prototype.stringify = function stringify (obj) {
  var result = '{';
  
  var handleValue = (value) => {
    if (typeof value === 'object' && !value.forEach) {
      result += stringify(value);
    } else if (value.forEach) {
      result += '[';
      value.forEach((item) => {
        handleValue(item);
        result += ', '
      });
      result = result.slice(0, -2);
      result += ']';
    } else if (typeof value === 'string'){
      result += '"' + value + '"';
    } else {
      // number
      result += value;
    }
  };
  
  for (let key in obj) {
    if (!obj.hasOwnProperty(key)) continue;
    let value = obj[key];
    result += key + ': ';
    // object but not an array
    handleValue(value);
    result += ', ';
  }
  // remove last comma
  result = result.slice(0, -2);
  result += '}';
  
  return result;
};

var jsony = new JSONY();


var runTests = () => {
  var obj;
  var expect = (actualValue, correctValue) => {
    if (actualValue !== correctValue) {
      throw new Error('Error: ' + correctValue + 'expected, got ' + actualValue);
    }
  };
  
  obj = jsony.parse('{ hello: \"world\" }');
  expect(obj.hello, 'world');
  
  obj = jsony.parse('{ age: 10, height: 120 }');
  expect(obj.age, 10);
  expect(obj.height, 120);
  
  obj = jsony.parse('{ people: [\"Ann\", \"Alice\", \"Bob\"] }');
  expect(obj.people.length, 3);
  expect(obj.people[1], 'Alice');
  
  obj = jsony.parse('{ config: { servers: [10, 15, 13], ngnix: { gate: 0, ip: \"127.0.0.1\" } } }');
  expect(typeof obj.config, 'object');
  expect(obj.config.ngnix.gate, 0);
  expect(obj.config.ngnix.ip, '127.0.0.1');
  
  obj = jsony.parse('{afasd: [1, 2, { a: 1}], asddsss: 434, hh: { a: [1, "123", [1, 2, 4]]}}');
  expect(obj.afasd.length, 3);
  expect(obj.afasd[2].a, 1);
  expect(obj.hh.a[2][2], 4);
  
  // invalid objects
  try {
    obj = jsony.parse('{a : }');
    // should not execute
    expect(true, false);
  } catch (e) {
    expect(e.constructor, SyntaxError);
  }
  
  try {
    obj = jsony.parse('{a : 1');
    // should not execute
    expect(true, false);
  } catch (e) {
    expect(e.message, 'Unexpected end of input');
  }
  
  // stringify
  var str = '{afasd: [1, 2, {a: 1}], asddsss: 434, hh: {a: [1, "123", [1, 2, 4]]}}';
  var newStr = jsony.stringify(jsony.parse(str));
  expect(str, newStr);
  
  console.log('Passed');
};

runTests();