// Written in the D programming language.

/*
   Copyright: Copyright The D Language Foundation 2000-2013.

   License: $(HTTP boost.org/LICENSE_1_0.txt, Boost License 1.0).

   Authors: $(HTTP walterbright.com, Walter Bright), $(HTTP erdani.com,
   Andrei Alexandrescu), and Kenji Hara

   Source: $(PHOBOSSRC std/format/internal/read.d)
 */
module std.format.internal.read;

import std.range.primitives : ElementEncodingType, ElementType, isInputRange;

import std.traits : isAggregateType, isArray, isAssociativeArray,
    isDynamicArray, isFloatingPoint, isIntegral, isSomeChar, isSomeString,
    isStaticArray, StringTypeOf;

import std.format.spec : FormatSpec;

package(std.format):

void skipData(Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
{
    import std.ascii : isDigit;
    import std.conv : text;
    import std.range.primitives : empty, front, popFront;

    switch (spec.spec)
    {
        case 'c': input.popFront(); break;
        case 'd':
            if (input.front == '+' || input.front == '-') input.popFront();
            goto case 'u';
        case 'u':
            while (!input.empty && isDigit(input.front)) input.popFront();
            break;
        default:
            assert(false,
                   text("Format specifier not understood: %", spec.spec));
    }
}

private template acceptedSpecs(T)
{
    static if (isIntegral!T)
        enum acceptedSpecs = "bdosuxX";
    else static if (isFloatingPoint!T)
        enum acceptedSpecs = "seEfgG";
    else static if (isSomeChar!T)
        enum acceptedSpecs = "bcdosuxX";    // integral + 'c'
    else
        enum acceptedSpecs = "";
}

T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isInputRange!Range && is(immutable T == immutable bool))
{
    import std.algorithm.searching : find;
    import std.conv : parse, text;
    import std.format : enforceFmt, unformatValue;

    if (spec.spec == 's') return parse!T(input);

    enforceFmt(find(acceptedSpecs!long, spec.spec).length,
               text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof));

    return unformatValue!long(input, spec) != 0;
}

T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isInputRange!Range && is(T == typeof(null)))
{
    import std.conv : parse, text;
    import std.format : enforceFmt;

    enforceFmt(spec.spec == 's',
               text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof));

    return parse!T(input);
}

T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isInputRange!Range && isIntegral!T && !is(T == enum) && isSomeChar!(ElementType!Range))
{
    import std.algorithm.searching : find;
    import std.conv : parse, text;
    import std.format : enforceFmt, FormatException;

    if (spec.spec == 'r')
    {
        static if (is(immutable ElementEncodingType!Range == immutable char)
                || is(immutable ElementEncodingType!Range == immutable byte)
                || is(immutable ElementEncodingType!Range == immutable ubyte))
            return rawRead!T(input);
        else
            throw new FormatException(
                "The raw read specifier %r may only be used with narrow strings and ranges of bytes."
            );
    }

    enforceFmt(find(acceptedSpecs!T, spec.spec).length,
               text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof));

    enforceFmt(spec.width == 0, "Parsing integers with a width specification is not implemented");   // TODO

    immutable uint base =
        spec.spec == 'x' || spec.spec == 'X' ? 16 :
        spec.spec == 'o' ? 8 :
        spec.spec == 'b' ? 2 :
        spec.spec == 's' || spec.spec == 'd' || spec.spec == 'u' ? 10 : 0;
    assert(base != 0, "base must be not equal to zero");

    return parse!T(input, base);

}

T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isFloatingPoint!T && !is(T == enum) && isInputRange!Range
    && isSomeChar!(ElementType!Range)&& !is(Range == enum))
{
    import std.algorithm.searching : find;
    import std.conv : parse, text;
    import std.format : enforceFmt, FormatException;

    if (spec.spec == 'r')
    {
        static if (is(immutable ElementEncodingType!Range == immutable char)
                || is(immutable ElementEncodingType!Range == immutable byte)
                || is(immutable ElementEncodingType!Range == immutable ubyte))
            return rawRead!T(input);
        else
            throw new FormatException(
                "The raw read specifier %r may only be used with narrow strings and ranges of bytes."
            );
    }

    enforceFmt(find(acceptedSpecs!T, spec.spec).length,
               text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof));

    return parse!T(input);
}

T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isInputRange!Range && isSomeChar!T && !is(T == enum) && isSomeChar!(ElementType!Range))
{
    import std.algorithm.searching : find;
    import std.conv : to, text;
    import std.range.primitives : empty, front, popFront;
    import std.format : enforceFmt, unformatValue;

    if (spec.spec == 's' || spec.spec == 'c')
    {
        auto result = to!T(input.front);
        input.popFront();
        return result;
    }

    enforceFmt(find(acceptedSpecs!T, spec.spec).length,
               text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof));

    static if (T.sizeof == 1)
        return unformatValue!ubyte(input, spec);
    else static if (T.sizeof == 2)
        return unformatValue!ushort(input, spec);
    else static if (T.sizeof == 4)
        return unformatValue!uint(input, spec);
    else
        static assert(false, T.stringof ~ ".sizeof must be 1, 2, or 4 not " ~
                      to!string(T.sizeof));
}

T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char fmt)
if (isInputRange!Range && is(StringTypeOf!T) && !isAggregateType!T && !is(T == enum))
{
    import std.conv : text;
    import std.range.primitives : empty, front, popFront, put;
    import std.format : enforceFmt;

    const spec = fmt.spec;
    if (spec == '(')
    {
        return unformatRange!T(input, fmt);
    }
    enforceFmt(spec == 's',
               text("Wrong unformat specifier '%", spec , "' for ", T.stringof));

    static if (isStaticArray!T)
    {
        T result;
        auto app = result[];
    }
    else
    {
        import std.array : appender;
        auto app = appender!T();
    }
    if (fmt.trailing.empty)
    {
        for (; !input.empty; input.popFront())
        {
            static if (isStaticArray!T)
                if (app.empty)
                    break;
            app.put(input.front);
        }
    }
    else
    {
        immutable end = fmt.trailing.front;
        for (; !input.empty && input.front != end; input.popFront())
        {
            static if (isStaticArray!T)
                if (app.empty)
                    break;
            app.put(input.front);
        }
    }
    static if (isStaticArray!T)
    {
        enforceFmt(app.empty, "need more input");
        return result;
    }
    else
        return app.data;
}

T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char fmt)
if (isInputRange!Range && isArray!T && !is(StringTypeOf!T) && !isAggregateType!T
    && !is(T == enum))
{
    import std.conv : parse, text;
    import std.format : enforceFmt;

    const spec = fmt.spec;
    if (spec == '(')
    {
        return unformatRange!T(input, fmt);
    }

    enforceFmt(spec == 's',
               text("Wrong unformat specifier '%", spec , "' for ", T.stringof));

    return parse!T(input);
}

T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char fmt)
if (isInputRange!Range && isAssociativeArray!T && !is(T == enum))
{
    import std.conv : parse, text;
    import std.format : enforceFmt;

    const spec = fmt.spec;
    if (spec == '(')
    {
        return unformatRange!T(input, fmt);
    }

    enforceFmt(spec == 's',
               text("Wrong unformat specifier '%", spec , "' for ", T.stringof));

    return parse!T(input);
}

/*
 * Function that performs raw reading. Used by unformatValue
 * for integral and float types.
 */
private T rawRead(T, Range)(ref Range input)
if (is(immutable ElementEncodingType!Range == immutable char)
    || is(immutable ElementEncodingType!Range == immutable byte)
    || is(immutable ElementEncodingType!Range == immutable ubyte))
{
    import std.range.primitives : popFront;

    union X
    {
        ubyte[T.sizeof] raw;
        T typed;
    }
    X x;
    foreach (i; 0 .. T.sizeof)
    {
        static if (isSomeString!Range)
        {
            x.raw[i] = input[0];
            input = input[1 .. $];
        }
        else
        {
            // TODO: recheck this
            x.raw[i] = input.front;
            input.popFront();
        }
    }
    return x.typed;
}

private T unformatRange(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
in (spec.spec == '(', "spec.spec must be '(' not " ~ spec.spec)
{
    import std.range.primitives : empty, front, popFront;
    import std.format : enforceFmt, format;

    T result;
    static if (isStaticArray!T)
    {
        size_t i;
    }

    const(Char)[] cont = spec.trailing;
    for (size_t j = 0; j < spec.trailing.length; ++j)
    {
        if (spec.trailing[j] == '%')
        {
            cont = spec.trailing[0 .. j];
            break;
        }
    }

    bool checkEnd()
    {
        return input.empty || !cont.empty && input.front == cont.front;
    }

    if (!checkEnd())
    {
        for (;;)
        {
            auto fmt = FormatSpec!Char(spec.nested);
            fmt.readUpToNextSpec(input);
            enforceFmt(!input.empty, "Unexpected end of input when parsing range");

            static if (isStaticArray!T)
            {
                result[i++] = unformatElement!(typeof(T.init[0]))(input, fmt);
            }
            else static if (isDynamicArray!T)
            {
                import std.conv : WideElementType;
                result ~= unformatElement!(WideElementType!T)(input, fmt);
            }
            else static if (isAssociativeArray!T)
            {
                auto key = unformatElement!(typeof(T.init.keys[0]))(input, fmt);
                fmt.readUpToNextSpec(input);        // eat key separator

                result[key] = unformatElement!(typeof(T.init.values[0]))(input, fmt);
            }

            static if (isStaticArray!T)
            {
                enforceFmt(i <= T.length,
                           "Too many format specifiers for static array of length %d".format(T.length));
            }

            if (spec.sep !is null)
                fmt.readUpToNextSpec(input);
            auto sep = spec.sep !is null ? spec.sep : fmt.trailing;

            if (checkEnd())
                break;

            if (!sep.empty && input.front == sep.front)
            {
                while (!sep.empty)
                {
                    enforceFmt(!input.empty,
                               "Unexpected end of input when parsing range separator");
                    enforceFmt(input.front == sep.front,
                               "Unexpected character when parsing range separator");
                    input.popFront();
                    sep.popFront();
                }
            }
        }
    }
    static if (isStaticArray!T)
    {
        enforceFmt(i == T.length,
                   "Too few (%d) format specifiers for static array of length %d".format(i, T.length));
    }
    return result;
}

T unformatElement(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isInputRange!Range)
{
    import std.conv : parseElement;
    import std.format.read : unformatValue;

    static if (isSomeString!T)
    {
        if (spec.spec == 's')
        {
            return parseElement!T(input);
        }
    }
    else static if (isSomeChar!T)
    {
        if (spec.spec == 's')
        {
            return parseElement!T(input);
        }
    }

    return unformatValue!T(input, spec);
}
