//          Copyright Ferdinand Majerech 2011.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

/**
 * YAML emitter.
 * Code based on PyYAML: http://www.pyyaml.org
 */
module dub.internal.dyaml.emitter;


import std.algorithm;
import std.array;
import std.ascii;
import std.conv;
import std.encoding;
import std.exception;
import std.format;
import std.range;
import std.string;
import std.system;
import std.typecons;
import std.utf;

import dub.internal.dyaml.encoding;
import dub.internal.dyaml.escapes;
import dub.internal.dyaml.event;
import dub.internal.dyaml.exception;
import dub.internal.dyaml.linebreak;
import dub.internal.dyaml.queue;
import dub.internal.dyaml.scanner;
import dub.internal.dyaml.style;
import dub.internal.dyaml.tagdirective;


package:

//Stores results of analysis of a scalar, determining e.g. what scalar style to use.
struct ScalarAnalysis
{
    //Scalar itself.
    string scalar;

    enum AnalysisFlags
    {
        empty = 1<<0,
        multiline = 1<<1,
        allowFlowPlain = 1<<2,
        allowBlockPlain = 1<<3,
        allowSingleQuoted = 1<<4,
        allowDoubleQuoted = 1<<5,
        allowBlock = 1<<6,
        isNull = 1<<7
    }

    ///Analysis results.
    BitFlags!AnalysisFlags flags;
}

private alias isNewLine = among!('\n', '\u0085', '\u2028', '\u2029');

private alias isSpecialChar = among!('#', ',', '[', ']', '{', '}', '&', '*', '!', '|', '>', '\\', '\'', '"', '%', '@', '`');

private alias isFlowIndicator = among!(',', '?', '[', ']', '{', '}');

private alias isSpace = among!('\0', '\n', '\r', '\u0085', '\u2028', '\u2029', ' ', '\t');

//Emits YAML events into a file/stream.
struct Emitter(Range, CharType) if (isOutputRange!(Range, CharType))
{
    private:
        ///Default tag handle shortcuts and replacements.
        static TagDirective[] defaultTagDirectives_ =
            [TagDirective("!", "!"), TagDirective("!!", "tag:yaml.org,2002:")];

        ///Stream to write to.
        Range stream_;

        /// Type used for upcoming emitter steps
        alias EmitterFunction = void function(scope typeof(this)*) @safe;

        ///Stack of states.
        Appender!(EmitterFunction[]) states_;

        ///Current state.
        EmitterFunction state_;

        ///Event queue.
        Queue!Event events_;
        ///Event we're currently emitting.
        Event event_;

        ///Stack of previous indentation levels.
        Appender!(int[]) indents_;
        ///Current indentation level.
        int indent_ = -1;

        ///Level of nesting in flow context. If 0, we're in block context.
        uint flowLevel_ = 0;

        /// Describes context (where we are in the document).
        enum Context
        {
            /// Root node of a document.
            root,
            /// Sequence.
            sequence,
            /// Mapping.
            mappingNoSimpleKey,
            /// Mapping, in a simple key.
            mappingSimpleKey,
        }
        /// Current context.
        Context context_;

        ///Characteristics of the last emitted character:

        ///Line.
        uint line_ = 0;
        ///Column.
        uint column_ = 0;
        ///Whitespace character?
        bool whitespace_ = true;
        ///indentation space, '-', '?', or ':'?
        bool indentation_ = true;

        ///Does the document require an explicit document indicator?
        bool openEnded_;

        ///Formatting details.

        ///Canonical scalar format?
        bool canonical_;
        ///Best indentation width.
        uint bestIndent_ = 2;
        ///Best text width.
        uint bestWidth_ = 80;
        ///Best line break character/s.
        LineBreak bestLineBreak_;

        ///Tag directive handle - prefix pairs.
        TagDirective[] tagDirectives_;

        ///Anchor/alias to process.
        string preparedAnchor_ = null;
        ///Tag to process.
        string preparedTag_ = null;

        ///Analysis result of the current scalar.
        ScalarAnalysis analysis_;
        ///Style of the current scalar.
        ScalarStyle style_ = ScalarStyle.invalid;

    public:
        @disable int opCmp(ref Emitter);
        @disable bool opEquals(ref Emitter);

        /**
         * Construct an emitter.
         *
         * Params:  stream    = Output range to write to.
         *          canonical = Write scalars in canonical form?
         *          indent    = Indentation width.
         *          lineBreak = Line break character/s.
         */
        this(Range stream, const bool canonical, const int indent, const int width,
             const LineBreak lineBreak) @safe
        {
            states_.reserve(32);
            indents_.reserve(32);
            stream_ = stream;
            canonical_ = canonical;
            nextExpected!"expectStreamStart"();

            if(indent > 1 && indent < 10){bestIndent_ = indent;}
            if(width > bestIndent_ * 2)  {bestWidth_ = width;}
            bestLineBreak_ = lineBreak;

            analysis_.flags.isNull = true;
        }

        ///Emit an event.
        void emit(Event event) @safe
        {
            events_.push(event);
            while(!needMoreEvents())
            {
                event_ = events_.pop();
                callNext();
                event_.destroy();
            }
        }

    private:
        ///Pop and return the newest state in states_.
        EmitterFunction popState() @safe
            in(states_.data.length > 0,
                "Emitter: Need to pop a state but there are no states left")
        {
            const result = states_.data[$-1];
            states_.shrinkTo(states_.data.length - 1);
            return result;
        }

        void pushState(string D)() @safe
        {
            states_ ~= mixin("function(typeof(this)* self) { self."~D~"(); }");
        }

        ///Pop and return the newest indent in indents_.
        int popIndent() @safe
            in(indents_.data.length > 0,
                "Emitter: Need to pop an indent level but there" ~
                " are no indent levels left")
        {
            const result = indents_.data[$-1];
            indents_.shrinkTo(indents_.data.length - 1);
            return result;
        }

        ///Write a string to the file/stream.
        void writeString(const scope char[] str) @safe
        {
            static if(is(CharType == char))
            {
                copy(str, stream_);
            }
            static if(is(CharType == wchar))
            {
                const buffer = to!wstring(str);
                copy(buffer, stream_);
            }
            static if(is(CharType == dchar))
            {
                const buffer = to!dstring(str);
                copy(buffer, stream_);
            }
        }

        ///In some cases, we wait for a few next events before emitting.
        bool needMoreEvents() @safe nothrow
        {
            if(events_.length == 0){return true;}

            const event = events_.peek();
            if(event.id == EventID.documentStart){return needEvents(1);}
            if(event.id == EventID.sequenceStart){return needEvents(2);}
            if(event.id == EventID.mappingStart) {return needEvents(3);}

            return false;
        }

        ///Determines if we need specified number of more events.
        bool needEvents(in uint count) @safe nothrow
        {
            int level;

            foreach(const event; events_.range)
            {
                if(event.id.among!(EventID.documentStart, EventID.sequenceStart, EventID.mappingStart)) {++level;}
                else if(event.id.among!(EventID.documentEnd, EventID.sequenceEnd, EventID.mappingEnd)) {--level;}
                else if(event.id == EventID.streamStart){level = -1;}

                if(level < 0)
                {
                    return false;
                }
            }

            return events_.length < (count + 1);
        }

        ///Increase indentation level.
        void increaseIndent(const Flag!"flow" flow = No.flow, const bool indentless = false) @safe
        {
            indents_ ~= indent_;
            if(indent_ == -1)
            {
                indent_ = flow ? bestIndent_ : 0;
            }
            else if(!indentless)
            {
                indent_ += bestIndent_;
            }
        }

        ///Determines if the type of current event is as specified. Throws if no event.
        bool eventTypeIs(in EventID id) const pure @safe
            in(!event_.isNull, "Expected an event, but no event is available.")
        {
            return event_.id == id;
        }


        //States.


        //Stream handlers.

        ///Handle start of a file/stream.
        void expectStreamStart() @safe
            in(eventTypeIs(EventID.streamStart),
                "Expected streamStart, but got " ~ event_.idString)
        {

            writeStreamStart();
            nextExpected!"expectDocumentStart!(Yes.first)"();
        }

        ///Expect nothing, throwing if we still have something.
        void expectNothing() @safe
        {
            assert(0, "Expected nothing, but got " ~ event_.idString);
        }

        //Document handlers.

        ///Handle start of a document.
        void expectDocumentStart(Flag!"first" first)() @safe
            in(eventTypeIs(EventID.documentStart) || eventTypeIs(EventID.streamEnd),
                "Expected documentStart or streamEnd, but got " ~ event_.idString)
        {

            if(event_.id == EventID.documentStart)
            {
                const YAMLVersion = event_.value;
                auto tagDirectives = event_.tagDirectives;
                if(openEnded_ && (YAMLVersion !is null || tagDirectives !is null))
                {
                    writeIndicator("...", Yes.needWhitespace);
                    writeIndent();
                }

                if(YAMLVersion !is null)
                {
                    writeVersionDirective(prepareVersion(YAMLVersion));
                }

                if(tagDirectives !is null)
                {
                    tagDirectives_ = tagDirectives;
                    sort!"icmp(a.handle, b.handle) < 0"(tagDirectives_);

                    foreach(ref pair; tagDirectives_)
                    {
                        writeTagDirective(prepareTagHandle(pair.handle),
                                          prepareTagPrefix(pair.prefix));
                    }
                }

                bool eq(ref TagDirective a, ref TagDirective b){return a.handle == b.handle;}
                //Add any default tag directives that have not been overriden.
                foreach(ref def; defaultTagDirectives_)
                {
                    if(!std.algorithm.canFind!eq(tagDirectives_, def))
                    {
                        tagDirectives_ ~= def;
                    }
                }

                const implicit = first && !event_.explicitDocument && !canonical_ &&
                                 YAMLVersion is null && tagDirectives is null &&
                                 !checkEmptyDocument();
                if(!implicit)
                {
                    writeIndent();
                    writeIndicator("---", Yes.needWhitespace);
                    if(canonical_){writeIndent();}
                }
                nextExpected!"expectRootNode"();
            }
            else if(event_.id == EventID.streamEnd)
            {
                if(openEnded_)
                {
                    writeIndicator("...", Yes.needWhitespace);
                    writeIndent();
                }
                writeStreamEnd();
                nextExpected!"expectNothing"();
            }
        }

        ///Handle end of a document.
        void expectDocumentEnd() @safe
            in(eventTypeIs(EventID.documentEnd),
                "Expected DocumentEnd, but got " ~ event_.idString)
        {

            writeIndent();
            if(event_.explicitDocument)
            {
                writeIndicator("...", Yes.needWhitespace);
                writeIndent();
            }
            nextExpected!"expectDocumentStart!(No.first)"();
        }

        ///Handle the root node of a document.
        void expectRootNode() @safe
        {
            pushState!"expectDocumentEnd"();
            expectNode(Context.root);
        }

        ///Handle a mapping node.
        //
        //Params: simpleKey = Are we in a simple key?
        void expectMappingNode(const bool simpleKey = false) @safe
        {
            expectNode(simpleKey ? Context.mappingSimpleKey : Context.mappingNoSimpleKey);
        }

        ///Handle a sequence node.
        void expectSequenceNode() @safe
        {
            expectNode(Context.sequence);
        }

        ///Handle a new node. Context specifies where in the document we are.
        void expectNode(const Context context) @safe
        {
            context_ = context;

            const flowCollection = event_.collectionStyle == CollectionStyle.flow;

            switch(event_.id)
            {
                case EventID.alias_: expectAlias(); break;
                case EventID.scalar:
                     processAnchor("&");
                     processTag();
                     expectScalar();
                     break;
                case EventID.sequenceStart:
                     processAnchor("&");
                     processTag();
                     if(flowLevel_ > 0 || canonical_ || flowCollection || checkEmptySequence())
                     {
                         expectFlowSequence();
                     }
                     else
                     {
                         expectBlockSequence();
                     }
                     break;
                case EventID.mappingStart:
                     processAnchor("&");
                     processTag();
                     if(flowLevel_ > 0 || canonical_ || flowCollection || checkEmptyMapping())
                     {
                         expectFlowMapping();
                     }
                     else
                     {
                         expectBlockMapping();
                     }
                     break;
                default:
                     assert(0, "Expected alias_, scalar, sequenceStart or " ~
                                     "mappingStart, but got: " ~ event_.idString);
            }
        }
        ///Handle an alias.
        void expectAlias() @safe
            in(event_.anchor != "", "Anchor is not specified for alias")
        {
            processAnchor("*");
            nextExpected(popState());
        }

        ///Handle a scalar.
        void expectScalar() @safe
        {
            increaseIndent(Yes.flow);
            processScalar();
            indent_ = popIndent();
            nextExpected(popState());
        }

        //Flow sequence handlers.

        ///Handle a flow sequence.
        void expectFlowSequence() @safe
        {
            writeIndicator("[", Yes.needWhitespace, Yes.whitespace);
            ++flowLevel_;
            increaseIndent(Yes.flow);
            nextExpected!"expectFlowSequenceItem!(Yes.first)"();
        }

        ///Handle a flow sequence item.
        void expectFlowSequenceItem(Flag!"first" first)() @safe
        {
            if(event_.id == EventID.sequenceEnd)
            {
                indent_ = popIndent();
                --flowLevel_;
                static if(!first) if(canonical_)
                {
                    writeIndicator(",", No.needWhitespace);
                    writeIndent();
                }
                writeIndicator("]", No.needWhitespace);
                nextExpected(popState());
                return;
            }
            static if(!first){writeIndicator(",", No.needWhitespace);}
            if(canonical_ || column_ > bestWidth_){writeIndent();}
            pushState!"expectFlowSequenceItem!(No.first)"();
            expectSequenceNode();
        }

        //Flow mapping handlers.

        ///Handle a flow mapping.
        void expectFlowMapping() @safe
        {
            writeIndicator("{", Yes.needWhitespace, Yes.whitespace);
            ++flowLevel_;
            increaseIndent(Yes.flow);
            nextExpected!"expectFlowMappingKey!(Yes.first)"();
        }

        ///Handle a key in a flow mapping.
        void expectFlowMappingKey(Flag!"first" first)() @safe
        {
            if(event_.id == EventID.mappingEnd)
            {
                indent_ = popIndent();
                --flowLevel_;
                static if (!first) if(canonical_)
                {
                    writeIndicator(",", No.needWhitespace);
                    writeIndent();
                }
                writeIndicator("}", No.needWhitespace);
                nextExpected(popState());
                return;
            }

            static if(!first){writeIndicator(",", No.needWhitespace);}
            if(canonical_ || column_ > bestWidth_){writeIndent();}
            if(!canonical_ && checkSimpleKey())
            {
                pushState!"expectFlowMappingSimpleValue"();
                expectMappingNode(true);
                return;
            }

            writeIndicator("?", Yes.needWhitespace);
            pushState!"expectFlowMappingValue"();
            expectMappingNode();
        }

        ///Handle a simple value in a flow mapping.
        void expectFlowMappingSimpleValue() @safe
        {
            writeIndicator(":", No.needWhitespace);
            pushState!"expectFlowMappingKey!(No.first)"();
            expectMappingNode();
        }

        ///Handle a complex value in a flow mapping.
        void expectFlowMappingValue() @safe
        {
            if(canonical_ || column_ > bestWidth_){writeIndent();}
            writeIndicator(":", Yes.needWhitespace);
            pushState!"expectFlowMappingKey!(No.first)"();
            expectMappingNode();
        }

        //Block sequence handlers.

        ///Handle a block sequence.
        void expectBlockSequence() @safe
        {
            const indentless = (context_ == Context.mappingNoSimpleKey ||
                                context_ == Context.mappingSimpleKey) && !indentation_;
            increaseIndent(No.flow, indentless);
            nextExpected!"expectBlockSequenceItem!(Yes.first)"();
        }

        ///Handle a block sequence item.
        void expectBlockSequenceItem(Flag!"first" first)() @safe
        {
            static if(!first) if(event_.id == EventID.sequenceEnd)
            {
                indent_ = popIndent();
                nextExpected(popState());
                return;
            }

            writeIndent();
            writeIndicator("-", Yes.needWhitespace, No.whitespace, Yes.indentation);
            pushState!"expectBlockSequenceItem!(No.first)"();
            expectSequenceNode();
        }

        //Block mapping handlers.

        ///Handle a block mapping.
        void expectBlockMapping() @safe
        {
            increaseIndent(No.flow);
            nextExpected!"expectBlockMappingKey!(Yes.first)"();
        }

        ///Handle a key in a block mapping.
        void expectBlockMappingKey(Flag!"first" first)() @safe
        {
            static if(!first) if(event_.id == EventID.mappingEnd)
            {
                indent_ = popIndent();
                nextExpected(popState());
                return;
            }

            writeIndent();
            if(checkSimpleKey())
            {
                pushState!"expectBlockMappingSimpleValue"();
                expectMappingNode(true);
                return;
            }

            writeIndicator("?", Yes.needWhitespace, No.whitespace, Yes.indentation);
            pushState!"expectBlockMappingValue"();
            expectMappingNode();
        }

        ///Handle a simple value in a block mapping.
        void expectBlockMappingSimpleValue() @safe
        {
            writeIndicator(":", No.needWhitespace);
            pushState!"expectBlockMappingKey!(No.first)"();
            expectMappingNode();
        }

        ///Handle a complex value in a block mapping.
        void expectBlockMappingValue() @safe
        {
            writeIndent();
            writeIndicator(":", Yes.needWhitespace, No.whitespace, Yes.indentation);
            pushState!"expectBlockMappingKey!(No.first)"();
            expectMappingNode();
        }

        //Checkers.

        ///Check if an empty sequence is next.
        bool checkEmptySequence() const @safe pure nothrow
        {
            return event_.id == EventID.sequenceStart && events_.length > 0
                   && events_.peek().id == EventID.sequenceEnd;
        }

        ///Check if an empty mapping is next.
        bool checkEmptyMapping() const @safe pure nothrow
        {
            return event_.id == EventID.mappingStart && events_.length > 0
                   && events_.peek().id == EventID.mappingEnd;
        }

        ///Check if an empty document is next.
        bool checkEmptyDocument() const @safe pure nothrow
        {
            if(event_.id != EventID.documentStart || events_.length == 0)
            {
                return false;
            }

            const event = events_.peek();
            const emptyScalar = event.id == EventID.scalar && (event.anchor is null) &&
                                (event.tag is null) && event.implicit && event.value == "";
            return emptyScalar;
        }

        ///Check if a simple key is next.
        bool checkSimpleKey() @safe
        {
            uint length;
            const id = event_.id;
            const scalar = id == EventID.scalar;
            const collectionStart = id == EventID.mappingStart ||
                                    id == EventID.sequenceStart;

            if((id == EventID.alias_ || scalar || collectionStart)
               && (event_.anchor !is null))
            {
                if(preparedAnchor_ is null)
                {
                    preparedAnchor_ = prepareAnchor(event_.anchor);
                }
                length += preparedAnchor_.length;
            }

            if((scalar || collectionStart) && (event_.tag !is null))
            {
                if(preparedTag_ is null){preparedTag_ = prepareTag(event_.tag);}
                length += preparedTag_.length;
            }

            if(scalar)
            {
                if(analysis_.flags.isNull){analysis_ = analyzeScalar(event_.value);}
                length += analysis_.scalar.length;
            }

            if(length >= 128){return false;}

            return id == EventID.alias_ ||
                   (scalar && !analysis_.flags.empty && !analysis_.flags.multiline) ||
                   checkEmptySequence() ||
                   checkEmptyMapping();
        }

        ///Process and write a scalar.
        void processScalar() @safe
        {
            if(analysis_.flags.isNull){analysis_ = analyzeScalar(event_.value);}
            if(style_ == ScalarStyle.invalid)
            {
                style_ = chooseScalarStyle();
            }

            //if(analysis_.flags.multiline && (context_ != Context.mappingSimpleKey) &&
            //   ([ScalarStyle.invalid, ScalarStyle.plain, ScalarStyle.singleQuoted, ScalarStyle.doubleQuoted)
            //    .canFind(style_))
            //{
            //    writeIndent();
            //}
            auto writer = ScalarWriter!(Range, CharType)(&this, analysis_.scalar,
                                       context_ != Context.mappingSimpleKey);
            final switch(style_)
            {
                case ScalarStyle.invalid:      assert(false);
                case ScalarStyle.doubleQuoted: writer.writeDoubleQuoted(); break;
                case ScalarStyle.singleQuoted: writer.writeSingleQuoted(); break;
                case ScalarStyle.folded:       writer.writeFolded();       break;
                case ScalarStyle.literal:      writer.writeLiteral();      break;
                case ScalarStyle.plain:        writer.writePlain();        break;
            }
            analysis_.flags.isNull = true;
            style_ = ScalarStyle.invalid;
        }

        ///Process and write an anchor/alias.
        void processAnchor(const string indicator) @safe
        {
            if(event_.anchor is null)
            {
                preparedAnchor_ = null;
                return;
            }
            if(preparedAnchor_ is null)
            {
                preparedAnchor_ = prepareAnchor(event_.anchor);
            }
            if(preparedAnchor_ !is null && preparedAnchor_ != "")
            {
                writeIndicator(indicator, Yes.needWhitespace);
                writeString(preparedAnchor_);
            }
            preparedAnchor_ = null;
        }

        ///Process and write a tag.
        void processTag() @safe
        {
            string tag = event_.tag;

            if(event_.id == EventID.scalar)
            {
                if(style_ == ScalarStyle.invalid){style_ = chooseScalarStyle();}
                if((!canonical_ || (tag is null)) &&
                   (((tag == "tag:yaml.org,2002:str") && event_.implicit) || (style_ == ScalarStyle.plain ? event_.implicit : !event_.implicit && (tag is null))))
                {
                    preparedTag_ = null;
                    return;
                }
                if(event_.implicit && (tag is null))
                {
                    tag = "!";
                    preparedTag_ = null;
                }
            }
            else if((!canonical_ || (tag is null)) && event_.implicit)
            {
                preparedTag_ = null;
                return;
            }

            assert(tag != "", "Tag is not specified");
            if(preparedTag_ is null){preparedTag_ = prepareTag(tag);}
            if(preparedTag_ !is null && preparedTag_ != "")
            {
                writeIndicator(preparedTag_, Yes.needWhitespace);
            }
            preparedTag_ = null;
        }

        ///Determine style to write the current scalar in.
        ScalarStyle chooseScalarStyle() @safe
        {
            if(analysis_.flags.isNull){analysis_ = analyzeScalar(event_.value);}

            const style          = event_.scalarStyle;
            const invalidOrPlain = style == ScalarStyle.invalid || style == ScalarStyle.plain;
            const block          = style == ScalarStyle.literal || style == ScalarStyle.folded;
            const singleQuoted   = style == ScalarStyle.singleQuoted;
            const doubleQuoted   = style == ScalarStyle.doubleQuoted;

            const allowPlain     = flowLevel_ > 0 ? analysis_.flags.allowFlowPlain
                                                  : analysis_.flags.allowBlockPlain;
            //simple empty or multiline scalars can't be written in plain style
            const simpleNonPlain = (context_ == Context.mappingSimpleKey) &&
                                   (analysis_.flags.empty || analysis_.flags.multiline);

            if(doubleQuoted || canonical_)
            {
                return ScalarStyle.doubleQuoted;
            }

            if(invalidOrPlain && event_.implicit && !simpleNonPlain && allowPlain)
            {
                return ScalarStyle.plain;
            }

            if(block && flowLevel_ == 0 && context_ != Context.mappingSimpleKey &&
               analysis_.flags.allowBlock)
            {
                return style;
            }

            if((invalidOrPlain || singleQuoted) &&
               analysis_.flags.allowSingleQuoted &&
               !(context_ == Context.mappingSimpleKey && analysis_.flags.multiline))
            {
                return ScalarStyle.singleQuoted;
            }

            return ScalarStyle.doubleQuoted;
        }

        ///Prepare YAML version string for output.
        static string prepareVersion(const string YAMLVersion) @safe
            in(YAMLVersion.split(".")[0] == "1",
                "Unsupported YAML version: " ~ YAMLVersion)
        {
            return YAMLVersion;
        }

        ///Encode an Unicode character for tag directive and write it to writer.
        static void encodeChar(Writer)(ref Writer writer, in dchar c) @safe
        {
            char[4] data;
            const bytes = encode(data, c);
            //For each byte add string in format %AB , where AB are hex digits of the byte.
            foreach(const char b; data[0 .. bytes])
            {
                formattedWrite(writer, "%%%02X", cast(ubyte)b);
            }
        }

        ///Prepare tag directive handle for output.
        static string prepareTagHandle(const string handle) @safe
            in(handle != "", "Tag handle must not be empty")
            in(handle.drop(1).dropBack(1).all!(c => isAlphaNum(c) || c.among!('-', '_')),
                "Tag handle contains invalid characters")
        {
            return handle;
        }

        ///Prepare tag directive prefix for output.
        static string prepareTagPrefix(const string prefix) @safe
            in(prefix != "", "Tag prefix must not be empty")
        {
            auto appender = appender!string();
            const int offset = prefix[0] == '!';
            size_t start, end;

            foreach(const size_t i, const dchar c; prefix)
            {
                const size_t idx = i + offset;
                if(isAlphaNum(c) || c.among!('-', ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '_', '.', '!', '~', '*', '\\', '\'', '(', ')', '[', ']', '%'))
                {
                    end = idx + 1;
                    continue;
                }

                if(start < idx){appender.put(prefix[start .. idx]);}
                start = end = idx + 1;

                encodeChar(appender, c);
            }

            end = min(end, prefix.length);
            if(start < end){appender.put(prefix[start .. end]);}
            return appender.data;
        }

        ///Prepare tag for output.
        string prepareTag(in string tag) @safe
            in(tag != "", "Tag must not be empty")
        {

            string tagString = tag;
            if (tagString == "!") return "!";
            string handle;
            string suffix = tagString;

            //Sort lexicographically by prefix.
            sort!"icmp(a.prefix, b.prefix) < 0"(tagDirectives_);
            foreach(ref pair; tagDirectives_)
            {
                auto prefix = pair.prefix;
                if(tagString.startsWith(prefix) &&
                   (prefix != "!" || prefix.length < tagString.length))
                {
                    handle = pair.handle;
                    suffix = tagString[prefix.length .. $];
                }
            }

            auto appender = appender!string();
            appender.put(handle !is null && handle != "" ? handle : "!<");
            size_t start, end;
            foreach(const dchar c; suffix)
            {
                if(isAlphaNum(c) || c.among!('-', ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '_', '.', '~', '*', '\\', '\'', '(', ')', '[', ']') ||
                   (c == '!' && handle != "!"))
                {
                    ++end;
                    continue;
                }
                if(start < end){appender.put(suffix[start .. end]);}
                start = end = end + 1;

                encodeChar(appender, c);
            }

            if(start < end){appender.put(suffix[start .. end]);}
            if(handle is null || handle == ""){appender.put(">");}

            return appender.data;
        }

        ///Prepare anchor for output.
        static string prepareAnchor(const string anchor) @safe
            in(anchor != "",  "Anchor must not be empty")
            in(anchor.all!isNSAnchorName, "Anchor contains invalid characters")
        {
            return anchor;
        }

        ///Analyze specifed scalar and return the analysis result.
        static ScalarAnalysis analyzeScalar(string scalar) @safe
        {
            ScalarAnalysis analysis;
            analysis.flags.isNull = false;
            analysis.scalar = scalar;

            //Empty scalar is a special case.
            if(scalar is null || scalar == "")
            {
                with(ScalarAnalysis.AnalysisFlags)
                    analysis.flags =
                        empty |
                        allowBlockPlain |
                        allowSingleQuoted |
                        allowDoubleQuoted;
                return analysis;
            }

            //Indicators and special characters (All false by default).
            bool blockIndicators, flowIndicators, lineBreaks, specialCharacters;

            //Important whitespace combinations (All false by default).
            bool leadingSpace, leadingBreak, trailingSpace, trailingBreak,
                 breakSpace, spaceBreak;

            //Check document indicators.
            if(scalar.startsWith("---", "..."))
            {
                blockIndicators = flowIndicators = true;
            }

            //First character or preceded by a whitespace.
            bool preceededByWhitespace = true;

            //Last character or followed by a whitespace.
            bool followedByWhitespace = scalar.length == 1 ||
                                        scalar[1].among!(' ', '\t', '\0', '\n', '\r', '\u0085', '\u2028', '\u2029');

            //The previous character is a space/break (false by default).
            bool previousSpace, previousBreak;

            foreach(const size_t index, const dchar c; scalar)
            {
                //Check for indicators.
                if(index == 0)
                {
                    //Leading indicators are special characters.
                    if(c.isSpecialChar)
                    {
                        flowIndicators = blockIndicators = true;
                    }
                    if(':' == c || '?' == c)
                    {
                        flowIndicators = true;
                        if(followedByWhitespace){blockIndicators = true;}
                    }
                    if(c == '-' && followedByWhitespace)
                    {
                        flowIndicators = blockIndicators = true;
                    }
                }
                else
                {
                    //Some indicators cannot appear within a scalar as well.
                    if(c.isFlowIndicator){flowIndicators = true;}
                    if(c == ':')
                    {
                        flowIndicators = true;
                        if(followedByWhitespace){blockIndicators = true;}
                    }
                    if(c == '#' && preceededByWhitespace)
                    {
                        flowIndicators = blockIndicators = true;
                    }
                }

                //Check for line breaks, special, and unicode characters.
                if(c.isNewLine){lineBreaks = true;}
                if(!(c == '\n' || (c >= '\x20' && c <= '\x7E')) &&
                   !((c == '\u0085' || (c >= '\xA0' && c <= '\uD7FF') ||
                     (c >= '\uE000' && c <= '\uFFFD')) && c != '\uFEFF'))
                {
                    specialCharacters = true;
                }

                //Detect important whitespace combinations.
                if(c == ' ')
                {
                    if(index == 0){leadingSpace = true;}
                    if(index == scalar.length - 1){trailingSpace = true;}
                    if(previousBreak){breakSpace = true;}
                    previousSpace = true;
                    previousBreak = false;
                }
                else if(c.isNewLine)
                {
                    if(index == 0){leadingBreak = true;}
                    if(index == scalar.length - 1){trailingBreak = true;}
                    if(previousSpace){spaceBreak = true;}
                    previousSpace = false;
                    previousBreak = true;
                }
                else
                {
                    previousSpace = previousBreak = false;
                }

                //Prepare for the next character.
                preceededByWhitespace = c.isSpace != 0;
                followedByWhitespace = index + 2 >= scalar.length ||
                                       scalar[index + 2].isSpace;
            }

            with(ScalarAnalysis.AnalysisFlags)
            {
                //Let's decide what styles are allowed.
                analysis.flags |= allowFlowPlain | allowBlockPlain | allowSingleQuoted |
                               allowDoubleQuoted | allowBlock;

                //Leading and trailing whitespaces are bad for plain scalars.
                if(leadingSpace || leadingBreak || trailingSpace || trailingBreak)
                {
                    analysis.flags &= ~(allowFlowPlain | allowBlockPlain);
                }

                //We do not permit trailing spaces for block scalars.
                if(trailingSpace)
                {
                    analysis.flags &= ~allowBlock;
                }

                //Spaces at the beginning of a new line are only acceptable for block
                //scalars.
                if(breakSpace)
                {
                    analysis.flags &= ~(allowFlowPlain | allowBlockPlain | allowSingleQuoted);
                }

                //Spaces followed by breaks, as well as special character are only
                //allowed for double quoted scalars.
                if(spaceBreak || specialCharacters)
                {
                    analysis.flags &= ~(allowFlowPlain | allowBlockPlain | allowSingleQuoted | allowBlock);
                }

                //Although the plain scalar writer supports breaks, we never emit
                //multiline plain scalars.
                if(lineBreaks)
                {
                    analysis.flags &= ~(allowFlowPlain | allowBlockPlain);
                    analysis.flags |= multiline;
                }

                //Flow indicators are forbidden for flow plain scalars.
                if(flowIndicators)
                {
                    analysis.flags &= ~allowFlowPlain;
                }

                //Block indicators are forbidden for block plain scalars.
                if(blockIndicators)
                {
                    analysis.flags &= ~allowBlockPlain;
                }
            }
            return analysis;
        }

        @safe unittest
        {
            with(analyzeScalar("").flags)
            {
                // workaround for empty being std.range.primitives.empty here
                alias empty = ScalarAnalysis.AnalysisFlags.empty;
                assert(empty && allowBlockPlain && allowSingleQuoted && allowDoubleQuoted);
            }
            with(analyzeScalar("a").flags)
            {
                assert(allowFlowPlain && allowBlockPlain && allowSingleQuoted && allowDoubleQuoted && allowBlock);
            }
            with(analyzeScalar(" ").flags)
            {
                assert(allowSingleQuoted && allowDoubleQuoted);
            }
            with(analyzeScalar(" a").flags)
            {
                assert(allowSingleQuoted && allowDoubleQuoted);
            }
            with(analyzeScalar("a ").flags)
            {
                assert(allowSingleQuoted && allowDoubleQuoted);
            }
            with(analyzeScalar("\na").flags)
            {
                assert(allowSingleQuoted && allowDoubleQuoted);
            }
            with(analyzeScalar("a\n").flags)
            {
                assert(allowSingleQuoted && allowDoubleQuoted);
            }
            with(analyzeScalar("\n").flags)
            {
                assert(multiline && allowSingleQuoted && allowDoubleQuoted && allowBlock);
            }
            with(analyzeScalar(" \n").flags)
            {
                assert(multiline && allowDoubleQuoted);
            }
            with(analyzeScalar("\n a").flags)
            {
                assert(multiline && allowDoubleQuoted && allowBlock);
            }
        }

        //Writers.

        ///Start the YAML stream (write the unicode byte order mark).
        void writeStreamStart() @safe
        {
            //Write BOM (except for UTF-8)
            static if(is(CharType == wchar) || is(CharType == dchar))
            {
                stream_.put(cast(CharType)'\uFEFF');
            }
        }

        ///End the YAML stream.
        void writeStreamEnd() @safe {}

        ///Write an indicator (e.g. ":", "[", ">", etc.).
        void writeIndicator(const scope char[] indicator,
                            const Flag!"needWhitespace" needWhitespace,
                            const Flag!"whitespace" whitespace = No.whitespace,
                            const Flag!"indentation" indentation = No.indentation) @safe
        {
            const bool prefixSpace = !whitespace_ && needWhitespace;
            whitespace_  = whitespace;
            indentation_ = indentation_ && indentation;
            openEnded_   = false;
            column_ += indicator.length;
            if(prefixSpace)
            {
                ++column_;
                writeString(" ");
            }
            writeString(indicator);
        }

        ///Write indentation.
        void writeIndent() @safe
        {
            const indent = indent_ == -1 ? 0 : indent_;

            if(!indentation_ || column_ > indent || (column_ == indent && !whitespace_))
            {
                writeLineBreak();
            }
            if(column_ < indent)
            {
                whitespace_ = true;

                //Used to avoid allocation of arbitrary length strings.
                static immutable spaces = "    ";
                size_t numSpaces = indent - column_;
                column_ = indent;
                while(numSpaces >= spaces.length)
                {
                    writeString(spaces);
                    numSpaces -= spaces.length;
                }
                writeString(spaces[0 .. numSpaces]);
            }
        }

        ///Start new line.
        void writeLineBreak(const scope char[] data = null) @safe
        {
            whitespace_ = indentation_ = true;
            ++line_;
            column_ = 0;
            writeString(data is null ? lineBreak(bestLineBreak_) : data);
        }

        ///Write a YAML version directive.
        void writeVersionDirective(const string versionText) @safe
        {
            writeString("%YAML ");
            writeString(versionText);
            writeLineBreak();
        }

        ///Write a tag directive.
        void writeTagDirective(const string handle, const string prefix) @safe
        {
            writeString("%TAG ");
            writeString(handle);
            writeString(" ");
            writeString(prefix);
            writeLineBreak();
        }
        void nextExpected(string D)() @safe
        {
            state_ = mixin("function(typeof(this)* self) { self."~D~"(); }");
        }
        void nextExpected(EmitterFunction f) @safe
        {
            state_ = f;
        }
        void callNext() @safe
        {
            state_(&this);
        }
}


private:

///RAII struct used to write out scalar values.
struct ScalarWriter(Range, CharType)
{
    invariant()
    {
        assert(emitter_.bestIndent_ > 0 && emitter_.bestIndent_ < 10,
               "Emitter bestIndent must be 1 to 9 for one-character indent hint");
    }

    private:
        @disable int opCmp(ref Emitter!(Range, CharType));
        @disable bool opEquals(ref Emitter!(Range, CharType));

        ///Used as "null" UTF-32 character.
        static immutable dcharNone = dchar.max;

        ///Emitter used to emit the scalar.
        Emitter!(Range, CharType)* emitter_;

        ///UTF-8 encoded text of the scalar to write.
        string text_;

        ///Can we split the scalar into multiple lines?
        bool split_;
        ///Are we currently going over spaces in the text?
        bool spaces_;
        ///Are we currently going over line breaks in the text?
        bool breaks_;

        ///Start and end byte of the text range we're currently working with.
        size_t startByte_, endByte_;
        ///End byte of the text range including the currently processed character.
        size_t nextEndByte_;
        ///Start and end character of the text range we're currently working with.
        long startChar_, endChar_;

    public:
        ///Construct a ScalarWriter using emitter to output text.
        this(Emitter!(Range, CharType)* emitter, string text, const bool split = true) @safe nothrow
        {
            emitter_ = emitter;
            text_ = text;
            split_ = split;
        }

        ///Write text as single quoted scalar.
        void writeSingleQuoted() @safe
        {
            emitter_.writeIndicator("\'", Yes.needWhitespace);
            spaces_ = breaks_ = false;
            resetTextPosition();

            do
            {
                const dchar c = nextChar();
                if(spaces_)
                {
                    if(c != ' ' && tooWide() && split_ &&
                       startByte_ != 0 && endByte_ != text_.length)
                    {
                        writeIndent(Flag!"ResetSpace".no);
                        updateRangeStart();
                    }
                    else if(c != ' ')
                    {
                        writeCurrentRange(Flag!"UpdateColumn".yes);
                    }
                }
                else if(breaks_)
                {
                    if(!c.isNewLine)
                    {
                        writeStartLineBreak();
                        writeLineBreaks();
                        emitter_.writeIndent();
                    }
                }
                else if((c == dcharNone || c == '\'' || c == ' ' || c.isNewLine)
                        && startChar_ < endChar_)
                {
                    writeCurrentRange(Flag!"UpdateColumn".yes);
                }
                if(c == '\'')
                {
                    emitter_.column_ += 2;
                    emitter_.writeString("\'\'");
                    startByte_ = endByte_ + 1;
                    startChar_ = endChar_ + 1;
                }
                updateBreaks(c, Flag!"UpdateSpaces".yes);
            }while(endByte_ < text_.length);

            emitter_.writeIndicator("\'", No.needWhitespace);
        }

        ///Write text as double quoted scalar.
        void writeDoubleQuoted() @safe
        {
            resetTextPosition();
            emitter_.writeIndicator("\"", Yes.needWhitespace);
            do
            {
                const dchar c = nextChar();
                //handle special characters
                if(c == dcharNone || c.among!('\"', '\\', '\u0085', '\u2028', '\u2029', '\uFEFF') ||
                   !((c >= '\x20' && c <= '\x7E') ||
                     ((c >= '\xA0' && c <= '\uD7FF') || (c >= '\uE000' && c <= '\uFFFD'))))
                {
                    if(startChar_ < endChar_)
                    {
                        writeCurrentRange(Flag!"UpdateColumn".yes);
                    }
                    if(c != dcharNone)
                    {
                        auto appender = appender!string();
                        if(const dchar es = toEscape(c))
                        {
                            appender.put('\\');
                            appender.put(es);
                        }
                        else
                        {
                            //Write an escaped Unicode character.
                            const format = c <= 255   ? "\\x%02X":
                                           c <= 65535 ? "\\u%04X": "\\U%08X";
                            formattedWrite(appender, format, cast(uint)c);
                        }

                        emitter_.column_ += appender.data.length;
                        emitter_.writeString(appender.data);
                        startChar_ = endChar_ + 1;
                        startByte_ = nextEndByte_;
                    }
                }
                if((endByte_ > 0 && endByte_ < text_.length - strideBack(text_, text_.length))
                   && (c == ' ' || startChar_ >= endChar_)
                   && (emitter_.column_ + endChar_ - startChar_ > emitter_.bestWidth_)
                   && split_)
                {
                    //text_[2:1] is ok in Python but not in D, so we have to use min()
                    emitter_.writeString(text_[min(startByte_, endByte_) .. endByte_]);
                    emitter_.writeString("\\");
                    emitter_.column_ += startChar_ - endChar_ + 1;
                    startChar_ = max(startChar_, endChar_);
                    startByte_ = max(startByte_, endByte_);

                    writeIndent(Flag!"ResetSpace".yes);
                    if(charAtStart() == ' ')
                    {
                        emitter_.writeString("\\");
                        ++emitter_.column_;
                    }
                }
            }while(endByte_ < text_.length);
            emitter_.writeIndicator("\"", No.needWhitespace);
        }

        ///Write text as folded block scalar.
        void writeFolded() @safe
        {
            initBlock('>');
            bool leadingSpace = true;
            spaces_ = false;
            breaks_ = true;
            resetTextPosition();

            do
            {
                const dchar c = nextChar();
                if(breaks_)
                {
                    if(!c.isNewLine)
                    {
                        if(!leadingSpace && c != dcharNone && c != ' ')
                        {
                            writeStartLineBreak();
                        }
                        leadingSpace = (c == ' ');
                        writeLineBreaks();
                        if(c != dcharNone){emitter_.writeIndent();}
                    }
                }
                else if(spaces_)
                {
                    if(c != ' ' && tooWide())
                    {
                        writeIndent(Flag!"ResetSpace".no);
                        updateRangeStart();
                    }
                    else if(c != ' ')
                    {
                        writeCurrentRange(Flag!"UpdateColumn".yes);
                    }
                }
                else if(c == dcharNone || c.isNewLine || c == ' ')
                {
                    writeCurrentRange(Flag!"UpdateColumn".yes);
                    if(c == dcharNone){emitter_.writeLineBreak();}
                }
                updateBreaks(c, Flag!"UpdateSpaces".yes);
            }while(endByte_ < text_.length);
        }

        ///Write text as literal block scalar.
        void writeLiteral() @safe
        {
            initBlock('|');
            breaks_ = true;
            resetTextPosition();

            do
            {
                const dchar c = nextChar();
                if(breaks_)
                {
                    if(!c.isNewLine)
                    {
                        writeLineBreaks();
                        if(c != dcharNone){emitter_.writeIndent();}
                    }
                }
                else if(c == dcharNone || c.isNewLine)
                {
                    writeCurrentRange(Flag!"UpdateColumn".no);
                    if(c == dcharNone){emitter_.writeLineBreak();}
                }
                updateBreaks(c, Flag!"UpdateSpaces".no);
            }while(endByte_ < text_.length);
        }

        ///Write text as plain scalar.
        void writePlain() @safe
        {
            if(emitter_.context_ == Emitter!(Range, CharType).Context.root){emitter_.openEnded_ = true;}
            if(text_ == ""){return;}
            if(!emitter_.whitespace_)
            {
                ++emitter_.column_;
                emitter_.writeString(" ");
            }
            emitter_.whitespace_ = emitter_.indentation_ = false;
            spaces_ = breaks_ = false;
            resetTextPosition();

            do
            {
                const dchar c = nextChar();
                if(spaces_)
                {
                    if(c != ' ' && tooWide() && split_)
                    {
                        writeIndent(Flag!"ResetSpace".yes);
                        updateRangeStart();
                    }
                    else if(c != ' ')
                    {
                        writeCurrentRange(Flag!"UpdateColumn".yes);
                    }
                }
                else if(breaks_)
                {
                    if(!c.isNewLine)
                    {
                        writeStartLineBreak();
                        writeLineBreaks();
                        writeIndent(Flag!"ResetSpace".yes);
                    }
                }
                else if(c == dcharNone || c.isNewLine || c == ' ')
                {
                    writeCurrentRange(Flag!"UpdateColumn".yes);
                }
                updateBreaks(c, Flag!"UpdateSpaces".yes);
            }while(endByte_ < text_.length);
        }

    private:
        ///Get next character and move end of the text range to it.
        @property dchar nextChar() pure @safe
        {
            ++endChar_;
            endByte_ = nextEndByte_;
            if(endByte_ >= text_.length){return dcharNone;}
            const c = text_[nextEndByte_];
            //c is ascii, no need to decode.
            if(c < 0x80)
            {
                ++nextEndByte_;
                return c;
            }
            return decode(text_, nextEndByte_);
        }

        ///Get character at start of the text range.
        @property dchar charAtStart() const pure @safe
        {
            size_t idx = startByte_;
            return decode(text_, idx);
        }

        ///Is the current line too wide?
        @property bool tooWide() const pure @safe nothrow
        {
            return startChar_ + 1 == endChar_ &&
                   emitter_.column_ > emitter_.bestWidth_;
        }

        ///Determine hints (indicators) for block scalar.
        size_t determineBlockHints(char[] hints, uint bestIndent) const pure @safe
        {
            size_t hintsIdx;
            if(text_.length == 0)
                return hintsIdx;

            dchar lastChar(const string str, ref size_t end)
            {
                size_t idx = end = end - strideBack(str, end);
                return decode(text_, idx);
            }

            size_t end = text_.length;
            const last = lastChar(text_, end);
            const secondLast = end > 0 ? lastChar(text_, end) : 0;

            if(text_[0].isNewLine || text_[0] == ' ')
            {
                hints[hintsIdx++] = cast(char)('0' + bestIndent);
            }
            if(!last.isNewLine)
            {
                hints[hintsIdx++] = '-';
            }
            else if(std.utf.count(text_) == 1 || secondLast.isNewLine)
            {
                hints[hintsIdx++] = '+';
            }
            return hintsIdx;
        }

        ///Initialize for block scalar writing with specified indicator.
        void initBlock(const char indicator) @safe
        {
            char[4] hints;
            hints[0] = indicator;
            const hintsLength = 1 + determineBlockHints(hints[1 .. $], emitter_.bestIndent_);
            emitter_.writeIndicator(hints[0 .. hintsLength], Yes.needWhitespace);
            if(hints.length > 0 && hints[$ - 1] == '+')
            {
                emitter_.openEnded_ = true;
            }
            emitter_.writeLineBreak();
        }

        ///Write out the current text range.
        void writeCurrentRange(const Flag!"UpdateColumn" updateColumn) @safe
        {
            emitter_.writeString(text_[startByte_ .. endByte_]);
            if(updateColumn){emitter_.column_ += endChar_ - startChar_;}
            updateRangeStart();
        }

        ///Write line breaks in the text range.
        void writeLineBreaks() @safe
        {
            foreach(const dchar br; text_[startByte_ .. endByte_])
            {
                if(br == '\n'){emitter_.writeLineBreak();}
                else
                {
                    char[4] brString;
                    const bytes = encode(brString, br);
                    emitter_.writeLineBreak(brString[0 .. bytes]);
                }
            }
            updateRangeStart();
        }

        ///Write line break if start of the text range is a newline.
        void writeStartLineBreak() @safe
        {
            if(charAtStart == '\n'){emitter_.writeLineBreak();}
        }

        ///Write indentation, optionally resetting whitespace/indentation flags.
        void writeIndent(const Flag!"ResetSpace" resetSpace) @safe
        {
            emitter_.writeIndent();
            if(resetSpace)
            {
                emitter_.whitespace_ = emitter_.indentation_ = false;
            }
        }

        ///Move start of text range to its end.
        void updateRangeStart() pure @safe nothrow
        {
            startByte_ = endByte_;
            startChar_ = endChar_;
        }

        ///Update the line breaks_ flag, optionally updating the spaces_ flag.
        void updateBreaks(in dchar c, const Flag!"UpdateSpaces" updateSpaces) pure @safe
        {
            if(c == dcharNone){return;}
            breaks_ = (c.isNewLine != 0);
            if(updateSpaces){spaces_ = c == ' ';}
        }

        ///Move to the beginning of text.
        void resetTextPosition() pure @safe nothrow
        {
            startByte_ = endByte_ = nextEndByte_ = 0;
            startChar_ = endChar_ = -1;
        }
}
