#include <hocon/config.hpp>
#include <hocon/config_parse_options.hpp>
#include <hocon/config_list.hpp>
#include <hocon/config_exception.hpp>
#include <internal/default_transformer.hpp>
#include <internal/resolve_context.hpp>
#include <internal/values/config_boolean.hpp>
#include <internal/values/config_null.hpp>
#include <internal/values/config_number.hpp>
#include <internal/values/config_double.hpp>
#include <internal/values/config_long.hpp>
#include <internal/values/config_int.hpp>
#include <internal/values/config_string.hpp>
#include <internal/values/simple_config_object.hpp>
#include <internal/parseable.hpp>
#include <internal/simple_includer.hpp>

#include <boost/lexical_cast.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <leatherman/util/environment.hpp>
#include <leatherman/locale/locale.hpp>

#include <cfenv>

// Mark string for translation (alias for leatherman::locale::format)
using leatherman::locale::_;

using namespace std;

namespace hocon {

    shared_config config::parse_file_any_syntax(std::string file_basename, config_parse_options options) {
        auto source = make_shared<file_name_source>();
        return simple_includer::from_basename(move(source), move(file_basename), move(options))->to_config();
    }

    shared_config config::parse_file_any_syntax(std::string file_basename) {
        return parse_file_any_syntax(move(file_basename), config_parse_options::defaults());
    }

    shared_config config::parse_string(string s, config_parse_options options)
    {
        return parseable::new_string(move(s), move(options))->parse()->to_config();
    }

    shared_config config::parse_string(string s)
    {
        return parse_string(move(s), config_parse_options());
    }

    config::config(shared_object object) : _object(move(object)) { }

    shared_object config::root() const {
        return _object;
    }

    shared_origin config::origin() const {
        return _object->origin();
    }

    shared_config config::resolve() const {
        return resolve(config_resolve_options());
    }

    shared_config config::resolve(config_resolve_options options) const {
        return resolve_with(shared_from_this(), move(options));
    }

    shared_config config::resolve_with(shared_config source) const {
        return resolve_with(source, config_resolve_options());
    }

    shared_config config::resolve_with(shared_config source, config_resolve_options options) const {
        auto resolved = resolve_context::resolve(_object, source->_object, move(options));

        if (resolved == _object) {
            return shared_from_this();
        } else {
            return make_shared<config>(dynamic_pointer_cast<const config_object>(resolved));
        }
    }

    shared_value config::has_path_peek(string const& path_expression) const {
        path raw_path = path::new_path(path_expression);
        shared_value peeked;
        try {
            peeked = _object->peek_path(raw_path);
        } catch (config_exception& ex) {
            if (_object->get_resolve_status() == resolve_status::RESOLVED) {
                throw ex;
            }
            throw config_exception(_("{1} has not been resolved, you need to call config::resolve()", raw_path.render()));
        }
        return peeked;
    }

    bool config::has_path(string const& path_expression) const {
        shared_value peeked = has_path_peek(path_expression);
        return peeked && peeked->value_type() != config_value::type::CONFIG_NULL;
    }

    bool config::has_path_or_null(string const& path) const {
        shared_value peeked = has_path_peek(path);
        return peeked != nullptr;
    }

    bool config::is_empty() const {
        return _object->is_empty();
    }

    void config::find_paths(set<pair<string, shared_ptr<const config_value>>>& entries, path parent,
                                   shared_object obj) {
        for (auto&& entry : obj->entry_set()) {
            string elem = entry.first;
            shared_value v = entry.second;
            path new_path = path::new_key(elem);
            if (!parent.empty()) {
                new_path = new_path.prepend(parent);
            }
            if (auto object = dynamic_pointer_cast<const config_object>(v)) {
                find_paths(entries, new_path, object);
            } else if (auto null_value = dynamic_pointer_cast<const config_null>(v)) {
                // nothing; nulls are conceptually not in a config
            } else {
                entries.insert(make_pair(new_path.render(), v));
            }
        }
    }

    set<pair<string, shared_ptr<const config_value>>> config::entry_set() const {
        set<pair<string, shared_ptr<const config_value>>> entries;
        find_paths(entries, path(), _object);
        return entries;
    }

    shared_value config::throw_if_null(shared_value v, config_value::type expected, path original_path) {
        if (v->value_type() == config_value::type::CONFIG_NULL) {
            // TODO Once we decide on a way of converting the type enum to a string, pass expected type string
            throw null_exception(*(v->origin()), original_path.render());
        } else {
            return v;
        }
    }

    shared_value config::find_key(shared_object self, string const& key, config_value::type expected,
                                         path original_path) {
        return throw_if_null(find_key_or_null(self, key, expected, original_path), expected, original_path);
    }

    shared_value config::find_key_or_null(shared_object self, string const& key, config_value::type expected,
                                                 path original_path) {
        shared_value v = self->peek_assuming_resolved(key, original_path);
        if (!v) {
            throw missing_exception(original_path.render());
        }

        if (expected != config_value::type::UNSPECIFIED) {
            v = default_transformer::transform(v, expected);
        }

        if (expected != config_value::type::UNSPECIFIED &&
                v->value_type() != expected &&
                v->value_type() != config_value::type::CONFIG_NULL) {
            throw wrong_type_exception(_("{1} could not be converted to the requested type", original_path.render()));
        } else {
            return v;
        }
    }

    shared_value config::find_or_null(shared_object self, path desired_path,
                                             config_value::type expected, path original_path) {
        try {
            string key = *desired_path.first();
            path next = desired_path.remainder();
            if (next.empty()) {
                return find_key_or_null(self, key, expected, original_path);
            } else {
                shared_object o = dynamic_pointer_cast<const config_object>(
                        find_key(self, key, config_value::type::OBJECT,
                                 original_path.sub_path(0, original_path.length() - next.length())));
                return find_or_null(o, next, expected, original_path);
            }
        } catch (config_exception& ex) {
            if (self->get_resolve_status() == resolve_status::RESOLVED) {
                throw ex;
            }
            throw config_exception(_("{1} has not been resolved, you need to call config::resolve()", desired_path.render()));
        }
    }

    shared_value config::find_or_null(string const& path_expression, config_value::type expected) const {
        path raw_path = path::new_path(path_expression);
        return find_or_null(raw_path, expected, raw_path);
    }

    shared_value config::find(string const& path_expression, config_value::type expected) const {
        path raw_path = path::new_path(path_expression);
        return find(raw_path, expected, raw_path);
    }

    bool config::get_is_null(string const& path_expression) const {
        shared_value v = find_or_null(path_expression, config_value::type::UNSPECIFIED);
        return v->value_type() == config_value::type::CONFIG_NULL;
    }

    shared_value config::get_value(string const& path_expression) const {
        return find(path_expression, config_value::type::UNSPECIFIED);
    }

    bool config::get_bool(string const& path_expression) const {
        shared_value v = find(path_expression, config_value::type::BOOLEAN);
        return dynamic_pointer_cast<const config_boolean>(v)->bool_value();
    }

    int config::get_int(string const& path_expression) const {
        shared_value v = find(path_expression, config_value::type::NUMBER);
        return dynamic_pointer_cast<const config_number>(v)->int_value_range_checked(path_expression);
    }

    int64_t config::get_long(string const& path_expression) const {
        shared_value v = find(path_expression, config_value::type::NUMBER);
        return dynamic_pointer_cast<const config_number>(v)->long_value();
    }

    double config::get_double(string const& path_expression) const {
        shared_value v = find(path_expression, config_value::type::NUMBER);
        return dynamic_pointer_cast<const config_number>(v)->double_value();
    }

    string config::get_string(string const& path_expression) const {
        shared_value v = find(path_expression, config_value::type::STRING);
        return dynamic_pointer_cast<const config_string>(v)->transform_to_string();
    }

    shared_ptr<const config_object> config::get_object(string const& path_expression) const {
        return dynamic_pointer_cast<const config_object>(find(path_expression, config_value::type::OBJECT));
    }

    unwrapped_value config::get_any_ref(string const& path_expression) const {
        return find(path_expression, config_value::type::UNSPECIFIED)->unwrapped();
    }

    shared_config config::get_config(string const& path_expression) const {
        return get_object(path_expression)->to_config();
    }

    shared_list config::get_list(string const& path_expression) const {
        return dynamic_pointer_cast<const config_list>(find(path_expression, config_value::type::LIST));
    }

    vector<bool> config::get_bool_list(string const& path) const {
        return get_homogeneous_unwrapped_list<bool>(path);
    }

    std::vector<int> config::get_int_list(std::string const& path) const {
        return get_homogeneous_unwrapped_list<int>(path);
    }

    std::vector<int64_t> config::get_long_list(std::string const& path) const {
        return get_homogeneous_unwrapped_list<int64_t>(path);
    }

    std::vector<double> config::get_double_list(std::string const& path) const {
        return get_homogeneous_unwrapped_list<double>(path);
    }

    std::vector<std::string> config::get_string_list(std::string const& path) const {
        return get_homogeneous_unwrapped_list<string>(path);
    }

    std::vector<shared_object> config::get_object_list(std::string const& path) const {
        auto list = get_list(path);
        vector<shared_object> object_list;
        for (auto item : *list) {
            shared_object obj = dynamic_pointer_cast<const config_object>(item);
            if (obj == nullptr) {
                throw new config_exception(_("List does not contain only config_objects."));
            }
            object_list.push_back(obj);
        }
        return object_list;
    }

    std::vector<shared_config> config::get_config_list(std::string const& path) const {
        auto list = get_list(path);
        vector<shared_config> object_list;
        for (auto item : *list) {
            shared_config obj = dynamic_pointer_cast<const config>(item);
            if (obj == nullptr) {
                throw config_exception(_("List does not contain only configs."));
            }
            object_list.push_back(obj);
        }
        return object_list;
    }

    template<>
    std::vector<int64_t> config::get_homogeneous_unwrapped_list(std::string const& path) const {
        auto list = boost::get<std::vector<unwrapped_value>>(get_list(path)->unwrapped());
        std::vector<int64_t> long_list;
        for (auto item : list) {
            // Even if the parser stored the number as an int, we want to treat it as a long.
            try {
                long_list.push_back(boost::get<int64_t>(item));
            } catch (std::exception& ex) {
                try {
                    long_list.push_back(boost::get<int>(item));
                } catch (boost::bad_get &ex) {
                    throw config_exception(_("The list did not contain only the desired type."));
                }
            }
        }
        return long_list;
    }

    duration config::get_duration(string const& path) const {
        auto v = get_value(path);
        if (auto d = dynamic_pointer_cast<const config_double>(v)) {
            return convert(d->double_value(), time_unit::MILLISECONDS);
        } else if (auto l = dynamic_pointer_cast<const config_long>(v)) {
            return convert(l->long_value(), time_unit::MILLISECONDS);
        } else if (auto i = dynamic_pointer_cast<const config_int>(v)) {
            return convert(i->long_value(), time_unit::MILLISECONDS);
        } else if (auto str = dynamic_pointer_cast<const config_string>(v)) {
            return parse_duration(str->transform_to_string(), str->origin(), path);
        } else {
            throw bad_value_exception(*v->origin(), path, _("Value at '{1}' was not a number or string.", path));
        }
    }

    int64_t config::get_duration(string const& path, time_unit unit) const {
        auto timespan = get_duration(path);
        int64_t result = 0;
        switch (unit) {
            case time_unit::NANOSECONDS:
                result = (timespan.first * 1000000000) + timespan.second;
                break;
            case time_unit::MICROSECONDS:
                result = (timespan.first * 1000000) + (timespan.second / 1000);
                break;
            case time_unit::MILLISECONDS:
                result = (timespan.first * 1000) + (timespan.second / 1000000);
                break;
            case time_unit::SECONDS:
                result = timespan.first;
                break;
            case time_unit::MINUTES:
                result = timespan.first / 60;
                break;
            case time_unit::HOURS:
                result = timespan.first / 3600;
                break;
            case time_unit::DAYS:
                result = timespan.first / 86400;
                break;
            default:
                throw config_exception(_("Not a valid time_unit"));
        }
        if ((result >= 0) != (timespan.first >= 0)) {
            throw config_exception(_("as_long: Overflow occurred during time conversion"));
        }
        return result;
    }

    duration config::convert(int64_t number, time_unit units) {
        int64_t seconds = 0;
        int nanos = 0;
        switch (units) {
            case time_unit::NANOSECONDS:
                seconds = number / 1000000000;
                nanos = number % 1000000000;
                break;
            case time_unit::MICROSECONDS:
                seconds = number / 1000000;
                nanos = (number % 1000000) * 1000;
                break;
            case time_unit::MILLISECONDS:
                seconds = number / 1000;
                nanos = (number % 1000) * 1000000;
                break;
            case time_unit::SECONDS:
                seconds = number;
                break;
            case time_unit::MINUTES:
                seconds = number * 60;
                break;
            case time_unit::HOURS:
                seconds = number * 3600;
                break;
            case time_unit::DAYS:
                seconds = number * 86400;
                break;
            default:
                throw config_exception(_("Not a valid time_unit"));
        }
        if ((number >= 0) != (seconds >= 0)) {
            throw config_exception(_("convert_long: Overflow occurred during time conversion"));
        }
        return duration(seconds, nanos);
    }

    duration config::convert(double number, time_unit units) {
        double seconds = 0;
        double nanos = 0;
        switch (units) {
            case time_unit::NANOSECONDS:
                seconds = number / 1000000000;
                nanos = fmod(number, 1000000000);
                break;
            case time_unit::MICROSECONDS:
                seconds = number / 1000000;
                nanos = fmod(number, 1000000) * 1000;
                break;
            case time_unit::MILLISECONDS:
                seconds = number / 1000;
                nanos = fmod(number, 1000) * 1000000;
                break;
            case time_unit::SECONDS:
                seconds = number;
                nanos = fmod(number, 1) * 1000000000;
                break;
            case time_unit::MINUTES:
                seconds = number * 60;
                nanos = fmod(number * 60,  1) * 1000000000;
                break;
            case time_unit::HOURS:
                seconds = number * 3600;
                nanos = fmod(number * 3600, 1) * 1000000000;
                break;
            case time_unit::DAYS:
                seconds = number * 86400;
                nanos = fmod(number * 86400, 1) * 1000000000;
                break;
            default:
                throw config_exception(_("Not a valid time_unit"));
        }
        if (!isnormal(seconds) || !isnormal(nanos)) {
            throw config_exception(_("convert_double: Overflow occurred during time conversion"));
        }
        return duration(static_cast<int64_t>(seconds), static_cast<int>(nanos));
    }

    time_unit config::get_units(string const& unit_string) {
        if (unit_string == "ns" || unit_string == "nanos" || unit_string == "nanoseconds") {
            return time_unit::NANOSECONDS;
        } else if (unit_string == "us" || unit_string == "micros" || unit_string == "microseconds") {
            return time_unit::MICROSECONDS;
        } else if (unit_string.empty() || unit_string == "ms" || unit_string == "millis" || unit_string == "milliseconds") {
            return time_unit::MILLISECONDS;
        } else if (unit_string == "s" || unit_string == "seconds") {
            return time_unit::SECONDS;
        } else if (unit_string == "m" || unit_string == "minutes") {
            return time_unit::MINUTES;
        } else if (unit_string == "h" || unit_string == "hours") {
            return time_unit::HOURS;
        } else if (unit_string == "d" || unit_string == "days") {
            return time_unit::DAYS;
        } else {
            throw config_exception(_("Could not parse time unit '{1}' (try ns, us, ms, s, m, h, or d)", unit_string));
        }
    }

    duration config::parse_duration(string input, shared_origin origin_for_exception, string path_for_exception) {
        boost::algorithm::trim(input);
        string original_unit_string = boost::algorithm::trim_left_copy_if(input, !boost::algorithm::is_alpha());
        string unit_string = original_unit_string;
        string number_string = boost::algorithm::trim_copy(input.substr(0, input.length() - unit_string.length()));

        if (number_string.empty()) {
            throw bad_value_exception(*origin_for_exception, path_for_exception, _("No number in duration value '{1}'", input));
        }

        if (unit_string.length() > 2 && unit_string.back() != 's') {
            unit_string += "s";
        }

        try {
            int64_t number = boost::lexical_cast<int64_t>(number_string);
            return convert(number, get_units(unit_string));
        } catch (boost::bad_lexical_cast& ex) {
            try {
                double number = boost::lexical_cast<double>(number_string);
                return convert(number, get_units(unit_string));
            }
            catch (boost::bad_lexical_cast& ex) {
                throw bad_value_exception(*origin_for_exception, path_for_exception, _("Value '{1}' could not be converted to a number.", number_string));
            }
        }
    }

    shared_value config::to_fallback_value() const {
        return _object;
    }

    shared_ptr<const config_mergeable> config::with_fallback(shared_ptr<const config_mergeable> other) const {
        if (auto newobj = dynamic_pointer_cast<const config_object>(_object->with_fallback(other))) {
            return newobj->to_config();
        } else {
            throw bug_or_broken_exception(_("Creating new object from config_object did not return a config_object"));
        }
    }

    bool config::operator==(config const &other) const {
        return _object == other._object;
    }

    shared_config config::with_value(string const& path_expression, shared_ptr<const config_value> value) const {
        path raw_path = path::new_path(path_expression);
        return make_shared<config>(root()->with_value(raw_path, value));
    }

    bool config::is_resolved() const {
        return root()->get_resolve_status() == resolve_status::RESOLVED;
    }

    shared_config config::without_path(string const& path_expression) const {
        path raw_path = path::new_path(path_expression);
        return make_shared<config>(root()->without_path(raw_path));
    }

    shared_config config::with_only_path(string const& path_expression) const {
        path raw_path = path::new_path(path_expression);
        return make_shared<config>(root()->with_only_path(raw_path));
    }

    shared_config config::at_key(shared_origin origin, string const& key) const {
        return root()->at_key(origin, key);
    }

    shared_config config::at_key(std::string const& key) const {
        return root()->at_key(key);
    }

    shared_config config::at_path(std::string const& path) const {
        return root()->at_path(path);
    }

    shared_includer config::default_includer() {
        static auto _default_includer = make_shared<simple_includer>(nullptr);
        return _default_includer;
    }

    void config::check_valid(shared_config reference, std::vector<std::string> restrict_to_paths) const {
        // TODO: implement this once resolve functionality is working
        throw runtime_error(_("Method not implemented"));
    }

    shared_value config::peek_path(path desired_path) const {
        return root()->peek_path(desired_path);
    }

    shared_value config::find_or_null(path path_expression, config_value::type expected,
            path original_path) const {
        return find_or_null(_object, path_expression, expected, original_path);
    }

    shared_value config::find(path path_expression, config_value::type expected, path original_path) const {
        return throw_if_null(find_or_null(_object, path_expression, expected, original_path), expected, original_path);
    }

    shared_object config::env_variables_as_config_object() {
        unordered_map<string, shared_value> values;
        leatherman::util::environment::each([&](string& k, string& v) {
            auto origin = make_shared<simple_config_origin>("env var " + k);
            values.emplace(k, make_shared<config_string>(origin, v, config_string_type::QUOTED));
            return true;
        });
        auto origin = make_shared<simple_config_origin>("env variables");
        return make_shared<simple_config_object>(origin, move(values), resolve_status::RESOLVED, false);
    }


}  // namespace hocon
