#ifndef ARRAY_HPP
#define ARRAY_HPP

#include "symbol.hpp"
#include "scalar.hpp"
#include "string.hpp"
#include "range.hpp"
#include "context.hpp"
#include "collection.hpp"
#include <vector>
#include <algorithm>
#include <utility>

// Should define:
// template<typename T>
// struct type_traits {
//     static std::string serialize(const T& t, const interpreter_context& context);
//     static const std::string type_name;
//     using symbol_t = void;
//     static const bool is_numeric;
// };

template<typename array_t, typename T, bool isnum>
struct array_reg_helper;

using dim_t   = std::vector<std::size_t>;
using range_t = std::pair<std::size_t, std::size_t>;

template<typename T>
class array : public symbol_impl_proxy<array<T>>, private std::vector<T> {
    using base = std::vector<T>;
    using symbol_t = typename type_traits<T>::symbol_t;

    dim_t dim;

    static std::size_t total_dim_(const dim_t& dim) {
        if (dim.empty()) return 0;
        std::size_t n = 1;
        for (auto d : dim) {
            n *= d;
        }

        return n;
    }

    void fill_(const array& a, const std::vector<range_t>& ra, std::size_t ri,
        std::size_t on, std::size_t pn, std::size_t oo, std::size_t po) {

        std::size_t no = a.dim[ri];
        std::size_t nn = dim[ri];
        const range_t& r = ra[ri];
        std::size_t k = 0;

        if (ri == 0) {
            for (std::size_t i = r.first; i <= r.second; ++i, ++k) {
                (*this)[k*pn + on] = a[i*po + oo];
            }
        } else {
            for (std::size_t i = r.first; i <= r.second; ++i, ++k) {
                fill_(a, ra, ri-1, k*pn + on, pn*nn, i*po + oo, po*no);
            }
        }
    }

    void simplify_dim_() {
        auto iter = dim.begin();
        while (iter != dim.end()) {
            if (*iter == 1) {
                iter = dim.erase(iter);
            } else {
                ++iter;
            }
        }
    }

public :
    using iterator = typename base::iterator;
    using const_iterator = typename base::const_iterator;
    using reverse_iterator = typename base::reverse_iterator;
    using const_reverse_iterator = typename base::const_reverse_iterator;

    array() {}
    explicit array(const array& a) = default;
    array(array&& a) = default;

    explicit array(std::size_t n, const T& t = T()) : base(n, t), dim({n}) {}
    explicit array(const dim_t& n, const T& t = T()) : base(total_dim_(n), t), dim(n) {}
    explicit array(dim_t&& n, const T& t = T()) : base(total_dim_(n), t), dim(std::move(n)) {}
    array(const array& a, const dim_t& n, const std::vector<range_t>& ra) : base(total_dim_(n)), dim(n) {
        fill_(a, ra, ra.size()-1, 0, 1, 0, 1);
        simplify_dim_();
    }

    using base::push_back;
    using base::emplace_back;
    using base::front;
    using base::back;
    using base::empty;
    using base::size;
    using base::resize;
    using base::reserve;
    using base::operator[];
    using base::begin;
    using base::rbegin;
    using base::end;
    using base::rend;

    const dim_t& dimensions() const {
        return dim;
    }

    template<typename ntype>
    range_t check_range(const interpreter_context& context, const range& r, std::size_t j = 0) const {
        if (r.begin.type_is<scalar_t<ntype>>() && r.end.type_is<scalar_t<ntype>>()) {
            auto d     = dim[j];
            auto begin = context.get_index(r.begin.as<scalar_t<ntype>>().value, d);
            auto end   = context.get_index(r.end.as<scalar_t<ntype>>().value,   d);

            if (end < begin) {
                context.error("indices in a range must be sorted in ascending order (low:high)");
                throw parser_exception();
            }

            return std::make_pair(begin, end);
        } else {
            context.error("cannot call 'operator []' with ranges that are not scalar");
            throw parser_exception();
        }
    }
    template<typename ntype>
    range_t check_id(const interpreter_context& context, const scalar_t<ntype>& s, std::size_t j = 0) const {
        auto id = context.get_index(s.value, dim[j]);
        return std::make_pair(id, id);
    }

    bool is_same_dim(const array& a) const {
        return is_same_dim(a.dim);
    }

    bool is_same_dim(const dim_t& td) const {
        const std::size_t ad = dim.size();
        if (td.size() != ad) return false;

        for (std::size_t i = 0; i < ad; ++i) {
            if (dim[i] != td[i]) return false;
        }

        return true;
    }

    array* clone() const override {
        return new array(*this);
    }

    array* evaluate(const interpreter_context& context) const override {
        // TODO: return an "array_ref" that references the array?
        return clone();
    }

    std::string serialize(const interpreter_context& context) const override {
        if (empty()) return "()";

        std::string s = "(" + type_traits<T>::serialize(front(), context);

        for (std::size_t i = 1; i < size(); ++i) {
            s += ", " + type_traits<T>::serialize((*this)[i], context);
        }

        s += ")";

        return s;
    }

    static const std::string& static_type_name() {
        static const std::string name = "array<" + type_traits<T>::type_name + ">";
        return name;
    }

    bool is_litteral() const override {
        return true;
    }

    std::pair<std::size_t,std::size_t> priority(const interpreter_context&) const override {
        return std::pair<std::size_t,std::size_t>(0, 0);
    }

    static void reg(interpreter_context& context) {
        if (context.high_precision) {
        context.register_operator<array, scalar_hp>("[]", [&context](const symbol& s1, const symbol& s2) {
            const array& a = s1.as<array>();
            if (a.empty()) return (symbol_impl*)new no_type();
            auto i = context.get_index(s2.as<scalar_hp>().value, a.size());
            return (symbol_impl*)new symbol_t(a[i]);
        });
        } else {
        context.register_operator<array, scalar>("[]", [&context](const symbol& s1, const symbol& s2) {
            const array& a = s1.as<array>();
            if (a.empty()) return (symbol_impl*)new no_type();
            auto i = context.get_index(s2.as<scalar>().value, a.size());
            return (symbol_impl*)new symbol_t(a[i]);
        });
        }

        if (context.high_precision) {
        context.register_operator<array, range>("[]", [&context](const symbol& s1, const symbol& s2) {
            const array& a = s1.as<array>();
            if (a.empty()) return (symbol_impl*)new no_type();
            const range& r = s2.as<range>();
            if (!r.begin.type_is<scalar_hp>() && !r.end.type_is<scalar_hp>()) {
                context.error("cannot call 'operator []' with ranges that are not scalar");
                throw parser_exception();
            }

            auto ra = a.check_range<scalar_hp::ntype>(context, r);
            if (ra.first == ra.second) {
                return (symbol_impl*)new symbol_t(a[ra.first]);
            } else {
                return (symbol_impl*)new array(a, {ra.second - ra.first + 1}, {ra});
            }
        });
        } else {
        context.register_operator<array, range>("[]", [&context](const symbol& s1, const symbol& s2) {
            const array& a = s1.as<array>();
            if (a.empty()) return (symbol_impl*)new no_type();
            const range& r = s2.as<range>();
            if (!r.begin.type_is<scalar>() && !r.end.type_is<scalar>()) {
                context.error("cannot call 'operator []' with ranges that are not scalar");
                throw parser_exception();
            }

            auto ra = a.check_range<scalar::ntype>(context, r);
            if (ra.first == ra.second) {
                return (symbol_impl*)new symbol_t(a[ra.first]);
            } else {
                return (symbol_impl*)new array(a, {ra.second - ra.first + 1}, {ra});
            }
        });
        }

        if (context.high_precision) {
        context.register_operator<array, collection>("[]", [&context](const symbol& s1, const symbol& s2) {
            const array&      a = s1.as<array>();
            const collection& i = s2.as<collection>();
            if (a.empty()) return (symbol_impl*)new no_type();
            const std::size_t ad = a.dim.size();
            if (i.size() != ad) {
                context.error("cannot index an '"+array::static_type_name()+"' of dimension "+
                    to_string(ad)+" with "+to_string(i.size())+" indices");
                throw parser_exception();
            }

            dim_t dim;
            std::vector<range_t> ra;
            for (std::size_t j = 0; j < ad; ++j) {
                const auto& s = i[j];
                if (s.type_is<range>()) {
                    ra.push_back(a.check_range<scalar_hp::ntype>(context, s.as<range>(), j));
                    dim.push_back(ra.back().second - ra.back().first + 1);
                } else if (s.type_is<scalar_hp>()) {
                    ra.push_back(a.check_id(context, s.as<scalar_hp>(), j));
                    dim.push_back(1);
                } else {
                    throw no_binary_operator_match_exception(array::static_type_name(), "[]", s.type_name());
                }
            }

            symbol na(new array(a, dim, ra));
            if (na.as<array>().size() == 1) {
                return (symbol_impl*)new symbol_t(na.as<array>()[0]);
            } else {
                return na.release();
            }
        });
        } else {
        context.register_operator<array, collection>("[]", [&context](const symbol& s1, const symbol& s2) {
            const array&      a = s1.as<array>();
            const collection& i = s2.as<collection>();
            if (a.empty()) return (symbol_impl*)new no_type();
            const std::size_t ad = a.dim.size();
            if (i.size() != ad) {
                context.error("cannot index an '"+array::static_type_name()+"' of dimension "+
                    to_string(ad)+" with "+to_string(i.size())+" indices");
                throw parser_exception();
            }

            dim_t dim;
            std::vector<range_t> ra;
            for (std::size_t j = 0; j < ad; ++j) {
                const auto& s = i[j];
                if (s.type_is<range>()) {
                    ra.push_back(a.check_range<scalar::ntype>(context, s.as<range>(), j));
                    dim.push_back(ra.back().second - ra.back().first + 1);
                } else if (s.type_is<scalar>()) {
                    ra.push_back(a.check_id(context, s.as<scalar>(), j));
                    dim.push_back(1);
                } else {
                    throw no_binary_operator_match_exception(array::static_type_name(), "[]", s.type_name());
                }
            }

            symbol na(new array(a, dim, ra));
            if (na.as<array>().size() == 1) {
                return (symbol_impl*)new symbol_t(na.as<array>()[0]);
            } else {
                return na.release();
            }
        });
        }

        context.register_function<array>("size", [&context](const std::vector<symbol>& args) -> symbol_impl* {
            const auto& dim = args[0].as<array>().dimensions();
            if (dim.size() == 1) {
                if (context.high_precision) {
                    return new scalar_hp(dim[0]);
                } else {
                    return new scalar(dim[0]);
                }
            } else {
                if (context.high_precision) {
                    array<scalar_hp::ntype>* a = new array<scalar_hp::ntype>(dim.size());
                    for (std::size_t i = 0; i < dim.size(); ++i) {
                        (*a)[i] = dim[i];
                    }
                    return a;
                } else {
                    array<scalar::ntype>* a = new array<scalar::ntype>(dim.size());
                    for (std::size_t i = 0; i < dim.size(); ++i) {
                        (*a)[i] = dim[i];
                    }
                    return a;
                }
            }
        });

        context.register_function<array>("reverse", [&context](const std::vector<symbol>& args) -> symbol_impl* {
            symbol sa = args[0].clone();
            array& a = sa.as<array>();
            std::reverse(a.begin(), a.end());
            return sa.release();
        });

        if (context.high_precision) {
        context.register_function<symbol_t, scalar_hp>("replicate", [&context](const std::vector<symbol>& args) -> symbol_impl* {
            std::size_t n = numeric_traits<scalar_hp::ntype>::to_uint(args[1].as<scalar_hp>().value);
            const T& t = args[0].as<symbol_t>().value;
            return new array(n, t);
        });
        } else {
        context.register_function<symbol_t, scalar>("replicate", [&context](const std::vector<symbol>& args) -> symbol_impl* {
            std::size_t n = numeric_traits<scalar::ntype>::to_uint(args[1].as<scalar>().value);
            const T& t = args[0].as<symbol_t>().value;
            return new array(n, t);
        });
        }

        if (context.high_precision) {
        context.register_function<array, scalar_hp>("replicate", [&context](const std::vector<symbol>& args) -> symbol_impl* {
            std::size_t n = numeric_traits<scalar_hp::ntype>::to_uint(args[1].as<scalar_hp>().value);
            const array& a = args[0].as<array>();
            std::size_t m = a.size();
            dim_t d = a.dimensions();
            d.insert(d.begin(), n);
            array* na = new array(d);
            for (std::size_t i = 0; i < n; ++i) {
                for (std::size_t j = 0; j < m; ++j) {
                    (*na)[i*m+j] = a[j];
                }
            }
            return na;
        });
        } else {
        context.register_function<array, scalar>("replicate", [&context](const std::vector<symbol>& args) -> symbol_impl* {
            std::size_t n = numeric_traits<scalar::ntype>::to_uint(args[1].as<scalar>().value);
            const array& a = args[0].as<array>();
            std::size_t m = a.size();
            dim_t d = a.dimensions();
            d.insert(d.begin(), n);
            array* na = new array(d);
            for (std::size_t i = 0; i < n; ++i) {
                for (std::size_t j = 0; j < m; ++j) {
                    (*na)[i*m+j] = a[j];
                }
            }
            return na;
        });
        }

        context.register_function<symbol_t, collection>("array", [&context](const std::vector<symbol>& args) -> symbol_impl* {
            const symbol_t& t = args[0].as<symbol_t>();
            const collection& c = args[1].as<collection>();
            array* a = new array(c.size()+1);
            (*a)[0] = t.value;

            std::size_t i = 1;
            for (auto& s : c) {
                if (!s.type_is<symbol_t>()) {
                    context.error("arrays cannot be heterogeneous, consider using collections");
                    context.note("trying to create an array containing both '"+
                        symbol_t::static_type_name()+"' and '"+s.type_name()+"'");
                    delete a;
                    throw parser_exception();
                }

                (*a)[i] = s.as<symbol_t>().value;
                ++i;
            }

            return a;
        });

        context.register_function<array, collection>("array", [&context](const std::vector<symbol>& args) -> symbol_impl* {
            const array& fa = args[0].as<array>();
            const collection& c = args[1].as<collection>();

            const dim_t& odim = fa.dimensions();
            const std::size_t ods = odim.size();
            dim_t ndim(ods+1);
            ndim[0] = c.size()+1;
            std::size_t nelem = 1;
            for (std::size_t i = 0; i < ods; ++i) {
                ndim[i+1] = odim[i];
                nelem *= odim[i];
            }

            array* a = new array(std::move(ndim));

            for (std::size_t j = 0; j < nelem; ++j) {
                (*a)[j] = fa[j];
            }

            std::size_t i = 1;
            for (auto& s : c) {
                if (!s.type_is<array>()) {
                    context.error("arrays cannot be heterogeneous, consider using collections");
                    context.note("trying to create an array containing both '"+
                        symbol_t::static_type_name()+"' and '"+s.type_name()+"'");
                    delete a;
                    throw parser_exception();
                }

                const array& ta = s.as<array>();
                if (!fa.is_same_dim(ta)) {
                    context.error("arrays cannot be heterogeneous, consider using collections");
                    context.note("trying to create an array containing '"+
                        static_type_name()+"' of dimensions both "+to_string(odim)+
                        " and "+to_string(ta.dimensions()));
                    delete a;
                    throw parser_exception();
                }

                for (std::size_t j = 0; j < nelem; ++j) {
                    (*a)[i*nelem+j] = ta[j];
                }

                ++i;
            }

            return a;
        });

        array_reg_helper<array, T, type_traits<T>::is_numeric>::reg(context);
    }
};

template<typename array_t, typename T>
struct array_reg_helper<array_t, T, true> {
    using symbol_t = typename type_traits<T>::symbol_t;
    static void reg(interpreter_context& context) {
        context.register_operator<array_t>("+", [&context](const symbol& s) {
            return s.clone().release();
        });
        context.register_operator<array_t>("-", [&context](const symbol& s) {
            array_t* a = new array_t(s.as<array_t>());
            for (auto& v : *a) {
                v *= -numeric_traits<T>::one;
            }
            return a;
        });

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

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

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

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

        context.register_operator<array_t, symbol_t>("^", [&context](const symbol& s1, const symbol& s2) {
            const array_t& oa = s1.as<array_t>();
            const T& t = s2.as<symbol_t>().value;
            array_t* na = new array_t(oa.size());
            for (std::size_t i = 0; i < oa.size(); ++i) {
                (*na)[i] = pow(oa[i], t);
            }
            return na;
        });

        context.register_operator<any_type, array_t>("^", [&context](const symbol& s1, const symbol& s2) {
            const array_t& oa = s2.as<array_t>();
            const T& t = s1.as<symbol_t>().value;
            array_t* na = new array_t(oa.size());
            for (std::size_t i = 0; i < oa.size(); ++i) {
                (*na)[i] = pow(t, oa[i]);
            }
            return na;
        });

        context.register_operator<array_t, any_type>("*", [&context](const symbol& s1, const symbol& s2) {
            const array_t& oa = s1.as<array_t>();
            const T& t = s2.as<symbol_t>().value;
            array_t* na = new array_t(oa.size());
            for (std::size_t i = 0; i < oa.size(); ++i) {
                (*na)[i] = oa[i]*t;
            }
            return na;
        });

        context.register_operator<any_type, array_t>("*", [&context](const symbol& s1, const symbol& s2) {
            const array_t& oa = s2.as<array_t>();
            const T& t = s1.as<symbol_t>().value;
            array_t* na = new array_t(oa.size());
            for (std::size_t i = 0; i < oa.size(); ++i) {
                (*na)[i] = t*oa[i];
            }
            return na;
        });

        context.register_operator<array_t, any_type>("+", [&context](const symbol& s1, const symbol& s2) {
            const array_t& oa = s1.as<array_t>();
            const T& t = s2.as<symbol_t>().value;
            array_t* na = new array_t(oa.size());
            for (std::size_t i = 0; i < oa.size(); ++i) {
                (*na)[i] = oa[i] + t;
            }
            return na;
        });

        context.register_operator<any_type, array_t>("+", [&context](const symbol& s1, const symbol& s2) {
            const array_t& oa = s2.as<array_t>();
            const T& t = s1.as<symbol_t>().value;
            array_t* na = new array_t(oa.size());
            for (std::size_t i = 0; i < oa.size(); ++i) {
                (*na)[i] = t + oa[i];
            }
            return na;
        });

        context.register_operator<array_t, any_type>("-", [&context](const symbol& s1, const symbol& s2) {
            const array_t& oa = s1.as<array_t>();
            const T& t = s2.as<symbol_t>().value;
            array_t* na = new array_t(oa.size());
            for (std::size_t i = 0; i < oa.size(); ++i) {
                (*na)[i] = oa[i] - t;
            }
            return na;
        });

        context.register_operator<any_type, array_t>("-", [&context](const symbol& s1, const symbol& s2) {
            const array_t& oa = s2.as<array_t>();
            const T& t = s1.as<symbol_t>().value;
            array_t* na = new array_t(oa.size());
            for (std::size_t i = 0; i < oa.size(); ++i) {
                (*na)[i] = t - oa[i];
            }
            return na;
        });

        context.register_operator<array_t, any_type>("/", [&context](const symbol& s1, const symbol& s2) {
            const array_t& oa = s1.as<array_t>();
            const T& t = s2.as<symbol_t>().value;
            array_t* na = new array_t(oa.size());
            for (std::size_t i = 0; i < oa.size(); ++i) {
                (*na)[i] = oa[i]/t;
            }
            return na;
        });

        context.register_operator<any_type, array_t>("/", [&context](const symbol& s1, const symbol& s2) {
            const array_t& oa = s2.as<array_t>();
            const T& t = s1.as<symbol_t>().value;
            array_t* na = new array_t(oa.size());
            for (std::size_t i = 0; i < oa.size(); ++i) {
                (*na)[i] = t/oa[i];
            }
            return na;
        });

        context.register_function<array_t>({"real", "re"}, [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(real(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = real(a[i]);
            }

            return na;
        });

        context.register_function<array_t>({"imaginary", "im"}, [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(imag(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = imag(a[i]);
            }

            return na;
        });

        context.register_function<array_t>("round", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(round(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = round(a[i]);
            }

            return na;
        });

        context.register_function<array_t>({"round_up", "ceil"}, [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(ceil(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = ceil(a[i]);
            }

            return na;
        });

        context.register_function<array_t>({"round_down", "floor"}, [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(floor(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = floor(a[i]);
            }

            return na;
        });

        context.register_function<array_t>({"square_root", "sqrt"}, [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(sqrt(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = sqrt(a[i]);
            }

            return na;
        });

        context.register_function<array_t>("exp", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(exp(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = exp(a[i]);
            }

            return na;
        });

        context.register_function<array_t>("log", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(log(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = log(a[i]);
            }

            return na;
        });

        context.register_function<array_t>("log2", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(log(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            auto conv = numeric_traits<T>::one/log(T(numeric_traits<T>::one + numeric_traits<T>::one));
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = log(a[i])*conv;
            }

            return na;
        });

        context.register_function<array_t>("log10", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(log10(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = log10(a[i]);
            }

            return na;
        });

        context.register_function<array_t>("cos", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(cos(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = cos(a[i]);
            }

            return na;
        });

        context.register_function<array_t>({"cosh", "ch"}, [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(cosh(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = cosh(a[i]);
            }

            return na;
        });

        context.register_function<array_t>("acos", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(acos(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = acos(a[i]);
            }

            return na;
        });

        context.register_function<array_t>("sin", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(sin(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = sin(a[i]);
            }

            return na;
        });

        context.register_function<array_t>({"sinh", "sh"}, [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(sinh(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = sinh(a[i]);
            }

            return na;
        });

        context.register_function<array_t>("asin", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(asin(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = asin(a[i]);
            }

            return na;
        });

        context.register_function<array_t>("tan", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(tan(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = tan(a[i]);
            }

            return na;
        });

        context.register_function<array_t>({"tanh", "th"}, [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(tanh(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = tanh(a[i]);
            }

            return na;
        });

        context.register_function<array_t>("atan", [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(atan(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = atan(a[i]);
            }

            return na;
        });

        context.register_function<array_t>({"abs", "norm"}, [](const std::vector<symbol>& args) {
            const array_t& a = args[0].as<array_t>();
            auto* na = new array<typename std::remove_cv<decltype(abs(std::declval<T>()))>::type>(a.dimensions());
            std::size_t n = a.size();
            for (std::size_t i = 0; i < n; ++i) {
                (*na)[i] = abs(a[i]);
            }

            return na;
        });
    }
};

template<>
struct array_reg_helper<array<std::string>, std::string, false> {
    using array_t = array<std::string>;
    using symbol_t = typename type_traits<std::string>::symbol_t;
    static void reg(interpreter_context& context);
};

#endif
