/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * This module provides functions for converting between different types.
 *
 * Copyright: Eugene Wissner 2017-2019.
 * License: $(LINK2 https://www.mozilla.org/en-US/MPL/2.0/,
 *                  Mozilla Public License, v. 2.0).
 * Authors: $(LINK2 mailto:info@caraus.de, Eugene Wissner)
 * Source: $(LINK2 https://github.com/caraus-ecms/tanya/blob/master/source/tanya/conv.d,
 *                 tanya/conv.d)
 */
module tanya.conv;

import tanya.container.string;
import tanya.memory.allocator;
deprecated("Use tanya.memory.lifetime.emplace instead")
public import tanya.memory.lifetime : emplace;
import tanya.meta.trait;
import tanya.meta.transform;
import tanya.range;

/**
 * Thrown if a type conversion fails.
 */
final class ConvException : Exception
{
    /**
     * Params:
     *  msg  = The message for the exception.
     *  file = The file where the exception occurred.
     *  line = The line number where the exception occurred.
     *  next = The previous exception in the chain of exceptions, if any.
     */
    this(string msg,
         string file = __FILE__,
         size_t line = __LINE__,
         Throwable next = null) @nogc @safe pure nothrow
    {
        super(msg, file, line, next);
    }
}

/*
 * Converts a string $(D_PARAM range) into an integral value of type
 * $(D_PARAM T) in $(D_PARAM base).
 *
 * The convertion stops when $(D_PARAM range) is empty of if the next character
 * cannot be converted because it is not a digit (with respect to the
 * $(D_PARAM base)) or if the reading the next character would cause integer
 * overflow. The function returns the value converted so far then. The front
 * element of the $(D_PARAM range) points to the first character cannot be
 * converted or $(D_PARAM range) is empty if the whole string could be
 * converted.
 *
 * Base must be between 2 and 36 inclursive. Default base is 10.
 *
 * The function doesn't handle the sign (+ or -) or number prefixes (like 0x).
 */
package T readIntegral(T, R)(ref R range, const ubyte base = 10)
if (isInputRange!R
 && isSomeChar!(ElementType!R)
 && isIntegral!T
 && isUnsigned!T)
in (base >= 2)
in (base <= 36)
{
    T boundary = cast(T) (T.max / base);
    if (range.empty)
    {
        return T.init;
    }

    T n;
    int digit;
    do
    {
        if (range.front >= 'a')
        {
            digit = range.front - 'W';
        }
        else if (range.front >= 'A' && range.front <= 'Z')
        {
            digit = range.front - '7';
        }
        else if (range.front >= '0' && range.front <= '9')
        {
            digit = range.front - '0';
        }
        else
        {
            return n;
        }
        if (digit >= base)
        {
            return n;
        }
        n = cast(T) (n * base + digit);
        range.popFront();

        if (range.empty)
        {
            return n;
        }
    }
    while (n < boundary);

    if (range.front >= 'a')
    {
        digit = range.front - 'W';
    }
    else if (range.front >= 'A')
    {
        digit = range.front - '7';
    }
    else if (range.front >= '0')
    {
        digit = range.front - '0';
    }
    else
    {
        return n;
    }
    if (n > cast(T) ((T.max - digit) / base))
    {
        return n;
    }
    n = cast(T) (n * base + digit);
    range.popFront();

    return n;
}

/**
 * If the source type $(D_PARAM From) and the target type $(D_PARAM To) are
 * equal, does nothing. If $(D_PARAM From) can be implicitly converted to
 * $(D_PARAM To), just returns $(D_PARAM from).
 *
 * Params:
 *  To = Target type.
 *
 * Returns: $(D_PARAM from).
 */
template to(To)
{
    /**
     * Params:
     *  From = Source type.
     *  from = Source value.
     */
    ref To to(From)(ref From from)
    if (is(To == From))
    {
        return from;
    }

    /// ditto
    To to(From)(From from)
    if (is(Unqual!To == Unqual!From) || (isNumeric!From && isFloatingPoint!To))
    {
        return from;
    }
}

///
@nogc nothrow pure @safe unittest
{
    auto val = 5.to!int();
    assert(val == 5);
    static assert(is(typeof(val) == int));
}

/**
 * Performs checked conversion from an integral type $(D_PARAM From) to an
 * integral type $(D_PARAM To).
 *
 * Params:
 *  From = Source type.
 *  To   = Target type.
 *  from = Source value.
 *
 * Returns: $(D_PARAM from) converted to $(D_PARAM To).
 *
 * Throws: $(D_PSYMBOL ConvException) if $(D_PARAM from) is too small or too
 *         large to be represented by $(D_PARAM To).
 */
To to(To, From)(From from)
if (isIntegral!From
 && isIntegral!To
 && !is(Unqual!To == Unqual!From)
 && !is(To == enum))
{
    static if ((isUnsigned!From && isSigned!To && From.sizeof == To.sizeof)
            || From.sizeof > To.sizeof)
    {
        if (from > To.max)
        {
            throw make!ConvException(defaultAllocator,
                                     "Positive integer overflow");
        }
    }
    static if (isSigned!From)
    {
        static if (isUnsigned!To)
        {
            if (from < 0)
            {
                throw make!ConvException(defaultAllocator,
                                         "Negative integer overflow");
            }
        }
        else static if (From.sizeof > To.sizeof)
        {
            if (from < To.min)
            {
                throw make!ConvException(defaultAllocator,
                                         "Negative integer overflow");
            }
        }
    }
    static if (From.sizeof <= To.sizeof)
    {
        return from;
    }
    else static if (isSigned!To)
    {
        return cast(To) from;
    }
    else
    {
        return from & To.max;
    }
}

/**
 * Converts a floating point number to an integral type.
 *
 * Params:
 *  From = Source type.
 *  To   = Target type.
 *  from = Source value.
 *
 * Returns: Truncated $(D_PARAM from) (everything after the decimal point is
 *          dropped).
 *
 * Throws: $(D_PSYMBOL ConvException) if
 *         $(D_INLINECODE from < To.min || from > To.max).
 */
To to(To, From)(From from)
if (isFloatingPoint!From
 && isIntegral!To
 && !is(Unqual!To == Unqual!From)
 && !is(To == enum))
{
    if (from > To.max)
    {
        throw make!ConvException(defaultAllocator,
                                 "Positive number overflow");
    }
    else if (from < To.min)
    {
        throw make!ConvException(defaultAllocator,
                                 "Negative number overflow");
    }
    return cast(To) from;
}

///
@nogc pure @safe unittest
{
    assert(1.5.to!int == 1);
    assert(2147483646.5.to!int == 2147483646);
    assert((-2147483647.5).to!int == -2147483647);
    assert(2147483646.5.to!uint == 2147483646);
}

/**
 * Performs checked conversion from an integral type $(D_PARAM From) to an
 * $(D_KEYWORD enum).
 *
 * Params:
 *  From = Source type.
 *  To   = Target type.
 *  from = Source value.
 *
 * Returns: $(D_KEYWORD enum) value.
 *
 * Throws: $(D_PSYMBOL ConvException) if $(D_PARAM from) is not a member of
 *         $(D_PSYMBOL To).
 */
To to(To, From)(From from)
if (isIntegral!From && is(To == enum))
{
    foreach (m; EnumMembers!To)
    {
        if (from == m)
        {
            return m;
        }
    }
    throw make!ConvException(defaultAllocator,
                             "Value not found in enum '" ~ To.stringof ~ "'");
}

///
@nogc pure @safe unittest
{
    enum Test : int
    {
        one,
        two,
    }
    static assert(is(typeof(1.to!Test) == Test));
    assert(0.to!Test == Test.one);
    assert(1.to!Test == Test.two);
}

/**
 * Converts $(D_PARAM from) to a boolean.
 *
 * If $(D_PARAM From) is a numeric type, then `1` becomes $(D_KEYWORD true),
 * `0` $(D_KEYWORD false). Otherwise $(D_PSYMBOL ConvException) is thrown.
 *
 * If $(D_PARAM To) is a string (built-in string or $(D_PSYMBOL String)),
 * then `"true"` or `"false"` are converted to the appropriate boolean value.
 * Otherwise $(D_PSYMBOL ConvException) is thrown.
 *
 * Params:
 *  From = Source type.
 *  To   = Target type.
 *  from = Source value.
 *
 * Returns: $(D_KEYWORD from) converted to a boolean.
 *
 * Throws: $(D_PSYMBOL ConvException) if $(D_PARAM from) isn't convertible.
 */
To to(To, From)(From from)
if (isNumeric!From && is(Unqual!To == bool) && !is(Unqual!To == Unqual!From))
{
    if (from == 0)
    {
        return false;
    }
    else if (from < 0)
    {
        throw make!ConvException(defaultAllocator,
                                 "Negative number overflow");
    }
    else if (from <= 1)
    {
        return true;
    }
    throw make!ConvException(defaultAllocator,
                             "Positive number overflow");
}

///
@nogc pure @safe unittest
{
    assert(!0.0.to!bool);
    assert(0.2.to!bool);
    assert(0.5.to!bool);
    assert(1.0.to!bool);

    assert(!0.to!bool);
    assert(1.to!bool);
}

/// ditto
To to(To, From)(auto ref const From from)
if ((is(From == String) || isSomeString!From) && is(Unqual!To == bool))
{
    if (from == "true")
    {
        return true;
    }
    else if (from == "false")
    {
        return false;
    }
    throw make!ConvException(defaultAllocator,
                             "String doesn't contain a boolean value");
}

///
@nogc pure @safe unittest
{
    assert("true".to!bool);
    assert(!"false".to!bool);
    assert(String("true").to!bool);
    assert(!String("false").to!bool);

}

/**
 * Converts a boolean to $(D_PARAM To).
 *
 * If $(D_PARAM To) is a numeric type, then $(D_KEYWORD true) becomes `1`,
 * $(D_KEYWORD false) `0`.
 *
 * If $(D_PARAM To) is a $(D_PSYMBOL String), then `"true"` or `"false"`
 * is returned.
 *
 * Params:
 *  From = Source type.
 *  To   = Target type.
 *  from = Source value.
 *
 * Returns: $(D_PARAM from) converted to $(D_PARAM To).
 */
To to(To, From)(From from)
if (is(Unqual!From == bool) && isNumeric!To && !is(Unqual!To == Unqual!From))
{
    return from;
}

///
@nogc nothrow pure @safe unittest
{
    assert(true.to!float == 1.0);
    assert(true.to!double == 1.0);
    assert(true.to!ubyte == 1);
    assert(true.to!byte == 1);
    assert(true.to!ushort == 1);
    assert(true.to!short == 1);
    assert(true.to!uint == 1);
    assert(true.to!int == 1);

    assert(false.to!float == 0);
    assert(false.to!double == 0);
    assert(false.to!ubyte == 0);
    assert(false.to!byte == 0);
    assert(false.to!ushort == 0);
    assert(false.to!short == 0);
    assert(false.to!uint == 0);
    assert(false.to!int == 0);
}

/**
 * Converts a stringish range to an integral value.
 *
 * Params:
 *  From = Source type.
 *  To   = Target type.
 *  from = Source value.
 *
 * Returns: $(D_PARAM from) converted to $(D_PARAM To).
 *
 * Throws: $(D_PSYMBOL ConvException) if $(D_PARAM from) doesn't contain an
 *         integral value.
 */
To to(To, From)(auto ref From from)
if (isInputRange!From && isSomeChar!(ElementType!From) && isIntegral!To)
{
    if (from.empty)
    {
        throw make!ConvException(defaultAllocator, "Input range is empty");
    }

    static if (isSigned!To)
    {
        bool negative;
    }
    if (from.front == '-')
    {
        static if (isUnsigned!To)
        {
            throw make!ConvException(defaultAllocator,
                                     "Negative integer overflow");
        }
        else
        {
            negative = true;
            from.popFront();
        }
    }

    if (from.empty)
    {
        throw make!ConvException(defaultAllocator, "Input range is empty");
    }

    ubyte base = 10;
    if (from.front == '0')
    {
        from.popFront();
        if (from.empty)
        {
            return To.init;
        }
        else if (from.front == 'x' || from.front == 'X')
        {
            base = 16;
            from.popFront();
        }
        else if (from.front == 'b' || from.front == 'B')
        {
            base = 2;
            from.popFront();
        }
        else
        {
            base = 8;
        }
    }

    auto unsigned = readIntegral!(Unsigned!To, From)(from, base);
    if (!from.empty)
    {
        throw make!ConvException(defaultAllocator, "Integer overflow");
    }

    static if (isSigned!To)
    {
        if (negative)
        {
            auto predecessor = cast(Unsigned!To) (unsigned - 1);
            if (predecessor > cast(Unsigned!To) To.max)
            {
                throw make!ConvException(defaultAllocator,
                                         "Negative integer overflow");
            }
            return cast(To) (-(cast(Largest!(To, ptrdiff_t)) predecessor) - 1);
        }
        else if (unsigned > cast(Unsigned!To) To.max)
        {
            throw make!ConvException(defaultAllocator, "Integer overflow");
        }
        else
        {
            return unsigned;
        }
    }
    else
    {
        return unsigned;
    }
}

///
@nogc pure @safe unittest
{
    assert("1234".to!uint() == 1234);
    assert("1234".to!int() == 1234);
    assert("1234".to!int() == 1234);

    assert("0".to!int() == 0);
    assert("-0".to!int() == 0);

    assert("0x10".to!int() == 16);
    assert("0X10".to!int() == 16);
    assert("-0x10".to!int() == -16);

    assert("0b10".to!int() == 2);
    assert("0B10".to!int() == 2);
    assert("-0b10".to!int() == -2);

    assert("010".to!int() == 8);
    assert("-010".to!int() == -8);

    assert("-128".to!byte == cast(byte) -128);
}
