module util.money;

import std.traits : isSomeString, EnumMembers;

/**
 * Basic information about a monetary currency, as defined by ISO 4217.
 * https://en.wikipedia.org/wiki/ISO_4217
 */
struct Currency {
    // The common name of the currency.
    string name;
    /// The common 3-character code for the currency, like "USD".
    char[3] code;
    /// The number of digits after the decimal place that the currency supports.
    ubyte fractionalDigits;
    /// The ISO 4217 numeric code for the currency.
    ushort numericCode;
    /// The symbol used when writing monetary values of this currency.
    string symbol;

    static Currency ofCode(S)(in S code) if (isSomeString!S) {
        if (code.length != 3) {
            throw new Exception("Invalid currency code: " ~ code);
        }
        static foreach (c; ALL_CURRENCIES) {
            if (c.code == code) return c;
        }
        throw new Exception("Unknown currency code: " ~ code);
    }

    string codeString() const {
        import std.conv : to;
        return code.to!string;
    }
}

/**
 * An enumeration defining all available currencies. This is generated at
 * compile time by reading currency data from CSV files and generating a list
 * of currency declarations.
 */
mixin("enum Currencies : Currency {\n" ~ getCurrenciesEnumMembers() ~ "\n}");

/**
 * A list of all currencies, as a convenience for getting all members of the
 * `Currencies` enum.
 */
immutable(Currency[]) ALL_CURRENCIES = cast(Currency[]) [EnumMembers!Currencies];

private Currency[] readCurrenciesFromFile() {
    import std.csv;
    import std.stdio;
    import std.algorithm;
    import std.typecons;
    import std.array;
    import std.string;
    import std.conv;
    // First read the list of known currency symbols and use it as a lookup table.
    string[string] knownCurrencySymbols;
    const string currencySymbolsFile = import("currency_symbols.csv");
    foreach (record; currencySymbolsFile.csvReader!(Tuple!(string, string))) {
        string code = record[0].strip();
        string symbol = record[1].strip();
        knownCurrencySymbols[code] = symbol;
    }
    // Then read the list of currencies.
    auto app = appender!(Currency[]);
    auto codes = appender!(string[]);
    const string currenciesFile = import("currency_codes_ISO4217.csv");
    foreach (record; currenciesFile.csvReader!(Tuple!(string, string, string, string, string, string))) {
        string currencyName = record[1].strip();
        string code = record[2].strip();
        string numericCode = record[3].strip();
        string minorUnit = record[4].strip();
        string withdrawalDate = record[5].strip();
        string symbol;
        if (code in knownCurrencySymbols) {
            symbol = knownCurrencySymbols[code];
        } else {
            symbol = "$";
        }
        if (
            withdrawalDate.length > 0 ||
            canFind(codes[], code) ||
            code.length != 3
        ) {
            continue;
        }
        if (minorUnit == "-") {
            minorUnit = "0";
        }
        app ~= Currency(currencyName, code[0..3], minorUnit.to!ubyte, numericCode.to!ushort, symbol);
        codes ~= code;
    }
    return app[];
}

private string getCurrenciesEnumMembers() {
    import std.algorithm;
    import std.array;
    import std.format;
    import std.conv;
    auto currencies = readCurrenciesFromFile();
    return currencies
        .map!(c => format!"%s = Currency(\"%s\", \"%s\", %d, %d, \"%s\")"(
            c.code,
            c.name,
            c.code,
            c.fractionalDigits,
            c.numericCode,
            c.symbol
        ))
        .joiner(",\n")
        .array.to!string;
}

unittest {
    assert(Currency.ofCode("USD") == Currencies.USD);
}

/**
 * A monetary value consisting of an integer value, and a currency. The value
 * is interpreted as a multiple of the smallest denomination of the currency,
 * so for example, with USD currency, a value of 123 indicates $1.23.
 */
struct MoneyValue {
    immutable Currency currency;
    immutable long value;

    int opCmp(in MoneyValue other) const {
        if (other.currency != this.currency) return 0;
        if (this.value < other.value) return -1;
        if (this.value > other.value) return 1;
        return 0;
    }

    MoneyValue opBinary(string op)(in MoneyValue rhs) const {
        if (rhs.currency != this.currency)
            throw new Exception("Cannot perform binary operations on MoneyValues with different currencies.");
        static if (op == "+") return MoneyValue(currency, this.value + rhs.value);
        static if (op == "-") return MoneyValue(currency, this.value - rhs.value);
        static assert(false, "Operator " ~ op ~ " is not supported.");
    }

    MoneyValue opBinary(string op)(int rhs) const {
        static if (op == "+") return MoneyValue(currency, this.value + rhs);
        static if (op == "-") return MoneyValue(currency, this.value - rhs);
        static if (op == "*") return MoneyValue(currency, this.value * rhs);
        static if (op == "/") return MoneyValue(currency, this.value / rhs);
        static assert(false, "Operator " ~ op ~ " is not supported.");
    }

    MoneyValue opUnary(string op)() const {
        static if (op == "-") return MoneyValue(currency, -this.value);
        static assert(false, "Operator " ~ op ~ " is not supported.");
    }

    double toFloatingPoint() const {
        double factor = 1;
        for (int i = 0; i < currency.fractionalDigits; i++) {
            factor *= 10;
        }
        return value / factor;
    }
}