'use strict';


var common         = require('./common');
var YAMLException  = require('./exception');
var Mark           = require('./mark');
var NIL            = common.NIL;
var SAFE_SCHEMA    = require('./schema/safe');
var DEFAULT_SCHEMA = require('./schema/default');


var _hasOwnProperty = Object.prototype.hasOwnProperty;


var KIND_STRING = 'string';
var KIND_ARRAY  = 'array';
var KIND_OBJECT = 'object';


var CONTEXT_FLOW_IN   = 1;
var CONTEXT_FLOW_OUT  = 2;
var CONTEXT_BLOCK_IN  = 3;
var CONTEXT_BLOCK_OUT = 4;


var CHOMPING_CLIP  = 1;
var CHOMPING_STRIP = 2;
var CHOMPING_KEEP  = 3;


var CHAR_TAB                  = 0x09;   /* Tab */
var CHAR_LINE_FEED            = 0x0A;   /* LF */
var CHAR_CARRIAGE_RETURN      = 0x0D;   /* CR */
var CHAR_SPACE                = 0x20;   /* Space */
var CHAR_EXCLAMATION          = 0x21;   /* ! */
var CHAR_DOUBLE_QUOTE         = 0x22;   /* " */
var CHAR_SHARP                = 0x23;   /* # */
var CHAR_PERCENT              = 0x25;   /* % */
var CHAR_AMPERSAND            = 0x26;   /* & */
var CHAR_SINGLE_QUOTE         = 0x27;   /* ' */
var CHAR_ASTERISK             = 0x2A;   /* * */
var CHAR_PLUS                 = 0x2B;   /* + */
var CHAR_COMMA                = 0x2C;   /* , */
var CHAR_MINUS                = 0x2D;   /* - */
var CHAR_DOT                  = 0x2E;   /* . */
var CHAR_SLASH                = 0x2F;   /* / */
var CHAR_DIGIT_ZERO           = 0x30;   /* 0 */
var CHAR_DIGIT_ONE            = 0x31;   /* 1 */
var CHAR_DIGIT_NINE           = 0x39;   /* 9 */
var CHAR_COLON                = 0x3A;   /* : */
var CHAR_LESS_THAN            = 0x3C;   /* < */
var CHAR_GREATER_THAN         = 0x3E;   /* > */
var CHAR_QUESTION             = 0x3F;   /* ? */
var CHAR_COMMERCIAL_AT        = 0x40;   /* @ */
var CHAR_CAPITAL_A            = 0x41;   /* A */
var CHAR_CAPITAL_F            = 0x46;   /* F */
var CHAR_CAPITAL_L            = 0x4C;   /* L */
var CHAR_CAPITAL_N            = 0x4E;   /* N */
var CHAR_CAPITAL_P            = 0x50;   /* P */
var CHAR_CAPITAL_U            = 0x55;   /* U */
var CHAR_LEFT_SQUARE_BRACKET  = 0x5B;   /* [ */
var CHAR_BACKSLASH            = 0x5C;   /* \ */
var CHAR_RIGHT_SQUARE_BRACKET = 0x5D;   /* ] */
var CHAR_UNDERSCORE           = 0x5F;   /* _ */
var CHAR_GRAVE_ACCENT         = 0x60;   /* ` */
var CHAR_SMALL_A              = 0x61;   /* a */
var CHAR_SMALL_B              = 0x62;   /* b */
var CHAR_SMALL_E              = 0x65;   /* e */
var CHAR_SMALL_F              = 0x66;   /* f */
var CHAR_SMALL_N              = 0x6E;   /* n */
var CHAR_SMALL_R              = 0x72;   /* r */
var CHAR_SMALL_T              = 0x74;   /* t */
var CHAR_SMALL_U              = 0x75;   /* u */
var CHAR_SMALL_V              = 0x76;   /* v */
var CHAR_SMALL_X              = 0x78;   /* x */
var CHAR_LEFT_CURLY_BRACKET   = 0x7B;   /* { */
var CHAR_VERTICAL_LINE        = 0x7C;   /* | */
var CHAR_RIGHT_CURLY_BRACKET  = 0x7D;   /* } */


var SIMPLE_ESCAPE_SEQUENCES = {};

SIMPLE_ESCAPE_SEQUENCES[CHAR_DIGIT_ZERO]   = '\x00';
SIMPLE_ESCAPE_SEQUENCES[CHAR_SMALL_A]      = '\x07';
SIMPLE_ESCAPE_SEQUENCES[CHAR_SMALL_B]      = '\x08';
SIMPLE_ESCAPE_SEQUENCES[CHAR_SMALL_T]      = '\x09';
SIMPLE_ESCAPE_SEQUENCES[CHAR_TAB]          = '\x09';
SIMPLE_ESCAPE_SEQUENCES[CHAR_SMALL_N]      = '\x0A';
SIMPLE_ESCAPE_SEQUENCES[CHAR_SMALL_V]      = '\x0B';
SIMPLE_ESCAPE_SEQUENCES[CHAR_SMALL_F]      = '\x0C';
SIMPLE_ESCAPE_SEQUENCES[CHAR_SMALL_R]      = '\x0D';
SIMPLE_ESCAPE_SEQUENCES[CHAR_SMALL_E]      = '\x1B';
SIMPLE_ESCAPE_SEQUENCES[CHAR_SPACE]        = ' ';
SIMPLE_ESCAPE_SEQUENCES[CHAR_DOUBLE_QUOTE] = '\x22';
SIMPLE_ESCAPE_SEQUENCES[CHAR_SLASH]        = '/';
SIMPLE_ESCAPE_SEQUENCES[CHAR_BACKSLASH]    = '\x5C';
SIMPLE_ESCAPE_SEQUENCES[CHAR_CAPITAL_N]    = '\x85';
SIMPLE_ESCAPE_SEQUENCES[CHAR_UNDERSCORE]   = '\xA0';
SIMPLE_ESCAPE_SEQUENCES[CHAR_CAPITAL_L]    = '\u2028';
SIMPLE_ESCAPE_SEQUENCES[CHAR_CAPITAL_P]    = '\u2029';


var HEXADECIMAL_ESCAPE_SEQUENCES = {};

HEXADECIMAL_ESCAPE_SEQUENCES[CHAR_SMALL_X]   = 2;
HEXADECIMAL_ESCAPE_SEQUENCES[CHAR_SMALL_U]   = 4;
HEXADECIMAL_ESCAPE_SEQUENCES[CHAR_CAPITAL_U] = 8;


var PATTERN_NON_PRINTABLE         = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uD800-\uDFFF\uFFFE\uFFFF]/;
var PATTERN_NON_ASCII_LINE_BREAKS = /[\x85\u2028\u2029]/;
var PATTERN_FLOW_INDICATORS       = /[,\[\]\{\}]/;
var PATTERN_TAG_HANDLE            = /^(?:!|!!|![a-z\-]+!)$/i;
var PATTERN_TAG_URI               = /^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i;


function loadAll(input, output, options) {
  options = options || {};

  var filename = options['filename'] || null,
      schema   = options['schema']   || DEFAULT_SCHEMA,
      resolve  = options['resolve']  || true,
      validate = options['validate'] || true,
      strict   = options['strict']   || false,
      legacy   = options['legacy']   || false,

      directiveHandlers = {},
      implicitTypes     = schema.compiledImplicit,
      typeMap           = schema.compiledTypeMap,

      length     = input.length,
      position   = 0,
      line       = 0,
      lineStart  = 0,
      lineIndent = 0,
      character  = input.charCodeAt(position),

      version,
      checkLineBreaks,
      tagMap,
      anchorMap,
      tag,
      anchor,
      kind,
      result;

  function generateError(message) {
    return new YAMLException(
      message,
      new Mark(filename, input, position, line, (position - lineStart)));
  }

  function throwError(message) {
    throw generateError(message);
  }

  function throwWarning(message) {
    var error = generateError(message);

    if (strict) {
      throw error;
    } else {
      console.warn(error.toString());
    }
  }

  directiveHandlers['YAML'] = function handleYamlDirective(name, args) {
    var match, major, minor;

    if (null !== version) {
      throwError('duplication of %YAML directive');
    }

    if (1 !== args.length) {
      throwError('YAML directive accepts exactly one argument');
    }

    match = /^([0-9]+)\.([0-9]+)$/.exec(args[0]);

    if (null === match) {
      throwError('ill-formed argument of the YAML directive');
    }

    major = parseInt(match[1], 10);
    minor = parseInt(match[2], 10);

    if (1 !== major) {
      throwError('unacceptable YAML version of the document');
    }

    version = args[0];
    checkLineBreaks = (minor < 2);

    if (1 !== minor && 2 !== minor) {
      throwWarning('unsupported YAML version of the document');
    }
  };

  directiveHandlers['TAG'] = function handleTagDirective(name, args) {
    var handle, prefix;

    if (2 !== args.length) {
      throwError('TAG directive accepts exactly two arguments');
    }

    handle = args[0];
    prefix = args[1];

    if (!PATTERN_TAG_HANDLE.test(handle)) {
      throwError('ill-formed tag handle (first argument) of the TAG directive');
    }

    if (_hasOwnProperty.call(tagMap, handle)) {
      throwError('there is a previously declared suffix for "' + handle + '" tag handle');
    }

    if (!PATTERN_TAG_URI.test(prefix)) {
      throwError('ill-formed tag prefix (second argument) of the TAG directive');
    }

    tagMap[handle] = prefix;
  };

  function captureSegment(start, end, checkJson) {
    var _position, _length, _character, _result;

    if (start < end) {
      _result = input.slice(start, end);

      if (checkJson && validate) {
        for (_position = 0, _length = _result.length;
             _position < _length;
             _position += 1) {
          _character = _result.charCodeAt(_position);
          if (!(0x09 === _character ||
                0x20 <= _character && _character <= 0x10FFFF)) {
            throwError('expected valid JSON character');
          }
        }
      }

      result += _result;
    }
  }

  function mergeMappings(destination, source) {
    var sourceKeys, key, index, quantity;

    if (!common.isObject(source)) {
      throwError('cannot merge mappings; the provided source object is unacceptable');
    }

    sourceKeys = Object.keys(source);

    for (index = 0, quantity = sourceKeys.length; index < quantity; index += 1) {
      key = sourceKeys[index];

      if (!_hasOwnProperty.call(destination, key)) {
        destination[key] = source[key];
      }
    }
  }

  function storeMappingPair(_result, keyTag, keyNode, valueNode) {
    var index, quantity;

    keyNode = String(keyNode);

    if (null === _result) {
      _result = {};
    }

    if ('tag:yaml.org,2002:merge' === keyTag) {
      if (Array.isArray(valueNode)) {
        for (index = 0, quantity = valueNode.length; index < quantity; index += 1) {
          mergeMappings(_result, valueNode[index]);
        }
      } else {
        mergeMappings(_result, valueNode);
      }
    } else {
      _result[keyNode] = valueNode;
    }

    return _result;
  }

  function readLineBreak() {
    if (CHAR_LINE_FEED === character) {
      position += 1;
    } else if (CHAR_CARRIAGE_RETURN === character) {
      if (CHAR_LINE_FEED === input.charCodeAt(position + 1)) {
        position += 2;
      } else {
        position += 1;
      }
    } else {
      throwError('a line break is expected');
    }

    line += 1;
    lineStart = position;
    character = input.charCodeAt(position);
  }

  function skipSeparationSpace(allowComments, checkIndent) {
    var lineBreaks = 0;

    while (position < length) {
      while (CHAR_SPACE === character || CHAR_TAB === character) {
        character = input.charCodeAt(++position);
      }

      if (allowComments && CHAR_SHARP === character) {
        do { character = input.charCodeAt(++position); }
        while (position < length &&
               CHAR_LINE_FEED !== character &&
               CHAR_CARRIAGE_RETURN !== character);
      }

      if (CHAR_LINE_FEED === character || CHAR_CARRIAGE_RETURN === character) {
        readLineBreak();
        lineBreaks += 1;
        lineIndent = 0;

        while (CHAR_SPACE === character) {
          lineIndent += 1;
          character = input.charCodeAt(++position);
        }

        if (lineIndent < checkIndent) {
          throwWarning('deficient indentation');
        }
      } else {
        break;
      }
    }

    return lineBreaks;
  }

  function testDocumentSeparator() {
    var _position, _character;

    if (position === lineStart &&
        (CHAR_MINUS === character || CHAR_DOT === character) &&
        input.charCodeAt(position + 1) === character &&
        input.charCodeAt(position + 2) === character) {

      _position = position + 3;
      _character = input.charCodeAt(_position);

      if (_position >= length ||
          CHAR_SPACE           === _character ||
          CHAR_TAB             === _character ||
          CHAR_LINE_FEED       === _character ||
          CHAR_CARRIAGE_RETURN === _character) {
        return true;
      }
    }

    return false;
  }

  function writeFoldedLines(count) {
    if (1 === count) {
      result += ' ';
    } else if (count > 1) {
      result += common.repeat('\n', count - 1);
    }
  }

  function readPlainScalar(nodeIndent, withinFlowCollection) {
    var preceding,
        following,
        captureStart,
        captureEnd,
        hasPendingContent,
        _line,
        _lineStart,
        _lineIndent,
        _kind = kind,
        _result = result;

    if (CHAR_SPACE                === character ||
        CHAR_TAB                  === character ||
        CHAR_LINE_FEED            === character ||
        CHAR_CARRIAGE_RETURN      === character ||
        CHAR_COMMA                === character ||
        CHAR_LEFT_SQUARE_BRACKET  === character ||
        CHAR_RIGHT_SQUARE_BRACKET === character ||
        CHAR_LEFT_CURLY_BRACKET   === character ||
        CHAR_RIGHT_CURLY_BRACKET  === character ||
        CHAR_SHARP                === character ||
        CHAR_AMPERSAND            === character ||
        CHAR_ASTERISK             === character ||
        CHAR_EXCLAMATION          === character ||
        CHAR_VERTICAL_LINE        === character ||
        CHAR_GREATER_THAN         === character ||
        CHAR_SINGLE_QUOTE         === character ||
        CHAR_DOUBLE_QUOTE         === character ||
        CHAR_PERCENT              === character ||
        CHAR_COMMERCIAL_AT        === character ||
        CHAR_GRAVE_ACCENT         === character) {
      return false;
    }

    if (CHAR_QUESTION === character ||
        CHAR_MINUS === character) {
      following = input.charCodeAt(position + 1);

      if (CHAR_SPACE                 === following ||
          CHAR_TAB                   === following ||
          CHAR_LINE_FEED             === following ||
          CHAR_CARRIAGE_RETURN       === following ||
          withinFlowCollection &&
          (CHAR_COMMA                === following ||
           CHAR_LEFT_SQUARE_BRACKET  === following ||
           CHAR_RIGHT_SQUARE_BRACKET === following ||
           CHAR_LEFT_CURLY_BRACKET   === following ||
           CHAR_RIGHT_CURLY_BRACKET  === following)) {
        return false;
      }
    }

    kind = KIND_STRING;
    result = '';
    captureStart = captureEnd = position;
    hasPendingContent = false;

    while (position < length) {
      if (CHAR_COLON === character) {
        following = input.charCodeAt(position + 1);

        if (CHAR_SPACE                 === following ||
            CHAR_TAB                   === following ||
            CHAR_LINE_FEED             === following ||
            CHAR_CARRIAGE_RETURN       === following ||
            withinFlowCollection &&
            (CHAR_COMMA                === following ||
             CHAR_LEFT_SQUARE_BRACKET  === following ||
             CHAR_RIGHT_SQUARE_BRACKET === following ||
             CHAR_LEFT_CURLY_BRACKET   === following ||
             CHAR_RIGHT_CURLY_BRACKET  === following)) {
          break;
        }

      } else if (CHAR_SHARP === character) {
        preceding = input.charCodeAt(position - 1);

        if (CHAR_SPACE           === preceding ||
            CHAR_TAB             === preceding ||
            CHAR_LINE_FEED       === preceding ||
            CHAR_CARRIAGE_RETURN === preceding) {
          break;
        }

      } else if ((position === lineStart && testDocumentSeparator()) ||
                 withinFlowCollection &&
                 (CHAR_COMMA                === character ||
                  CHAR_LEFT_SQUARE_BRACKET  === character ||
                  CHAR_RIGHT_SQUARE_BRACKET === character ||
                  CHAR_LEFT_CURLY_BRACKET   === character ||
                  CHAR_RIGHT_CURLY_BRACKET  === character)) {
        break;

      } else if (CHAR_LINE_FEED === character ||
                 CHAR_CARRIAGE_RETURN === character) {
        _line = line;
        _lineStart = lineStart;
        _lineIndent = lineIndent;
        skipSeparationSpace(false, -1);

        if (lineIndent >= nodeIndent) {
          hasPendingContent = true;
          continue;
        } else {
          position = captureEnd;
          line = _line;
          lineStart = _lineStart;
          lineIndent = _lineIndent;
          character = input.charCodeAt(position);
          break;
        }
      }

      if (hasPendingContent) {
        captureSegment(captureStart, captureEnd, false);
        writeFoldedLines(line - _line);
        captureStart = captureEnd = position;
        hasPendingContent = false;
      }

      if (CHAR_SPACE !== character && CHAR_TAB !== character) {
        captureEnd = position + 1;
      }

      character = input.charCodeAt(++position);
    }

    captureSegment(captureStart, captureEnd, false);

    if (result) {
      return true;
    } else {
      kind = _kind;
      result = _result;
      return false;
    }
  }

  function readSingleQuotedScalar(nodeIndent) {
    var captureStart, captureEnd;

    if (CHAR_SINGLE_QUOTE !== character) {
      return false;
    }

    kind = KIND_STRING;
    result = '';
    character = input.charCodeAt(++position);
    captureStart = captureEnd = position;

    while (position < length) {
      if (CHAR_SINGLE_QUOTE === character) {
        captureSegment(captureStart, position, true);
        character = input.charCodeAt(++position);

        if (CHAR_SINGLE_QUOTE === character) {
          captureStart = captureEnd = position;
          character = input.charCodeAt(++position);
        } else {
          return true;
        }

      } else if (CHAR_LINE_FEED === character ||
                 CHAR_CARRIAGE_RETURN === character) {
        captureSegment(captureStart, captureEnd, true);
        writeFoldedLines(skipSeparationSpace(false, nodeIndent));
        captureStart = captureEnd = position;
        character = input.charCodeAt(position);

      } else if (position === lineStart && testDocumentSeparator()) {
        throwError('unexpected end of the document within a single quoted scalar');

      } else {
        character = input.charCodeAt(++position);
        captureEnd = position;
      }
    }

    throwError('unexpected end of the stream within a single quoted scalar');
  }

  function readDoubleQuotedScalar(nodeIndent) {
    var captureStart,
        captureEnd,
        hexLength,
        hexIndex,
        hexOffset,
        hexResult;

    if (CHAR_DOUBLE_QUOTE !== character) {
      return false;
    }

    kind = KIND_STRING;
    result = '';
    character = input.charCodeAt(++position);
    captureStart = captureEnd = position;

    while (position < length) {
      if (CHAR_DOUBLE_QUOTE === character) {
        captureSegment(captureStart, position, true);
        character = input.charCodeAt(++position);
        return true;

      } else if (CHAR_BACKSLASH === character) {
        captureSegment(captureStart, position, true);
        character = input.charCodeAt(++position);

        if (CHAR_LINE_FEED       === character ||
            CHAR_CARRIAGE_RETURN === character) {
          skipSeparationSpace(false, nodeIndent);

        } else if (SIMPLE_ESCAPE_SEQUENCES[character]) {
          result += SIMPLE_ESCAPE_SEQUENCES[character];
          character = input.charCodeAt(++position);

        } else if (HEXADECIMAL_ESCAPE_SEQUENCES[character]) {
          hexLength = HEXADECIMAL_ESCAPE_SEQUENCES[character];
          hexResult = 0;

          for (hexIndex = 1; hexIndex <= hexLength; hexIndex += 1) {
            hexOffset = (hexLength - hexIndex) * 4;
            character = input.charCodeAt(++position);

            if (CHAR_DIGIT_ZERO <= character && character <= CHAR_DIGIT_NINE) {
              hexResult |= (character - CHAR_DIGIT_ZERO) << hexOffset;

            } else if (CHAR_CAPITAL_A <= character && character <= CHAR_CAPITAL_F) {
              hexResult |= (character - CHAR_CAPITAL_A + 10) << hexOffset;

            } else if (CHAR_SMALL_A <= character && character <= CHAR_SMALL_F) {
              hexResult |= (character - CHAR_SMALL_A + 10) << hexOffset;

            } else {
              throwError('expected hexadecimal character');
            }
          }

          result += String.fromCharCode(hexResult);
          character = input.charCodeAt(++position);

        } else {
          throwError('unknown escape sequence');
        }

        captureStart = captureEnd = position;

      } else if (CHAR_LINE_FEED === character ||
                 CHAR_CARRIAGE_RETURN === character) {
        captureSegment(captureStart, captureEnd, true);
        writeFoldedLines(skipSeparationSpace(false, nodeIndent));
        captureStart = captureEnd = position;
        character = input.charCodeAt(position);

      } else if (position === lineStart && testDocumentSeparator()) {
        throwError('unexpected end of the document within a double quoted scalar');

      } else {
        character = input.charCodeAt(++position);
        captureEnd = position;
      }
    }

    throwError('unexpected end of the stream within a double quoted scalar');
  }

  function readFlowCollection(nodeIndent) {
    var readNext = true,
        _line,
        _tag     = tag,
        _result,
        following,
        terminator,
        isPair,
        isExplicitPair,
        isMapping,
        keyNode,
        keyTag,
        valueNode;

    switch (character) {
    case CHAR_LEFT_SQUARE_BRACKET:
      terminator = CHAR_RIGHT_SQUARE_BRACKET;
      isMapping = false;
      _result = [];
      break;

    case CHAR_LEFT_CURLY_BRACKET:
      terminator = CHAR_RIGHT_CURLY_BRACKET;
      isMapping = true;
      _result = {};
      break;

    default:
      return false;
    }

    if (null !== anchor) {
      anchorMap[anchor] = _result;
    }

    character = input.charCodeAt(++position);

    while (position < length) {
      skipSeparationSpace(true, nodeIndent);

      if (character === terminator) {
        character = input.charCodeAt(++position);
        tag = _tag;
        kind = isMapping ? KIND_OBJECT : KIND_ARRAY;
        result = _result;
        return true;
      } else if (!readNext) {
        throwError('missed comma between flow collection entries');
      }

      keyTag = keyNode = valueNode = null;
      isPair = isExplicitPair = false;

      if (CHAR_QUESTION === character) {
        following = input.charCodeAt(position + 1);

        if (CHAR_SPACE === following ||
            CHAR_TAB === following ||
            CHAR_LINE_FEED === following ||
            CHAR_CARRIAGE_RETURN === following) {
          isPair = isExplicitPair = true;
          position += 1;
          character = following;
          skipSeparationSpace(true, nodeIndent);
        }
      }

      _line = line;
      composeNode(nodeIndent, CONTEXT_FLOW_IN, false, true);
      keyTag = tag;
      keyNode = result;

      if ((isExplicitPair || line === _line) && CHAR_COLON === character) {
        isPair = true;
        character = input.charCodeAt(++position);
        skipSeparationSpace(true, nodeIndent);
        composeNode(nodeIndent, CONTEXT_FLOW_IN, false, true);
        valueNode = result;
      }

      if (isMapping) {
        storeMappingPair(_result, keyTag, keyNode, valueNode);
      } else if (isPair) {
        _result.push(storeMappingPair(null, keyTag, keyNode, valueNode));
      } else {
        _result.push(keyNode);
      }

      skipSeparationSpace(true, nodeIndent);

      if (CHAR_COMMA === character) {
        readNext = true;
        character = input.charCodeAt(++position);
      } else {
        readNext = false;
      }
    }

    throwError('unexpected end of the stream within a flow collection');
  }

  function readBlockScalar(nodeIndent) {
    var captureStart,
        folding,
        chomping       = CHOMPING_CLIP,
        detectedIndent = false,
        textIndent     = nodeIndent,
        emptyLines     = -1;

    switch (character) {
    case CHAR_VERTICAL_LINE:
      folding = false;
      break;

    case CHAR_GREATER_THAN:
      folding = true;
      break;

    default:
      return false;
    }

    kind = KIND_STRING;
    result = '';

    while (position < length) {
      character = input.charCodeAt(++position);

      if (CHAR_PLUS === character || CHAR_MINUS === character) {
        if (CHOMPING_CLIP === chomping) {
          chomping = (CHAR_PLUS === character) ? CHOMPING_KEEP : CHOMPING_STRIP;
        } else {
          throwError('repeat of a chomping mode identifier');
        }

      } else if (CHAR_DIGIT_ZERO <= character && character <= CHAR_DIGIT_NINE) {
        if (CHAR_DIGIT_ZERO === character) {
          throwError('bad explicit indentation width of a block scalar; it cannot be less than one');
        } else if (!detectedIndent) {
          textIndent = nodeIndent + (character - CHAR_DIGIT_ONE);
          detectedIndent = true;
        } else {
          throwError('repeat of an indentation width identifier');
        }

      } else {
        break;
      }
    }

    if (CHAR_SPACE === character || CHAR_TAB === character) {
      do { character = input.charCodeAt(++position); }
      while (CHAR_SPACE === character || CHAR_TAB === character);

      if (CHAR_SHARP === character) {
        do { character = input.charCodeAt(++position); }
        while (position < length &&
               CHAR_LINE_FEED !== character &&
               CHAR_CARRIAGE_RETURN !== character);
      }
    }

    while (position < length) {
      readLineBreak();
      lineIndent = 0;

      while ((!detectedIndent || lineIndent < textIndent) &&
             (CHAR_SPACE === character)) {
        lineIndent += 1;
        character = input.charCodeAt(++position);
      }

      if (!detectedIndent && lineIndent > textIndent) {
        textIndent = lineIndent;
      }

      if (CHAR_LINE_FEED === character || CHAR_CARRIAGE_RETURN === character) {
        emptyLines += 1;
        continue;
      }

      // End of the scalar. Perform the chomping.
      if (lineIndent < textIndent) {
        if (CHOMPING_KEEP === chomping) {
          result += common.repeat('\n', emptyLines + 1);
        } else if (CHOMPING_CLIP === chomping) {
          result += '\n';
        }
        break;
      }

      detectedIndent = true;

      if (folding) {
        if (CHAR_SPACE === character || CHAR_TAB === character) {
          result += common.repeat('\n', emptyLines + 1);
          emptyLines = 1;
        } else if (0 === emptyLines) {
          result += ' ';
          emptyLines = 0;
        } else {
          result += common.repeat('\n', emptyLines);
          emptyLines = 0;
        }
      } else {
        result += common.repeat('\n', emptyLines + 1);
        emptyLines = 0;
      }

      captureStart = position;

      do { character = input.charCodeAt(++position); }
      while (position < length &&
             CHAR_LINE_FEED !== character &&
             CHAR_CARRIAGE_RETURN !== character);

      captureSegment(captureStart, position, false);
    }

    return true;
  }

  function readBlockSequence(nodeIndent) {
    var _line,
        _tag      = tag,
        _result   = [],
        following,
        detected  = false;

    if (null !== anchor) {
      anchorMap[anchor] = _result;
    }

    while (position < length) {
      if (CHAR_MINUS !== character) {
        break;
      }

      following = input.charCodeAt(position + 1);

      if (CHAR_SPACE           !== following &&
          CHAR_TAB             !== following &&
          CHAR_LINE_FEED       !== following &&
          CHAR_CARRIAGE_RETURN !== following) {
        break;
      }

      detected = true;
      position += 1;
      character = following;

      if (skipSeparationSpace(true, -1)) {
        if (lineIndent <= nodeIndent) {
          _result.push(null);
          continue;
        }
      }

      _line = line;
      composeNode(nodeIndent, CONTEXT_BLOCK_IN, false, true);
      _result.push(result);
      skipSeparationSpace(true, -1);

      if ((line === _line || lineIndent > nodeIndent) && position < length) {
        throwError('bad indentation of a sequence entry');
      } else if (lineIndent < nodeIndent) {
        break;
      }
    }

    if (detected) {
      tag = _tag;
      kind = KIND_ARRAY;
      result = _result;
      return true;
    } else {
      return false;
    }
  }

  function readBlockMapping(nodeIndent) {
    var following,
        allowCompact,
        _line,
        _tag          = tag,
        _result       = {},
        keyTag        = null,
        keyNode       = null,
        valueNode     = null,
        atExplicitKey = false,
        detected      = false;

    if (null !== anchor) {
      anchorMap[anchor] = _result;
    }

    while (position < length) {
      following = input.charCodeAt(position + 1);
      _line = line; // Save the current line.

      if ((CHAR_QUESTION        === character ||
           CHAR_COLON           === character) &&
          (CHAR_SPACE           === following ||
           CHAR_TAB             === following ||
           CHAR_LINE_FEED       === following ||
           CHAR_CARRIAGE_RETURN === following)) {

        if (CHAR_QUESTION === character) {
          if (atExplicitKey) {
            storeMappingPair(_result, keyTag, keyNode, null);
            keyTag = keyNode = valueNode = null;
          }

          detected = true;
          atExplicitKey = true;
          allowCompact = true;

        } else if (atExplicitKey) {
          // i.e. CHAR_COLON === character after the explicit key.
          atExplicitKey = false;
          allowCompact = true;

        } else {
          throwError('incomplete explicit mapping pair; a key node is missed');
        }

        position += 1;
        character = following;

      } else if (composeNode(nodeIndent, CONTEXT_FLOW_OUT, false, true)) {
        if (line === _line) {
          // TODO: Remove this cycle when the flow readers will consume
          // trailing whitespaces like the block readers.
          while (CHAR_SPACE === character ||
                 CHAR_TAB === character) {
            character = input.charCodeAt(++position);
          }

          if (CHAR_COLON === character) {
            character = input.charCodeAt(++position);

            if (CHAR_SPACE           !== character &&
                CHAR_TAB             !== character &&
                CHAR_LINE_FEED       !== character &&
                CHAR_CARRIAGE_RETURN !== character) {
              throwError('a whitespace character is expected after the key-value separator within a block mapping');
            }

            if (atExplicitKey) {
              storeMappingPair(_result, keyTag, keyNode, null);
              keyTag = keyNode = valueNode = null;
            }

            detected = true;
            atExplicitKey = false;
            allowCompact = false;
            keyTag = tag;
            keyNode = result;

          } else if (detected) {
            throwError('can not read an implicit mapping pair; a colon is missed');

          } else {
            tag = _tag;
            return true; // Keep the result of `composeNode`.
          }

        } else if (detected) {
          throwError('can not read a block mapping entry; a multiline key may not be an implicit key');

        } else {
          tag = _tag;
          return true; // Keep the result of `composeNode`.
        }

      } else {
        break;
      }

      if (line === _line || lineIndent > nodeIndent) {
        if (composeNode(nodeIndent, CONTEXT_BLOCK_OUT, true, allowCompact)) {
          if (atExplicitKey) {
            keyNode = result;
          } else {
            valueNode = result;
          }
        }

        if (!atExplicitKey) {
          storeMappingPair(_result, keyTag, keyNode, valueNode);
          keyTag = keyNode = valueNode = null;
        }

        // TODO: It is needed only for flow node readers. It should be removed
        // when the flow readers will consume trailing whitespaces as well as
        // the block readers.
        skipSeparationSpace(true, -1);
      }

      if (lineIndent > nodeIndent && position < length) {
        throwError('bad indentation of a mapping entry');
      } else if (lineIndent < nodeIndent) {
        break;
      }
    }

    if (atExplicitKey) {
      storeMappingPair(_result, keyTag, keyNode, null);
    }

    if (detected) {
      tag = _tag;
      kind = KIND_OBJECT;
      result = _result;
    }

    return detected;
  }

  function readTagProperty() {
    var _position,
        isVerbatim = false,
        isNamed    = false,
        tagHandle,
        tagName;

    if (CHAR_EXCLAMATION !== character) {
      return false;
    }

    if (null !== tag) {
      throwError('duplication of a tag property');
    }

    character = input.charCodeAt(++position);

    if (CHAR_LESS_THAN === character) {
      isVerbatim = true;
      character = input.charCodeAt(++position);

    } else if (CHAR_EXCLAMATION === character) {
      isNamed = true;
      tagHandle = '!!';
      character = input.charCodeAt(++position);

    } else {
      tagHandle = '!';
    }

    _position = position;

    if (isVerbatim) {
      do { character = input.charCodeAt(++position); }
      while (position < length && CHAR_GREATER_THAN !== character);

      if (position < length) {
        tagName = input.slice(_position, position);
        character = input.charCodeAt(++position);
      } else {
        throwError('unexpected end of the stream within a verbatim tag');
      }
    } else {
      while (position < length &&
             CHAR_SPACE           !== character &&
             CHAR_TAB             !== character &&
             CHAR_LINE_FEED       !== character &&
             CHAR_CARRIAGE_RETURN !== character) {

        if (CHAR_EXCLAMATION === character) {
          if (!isNamed) {
            tagHandle = input.slice(_position - 1, position + 1);

            if (validate && !PATTERN_TAG_HANDLE.test(tagHandle)) {
              throwError('named tag handle cannot contain such characters');
            }

            isNamed = true;
            _position = position + 1;
          } else {
            throwError('tag suffix cannot contain exclamation marks');
          }
        }

        character = input.charCodeAt(++position);
      }

      tagName = input.slice(_position, position);

      if (validate && PATTERN_FLOW_INDICATORS.test(tagName)) {
        throwError('tag suffix cannot contain flow indicator characters');
      }
    }

    if (validate && tagName && !PATTERN_TAG_URI.test(tagName)) {
      throwError('tag name cannot contain such characters: ' + tagName);
    }

    if (isVerbatim) {
      tag = tagName;

    } else if (_hasOwnProperty.call(tagMap, tagHandle)) {
      tag = tagMap[tagHandle] + tagName;

    } else if ('!' === tagHandle) {
      tag = '!' + tagName;

    } else if ('!!' === tagHandle) {
      tag = 'tag:yaml.org,2002:' + tagName;

    } else {
      throwError('undeclared tag handle "' + tagHandle + '"');
    }

    return true;
  }

  function readAnchorProperty() {
    var _position;

    if (CHAR_AMPERSAND !== character) {
      return false;
    }

    if (null !== anchor) {
      throwError('duplication of an anchor property');
    }

    character = input.charCodeAt(++position);
    _position = position;

    while (position < length &&
           CHAR_SPACE                !== character &&
           CHAR_TAB                  !== character &&
           CHAR_LINE_FEED            !== character &&
           CHAR_CARRIAGE_RETURN      !== character &&
           CHAR_COMMA                !== character &&
           CHAR_LEFT_SQUARE_BRACKET  !== character &&
           CHAR_RIGHT_SQUARE_BRACKET !== character &&
           CHAR_LEFT_CURLY_BRACKET   !== character &&
           CHAR_RIGHT_CURLY_BRACKET  !== character) {
      character = input.charCodeAt(++position);
    }

    if (position === _position) {
      throwError('name of an anchor node must contain at least one character');
    }

    anchor = input.slice(_position, position);
    return true;
  }

  function readAlias() {
    var _position, alias;

    if (CHAR_ASTERISK !== character) {
      return false;
    }

    character = input.charCodeAt(++position);
    _position = position;

    while (position < length &&
           CHAR_SPACE                !== character &&
           CHAR_TAB                  !== character &&
           CHAR_LINE_FEED            !== character &&
           CHAR_CARRIAGE_RETURN      !== character &&
           CHAR_COMMA                !== character &&
           CHAR_LEFT_SQUARE_BRACKET  !== character &&
           CHAR_RIGHT_SQUARE_BRACKET !== character &&
           CHAR_LEFT_CURLY_BRACKET   !== character &&
           CHAR_RIGHT_CURLY_BRACKET  !== character) {
      character = input.charCodeAt(++position);
    }

    if (position === _position) {
      throwError('name of an alias node must contain at least one character');
    }

    alias = input.slice(_position, position);

    if (!anchorMap.hasOwnProperty(alias)) {
      throwError('unidentified alias "' + alias + '"');
    }

    result = anchorMap[alias];
    skipSeparationSpace(true, -1);
    return true;
  }

  function composeNode(parentIndent, nodeContext, allowToSeek, allowCompact) {
    var allowBlockStyles,
        allowBlockScalars,
        allowBlockCollections,
        atNewLine  = false,
        isIndented = true,
        hasContent = false,
        typeIndex,
        typeQuantity,
        type,
        typeLoader,
        flowIndent,
        blockIndent,
        _result;

    tag    = null;
    anchor = null;
    kind   = null;
    result = null;

    allowBlockStyles = allowBlockScalars = allowBlockCollections =
      CONTEXT_BLOCK_OUT === nodeContext ||
      CONTEXT_BLOCK_IN  === nodeContext;

    if (allowToSeek) {
      if (skipSeparationSpace(true, -1)) {
        atNewLine = true;

        if (lineIndent === parentIndent) {
          isIndented = false;

        } else if (lineIndent > parentIndent) {
          isIndented = true;

        } else {
          return false;
        }
      }
    }

    if (isIndented) {
      while (readTagProperty() || readAnchorProperty()) {
        if (skipSeparationSpace(true, -1)) {
          atNewLine = true;

          if (lineIndent > parentIndent) {
            isIndented = true;
            allowBlockCollections = allowBlockStyles;

          } else if (lineIndent === parentIndent) {
            isIndented = false;
            allowBlockCollections = allowBlockStyles;

          } else {
            return true;
          }
        } else {
          allowBlockCollections = false;
        }
      }
    }

    if (allowBlockCollections) {
      allowBlockCollections = atNewLine || allowCompact;
    }

    if (isIndented || CONTEXT_BLOCK_OUT === nodeContext) {
      if (CONTEXT_FLOW_IN === nodeContext || CONTEXT_FLOW_OUT === nodeContext) {
        flowIndent = parentIndent;
      } else {
        flowIndent = parentIndent + 1;
      }

      blockIndent = position - lineStart;

      if (isIndented) {
        if (allowBlockCollections &&
            (readBlockSequence(blockIndent) ||
             readBlockMapping(blockIndent)) ||
            readFlowCollection(flowIndent)) {
          hasContent = true;
        } else {
          if ((allowBlockScalars && readBlockScalar(flowIndent)) ||
              readSingleQuotedScalar(flowIndent) ||
              readDoubleQuotedScalar(flowIndent)) {
            hasContent = true;

          } else if (readAlias()) {
            hasContent = true;

            if (null !== tag || null !== anchor) {
              throwError('alias node should not have any properties');
            }

          } else if (readPlainScalar(flowIndent, CONTEXT_FLOW_IN === nodeContext)) {
            hasContent = true;

            if (null === tag) {
              tag = '?';
            }
          }

          if (null !== anchor) {
            anchorMap[anchor] = result;
          }
        }
      } else {
        hasContent = allowBlockCollections && readBlockSequence(blockIndent);
      }
    }

    if (null !== tag && '!' !== tag) {
      if ('?' === tag) {
        if (resolve) {
          for (typeIndex = 0, typeQuantity = implicitTypes.length;
               typeIndex < typeQuantity;
               typeIndex += 1) {
            type = implicitTypes[typeIndex];

            // Implicit resolving is not allowed for non-scalar types, and '?'
            // non-specific tag is only assigned to plain scalars. So, it isn't
            // needed to check for 'kind' conformity.
            _result = type.loader.resolver(result, false);

            if (NIL !== _result) {
              tag = type.tag;
              result = _result;
              break;
            }
          }
        }
      } else if (_hasOwnProperty.call(typeMap, tag)) {
        typeLoader = typeMap[tag].loader;

        if (null !== result && typeLoader.kind !== kind) {
          throwError('unacceptable node kind for !<' + tag + '> tag; it should be "' + typeLoader.kind + '", not "' + kind + '"');
        }

        if (typeLoader.resolver) {
          _result = typeLoader.resolver(result, true);

          if (NIL !== _result) {
            result = _result;
          } else {
            throwError('cannot resolve a node with !<' + tag + '> explicit tag');
          }
        }
      } else {
        throwWarning('unknown tag !<' + tag + '>');
      }
    }

    return null !== tag || null !== anchor || hasContent;
  }

  function readDocument() {
    var documentStart = position,
        _position,
        directiveName,
        directiveArgs,
        hasDirectives = false;

    version = null;
    checkLineBreaks = legacy;
    tagMap = {};
    anchorMap = {};

    while (position < length) {
      skipSeparationSpace(true, -1);

      if (lineIndent > 0 || CHAR_PERCENT !== character) {
        break;
      }

      hasDirectives = true;
      character = input.charCodeAt(++position);
      _position = position;

      while (position < length &&
             CHAR_SPACE           !== character &&
             CHAR_TAB             !== character &&
             CHAR_LINE_FEED       !== character &&
             CHAR_CARRIAGE_RETURN !== character) {
        character = input.charCodeAt(++position);
      }

      directiveName = input.slice(_position, position);
      directiveArgs = [];

      if (directiveName.length < 1) {
        throwError('directive name must not be less than one character in length');
      }

      while (position < length) {
        while (CHAR_SPACE === character || CHAR_TAB === character) {
          character = input.charCodeAt(++position);
        }

        if (CHAR_SHARP === character) {
          do { character = input.charCodeAt(++position); }
          while (position < length &&
                 CHAR_LINE_FEED !== character &&
                 CHAR_CARRIAGE_RETURN !== character);
          break;
        }

        if (CHAR_LINE_FEED === character || CHAR_CARRIAGE_RETURN === character) {
          break;
        }

        _position = position;

        while (position < length &&
               CHAR_SPACE           !== character &&
               CHAR_TAB             !== character &&
               CHAR_LINE_FEED       !== character &&
               CHAR_CARRIAGE_RETURN !== character) {
          character = input.charCodeAt(++position);
        }

        directiveArgs.push(input.slice(_position, position));
      }

      if (position < length) {
        readLineBreak();
      }

      if (_hasOwnProperty.call(directiveHandlers, directiveName)) {
        directiveHandlers[directiveName](directiveName, directiveArgs);
      } else {
        throwWarning('unknown document directive "' + directiveName + '"');
      }
    }

    skipSeparationSpace(true, -1);

    if (0 === lineIndent &&
        CHAR_MINUS === character &&
        CHAR_MINUS === input.charCodeAt(position + 1) &&
        CHAR_MINUS === input.charCodeAt(position + 2)) {
      position += 3;
      character = input.charCodeAt(position);
      skipSeparationSpace(true, -1);

    } else if (hasDirectives) {
      throwError('directives end mark is expected');
    }

    composeNode(lineIndent - 1, CONTEXT_BLOCK_OUT, false, true);
    skipSeparationSpace(true, -1);

    if (validate && checkLineBreaks &&
        PATTERN_NON_ASCII_LINE_BREAKS.test(input.slice(documentStart, position))) {
      throwWarning('non-ASCII line breaks are interpreted as content');
    }

    output(result);

    if (position === lineStart && testDocumentSeparator()) {
      if (CHAR_DOT === character) {
        position += 3;
        character = input.charCodeAt(position);
        skipSeparationSpace(true, -1);
      }
      return;
    }

    if (position < length) {
      throwError('end of the stream or a document separator is expected');
    } else {
      return;
    }
  }

  if (validate && PATTERN_NON_PRINTABLE.test(input)) {
    throwError('the stream contains non-printable characters');
  }

  while (CHAR_SPACE === character) {
    lineIndent += 1;
    character = input.charCodeAt(++position);
  }

  while (position < length) {
    readDocument();
  }
}


function load(input, options) {
  var result = null, received = false;

  function callback(data) {
    if (!received) {
      result = data;
      received = true;
    } else {
      throw new YAMLException('expected a single document in the stream, but found more');
    }
  }

  loadAll(input, callback, options);

  return result;
}


function safeLoadAll(input, output, options) {
  loadAll(input, output, common.extend({ schema: SAFE_SCHEMA }, options));
}


function safeLoad(input, options) {
  return load(input, common.extend({ schema: SAFE_SCHEMA }, options));
}


module.exports.loadAll     = loadAll;
module.exports.load        = load;
module.exports.safeLoadAll = safeLoadAll;
module.exports.safeLoad    = safeLoad;
