#pragma once
#include <map>
#include <vector>
#include <cctype>
#include <iosfwd>
#include <typeinfo>
#include <stdexcept>
#include <initializer_list>
#include <panda/refcnt.h>
#include <panda/string.h>
#include <panda/uri/Query.h>
#include <panda/uri/encode.h>
#include <panda/string_view.h>
#include <panda/from_chars.h>

namespace panda { namespace uri {

struct URIError : std::logic_error {
  explicit URIError (const std::string& what_arg) : logic_error(what_arg) {}
};

struct WrongScheme : URIError {
  explicit WrongScheme (const std::string& what_arg) : URIError(what_arg) {}
};

struct URI;
using URISP = iptr<URI>;

struct URI : Refcnt {
    struct Flags {
        static constexpr const int allow_suffix_reference = 1; // https://tools.ietf.org/html/rfc3986#section-4.5 uri may omit leading "SCHEME://"
        static constexpr const int query_param_semicolon  = 2; // query params are delimited by ';' instead of '&'
        static constexpr const int allow_extended_chars   = 4; // non-RFC input: allow some unencoded chars in query string
    };

    template <class TYPE1, class TYPE2 = void> struct Strict;
    struct http; struct https; struct ftp; struct socks; struct ws; struct wss; struct ssh; struct telnet; struct sftp;

    using uricreator = URI*(*)(const URI& uri);

    struct SchemeInfo {
        int        index;
        string     scheme;
        uricreator creator;
        uint16_t   default_port;
        bool       secure;
        const std::type_info* type_info;
    };

    static void register_scheme (const string& scheme, uint16_t default_port, bool secure = false);
    static void register_scheme (const string& scheme, const std::type_info*, uricreator, uint16_t default_port, bool secure = false);

    static URISP create (const string& source, int flags = 0) {
        URI temp(source, flags);
        if (temp.scheme_info) return temp.scheme_info->creator(temp);
        else                  return new URI(temp);
    }

    static URISP create (const URI& source) {
        if (source.scheme_info) return source.scheme_info->creator(source);
        else                    return new URI(source);
    }

    URI ()                                               : scheme_info(NULL), _port(0), _qrev(1), _flags(0)     {}
    URI (const string& s, int flags = 0)                 : scheme_info(NULL), _port(0), _qrev(1), _flags(flags) { parse(s); }
    URI (const string& s, const Query& q, int flags = 0) : URI(s, flags)                                        { add_query(q); }
    URI (const URI& s)                                                                                          { assign(s); }

    URI& operator= (const URI& source)    { if (this != &source) assign(source); return *this; }
    URI& operator= (const string& source) { assign(source); return *this; }

    const string& scheme        () const { return _scheme; }
    const string& user_info     () const { return _user_info; }
    const string& host          () const { return _host; }
    const string& path          () const { return _path; }
    string        path_info     () const { return _path ? decode_uri_component(_path) : string(); }
    const string& fragment      () const { return _fragment; }
    uint16_t      explicit_port () const { return _port; }
    uint16_t      default_port  () const { return scheme_info ? scheme_info->default_port : 0; }
    uint16_t      port          () const { return _port ? _port : default_port(); }
    bool          secure        () const { return scheme_info ? scheme_info->secure : false; }
    bool          empty         () const { return _scheme.empty() && _host.empty() && _path.empty() && query_string().empty() && _fragment.empty(); }

    virtual void assign (const URI& source) {
        _scheme     = source._scheme;
        scheme_info = source.scheme_info;
        _user_info  = source._user_info;
        _host       = source._host;
        _path       = source._path;
        _qstr       = source._qstr;
        _query      = source._query;
        _query.rev  = source._query.rev;
        _qrev       = source._qrev;
        _fragment   = source._fragment;
        _port       = source._port;
        _flags      = source._flags;
    }

    void assign (const string& s, int flags = 0) {
        clear();
        _flags = flags;
        parse(s);
    }

    const string& query_string () const {
        sync_query_string();
        return _qstr;
    }

    const string raw_query () const {
        sync_query_string();
        return decode_uri_component(_qstr);
    }

    Query& query () {
        sync_query();
        return _query;
    }

    const Query& query () const {
        sync_query();
        return _query;
    }

    virtual void scheme (const string& scheme) {
        _scheme = scheme;
        sync_scheme_info();
    }

    void user_info (const string& user_info) { _user_info = user_info; }
    void host      (const string& host)      { _host      = host; }
    void fragment  (const string& fragment)  { _fragment  = fragment; }
    void port      (uint16_t port)           { _port      = port; }

    void path (const string& path) {
        if (path && path.front() != '/') {
            _path = '/';
            _path += path;
        }
        else _path = path;
    }

    void query_string (const string& qstr) {
        _qstr = qstr;
        ok_qstr();
    }

    void raw_query (const string& rq) {
        _qstr.clear();
        encode_uri_component(rq, _qstr, URIComponent::query);
        ok_qstr();
    }

    void query (const string& qstr) { query_string(qstr); }
    void query (const Query& query) {
        _query = query;
        ok_query();
    }

    void add_query (const string& qstr);
    void add_query (const Query& query);

    bool has_param (const string_view& key) const {
        sync_query();
        return _query.find(key) != _query.end();
    }

    const string& param (const string_view& key) const {
        sync_query();
        const auto& cq = _query;
        auto it = cq.find(key);
        return it == cq.cend() ? _empty : it->second;
    }

    void param (const string& key, const string& val);

    void remove_param (const string_view& key) {
        sync_query();
        _query.erase(key);
    }

    auto multiparam (const string_view& key) const -> decltype(Query().equal_range(string_view())) {
        sync_query();
        return _query.equal_range(key);
    }

    void multiparam (const string& key, const std::initializer_list<string>& values);

    string explicit_location () const {
        if (!_port) return _host;
        return location();
    }

    string location () const {
        string ret(_host.length() + 6); // port is 5 chars max
        if (_host) ret += _host;
        ret += ':';
        char* buf = ret.buf(); // has exactly 5 bytes left
        auto len = ret.length();
        auto ptr_start = buf + len;
        auto res = to_chars(ptr_start, buf + ret.capacity(), port());
        assert(!res.ec); // because buf is always enough
        ret.length(len + (res.ptr - ptr_start));
        return ret;
    }

    void location (const string& newloc) {
        if (!newloc) {
            _host.clear();
            _port = 0;
            return;
        }

        size_t delim = newloc.rfind(':');
        if (delim == string::npos) _host.assign(newloc);
        else {
            size_t ipv6end = newloc.rfind(']');
            if (ipv6end != string::npos && ipv6end > delim) _host.assign(newloc);
            else {
                _host.assign(newloc, 0, delim);
                _port = 0;
                from_chars(newloc.data() + delim + 1, newloc.data() + newloc.length(), _port);
            }
        }
    }

    std::vector<string> path_segments () const;

    template <class It>
    void path_segments (It begin, It end) {
        _path.clear();
        for (auto it = begin; it != end; ++it) {
            if (!it->length()) continue;
            _path += '/';
            _encode_uri_component_append(*it, _path, URIComponent::path_segment);
        }
    }

    void path_segments (std::initializer_list<string_view> l) { path_segments(l.begin(), l.end()); }

    string to_string (bool relative = false) const;
    string relative  () const { return to_string(true); }

    bool equals (const URI& uri) const {
        if (_path != uri._path || _host != uri._host || _user_info != uri._user_info || _fragment != uri._fragment || _scheme != uri._scheme) return false;
        if (_port != uri._port && port() != uri.port()) return false;
        sync_query_string();
        uri.sync_query_string();
        return _qstr == uri._qstr;
    }

    void swap (URI& uri);

    string user () const;
    void   user (const string& user);

    string password () const;
    void   password (const string& password);

    virtual ~URI () {}

protected:
    SchemeInfo* scheme_info;

    virtual void parse (const string&);

    static SchemeInfo* get_scheme_info (const std::type_info*);

private:
    string           _scheme;
    string           _user_info;
    string           _host;
    string           _path;
    string           _fragment;
    uint16_t         _port;
    mutable string   _qstr;
    mutable Query    _query;
    mutable uint32_t _qrev; // last query rev we've synced query string with (0 if query itself isn't synced with string)
    int              _flags;

    static const string _empty;

    void ok_qstr      () const { _qrev = 0; }
    void ok_query     () const { _qrev = _query.rev - 1; }
    void ok_qboth     () const { _qrev = _query.rev; }
    bool has_ok_qstr  () const { return !_qrev || _qrev == _query.rev; }
    bool has_ok_query () const { return _qrev != 0; }

    void clear () {
        _port = 0;
        _scheme.clear();
        scheme_info = NULL;
        _user_info.clear();
        _host.clear();
        _path.clear();
        _qstr.clear();
        _query.clear();
        _fragment.clear();
        ok_qboth();
        _flags = 0;
    }

    void guess_suffix_reference ();

    void compile_query () const;
    void parse_query   () const;

    void sync_query_string () const { if (!has_ok_qstr()) compile_query(); }
    void sync_query        () const { if (!has_ok_query()) parse_query(); }

    void sync_scheme_info ();

    static inline void _encode_uri_component_append (const string_view& src, string& dest, const char* unsafe) {
        char* buf = dest.reserve(dest.length() + src.length()*3) + dest.length();
        size_t final_size = encode_uri_component(src, buf, unsafe);
        dest.length(dest.length() + final_size);
    }

    bool _parse     (const string&, bool&);
    bool _parse_ext (const string&, bool&);
};

std::ostream& operator<< (std::ostream& os, const URI& uri);

inline bool operator== (const URI& lhs, const URI& rhs) { return lhs.equals(rhs); }
inline bool operator!= (const URI& lhs, const URI& rhs) { return !lhs.equals(rhs); }
inline void swap (URI& l, URI& r) { l.swap(r); }

}}
