const STATE_IN_NUN_VAR = 'nunvar';
const STATE_IN_NUN_TAG = 'nuntag';
const STATE_IN_NUN_COM = 'nuncom';
const STATE_IN_STRING = 'string';
const STATE_NONE = 'none';

const NUNJUCKS_OPEN_STATES = {
  '{{': STATE_IN_NUN_VAR,
  '{%': STATE_IN_NUN_TAG,
  '{#': STATE_IN_NUN_COM,
};

const NUNJUCKS_CLOSE_STATES = {
  '}}': STATE_IN_NUN_VAR,
  '%}': STATE_IN_NUN_TAG,
  '#}': STATE_IN_NUN_COM,
};

/**
 * Format a JSON string without parsing it as JavaScript.
 *
 * Code taken from jsonlint (http://zaa.ch/jsonlint/)
 *
 * @param json
 * @param indentChars
 * @param replaceUnicode
 * @returns {string}
 */
module.exports.prettify = function(json, indentChars = '\t', replaceUnicode = true) {
  if (!json) {
    return '';
  }

  // Convert the unicode. To correctly mimic JSON.stringify(JSON.parse(json), null, indentChars)
  // we need to convert all escaped unicode characters to proper unicode characters.
  if (replaceUnicode) {
    try {
      json = _convertUnicode(json);
    } catch (err) {
      // Just in case (should never happen)
      console.warn('Prettify failed to handle unicode', err);
    }
  }

  let i = 0;
  let il = json.length;
  let tab = indentChars;
  let newJson = '';
  let indentLevel = 0;
  let currentChar = null;
  let nextChar = null;
  let nextTwo = null;
  let state = STATE_NONE;

  for (; i < il; i += 1) {
    currentChar = json.charAt(i);
    nextChar = json.charAt(i + 1) || '';
    nextTwo = currentChar + nextChar;

    if (state === STATE_IN_STRING) {
      if (currentChar === '"') {
        state = STATE_NONE;
        newJson += currentChar;
        continue;
      } else if (currentChar === '\\') {
        newJson += currentChar + nextChar;
        i++;
        continue;
      } else {
        newJson += currentChar;
        continue;
      }
    }

    // Close Nunjucks states
    if (Object.values(NUNJUCKS_CLOSE_STATES).includes(state)) {
      const closeState = NUNJUCKS_CLOSE_STATES[nextTwo];
      if (closeState) {
        state = STATE_NONE;
        if (closeState === STATE_IN_NUN_COM) {
          // Put comments on their own lines
          newJson += nextTwo + '\n' + _repeatString(tab, indentLevel);
        } else {
          newJson += nextTwo;
        }
        i++;
        continue;
      } else {
        newJson += currentChar;
        continue;
      }
    }

    // ~~~~~~~~~~~~~~~~~~~~~~ //
    // Handle "nothing" State //
    // ~~~~~~~~~~~~~~~~~~~~~~ //

    // Open Nunjucks states
    const nextState = NUNJUCKS_OPEN_STATES[nextTwo];
    if (nextState) {
      state = nextState;
      newJson += nextTwo;
      i++;
      continue;
    }

    switch (currentChar) {
      case ',':
        newJson += currentChar + '\n' + _repeatString(tab, indentLevel);
        continue;
      case '{':
        if (nextChar === '}') {
          newJson += currentChar + nextChar;
          i++;
        } else {
          indentLevel++;
          newJson += currentChar + '\n' + _repeatString(tab, indentLevel);
        }
        continue;
      case '[':
        if (nextChar === ']') {
          newJson += currentChar + nextChar;
          i++;
        } else {
          indentLevel++;
          newJson += currentChar + '\n' + _repeatString(tab, indentLevel);
        }
        continue;
      case '}':
        indentLevel--;
        newJson += '\n' + _repeatString(tab, indentLevel) + currentChar;
        continue;
      case ']':
        indentLevel--;
        newJson += '\n' + _repeatString(tab, indentLevel) + currentChar;
        continue;
      case ':':
        newJson += ': ';
        continue;
      case '"':
        state = STATE_IN_STRING;
        newJson += currentChar;
        continue;
      case ' ':
      case '\n':
      case '\t':
      case '\r':
        // Don't add whitespace
        continue;
      default:
        newJson += currentChar;
        continue;
    }
  }

  // Remove lines that only contain whitespace
  return newJson.replace(/^\s*\n/gm, '');
};

function _repeatString(s, count) {
  return new Array(count + 1).join(s);
}

/**
 * Convert escaped unicode characters to real characters. Any JSON parser will do this by
 * default. This is really fast too. Around 25ms for ~2MB of data with LOTS of unicode.
 *
 * @param originalStr
 * @returns {string}
 * @private
 */
function _convertUnicode(originalStr) {
  let m;
  let c;
  let cStr;
  let lastI = 0;

  // Matches \u#### but not \\u####
  const unicodeRegex = /\\u[0-9a-fA-F]{4}/g;

  let convertedStr = '';
  while ((m = unicodeRegex.exec(originalStr))) {
    // Don't convert if the backslash itself is escaped
    if (originalStr[m.index - 1] === '\\') {
      continue;
    }

    try {
      cStr = m[0].slice(2); // Trim off start
      c = String.fromCharCode(parseInt(cStr, 16));
      if (c === '"') {
        // Escape it if it's double quotes
        c = `\\${c}`;
      }

      // + 1 to account for first matched (non-backslash) character
      convertedStr += originalStr.slice(lastI, m.index) + c;
      lastI = m.index + m[0].length;
    } catch (err) {
      // Some reason we couldn't convert a char. Should never actually happen
      console.warn('Failed to convert unicode char', m[0], err);
    }
  }

  // Finally, add the rest of the string to the end.
  convertedStr += originalStr.slice(lastI, originalStr.length);

  return convertedStr;
}
