/**
 * Copyright (c) 2008, SnakeYAML
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.yaml.snakeyaml.emitter;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.ScalarStyle;
import org.yaml.snakeyaml.DumperOptions.Version;
import org.yaml.snakeyaml.comments.CommentEventsCollector;
import org.yaml.snakeyaml.comments.CommentLine;
import org.yaml.snakeyaml.comments.CommentType;
import org.yaml.snakeyaml.error.YAMLException;
import org.yaml.snakeyaml.events.AliasEvent;
import org.yaml.snakeyaml.events.CollectionEndEvent;
import org.yaml.snakeyaml.events.CollectionStartEvent;
import org.yaml.snakeyaml.events.CommentEvent;
import org.yaml.snakeyaml.events.DocumentEndEvent;
import org.yaml.snakeyaml.events.DocumentStartEvent;
import org.yaml.snakeyaml.events.Event;
import org.yaml.snakeyaml.events.Event.ID;
import org.yaml.snakeyaml.events.MappingEndEvent;
import org.yaml.snakeyaml.events.MappingStartEvent;
import org.yaml.snakeyaml.events.NodeEvent;
import org.yaml.snakeyaml.events.ScalarEvent;
import org.yaml.snakeyaml.events.SequenceEndEvent;
import org.yaml.snakeyaml.events.SequenceStartEvent;
import org.yaml.snakeyaml.events.StreamEndEvent;
import org.yaml.snakeyaml.events.StreamStartEvent;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.reader.StreamReader;
import org.yaml.snakeyaml.scanner.Constant;
import org.yaml.snakeyaml.util.ArrayStack;

/**
 * <pre>
 * Emitter expects events obeying the following grammar:
 * stream ::= STREAM-START document* STREAM-END
 * document ::= DOCUMENT-START node DOCUMENT-END
 * node ::= SCALAR | sequence | mapping
 * sequence ::= SEQUENCE-START node* SEQUENCE-END
 * mapping ::= MAPPING-START (node node)* MAPPING-END
 * </pre>
 */
public final class Emitter implements Emitable {

  /**
   * indent cannot be zero spaces
   */
  public static final int MIN_INDENT = 1;
  /**
   * indent should not be more than 10 spaces
   */
  public static final int MAX_INDENT = 10;
  private static final char[] SPACE = {' '};

  private static final Pattern SPACES_PATTERN = Pattern.compile("\\s");
  private static final Set<Character> INVALID_ANCHOR = new HashSet<Character>();

  static {
    INVALID_ANCHOR.add('[');
    INVALID_ANCHOR.add(']');
    INVALID_ANCHOR.add('{');
    INVALID_ANCHOR.add('}');
    INVALID_ANCHOR.add(',');
    INVALID_ANCHOR.add('*');
    INVALID_ANCHOR.add('&');
  }

  private static final Map<Character, String> ESCAPE_REPLACEMENTS =
      new HashMap<Character, String>();

  static {
    ESCAPE_REPLACEMENTS.put('\0', "0");
    ESCAPE_REPLACEMENTS.put('\u0007', "a");
    ESCAPE_REPLACEMENTS.put('\u0008', "b");
    ESCAPE_REPLACEMENTS.put('\u0009', "t");
    ESCAPE_REPLACEMENTS.put('\n', "n");
    ESCAPE_REPLACEMENTS.put('\u000B', "v");
    ESCAPE_REPLACEMENTS.put('\u000C', "f");
    ESCAPE_REPLACEMENTS.put('\r', "r");
    ESCAPE_REPLACEMENTS.put('\u001B', "e");
    ESCAPE_REPLACEMENTS.put('"', "\"");
    ESCAPE_REPLACEMENTS.put('\\', "\\");
    ESCAPE_REPLACEMENTS.put('\u0085', "N");
    ESCAPE_REPLACEMENTS.put('\u00A0', "_");
    ESCAPE_REPLACEMENTS.put('\u2028', "L");
    ESCAPE_REPLACEMENTS.put('\u2029', "P");
  }

  private static final Map<String, String> DEFAULT_TAG_PREFIXES =
      new LinkedHashMap<String, String>();

  static {
    DEFAULT_TAG_PREFIXES.put("!", "!");
    DEFAULT_TAG_PREFIXES.put(Tag.PREFIX, "!!");
  }

  // The stream should have the methods `write` and possibly `flush`.
  private final Writer stream;

  // Encoding is defined by Writer (cannot be overridden by STREAM-START.)
  // private Charset encoding;

  // Emitter is a state machine with a stack of states to handle nested
  // structures.
  private final ArrayStack<EmitterState> states;
  private EmitterState state;

  // Current event and the event queue.
  private final Queue<Event> events;
  private Event event;

  // The current indentation level and the stack of previous indents.
  private final ArrayStack<Integer> indents;
  private Integer indent;

  // Flow level.
  private int flowLevel;

  // Contexts.
  private boolean rootContext;
  private boolean mappingContext;
  private boolean simpleKeyContext;

  //
  // Characteristics of the last emitted character:
  // - current position.
  // - is it a whitespace?
  // - is it an indention character
  // (indentation space, '-', '?', or ':')?
  // private int line; this variable is not used
  private int column;
  private boolean whitespace;
  private boolean indention;
  private boolean openEnded;

  // Formatting details.
  private final Boolean canonical;
  // pretty print flow by adding extra line breaks
  private final Boolean prettyFlow;

  private final boolean allowUnicode;
  private int bestIndent;
  private final int indicatorIndent;
  private final boolean indentWithIndicator;
  private int bestWidth;
  private final char[] bestLineBreak;
  private final boolean splitLines;
  private final int maxSimpleKeyLength;
  private final boolean emitComments;

  // Tag prefixes.
  private Map<String, String> tagPrefixes;

  // Prepared anchor and tag.
  private String preparedAnchor;
  private String preparedTag;

  // Scalar analysis and style.
  private ScalarAnalysis analysis;
  private DumperOptions.ScalarStyle style;

  // Comment processing
  private final CommentEventsCollector blockCommentsCollector;
  private final CommentEventsCollector inlineCommentsCollector;


  /**
   * Create
   *
   * @param stream - output to write to
   * @param opts - options
   */
  public Emitter(Writer stream, DumperOptions opts) {
    if (stream == null) {
      throw new NullPointerException("Writer must be provided.");
    }
    if (opts == null) {
      throw new NullPointerException("DumperOptions must be provided.");
    }
    // The stream should have the methods `write` and possibly `flush`.
    this.stream = stream;
    // Emitter is a state machine with a stack of states to handle nested structures.
    this.states = new ArrayStack<EmitterState>(100);
    this.state = new ExpectStreamStart();
    // Current event and the event queue.
    this.events = new ArrayDeque<>(100);
    this.event = null;
    // The current indentation level and the stack of previous indents.
    this.indents = new ArrayStack<Integer>(10);
    this.indent = null;
    // Flow level.
    this.flowLevel = 0;
    // Contexts.
    mappingContext = false;
    simpleKeyContext = false;

    //
    // Characteristics of the last emitted character:
    // - current position.
    // - is it a whitespace?
    // - is it an indention character
    // (indentation space, '-', '?', or ':')?
    column = 0;
    whitespace = true;
    indention = true;

    // Whether the document requires an explicit document indicator
    openEnded = false;

    // Formatting details.
    this.canonical = opts.isCanonical();
    this.prettyFlow = opts.isPrettyFlow();
    this.allowUnicode = opts.isAllowUnicode();
    this.bestIndent = 2;
    if ((opts.getIndent() > MIN_INDENT) && (opts.getIndent() < MAX_INDENT)) {
      this.bestIndent = opts.getIndent();
    }
    this.indicatorIndent = opts.getIndicatorIndent();
    this.indentWithIndicator = opts.getIndentWithIndicator();
    this.bestWidth = 80;
    if (opts.getWidth() > this.bestIndent * 2) {
      this.bestWidth = opts.getWidth();
    }
    this.bestLineBreak = opts.getLineBreak().getString().toCharArray();
    this.splitLines = opts.getSplitLines();
    this.maxSimpleKeyLength = opts.getMaxSimpleKeyLength();
    this.emitComments = opts.isProcessComments();

    // Tag prefixes.
    this.tagPrefixes = new LinkedHashMap<String, String>();

    // Prepared anchor and tag.
    this.preparedAnchor = null;
    this.preparedTag = null;

    // Scalar analysis and style.
    this.analysis = null;
    this.style = null;

    // Comment processing
    this.blockCommentsCollector =
        new CommentEventsCollector(events, CommentType.BLANK_LINE, CommentType.BLOCK);
    this.inlineCommentsCollector = new CommentEventsCollector(events, CommentType.IN_LINE);
  }

  public void emit(Event event) throws IOException {
    this.events.add(event);
    while (!needMoreEvents()) {
      this.event = this.events.poll();
      this.state.expect();
      this.event = null;
    }
  }

  // In some cases, we wait for a few next events before emitting.

  private boolean needMoreEvents() {
    if (events.isEmpty()) {
      return true;
    }

    Iterator<Event> iter = events.iterator();
    Event event = iter.next(); // it cannot be empty here
    while (event instanceof CommentEvent) {
      if (!iter.hasNext()) {
        return true;
      }
      event = iter.next();
    }

    if (event instanceof DocumentStartEvent) {
      return needEvents(iter, 1);
    } else if (event instanceof SequenceStartEvent) {
      return needEvents(iter, 2);
    } else if (event instanceof MappingStartEvent) {
      return needEvents(iter, 3);
    } else if (event instanceof StreamStartEvent) {
      return needEvents(iter, 2);
    } else if (event instanceof StreamEndEvent) {
      return false;
    } else if (emitComments) {
      return needEvents(iter, 1);
    }
    return false;
  }

  private boolean needEvents(Iterator<Event> iter, int count) {
    int level = 0;
    int actualCount = 0;
    while (iter.hasNext()) {
      Event event = iter.next();
      if (event instanceof CommentEvent) {
        continue;
      }
      actualCount++;
      if (event instanceof DocumentStartEvent || event instanceof CollectionStartEvent) {
        level++;
      } else if (event instanceof DocumentEndEvent || event instanceof CollectionEndEvent) {
        level--;
      } else if (event instanceof StreamEndEvent) {
        level = -1;
      }
      if (level < 0) {
        return false;
      }
    }
    return actualCount < count;
  }

  private void increaseIndent(boolean flow, boolean indentless) {
    indents.push(indent);
    if (indent == null) {
      if (flow) {
        indent = bestIndent;
      } else {
        indent = 0;
      }
    } else if (!indentless) {
      this.indent += bestIndent;
    }
  }

  // States

  // Stream handlers.

  private class ExpectStreamStart implements EmitterState {

    public void expect() throws IOException {
      if (event instanceof StreamStartEvent) {
        writeStreamStart();
        state = new ExpectFirstDocumentStart();
      } else {
        throw new EmitterException("expected StreamStartEvent, but got " + event);
      }
    }
  }

  private class ExpectNothing implements EmitterState {

    public void expect() throws IOException {
      throw new EmitterException("expecting nothing, but got " + event);
    }
  }

  // Document handlers.

  private class ExpectFirstDocumentStart implements EmitterState {

    public void expect() throws IOException {
      new ExpectDocumentStart(true).expect();
    }
  }

  private class ExpectDocumentStart implements EmitterState {

    private final boolean first;

    public ExpectDocumentStart(boolean first) {
      this.first = first;
    }

    public void expect() throws IOException {
      if (event instanceof DocumentStartEvent) {
        DocumentStartEvent ev = (DocumentStartEvent) event;
        if ((ev.getVersion() != null || ev.getTags() != null) && openEnded) {
          writeIndicator("...", true, false, false);
          writeIndent();
        }
        if (ev.getVersion() != null) {
          String versionText = prepareVersion(ev.getVersion());
          writeVersionDirective(versionText);
        }
        tagPrefixes = new LinkedHashMap<String, String>(DEFAULT_TAG_PREFIXES);
        if (ev.getTags() != null) {
          Set<String> handles = new TreeSet<String>(ev.getTags().keySet());
          for (String handle : handles) {
            String prefix = ev.getTags().get(handle);
            tagPrefixes.put(prefix, handle);
            String handleText = prepareTagHandle(handle);
            String prefixText = prepareTagPrefix(prefix);
            writeTagDirective(handleText, prefixText);
          }
        }
        boolean implicit = first && !ev.getExplicit() && !canonical && ev.getVersion() == null
            && (ev.getTags() == null || ev.getTags().isEmpty()) && !checkEmptyDocument();
        if (!implicit) {
          writeIndent();
          writeIndicator("---", true, false, false);
          if (canonical) {
            writeIndent();
          }
        }
        state = new ExpectDocumentRoot();
      } else if (event instanceof StreamEndEvent) {
        writeStreamEnd();
        state = new ExpectNothing();
      } else if (event instanceof CommentEvent) {
        blockCommentsCollector.collectEvents(event);
        writeBlockComment();
        // state = state; remains unchanged
      } else {
        throw new EmitterException("expected DocumentStartEvent, but got " + event);
      }
    }
  }

  private class ExpectDocumentEnd implements EmitterState {

    public void expect() throws IOException {
      event = blockCommentsCollector.collectEventsAndPoll(event);
      writeBlockComment();
      if (event instanceof DocumentEndEvent) {
        writeIndent();
        if (((DocumentEndEvent) event).getExplicit()) {
          writeIndicator("...", true, false, false);
          writeIndent();
        }
        flushStream();
        state = new ExpectDocumentStart(false);
      } else {
        throw new EmitterException("expected DocumentEndEvent, but got " + event);
      }
    }
  }

  private class ExpectDocumentRoot implements EmitterState {

    public void expect() throws IOException {
      event = blockCommentsCollector.collectEventsAndPoll(event);
      if (!blockCommentsCollector.isEmpty()) {
        writeBlockComment();
        if (event instanceof DocumentEndEvent) {
          new ExpectDocumentEnd().expect();
          return;
        }
      }
      states.push(new ExpectDocumentEnd());
      expectNode(true, false, false);
    }
  }

  // Node handlers.

  private void expectNode(boolean root, boolean mapping, boolean simpleKey) throws IOException {
    rootContext = root;
    mappingContext = mapping;
    simpleKeyContext = simpleKey;
    if (event instanceof AliasEvent) {
      expectAlias();
    } else if (event instanceof ScalarEvent || event instanceof CollectionStartEvent) {
      processAnchor("&");
      processTag();
      if (event instanceof ScalarEvent) {
        expectScalar();
      } else if (event instanceof SequenceStartEvent) {
        if (flowLevel != 0 || canonical || ((SequenceStartEvent) event).isFlow()
            || checkEmptySequence()) {
          expectFlowSequence();
        } else {
          expectBlockSequence();
        }
      } else {// MappingStartEvent
        if (flowLevel != 0 || canonical || ((MappingStartEvent) event).isFlow()
            || checkEmptyMapping()) {
          expectFlowMapping();
        } else {
          expectBlockMapping();
        }
      }
    } else {
      throw new EmitterException("expected NodeEvent, but got " + event);
    }
  }

  private void expectAlias() throws IOException {
    if (!(event instanceof AliasEvent)) {
      throw new EmitterException("Alias must be provided");
    }
    processAnchor("*");
    state = states.pop();
  }

  private void expectScalar() throws IOException {
    increaseIndent(true, false);
    processScalar();
    indent = indents.pop();
    state = states.pop();
  }

  // Flow sequence handlers.

  private void expectFlowSequence() throws IOException {
    writeIndicator("[", true, true, false);
    flowLevel++;
    increaseIndent(true, false);
    if (prettyFlow) {
      writeIndent();
    }
    state = new ExpectFirstFlowSequenceItem();
  }

  private class ExpectFirstFlowSequenceItem implements EmitterState {

    public void expect() throws IOException {
      if (event instanceof SequenceEndEvent) {
        indent = indents.pop();
        flowLevel--;
        writeIndicator("]", false, false, false);
        inlineCommentsCollector.collectEvents();
        writeInlineComments();
        state = states.pop();
      } else if (event instanceof CommentEvent) {
        blockCommentsCollector.collectEvents(event);
        writeBlockComment();
      } else {
        if (canonical || (column > bestWidth && splitLines) || prettyFlow) {
          writeIndent();
        }
        states.push(new ExpectFlowSequenceItem());
        expectNode(false, false, false);
        event = inlineCommentsCollector.collectEvents(event);
        writeInlineComments();
      }
    }
  }

  private class ExpectFlowSequenceItem implements EmitterState {

    public void expect() throws IOException {
      if (event instanceof SequenceEndEvent) {
        indent = indents.pop();
        flowLevel--;
        if (canonical) {
          writeIndicator(",", false, false, false);
          writeIndent();
        } else if (prettyFlow) {
          writeIndent();
        }
        writeIndicator("]", false, false, false);
        inlineCommentsCollector.collectEvents();
        writeInlineComments();
        if (prettyFlow) {
          writeIndent();
        }
        state = states.pop();
      } else if (event instanceof CommentEvent) {
        event = blockCommentsCollector.collectEvents(event);
      } else {
        writeIndicator(",", false, false, false);
        writeBlockComment();
        if (canonical || (column > bestWidth && splitLines) || prettyFlow) {
          writeIndent();
        }
        states.push(new ExpectFlowSequenceItem());
        expectNode(false, false, false);
        event = inlineCommentsCollector.collectEvents(event);
        writeInlineComments();
      }
    }
  }

  // Flow mapping handlers.

  private void expectFlowMapping() throws IOException {
    writeIndicator("{", true, true, false);
    flowLevel++;
    increaseIndent(true, false);
    if (prettyFlow) {
      writeIndent();
    }
    state = new ExpectFirstFlowMappingKey();
  }

  private class ExpectFirstFlowMappingKey implements EmitterState {

    public void expect() throws IOException {
      event = blockCommentsCollector.collectEventsAndPoll(event);
      writeBlockComment();
      if (event instanceof MappingEndEvent) {
        indent = indents.pop();
        flowLevel--;
        writeIndicator("}", false, false, false);
        inlineCommentsCollector.collectEvents();
        writeInlineComments();
        state = states.pop();
      } else {
        if (canonical || (column > bestWidth && splitLines) || prettyFlow) {
          writeIndent();
        }
        if (!canonical && checkSimpleKey()) {
          states.push(new ExpectFlowMappingSimpleValue());
          expectNode(false, true, true);
        } else {
          writeIndicator("?", true, false, false);
          states.push(new ExpectFlowMappingValue());
          expectNode(false, true, false);
        }
      }
    }
  }

  private class ExpectFlowMappingKey implements EmitterState {

    public void expect() throws IOException {
      if (event instanceof MappingEndEvent) {
        indent = indents.pop();
        flowLevel--;
        if (canonical) {
          writeIndicator(",", false, false, false);
          writeIndent();
        }
        if (prettyFlow) {
          writeIndent();
        }
        writeIndicator("}", false, false, false);
        inlineCommentsCollector.collectEvents();
        writeInlineComments();
        state = states.pop();
      } else {
        writeIndicator(",", false, false, false);
        event = blockCommentsCollector.collectEventsAndPoll(event);
        writeBlockComment();
        if (canonical || (column > bestWidth && splitLines) || prettyFlow) {
          writeIndent();
        }
        if (!canonical && checkSimpleKey()) {
          states.push(new ExpectFlowMappingSimpleValue());
          expectNode(false, true, true);
        } else {
          writeIndicator("?", true, false, false);
          states.push(new ExpectFlowMappingValue());
          expectNode(false, true, false);
        }
      }
    }
  }

  private class ExpectFlowMappingSimpleValue implements EmitterState {

    public void expect() throws IOException {
      writeIndicator(":", false, false, false);
      event = inlineCommentsCollector.collectEventsAndPoll(event);
      writeInlineComments();
      states.push(new ExpectFlowMappingKey());
      expectNode(false, true, false);
      inlineCommentsCollector.collectEvents();
      writeInlineComments();
    }
  }

  private class ExpectFlowMappingValue implements EmitterState {

    public void expect() throws IOException {
      if (canonical || (column > bestWidth) || prettyFlow) {
        writeIndent();
      }
      writeIndicator(":", true, false, false);
      event = inlineCommentsCollector.collectEventsAndPoll(event);
      writeInlineComments();
      states.push(new ExpectFlowMappingKey());
      expectNode(false, true, false);
      inlineCommentsCollector.collectEvents(event);
      writeInlineComments();
    }
  }

  // Block sequence handlers.

  private void expectBlockSequence() throws IOException {
    boolean indentless = mappingContext && !indention;
    increaseIndent(false, indentless);
    state = new ExpectFirstBlockSequenceItem();
  }

  private class ExpectFirstBlockSequenceItem implements EmitterState {

    public void expect() throws IOException {
      new ExpectBlockSequenceItem(true).expect();
    }
  }

  private class ExpectBlockSequenceItem implements EmitterState {

    private final boolean first;

    public ExpectBlockSequenceItem(boolean first) {
      this.first = first;
    }

    public void expect() throws IOException {
      if (!this.first && event instanceof SequenceEndEvent) {
        indent = indents.pop();
        state = states.pop();
      } else if (event instanceof CommentEvent) {
        blockCommentsCollector.collectEvents(event);
      } else {
        writeIndent();
        if (!indentWithIndicator || this.first) {
          writeWhitespace(indicatorIndent);
        }
        writeIndicator("-", true, false, true);
        if (indentWithIndicator && this.first) {
          indent += indicatorIndent;
        }
        if (!blockCommentsCollector.isEmpty()) {
          increaseIndent(false, false);
          writeBlockComment();
          if (event instanceof ScalarEvent) {
            ScalarEvent scalarEvent = (ScalarEvent) event;
            if (analysis == null) {
              analysis = analyzeScalar(scalarEvent.getValue());
            }
            if (style == null) {
              style = chooseScalarStyle();
            }
            if (!analysis.isEmpty() || style == ScalarStyle.SINGLE_QUOTED
                || style == ScalarStyle.DOUBLE_QUOTED) {
              writeIndent();
            }
          }
          indent = indents.pop();
        }
        states.push(new ExpectBlockSequenceItem(false));
        expectNode(false, false, false);
        inlineCommentsCollector.collectEvents();
        writeInlineComments();
      }
    }
  }

  // Block mapping handlers.
  private void expectBlockMapping() throws IOException {
    increaseIndent(false, false);
    state = new ExpectFirstBlockMappingKey();
  }

  private class ExpectFirstBlockMappingKey implements EmitterState {

    public void expect() throws IOException {
      new ExpectBlockMappingKey(true).expect();
    }
  }

  private class ExpectBlockMappingKey implements EmitterState {

    private final boolean first;

    public ExpectBlockMappingKey(boolean first) {
      this.first = first;
    }

    public void expect() throws IOException {
      event = blockCommentsCollector.collectEventsAndPoll(event);
      writeBlockComment();
      if (!this.first && event instanceof MappingEndEvent) {
        indent = indents.pop();
        state = states.pop();
      } else {
        writeIndent();
        if (checkSimpleKey()) {
          states.push(new ExpectBlockMappingSimpleValue());
          expectNode(false, true, true);
        } else {
          writeIndicator("?", true, false, true);
          states.push(new ExpectBlockMappingValue());
          expectNode(false, true, false);
        }
      }
    }
  }

  private boolean isFoldedOrLiteral(Event event) {
    if (!event.is(ID.Scalar)) {
      return false;
    }
    ScalarEvent scalarEvent = (ScalarEvent) event;
    ScalarStyle style = scalarEvent.getScalarStyle();
    return style == ScalarStyle.FOLDED || style == ScalarStyle.LITERAL;
  }

  private class ExpectBlockMappingSimpleValue implements EmitterState {

    public void expect() throws IOException {
      writeIndicator(":", false, false, false);
      event = inlineCommentsCollector.collectEventsAndPoll(event);
      if (!isFoldedOrLiteral(event)) {
        if (writeInlineComments()) {
          increaseIndent(true, false);
          writeIndent();
          indent = indents.pop();
        }
      }
      event = blockCommentsCollector.collectEventsAndPoll(event);
      if (!blockCommentsCollector.isEmpty()) {
        increaseIndent(true, false);
        writeBlockComment();
        writeIndent();
        indent = indents.pop();
      }
      states.push(new ExpectBlockMappingKey(false));
      expectNode(false, true, false);
      inlineCommentsCollector.collectEvents();
      writeInlineComments();
    }
  }

  private class ExpectBlockMappingValue implements EmitterState {

    public void expect() throws IOException {
      writeIndent();
      writeIndicator(":", true, false, true);
      event = inlineCommentsCollector.collectEventsAndPoll(event);
      writeInlineComments();
      event = blockCommentsCollector.collectEventsAndPoll(event);
      writeBlockComment();
      states.push(new ExpectBlockMappingKey(false));
      expectNode(false, true, false);
      inlineCommentsCollector.collectEvents(event);
      writeInlineComments();
    }
  }

  // Checkers.

  private boolean checkEmptySequence() {
    return event instanceof SequenceStartEvent && !events.isEmpty()
        && events.peek() instanceof SequenceEndEvent;
  }

  private boolean checkEmptyMapping() {
    return event instanceof MappingStartEvent && !events.isEmpty()
        && events.peek() instanceof MappingEndEvent;
  }

  private boolean checkEmptyDocument() {
    if (!(event instanceof DocumentStartEvent) || events.isEmpty()) {
      return false;
    }
    Event event = events.peek();
    if (event instanceof ScalarEvent) {
      ScalarEvent e = (ScalarEvent) event;
      return e.getAnchor() == null && e.getTag() == null && e.getImplicit() != null
          && e.getValue().isEmpty();
    }
    return false;
  }

  private boolean checkSimpleKey() {
    int length = 0;
    if (event instanceof NodeEvent && ((NodeEvent) event).getAnchor() != null) {
      if (preparedAnchor == null) {
        preparedAnchor = prepareAnchor(((NodeEvent) event).getAnchor());
      }
      length += preparedAnchor.length();
    }
    String tag = null;
    if (event instanceof ScalarEvent) {
      tag = ((ScalarEvent) event).getTag();
    } else if (event instanceof CollectionStartEvent) {
      tag = ((CollectionStartEvent) event).getTag();
    }
    if (tag != null) {
      if (preparedTag == null) {
        preparedTag = prepareTag(tag);
      }
      length += preparedTag.length();
    }
    if (event instanceof ScalarEvent) {
      if (analysis == null) {
        analysis = analyzeScalar(((ScalarEvent) event).getValue());
      }
      length += analysis.getScalar().length();
    }
    return length < maxSimpleKeyLength && (event instanceof AliasEvent
        || (event instanceof ScalarEvent && !analysis.isEmpty() && !analysis.isMultiline())
        || checkEmptySequence() || checkEmptyMapping());
  }

  // Anchor, Tag, and Scalar processors.

  private void processAnchor(String indicator) throws IOException {
    NodeEvent ev = (NodeEvent) event;
    if (ev.getAnchor() == null) {
      preparedAnchor = null;
      return;
    }
    if (preparedAnchor == null) {
      preparedAnchor = prepareAnchor(ev.getAnchor());
    }
    writeIndicator(indicator + preparedAnchor, true, false, false);
    preparedAnchor = null;
  }

  /**
   * Emit the tag for the current event
   */
  private void processTag() throws IOException {
    String tag = null;
    if (event instanceof ScalarEvent) {
      ScalarEvent ev = (ScalarEvent) event;
      tag = ev.getTag();
      if (style == null) {
        style = chooseScalarStyle();
      }
      // check when no tag is required
      if ((!canonical || tag == null)
          && ((style == ScalarStyle.PLAIN && ev.getImplicit().canOmitTagInPlainScalar())
              || (style != ScalarStyle.PLAIN && ev.getImplicit().canOmitTagInNonPlainScalar()))) {
        preparedTag = null;
        return; // no tag required
      }
      if (ev.getImplicit().canOmitTagInPlainScalar() && tag == null) {
        tag = "!";
        preparedTag = null;
      }
    } else {
      CollectionStartEvent ev = (CollectionStartEvent) event;
      tag = ev.getTag();
      if ((!canonical || tag == null) && ev.getImplicit()) {
        preparedTag = null;
        return; // no tag required
      }
    }
    if (tag == null) {
      throw new EmitterException("tag is not specified");
    }
    if (preparedTag == null) {
      preparedTag = prepareTag(tag);
    }
    writeIndicator(preparedTag, true, false, false);
    preparedTag = null;
  }

  /**
   * Choose the scalar style based on the contents of the scalar and scalar style chosen by
   * Representer
   *
   * @return ScalarStyle to apply for this scala event
   */
  private DumperOptions.ScalarStyle chooseScalarStyle() {
    ScalarEvent ev = (ScalarEvent) event;
    if (analysis == null) {
      analysis = analyzeScalar(ev.getValue());
    }
    if (!ev.isPlain() && ev.isDQuoted() || this.canonical) {
      return ScalarStyle.DOUBLE_QUOTED;
    }
    if (ev.isJson() && Tag.STR.getValue().equals(ev.getTag())) {
      // special case for strings which are always double-quoted in JSON
      return ScalarStyle.DOUBLE_QUOTED;
    }
    if ((ev.isPlain() || ev.isJson()) && ev.getImplicit().canOmitTagInPlainScalar()) {
      if (!(simpleKeyContext && (analysis.isEmpty() || analysis.isMultiline()))
          && ((flowLevel != 0 && analysis.isAllowFlowPlain())
              || (flowLevel == 0 && analysis.isAllowBlockPlain()))) {
        return ScalarStyle.PLAIN;
      }
    }
    if (ev.isLiteral() || ev.isFolded()) {
      if (flowLevel == 0 && !simpleKeyContext && analysis.isAllowBlock()) {
        return ev.getScalarStyle();
      }
    }
    if (ev.isPlain() || ev.isSQuoted()) {
      if (analysis.isAllowSingleQuoted() && !(simpleKeyContext && analysis.isMultiline())) {
        return ScalarStyle.SINGLE_QUOTED;
      }
    }
    return ScalarStyle.DOUBLE_QUOTED;
  }

  private void processScalar() throws IOException {
    ScalarEvent ev = (ScalarEvent) event;
    if (analysis == null) {
      analysis = analyzeScalar(ev.getValue());
    }
    boolean split = !simpleKeyContext && splitLines;
    switch (style) {
      case PLAIN:
        writePlain(analysis.getScalar(), split);
        break;
      case DOUBLE_QUOTED:
        writeDoubleQuoted(analysis.getScalar(), split);
        break;
      case SINGLE_QUOTED:
        writeSingleQuoted(analysis.getScalar(), split);
        break;
      case FOLDED:
        writeFolded(analysis.getScalar(), split);
        break;
      case LITERAL:
        writeLiteral(analysis.getScalar());
        break;
      default:
        throw new YAMLException("Unexpected style: " + style);
    }
    // reset scalar style for another scalar
    analysis = null;
    style = null;
  }

  // Analyzers.

  private String prepareVersion(Version version) {
    if (version.major() != 1) {
      throw new EmitterException("unsupported YAML version: " + version);
    }
    return version.getRepresentation();
  }

