#include "symbol.hpp"
#include "variable.hpp"
#include "expression.hpp"
#include "function.hpp"
#include "scalar.hpp"
#include "array.hpp"
#include <cmath>
#include <cstdlib>

const std::string symbol::type_name = "";
const std::string no_type::type_name = "void";
const std::string any_type::type_name = "any";
const std::string variable::type_name = "variable";
const std::string binary_expression::type_name = "expression";
const std::string unary_expression::type_name = "expression";
const std::string function::type_name = "function";
const std::string array::type_name = "array";

void array::reg(interpreter_context& context) {
    context.register_operator<array>("+", [&context](const symbol& s) {
        array* na = new array();
        try {
            const array& a = s.as<array>();
            for (size_t i = 0; i < a.size(); ++i)
                na->push(symbol(context.unary_operator("+", a[i])));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });
    context.register_operator<array>("-", [&context](const symbol& s) {
        array* na = new array();
        try {
            const array& a = s.as<array>();
            for (size_t i = 0; i < a.size(); ++i)
                na->push(symbol(context.unary_operator("-", a[i])));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });

    context.register_commutative_operator<array, array>("+", [&context](const symbol& s1, const symbol& s2) {
        array* na = new array();
        try {
            const array& a1 = s1.as<array>();
            const array& a2 = s2.as<array>();
            if (a1.size() != a2.size()) {
                context.msg("error: operator 'array + array' requires arrays of equal lengths");
                throw parser_exception();
            }
            for (size_t i = 0; i < a1.size(); ++i)
                na->push(symbol(context.binary_operator("+", a1[i], a2[i])));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });

    context.register_commutative_operator<array, array>("-", [&context](const symbol& s1, const symbol& s2) {
        array* na = new array();
        try {
            const array& a1 = s1.as<array>();
            const array& a2 = s2.as<array>();
            if (a1.size() != a2.size()) {
                context.msg("error: operator 'array - array' requires arrays of equal lengths");
                throw parser_exception();
            }
            for (size_t i = 0; i < a1.size(); ++i)
                na->push(symbol(context.binary_operator("-", a1[i], a2[i])));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });

    context.register_commutative_operator<array, array>("*", [&context](const symbol& s1, const symbol& s2) {
        array* na = new array();
        try {
            const array& a1 = s1.as<array>();
            const array& a2 = s2.as<array>();
            if (a1.size() != a2.size()) {
                context.msg("error: operator 'array * array' requires arrays of equal lengths");
                throw parser_exception();
            }
            for (size_t i = 0; i < a1.size(); ++i)
                na->push(symbol(context.binary_operator("*", a1[i], a2[i])));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });

    context.register_commutative_operator<array, array>("/", [&context](const symbol& s1, const symbol& s2) {
        array* na = new array();
        try {
            const array& a1 = s1.as<array>();
            const array& a2 = s2.as<array>();
            if (a1.size() != a2.size()) {
                context.msg("error: operator 'array / array' requires arrays of equal lengths");
                throw parser_exception();
            }
            for (size_t i = 0; i < a1.size(); ++i)
                na->push(symbol(context.binary_operator("/", a1[i], a2[i])));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });

    context.register_commutative_operator<array, any_type>("*", [&context](const symbol& s1, const symbol& s2) {
        array* na = new array();
        try {
            const array& oa = s1.as<array>();
            for (const symbol& s : oa.vals)
                na->push(symbol(context.binary_operator("*", s, s2)));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });

    context.register_commutative_operator<array, any_type>("+", [&context](const symbol& s1, const symbol& s2) {
        array* na = new array();
        try {
            const array& oa = s1.as<array>();
            for (const symbol& s : oa.vals)
                na->push(symbol(context.binary_operator("+", s, s2)));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });

    context.register_operator<array, any_type>("-", [&context](const symbol& s1, const symbol& s2) {
        array* na = new array();
        try {
            const array& oa = s1.as<array>();
            for (const symbol& s : oa.vals)
                na->push(symbol(context.binary_operator("-", s, s2)));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });

    context.register_operator<any_type, array>("-", [&context](const symbol& s1, const symbol& s2) {
        array* na = new array();
        try {
            const array& oa = s2.as<array>();
            for (const symbol& s : oa.vals)
                na->push(symbol(context.binary_operator("-", s1, s)));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });

    context.register_operator<array, any_type>("/", [&context](const symbol& s1, const symbol& s2) {
        array* na = new array();
        try {
            const array& oa = s1.as<array>();
            for (const symbol& s : oa.vals)
                na->push(symbol(context.binary_operator("/", s, s2)));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });

    context.register_operator<any_type, array>("/", [&context](const symbol& s1, const symbol& s2) {
        array* na = new array();
        try {
            const array& oa = s2.as<array>();
            for (const symbol& s : oa.vals)
                na->push(symbol(context.binary_operator("/", s1, s)));
            return na;
        } catch (...) {
            delete na;
            throw;
        }
    });
}

std::string numeric_traits<large_real>::to_string(const large_real& v, const interpreter_context& context) {
    mpfr::real_exp_t dot_pos, exp;
    char* ch = mpfr_get_str(
        0, &dot_pos, 10, context.digits, v._x, MPFR_REAL_CLASS_RND_DFLT
    );

    if (!ch) return "";

    std::string s = ch;
    mpfr_free_str(ch);

    // The exponent is the dot position minus one.
    // Decrement it and check for overflow.
    exp = dot_pos;
    if (dot_pos < --exp) return "0";

    bool negative = false;
    if (s[0] == '-') {
        negative = true;
        s.erase(0, 1);
    }

    if (exp < 0) {
        if (exp > -5) {
            s.insert(0, "0.");

            for (size_t i = 0; i < size_t(-1-exp); ++i)
                s.insert(2, "0");

            while (s[s.size()-1] == '0')
                s.erase(s.size()-1, 1);

            if (s[s.size()-1] == '.')
                s.erase(s.size()-1, 1);

            return negative ? "-" + s : s;
        }
    } else {
        if (size_t(exp) < context.digits) {
            if (size_t(exp) != context.digits)
                s.insert(dot_pos, ".");

            while (s[s.size()-1] == '0')
                s.erase(s.size()-1, 1);

            if (s[s.size()-1] == '.')
                s.erase(s.size()-1, 1);

            return negative ? "-" + s : s;
        }
    }

    s.insert(1, ".");

    while (s[s.size()-1] == '0')
        s.erase(s.size()-1, 1);

    if (s[s.size()-1] == '.')
        s.erase(s.size()-1, 1);

    std::stringstream ss;
    ss << exp;
    s += "e" + (exp > 0 ? "+" + ss.str() : ss.str());

    return negative ? "-" + s : s;
}
