/*
 * This file is part of the Amalthea library.
 * Copyright (C) 2018-2026 Eugene 'Vindex' Stulin
 * Distributed under the BSL 1.0 or the GNU LGPL 3.0 or later.
 */

/**
 * The module contains some useful functions for data processing,
 * including functions for working with dynamic arrays and associative arrays,
 * functions for working with command line arguments,
 * operations on arrays as sets,
 * some functions for working with strings, JSON, etc.
 */
module amalthea.dataprocessing;

public import amalthea.libcore;

import core.exception : RangeError;

import std.algorithm;
import std.array : appender, array, byPair, replace, split;
import std.conv : to;
import std.exception : enforce;
import std.range : empty;
import std.json;
import std.format : format, formattedWrite;
import std.range.primitives : isInputRange, ElementType;
import std.traits;
import std.typecons : Flag, No, Tuple, tuple, Yes;


/*******************************************************************************
 * Implementation of the intersection operation for arrays.
 */
T calcIntersection(T)(const T[] arrays ...) {
    import std.algorithm.setops : setIntersection;
    if (arrays.length == 1) return copyArray(arrays[0]);
    T result = setIntersection(
        sort(copyArray(arrays[0])), sort(copyArray(arrays[1]))
    ).array;
    for (auto i = 2; i < arrays.length; ++i) {
        result = setIntersection(result, sort(copyArray(arrays[i]))).array;
    }
    return result;
}
///
unittest {
    int[] arr = calcIntersection([3, 4, 1, -15], [9, 3, 1], [1, 99, 3]);
    assert(arr == [1, 3]);
}


/*******************************************************************************
 * Implementation of the union operation for arrays.
 */
T calcUnion(T)(const T[] arrays ...) {
    if (arrays.length == 1) {
        return copyArray(arrays[0]);
    }
    T result = copyArray(arrays[0]);
    for (size_t i = 1; i < arrays.length; ++i) {
        foreach(el; copyArray(arrays[i])) {
            if (!canFind(result, el)) {
                result ~= el;
            }
        }
    }
    return std.algorithm.sort(result).array;
}
///
unittest {
    int[] arr = calcUnion([3, 4, 1, -15], [9, 3, 1], [1, 99, 3]);
    assert(arr == [-15, 1, 3, 4, 9, 99]);
}


/*******************************************************************************
 * Implementation of the relative complement for arrays.
 */
T calcDifference(T)(const T[] arrays ...) {
    if (arrays.length == 1) {
        return arrays[0].copyArray;
    }
    T result = setDifference(
        arrays[0].copyArray.sort,
        arrays[1].copyArray.sort
    ).array;
    for (auto i = 2; i < arrays.length; ++i) {
        result = array(setDifference(result, arrays[i].copyArray.sort));
    }
    return result;
}
deprecated alias calcComplement = calcDifference;
///
unittest {
    int[] arr = calcDifference([3, 4, 1, -15], [9, 3, 1]);
    assert(arr == [-15, 4]);
}


/*******************************************************************************
 * The function removes consecutive string duplicates.
 */
@safe pure
string removeDuplicateConsecutiveSubstring(string s, string substring) {
    while(s.canFind(substring~substring)) {
        s = s.replace(substring~substring, substring);
    }
    return s;
}
///
unittest {
    string s = removeDuplicateConsecutiveSubstring("AAAAbooeAAAuuvAww", "A");
    assert(s == "AbooeAuuvAww");
    s = removeDuplicateConsecutiveSubstring("AAAAbcooeAAAgguvAww", "AA");
    assert(s == "AAbcooeAAAgguvAww");
}


/*******************************************************************************
 * The function removes duplicate elements from an array.
 */
ref T[] removeDuplicate(T)(ref T[] arr) {
    if (arr.empty) {
        return arr;
    }
    for (auto i = arr.length - 1; i > 0; --i) {
        if (arr[0 .. i].canFind(arr[i])) {
            arr = arr.remove(i);
        }
    }
    return arr;
}
///
unittest {
    int[] values = [1, 3, 1, 7, 2, 19, 5, 2, 2];
    removeDuplicate(values);
    assert(values == [1, 3, 7, 2, 19, 5]);
}


/*******************************************************************************
 * Returns array of tuples with pairs of keys and values from associative array.
 */
auto assocArrayItems(V, K)(V[K] aarray) {
    Tuple!(K, V)[] pairs;
    foreach(item; aarray.byPair) {
        pairs ~= item;
    }
    return pairs;
}


/*******************************************************************************
 * Makes key-sorted array of tuples with pairs of keys and values
 * from associative array.
 * Params:
 *     aarray = Associative array.
 *     desc = Flag for sorting (descending/ascending). By default, No.desc.
 * Returns: Key-sorted array of tuples with elements of the associative array.
 */
auto sortAssocArrayItemsByKeys(V, K)(V[K] aarray, Flag!"desc" desc = No.desc) {
    if (!desc) {
        return aarray.assocArrayItems.sort!((a, b) => a[0] < b[0]).array;
    }
    return aarray.assocArrayItems.sort!((a, b) => a[0] > b[0]).array;
}
unittest {
    int[string] aarray = ["x": 9, "y": 7];
    auto expected = [tuple("x", 9), tuple("y", 7)];
    assert(expected == sortAssocArrayItemsByKeys(aarray));
    expected = [tuple("y", 7), tuple("x", 9)];
    assert(expected == sortAssocArrayItemsByKeys(aarray, Yes.desc));
}


/*******************************************************************************
 * Makes value-sorted array of tuples with pairs of keys and values
 * from associative array.
 * Params:
 *     aarray = Associative array.
 *     desc = Flag for sorting (descending/ascending). By default, No.desc.
 * Returns: Value-sorted array of tuples with elements of the associative array.
 */
auto sortAssocArrayItemsByValues(V, K)(
    V[K] aarray, Flag!"desc" desc = No.desc
) {
    if (!desc) {
        return aarray.assocArrayItems.sort!((a, b) => a[1] < b[1]).array;
    }
    return aarray.assocArrayItems.sort!((a, b) => a[1] > b[1]).array;
}
unittest {
    int[string] aarray = ["x": 9, "y": 7];
    auto expected = [tuple("y", 7), tuple("x", 9)];
    assert(expected == sortAssocArrayItemsByValues(aarray));
    expected = [tuple("x", 9), tuple("y", 7)];
    assert(expected == sortAssocArrayItemsByValues(aarray, Yes.desc));
}


/*******************************************************************************
 * The function arranges the associative array
 * by the required sorting method (string "by" is equal to "values" or "keys"),
 * returns an array of keys and an array of values.
 */
deprecated("Use sortAssocArrayItemsByKeys and sortAssocArrayItemsByValues")
void orderAssociativeArray(alias by, V, K)(
    in V[K] aarray, out K[] orderedKeys, out V[] orderedValues
)
if (by == "values" || by == "keys") {
    if (aarray.length == 0) {
        return;
    }
    if (by == "keys") {
        orderedKeys = aarray.keys.dup;
        orderedKeys.sort;
        orderedValues.length = orderedKeys.length;
        foreach(i, k; orderedKeys) {
            orderedValues[i] = aarray[k];
        }
    } else {
        orderedValues = aarray.values.dup;
        orderedValues.sort;
        orderedKeys.length = orderedValues.length;
        size_t[] passedIndexes;
        foreach(k; aarray.keys) {
            foreach(i, v; orderedValues) {
                if (passedIndexes.canFind(i)) {
                    continue;
                }
                if (aarray[k] == v) {
                    orderedKeys[i] = k;
                    passedIndexes ~= i;
                    break;
                }
            }
        }
    }
}


/*******************************************************************************
 * The function arranges the associative array by keys,
 * returns an array of keys and an array of values.
 */
deprecated("Use sortAssocArrayItemsByKeys")
void orderAssociativeArrayByKeys(V, K)(
    in V[K] aarray, out K[] orderedKeys, out V[] orderedValues
) {
    orderAssociativeArray!"keys"(aarray, orderedKeys, orderedValues);
}


/*******************************************************************************
 * The function arranges the associative array by values,
 * returns an array of keys and an array of values.
 */
deprecated("sortAssocArrayItemsByValues")
void orderAssociativeArrayByValues(V, K)(
    in V[K] aarray, out K[] orderedKeys, out V[] orderedValues
) {
    orderAssociativeArray!"values"(aarray, orderedKeys, orderedValues);
}


/*******************************************************************************
 * This function combines two associative arrays, returns new associative array.
 * If two arrays have the same keys, the values from the first array are used.
 */
V[K] mixAssociativeArrays(K,V)(V[K] firstArray, V[K] secondArray) {
    V[K] array = firstArray.dup;
    foreach(k, v; secondArray) {
        if (k !in array) {
            array[k] = v;
        }
    }
    return array;
}


/*******************************************************************************
 * Function to get a value by parameter.
 * Used to process command line arguments.
 * Params:
 *     args = Command line arguments.
 *     flag = Parameter with "-" or "--".
 *     altFlag = Synonym for the flat. By default, empty.
 * Returns: The value of the required parameter.
 */
@safe pure
string getOptionValue(string[] args, string flag, string altFlag = "") {
    auto tmpArgs = args.dup;
    return extractValueForFlag(tmpArgs, flag, altFlag);
}
alias getValueForFlag = getOptionValue;


/*******************************************************************************
 * Function for extracting of value by flag.
 * Unlike getOptionValue(), this function changes the passed `args`,
 * removing the flag with a value.
 * Params:
 *     args = Command line arguments. Changes while the function is running.
 *     flag = Parameter with "-" or "--".
 *     altFlag = Synonym for the flat. By default, empty.
 * Returns: The value of the required parameter.
 */
@safe pure
string extractOptionValue(ref string[] args, string flag, string altFlag = "") {
    string value;
    ssize_t index1 = -1;

    string[] flags = [flag];
    if (!altFlag.empty) {
        flags ~= altFlag;
    }

    cycle1: foreach(option; flags) {
        foreach(i, arg; args) {
            if (arg == "--") {
                break cycle1;
            }
            if (arg == option || arg.startsWith(option ~ "=")) {
                index1 = i;
                flag = option;
                break cycle1;
            }
        }
    }
    if (index1 == -1) {
        return "";
    }

    if (args[index1].startsWith(flag ~ "=")) {
        value = args[index1][(flag ~ "=").length .. $];
        auto tempArgs = args[0 .. index1];
        if (args.length > index1 + 1) {
            tempArgs ~= args[index1 + 1 .. $];
        }
        args = tempArgs;
    } else {
        bool stopOptions = false;
        ssize_t valuePos = index1 + 1;
        if (args.length <= valuePos) {
            return "";
        }
        if (args[valuePos] == "--") {
            stopOptions = true;
            valuePos++;
            if (args.length <= valuePos) {
                return "";
            }
        }
        value = args[valuePos];
        auto tempArgs = args[0 .. index1];
        if (args.length > valuePos + 1) {
            if (stopOptions) {
                tempArgs ~= "--";
            }
            tempArgs ~= args[valuePos + 1 .. $];
        }
        args = tempArgs;
    }
    return value;
}
alias extractValueForFlag = extractOptionValue;

///
unittest {
    string[] args = "app -a value1 value2 -b parameter --nothing".split;
    assert("parameter" == args.getOptionValue("-b"));
    assert("value1" == args.getOptionValue("-a"));
    assert("" == args.getOptionValue("--nothing"));
    assert(args == "app -a value1 value2 -b parameter --nothing".split);

    assert("parameter" == args.extractValueForFlag("-b"));
    assert("app -a value1 value2 --nothing".split == args);
    assert("value1" == args.extractValueForFlag("-a"));
    assert("app value2 --nothing".split == args);

    args = "-a value1 value2 -b parameter --nothing".split;
    assert("parameter" == args.getOptionValue("-b"));
    assert("value1" == args.getOptionValue("-a"));
    assert("" == args.getOptionValue("--nothing"));
    assert("parameter" == args.extractValueForFlag("-b"));
    assert("-a value1 value2 --nothing".split == args);
    assert("value1" == args.extractValueForFlag("-a"));
    assert("value2 --nothing".split == args);

    args = `build --file=/home/user/Project_1/Makefile`.split;
    assert("/home/user/Project_1/Makefile" == args.getOptionValue("--file"));

    args = "app -a - value2 -b parameter --nothing".split;
    assert("-" == args.extractValueForFlag("-a"));

    // cases with --
    args = "app -c name -f -- -strange_name -q question".split;
    assert("name" == args.extractValueForFlag("-c"));
    assert("-strange_name" == args.extractValueForFlag("-f"));
    assert("" == args.extractValueForFlag("-q"));
    args = "app -c -- name".split;
    assert("name" == args.extractValueForFlag("-c"));
}


/*******************************************************************************
 * Function to get values as array by flag.
 * Params:
 *     args = Command line arguments.
 *     flag = Parameter with "--".
 *     altFlag = Synonym for the flat. By default, empty.
 * Returns: array of values after the flag.
 */
@safe pure
string[] getOptionRange(string[] args, string flag, string altFlag = "") {
    auto argsCopy = args.dup;
    return extractOptionRange(argsCopy, flag, altFlag);
}


/*******************************************************************************
 * Function for extracting of value as array by flag.
 * Unlike getOptionRange(), this function changes the passed arguments (args),
 * removing the flag with a value.
 * Params:
 *     args = Command line arguments. Changes while the function is running.
 *     flag = Parameter with "--".
 *     altFlag = Synonym for the flat. By default, empty.
 * Returns: array of values after the flag.
 */
@safe pure
string[] extractOptionRange(
    ref string[] args, string flag, string altFlag = ""
) {
    if (flag.empty) {
        return [];
    }
    string[] range;
    ssize_t index1 = args.countUntil(flag);
    string[] flags = [flag];
    if (!altFlag.empty) {
        flags ~= altFlag;
    }
    cycle1: foreach(option; flags) {
        foreach(i, arg; args) {
            if (arg == "--") {
                break cycle1;
            }
            if (arg == option || arg.startsWith(option ~ "=")) {
                index1 = i;
                flag = option;
                break cycle1;
            }
        }
    }
    if (index1 == -1) {
        return [];
    }
    ssize_t p = index1 + 1;  // value position


    if (p == args.length) {  // end
        return []; 
    } else if (args[p].startsWith("-") && args[p] != "-" && args[p] != "--") {
        return []; 
    }

    ssize_t index2 = -1;
    foreach(i, arg; args[p .. $]) {
        if (arg == "--") {
            break;
        }
        if (arg.startsWith("-") && arg != "-") {
            index2 = index1 + i;
            break;
        }
    }
    if (index2 != -1) {
        range = args[index1 + 1 .. index2 + 1];
        args = args[0 .. index1] ~ args[index2 + 1 .. $];
    } else {
        index2 = args.length - 1;
        range = args[index1 + 1 .. index2 + 1];
        args = args[0 .. index1];
    }
    ssize_t stopOptionPos = getIndex(range, "--");
    if (stopOptionPos == -1) {
        return range;
    } else if (stopOptionPos + 1 == range.length) {
        return range[0 .. $-1];
    }
    return range[0 .. stopOptionPos] ~ range[stopOptionPos + 1 .. $];
}


///
unittest {
    string[] args = "app -a value1 value2 -b parameter --nothing".split;
    assert(["value1", "value2"] == getOptionRange(args, "-a"));
    assert(["parameter"] == getOptionRange(args, "-b"));
    string[] nothing = getOptionRange(args, "--nothing");
    assert(nothing.empty);

    args = "app --alt value1 value2 -b parameter --nothing".split;
    assert(["value1", "value2"] == getOptionRange(args, "-a", "--alt"));

    args = "app -a -b thing".split;
    string[] vacuum = getOptionRange(args, "-a");
    assert(vacuum.empty);

    args = "app -a value1 value2 -b parameter --nothing".split;
    assert("value1 value2".split == extractOptionRange(args, "-a"));
    assert("app -b parameter --nothing".split == args);

    args = "-a value1 value2 -b parameter --nothing".split;
    assert("value1 value2".split == extractOptionRange(args, "-a"));
    assert("-b parameter --nothing".split == args);

    args = "-a value1 - value3 -b parameter --nothing".split;
    assert("value1 - value3".split == extractOptionRange(args, "-a"));

    // cases with --
    args = "app -c name -f -- -strange_name x y -q Q".split;
    assert("-strange_name x y -q Q".split == extractOptionRange(args, "-f"));
    args = "app -f -- -strange_name".split;
    assert(["-strange_name"] == extractOptionRange(args, "-f"));
    args = "app -f -- -strange_name --".split;
    assert("-strange_name --".split == extractOptionRange(args, "-f"));
    args = "app -f name --".split;
    assert(["name"] == extractOptionRange(args, "-f"));
    args = "app -f name -- name2".split;
    assert(["name", "name2"] == extractOptionRange(args, "-f"));
}


/*******************************************************************************
 * The function converts integer number into an octal form, returns as a string.
 */
@safe pure
string getOctalForm(ulong value) {
    auto writer = appender!string();
    formattedWrite(writer, "%o", value);
    return writer.data;
}
alias toOctalString = getOctalForm;
///
unittest {
    short eight = 8;
    assert(getOctalForm(eight) == "10");
    assert(getOctalForm(33_261) == "100755");
}


/*******************************************************************************
 * The function converts integer number into an binary form.
 */
@safe pure
string getBinaryForm(ulong value) {
    auto writer = appender!string();
    formattedWrite(writer, "%b", value);
    return writer.data;
}


/*******************************************************************************
 * The function returns the index of the array element by value.
 * Params:
 *     haystack = The array in which the search is performed.
 *     needle = Element value in the array.
 * Returns: The index of the array element. -1 if the element was not found.
 */
ssize_t getIndex(T)(T[] haystack, T needle) {
    foreach(i, el; haystack) {
        if (el == needle) {
            return i;
        }
    }
    return -1;
}


/*******************************************************************************
 * The function returns the index of the last occurrence
 * of the specified value in the array.
 * Params:
 *     haystack = The array in which the search is performed.
 *     needle = Element value in the array.
 * Returns: The index of the last occurrence. -1 if the element was not found.
 */
ssize_t getIndexOfLast(T)(const T[] haystack, T needle) {
    foreach_reverse(i, el; haystack) {
        if (el == needle) {
            return i;
        }
    }
    return -1;
}


unittest {
    auto line = "1.2.3";
    assert(getIndex(line, '.') == 1);
    assert(getIndexOfLast(line, '.') == 3);
}


/*******************************************************************************
 * The function returns an array without unnecessary elements.
 */
T[] removeElementsByContent(T)(T[] haystack, T needle) {
    return haystack.filter!(a => a != needle).array;
}
///
unittest {
    auto arr = [1, 8, 17, 4, 23, 8, 8, 3, 13, 19, 8];
    auto res = arr.removeElementsByContent(8);
    assert(res == [1, 17, 4, 23, 3, 13, 19]);

    auto lines = ["", "straw", "", "", "one more straw", "", ""];
    auto straws = lines.removeElementsByContent("");
    assert(straws == ["straw", "one more straw"]);
}


/*******************************************************************************
 * Encode associative array using www-form-urlencoding (copied from std.uri).
 * Params:
 *     values = An associative array containing the values to be encoded.
 * Returns: A string encoded using www-form-urlencoding.
 */
@safe pure
string urlEncode(in string[string] values) {
    import std.uri : encodeComponent;
    import std.array : Appender;
    import std.format : formattedWrite;
    if (values.length == 0) return "";
    Appender!string enc;
    enc.reserve(values.length * 128);
    bool first = true;
    foreach (k, v; values) {
        if (!first) {
            enc.put('&');
        }
        formattedWrite(enc, "%s=%s", encodeComponent(k), encodeComponent(v));
        first = false;
    }
    return enc.data;
}


/*******************************************************************************
 * The function converts JSONValue to double.
 */
@safe pure
double convJSONValueToDouble(std.json.JSONValue json) {
    double number;
    static if (__traits(compiles, JSONType.float_)) {
        switch(json.type) {
            case JSONType.float_:   number = json.floating;            break;
            case JSONType.integer:  number = to!double(json.integer);  break;
            case JSONType.uinteger: number = to!double(json.uinteger); break;
            case JSONType.string:   number = to!double(json.str);      break;
            default: break;
        }
    } else {
        switch(json.type) {
            case JSON_TYPE.FLOAT:    number = json.floating;            break;
            case JSON_TYPE.INTEGER:  number = to!double(json.integer);  break;
            case JSON_TYPE.UINTEGER: number = to!double(json.uinteger); break;
            case JSON_TYPE.STRING:   number = to!double(json.str);      break;
            default: break;
        }
    }
    return number;
}


/**
 * Structure for working with UDA.
 * Used in $(DP_REF serializeToJSON)/$(DP_REF deserializeFromJSON).
 */
struct JSONAttr {
    string name;
}
/// ditto
alias J = JSONAttr;


/// Simple serialization of structures to JSON.
JSONValue serializeToJSON(T)(T obj) {
    JSONValue result = JSONValue.emptyObject;
    static if (is(T == struct)) {
        foreach (member; FieldNameTuple!T) {
            enum uda = getUDAs!(__traits(getMember, obj, member), JSONAttr);
            static if (uda.length == 1) {
                auto fieldValue = __traits(getMember, obj, member);
                alias FT = typeof(fieldValue);
                result[uda[0].name] = serializeToJSON!FT(fieldValue);
            }
        }
    } else static if (is(T : string)) {
        return JSONValue(obj);
    } else static if (std.traits.isDynamicArray!T) {
        JSONValue[] jsonArray;
        foreach(elem; obj) {
            jsonArray ~= serializeToJSON!(ElementType!T)(elem);
        }
        result = JSONValue(jsonArray);
    } else {
        return JSONValue(obj);
    }
    return result;
}
unittest {
    struct SimpleStruct {
        @J("number")
        int number;

        @J("lines")
        string[] lines;
    }
    auto simpleStruct = SimpleStruct(3, ["four", "five"]);
    JSONValue json = serializeToJSON(simpleStruct);
    assert(json == parseJSON(`{"number": 3, "lines": ["four", "five"]}`));
}


/// Simple deserialization of JSON to structures.
T deserializeFromJSON(T)(JSONValue jsonValue) {
    static if (is(T == struct)) {
        T obj;
        foreach(memb; FieldNameTuple!T) {
            alias FT = typeof(__traits(getMember, obj, memb));
            enum uda = getUDAs!(__traits(getMember, obj, memb), JSONAttr);
            static if (uda.length != 1) {
                continue;
            }
            auto n = uda[0].name;
            if (n !in jsonValue.object) {
                continue;
            }
            __traits(getMember, obj, memb) =
                deserializeFromJSON!FT(jsonValue[n]);
        }
        return obj;
    } else static if (is(T == enum)) {
        return cast(T) jsonValue.str;
    } else static if (is(T : string)) {
        if (jsonValue.type == JSONType.string) {
            return jsonValue.str;
        } else {
            return to!string(jsonValue);
        }
    } else static if (is(T : long) || is(T : double)) {
        if (jsonValue.type == JSONType.integer) {
            return cast(T) jsonValue.integer;
        } else if (jsonValue.type == JSONType.float_) {
            return cast(T) jsonValue.floating;
        } else if (jsonValue.type == JSONType.string) {
            return to!T (jsonValue.str);
        }
        return 0;
    } else static if (is(T : bool)) {
        return jsonValue.type == JSONType.true_;
    } else static if (std.traits.isDynamicArray!T) {
        T arr;
        foreach(JSONValue elem; jsonValue.array) {
            arr ~= deserializeFromJSON!(ElementType!T)(elem);
        }
        return arr;
    } else static if (std.traits.isAssociativeArray!T) {
        T aarr;
        foreach(k, v; jsonValue.object) {
            auto key = to!(KeyType!T) (k);
            auto val = deserializeFromJSON!(ValueType!T)(v);
            aarr[key] = val;
        }
        return aarr;
    }
}
unittest {
    auto json = parseJSON(`{"number": 3, "lines": ["four", "five"]}`);
    struct SimpleStruct {
        @J("number")
        int number;

        @J("lines")
        string[] lines;
    }
    auto result = deserializeFromJSON!SimpleStruct(json);
    assert(result == SimpleStruct(3, ["four", "five"]));
}


/*******************************************************************************
 * The function similar to functions "canFind" and "among".
 */
bool isAmong(alias pred = (a, b) => a == b, Value, Values...)(
    const Value value, const Values values
)
if (
    Values.length != 0
) {
    foreach (ref v; values) {
        import std.functional : binaryFun;
        if (binaryFun!pred(value, v)) return true;
    }
    return false;
}
/// ditto
bool isAmong(alias pred = (a, b) => a == b, T1, T2)(
    const T1 value, const T2[] arr
) {
    foreach (ref v; arr) {
        import std.functional : binaryFun;
        if (binaryFun!pred(value, v)) return true;
    }
    return false;
}
unittest {
    uint[] arr = [1, 2, 3, 4, 5];
    uint value = 3;
    assert(value.isAmong(arr));
    assert(!isAmong(0, arr));
    assert("word".isAmong("dog", "cat", "word"));
}


/*******************************************************************************
 * Creates new array with specified content.
 */
T[] makeFilledArray(T)(size_t len, T filler) {
    T[] arr = new T[len];
    std.algorithm.fill(arr, filler);
    return arr;
}
///
unittest {
    assert(makeFilledArray(4, 5) == [5, 5, 5, 5]);
    assert(makeFilledArray(10, ' ') == "          ");
}


/*******************************************************************************
 * Cuts off consecutive unnecessary characters from the left side.
 * Returns: truncated string.
 */
@safe pure
string stripLeft(string line, in char symbol=' ') nothrow {
    while (!line.empty && line[0] == symbol) {
        line = line[1 .. $];
    }
    return line;
}
unittest {
    assert(stripLeft("   string ") == "string ");
    assert(stripLeft("ssstring", 's') == "tring");
}


/*******************************************************************************
 * Cuts off consecutive unnecessary characters from the left side.
 * Returns: truncated string.
 */
@safe pure
string stripLeftChars(string line, in char[] symbols) nothrow {
    foreach(char ch; line.dup) {
        if (ch.isAmong(symbols)) {
            line = line[1 .. $];
        } else {
            break;
        }
    }
    return line;
}
unittest {
    auto str = " \t\t string\t";
    auto spaces = [' ', '\t'];
    auto res = "string\t";
    assert(stripLeftChars(str, spaces) == res);
}


/*******************************************************************************
 * Cuts off consecutive unnecessary characters from the right side.
 * Returns: truncated string.
 */
@safe pure
string stripRight(string line, in char symbol=' ') nothrow {
    while (!line.empty && line[$-1] == symbol) {
        line = line[0 .. $-1];
    }
    return line;
}
unittest {
    assert(stripRight("   string ") == "   string");
    assert(stripRight("ssstringgg", 'g') == "ssstrin");
}


/*******************************************************************************
 * Cuts off consecutive unnecessary characters from the right side.
 * Returns: truncated string.
 */
@safe pure
string stripRightChars(string line, in char[] symbols) nothrow {
    foreach_reverse(char ch; line.dup) {
        if (ch.isAmong(symbols)) {
            line = line[0 .. $-1];
        } else {
            break;
        }
    }
    return line;
}
unittest {
    auto str = " \t\t string\t \t";
    auto spaces = [' ', '\t'];
    auto res = " \t\t string";
    assert(stripRightChars(str, spaces) == res);
}


/// Removes the specified prefix from the input string.
@safe pure
string trimPrefix(string line, string prefix) {
    if (line.startsWith(prefix)) {
        return line[prefix.length .. $];
    }
    return line;
}


/// Removes the specified postfix from the input string.
@safe pure
string trimPostfix(string line, string postfix) {
    if (line.endsWith(postfix)) {
        return line[0 .. $ - postfix.length];
    }
    return line;
}


/*******************************************************************************
 * Counts the number of required elements in a range.
 * Params:
 *     range = Some input range.
 *     value = Value to find.
 * Returns: number of required elements.
 */
size_t countElements(R, E)(R range, E value) nothrow pure @safe
if (isInputRange!R)
do {
    size_t res;
    foreach(elem; range) {
        res += (value == elem);
    }
    return res;
}
unittest {
    int[] arr = [0, 1, 3, 1, 5, 8, 5, 1];
    assert(countElements(arr, 1) == 3);
    assert(countElements(arr, 5) == 2);
    assert(countElements(arr, 9) == 0);
}


/*******************************************************************************
 * Divides an array: makes a new array of arrays containing elements 
 * from source array. Each subarray contains specified number of elements.
 * Params:
 *     arr = Source array.
 *     n = Number of elements in each new subarray.
 */
T[] divideByElemNumber(T)(T arr, size_t n) if (std.traits.isArray!T) {
    enforce!BadArguments(arr.length % n == 0);
    T[] res;
    for (size_t i = 0; i < arr.length; i += n) {
        res ~= arr[i .. i + n];
    }
    return res;
}
unittest {
    string s = "123456";
    assert(divideByElemNumber(s, 2) == ["12", "34", "56"]);
    assert(divideByElemNumber(s, 3) == ["123", "456"]);
    bool badArgs = false;
    try {
        divideByElemNumber(s, 4);
    } catch (BadArguments e) {
        badArgs = true;
    }
    assert(badArgs);
}


/*******************************************************************************
 * Creates a new dynamic array of the same size and
 * copies the contents of the given array into it.
 * Unlike the built-in `dup` function, this function supports
 * arrays with complex const or immutable structures.
 * Params:
 *     arr = The dynamic array.
 * Returns:
 *     A copy of the required array.
 */
T[] copyArray(T)(inout(T)[] arr) {
    T[] copy = new T[arr.length];
    static if (is(T == U[], U) && !is(T == immutable(Y)[], Y)) {
        foreach(i, ref v; copy) {
            v = copyArray(arr[i]);
        }
    } else static if (is(T == immutable(W)[], W)) {
        for (size_t i = 0; i < arr.length; i++) {
            copy[i] = arr[i].idup;
        }
    } else {
        for (size_t i = 0; i < arr.length; i++) {
            // it doesn't work with postblit, only with modern copy ctor
            T elem = arr[i];
            copy[i] = elem;
        }
    }
    return copy;
}

version(unittest) struct S {  // struct for tests
    int x;
    bool[] a;
    this(ref return scope const S rhs) {
        this.x = rhs.x;
        this.a = rhs.a.dup;
    }
}
unittest {
    /*
     * Simple copy of const S[]
     */
    const S[] arr = [S(8, [true, false]), S(9, [false, true])];
    // S[] copy = arr.dup;  // It can't be compiled!
    S[] copy = copyArray(arr);   // from const array to non-const one

    assert(copy.length == arr.length);
    for (size_t i = 0; i < arr.length; i++) {
        assert(arr[i].x == copy[i].x);
        assert(arr[i].a == copy[i].a);
        assert(arr[i].a.ptr != copy[i].a.ptr);
    }

    /*
     * Copy of immutable S[]
     */
    immutable S[] immutArr = [S(6, [false, true]), S(7, [true, false])];
    S[] immutCopy = copyArray(immutArr);
    assert(immutArr.length == immutCopy.length);
    for (size_t i = 0; i < immutArr.length; i++) {
        assert(immutArr[i].x == immutCopy[i].x);
        assert(immutArr[i].a == immutCopy[i].a);
        assert(immutArr[i].a.ptr != immutCopy[i].a.ptr);
    }

    /*
     * Copy of empty S[]
     */
    const S[] emptyArr = [];
    assert(emptyArr.ptr == null);
    S[] emptyCopy = copyArray(emptyArr);
    assert(emptyCopy.ptr == null);

    /*
     * Copy of const string[][]
     */
    const string[][] str2dArray = [["ABC", "DEF"], ["GHI", "JKL"]];
    string[][] str2dArrCopy = copyArray(str2dArray);
    foreach(i, ref subArr; str2dArray) {
        foreach(j, ref elem; subArr) {
            assert(elem == str2dArrCopy[i][j]);
            assert(elem.ptr != str2dArrCopy[i][j].ptr);
        }
    }
}


/**
 * A simple data structure based on the built-in associative array,
 * but with a fixed element order.
 */
struct OrderedMap(K, V) {
    /// Underlying associative array for key-value storage.
    V[K] map;

    /// Array of keys preserving insertion order.
    K[] keys;

    /// This method returns a copy of the map.
    OrderedMap dup() const {
        OrderedMap copy;
        copy.keys.length = this.keys.length;
        foreach(i, k; this.keys) {
            copy.keys[i] = k;
            copy.map[k] = this.map[k];
        }
        return copy;
    }

    /// Inserts a key-value pair into the map.
    void put(K key, V value) {
        if (key !in map) {
            keys ~= key;
        }
        map[key] = value;
    }

    /// Returns the value associated with the given key.
    V get(K key) const {
        return map[key];
    }

    /**
     * Safely returns the value associated with the given key.
     * If the key does not exist, returns `defaultValue`.
     */
    V get(K key, V defaultValue) const {
        const V* p = key in map;
        if (p) {
            return *p;
        }
        return defaultValue;
    }

    /**
     * Removes an element by the specified key.
     * Does nothing if the key is not present.
     */
    void remove(K key) {
        if (key !in map) {
            return;
        }
        map.remove(key);
        auto i = countUntil(keys, key);
        assert(i != -1);
        keys[i .. $ - 1] = keys[i + 1 .. $];
        keys.length--;
    }

    /// Returns array of values.
    V[] values() {
        V[] result = new V[keys.length];
        foreach(k; keys) {
            result ~= map[k];
        }
        return result;
    }

    /// Returns the number of elements in the map.
    size_t length() nothrow const {
        return keys.length;
    }

    /// Returns a string representation.
    string toString() const {
        string s = "[";
        foreach(i, k; keys) {
            auto v = map[k];
            s ~= format("%s: %s", formatElem(k), formatElem(v));
            if (i + 1 != keys.length) {
                s ~= ", ";
            }
        }
        s ~= "]";
        return s;
    }

    void opIndexAssign(V v, K k) {
        put(k, v);
    }

    ref inout(V) opIndex(K key) inout {
        enforce!RangeError(key in map, "Key not found");
        return map[key];
    }

    V* opBinaryRight(string op : "in")(K key) nothrow {
        return key in map;
    }

    int opApply(int delegate(K, V) d) {
        foreach(k; keys) {
            auto result = d(k, map[k]);
            if (result) {
                return result;
            }
        }
        return 0;
    }

    private string formatElem(T)(T elem) const {
        static if (std.traits.isSomeString!T) {
            return `"` ~ escapeString(elem).to!string ~ `"`;
        }
        return elem.to!string;
    }
}
unittest {
    OrderedMap!(dstring, int) map;
    map["pigs"] = 3;
    map["geese"] = 2;
    map["dog"] = 1;
    assert(map.toString() == `["pigs": 3, "geese": 2, "dog": 1]`);
}


private @safe pure S escapeString(S)(S s) {
    return s.replace(`\`, `\\`).replace(`"`, `\"`).replace(`'`, `\'`);
}


class BadArguments : Exception { mixin RealizeException; }
