#include "uri.h"
#include "parse.h"

#include <util/string/cast.h>
#include <util/string/util.h>
#include <util/system/yassert.h>

namespace NUri {
    TState::EParsed TUri::CheckHost(const TStringBuf& host) {
        if (host.empty())
            return ParsedOK;

        unsigned domainLevel = 0;
        unsigned domainLevelOfUnderscore = 0;

        bool isAlnum = false;
        bool startLabel = true;
        for (size_t i = 0; i != host.length(); ++i) {
            const char ch = host[i];

            if ('.' == ch) {                // label separator
                if (!isAlnum || startLabel) // previous label must end in alnum
                    return ParsedBadHost;
                startLabel = true;
                continue;
            }

            isAlnum = isalnum((const unsigned char)ch);

            if (startLabel) {              // label is starting
                if (!isAlnum && '_' != ch) // new label must start with alnum or '_'
                    return ParsedBadHost;
                startLabel = false;
                ++domainLevel;
                if (ch == '_')
                    domainLevelOfUnderscore = domainLevel;
                continue;
            }

            if (isAlnum || '-' == ch)
                continue;

            if (ch == '_') { // non-standard case we allow for certain hosts
                domainLevelOfUnderscore = domainLevel;
                continue;
            }

            return ParsedBadHost;
        }

        if (0 < domainLevelOfUnderscore && domainLevel < 2 + domainLevelOfUnderscore)
            return ParsedBadHost;

        return ParsedOK;
    }

    /********************************************************/
    TUri::TUri(const TStringBuf& host, ui16 port, const TStringBuf& path, const TStringBuf& query, const TStringBuf& scheme, unsigned defaultPort)
        : FieldsSet(0)
        , Port(port)
        , DefaultPort(0)
        , Scheme(SchemeEmpty)
        , FieldsDirty(0)
    {
        if (!scheme.empty()) {
            if (SetSchemeImpl(TSchemeInfo::Get(scheme)).Str.empty())
                FldSet(FieldScheme, scheme);
        }

        if (0 < defaultPort) // override the scheme's default port
            DefaultPort = static_cast<ui16>(defaultPort);

        char sport[6]; // enough for ui16
        if (0 != port) {
            const size_t len = ToString(port, sport, sizeof(sport));
            FldSet(FieldPort, TStringBuf(sport, len));
        }

        FldTrySet(FieldHost, host);
        FldTrySet(FieldPath, path);
        FldTrySet(FieldQuery, query);

        Rewrite();
    }

    /********************************************************/
    bool TUri::FldSetImpl(
        EField field, TStringBuf value, bool strconst, bool nocopy) {
        if (!FldIsValid(field))
            return false;

        switch (field) {
            case FieldScheme:
                if (!SetScheme(TSchemeInfo::Get(value)).Str.empty())
                    return false;
                break;

            case FieldPort:
                Port = value.empty() ? 0 : FromString<ui16>(value);
                break;

            default:
                break;
        }

        if (!value.IsInited()) {
            FldClr(field);
            return false;
        }

        if (strconst) { // string constants don't need to be saved in the buffer
            FldMarkClean(field);
            FldSetNoDirty(field, value);
            return false;
        }

        if (nocopy) {
            FldSet(field, value);
            return true;
        }

        return FldTryCpy(field, value);
    }

    /********************************************************/
    bool TUri::FldTryCpy(EField field, const TStringBuf& value) {
        if (!FldIsDirty(field)) {
            do {
                if (!FldIsSet(field))
                    break;

                TStringBuf& fld = Fields[field];
                if (fld.length() < value.length())
                    break;

                char* oldV = (char*)fld.data();
                if (!IsInBuffer(oldV))
                    break;

                memcpy(oldV, value.data(), value.length());
                oldV[value.length()] = 0;
                fld.Trunc(value.length());
                return false;
            } while (false);

            FldMarkDirty(field);
        }

        FldSetNoDirty(field, value);
        return true;
    }

    /********************************************************/
    void TUri::RewriteImpl() {
        size_t len = 0;
        for (int i = 0; i < FieldAllMAX; ++i) {
            const EField fld = EField(i);
            if (FldIsSet(fld))
                len += 1 + Fields[fld].length();
        }

        if (!len)
            Buffer.Clear();
        else {
            TBuffer newbuf;
            newbuf.Resize(len);
            TMemoryWriteBuffer out(newbuf.data(), newbuf.size());
            for (int i = 0; i < FieldAllMAX; ++i) {
                const EField fld = EField(i);
                if (!FldIsSet(fld))
                    continue;

                const char* beg = out.Buf();
                const TStringBuf& val = Fields[fld];
                out << val;
                FldSetNoDirty(fld, TStringBuf(beg, val.length()));
                out << '\0';
            }
            Buffer = std::move(newbuf);
        }

        CheckMissingFields();

        FieldsDirty = 0;
    }

    void TUri::CheckMissingFields() {
        // if host is set but path is not...
        if (FldSetCmp(FlagPath | FlagHost, FlagHost))
            // ... and the scheme requires a path...
            if (GetSchemeInfo().FldReq & FlagPath)
                // ... set path
                FldSetNoDirty(FieldPath, TStringBuf("/"));
    }

    /********************************************************/
    void TUri::Merge(const TUri& base, int correctAbs) {
        if (base.Scheme == SchemeUnknown)
            return;

        if (!base.IsValidGlobal())
            return;

        const TStringBuf& selfscheme = GetField(FieldScheme);
        // basescheme is present since IsValidGlobal() succeeded
        const TStringBuf& basescheme = base.GetField(FieldScheme);
        const bool noscheme = !selfscheme.IsInited();
        if (!noscheme && !EqualNoCase(selfscheme, basescheme))
            return;

        const ui32 cleanFields = ~FieldsDirty;
        do {
            static constexpr TStringBuf rootPath = "/";

            if (noscheme) {
                if (!basescheme.empty()) {
                    FldSetNoDirty(FieldScheme, basescheme);
                    // check if it is canonical
                    if (basescheme.data() != base.GetSchemeInfo().Str.data())
                        FldMarkDirty(FieldScheme);
                }
                Scheme = base.Scheme;
                DefaultPort = base.DefaultPort;
            }

            if (!IsNull(FlagHost))
                break; // no merge

            FldTrySet(FieldHost, base);
            FldChkSet(FieldPort, base);
            Port = base.Port;

            if (noscheme && IsNull(FlagQuery) && IsNull(FlagPath))
                FldTrySet(FieldQuery, base);

            if (IsNull(FlagAuth) && !base.IsNull(FlagAuth)) {
                FldChkSet(FieldUser, base);
                FldChkSet(FieldPass, base);
            }

            if (IsValidAbs())
                break;

            TStringBuf p0 = base.GetField(FieldPath);
            if (!p0.IsInited())
                p0 = rootPath;

            TStringBuf p1 = GetField(FieldPath);
            if (!p1.IsInited()) {
                if (p0.data() != rootPath.data())
                    FldSet(FieldPath, p0);
                else
                    FldSetNoDirty(FieldPath, rootPath);
                break;
            }
            if (p1 && '/' == p1[0])
                p1.Skip(1); // p0 will have one

            bool pathop = true;

            TTempBufOutput out(p0.length() + p1.length() + 4);
            out << p0;
            if ('/' != p0.back())
                out << "/../";
            else if (p1.empty() || '.' != p1[0])
                pathop = false;
            out << p1;

            char* beg = out.Data();
            char* end = beg + out.Filled();
            if (pathop && !PathOperation(beg, end, correctAbs)) {
                Clear();
                break;
            }

            // Needs immediate forced rewrite because of TTempBuf
            FldSetNoDirty(FieldPath, TStringBuf(beg, end));
            RewriteImpl();
        } while (false);

        CheckMissingFields();

        // rewrite only if borrowed fields from base
        if (cleanFields & FieldsDirty)
            RewriteImpl();
    }

    /********************************************************/
    TUri::TLinkType TUri::Normalize(const TUri& base,
                                    const TStringBuf& link, const TStringBuf& codebase, long careFlags, ECharset enc) {
        // parse URL
        if (ParsedOK != ParseImpl(link, careFlags, 0, SchemeEmpty, enc))
            return LinkIsBad;

        const TStringBuf& host = GetHost();

        // merge with base URL
        // taken either from _BASE_ property or from optional argument
        if (!codebase.empty()) {
            // if optional code base given -- parse it
            TUri codebaseUrl;
            if (codebaseUrl.ParseImpl(codebase, careFlags, 0, SchemeEmpty, enc) != ParsedOK || !codebaseUrl.IsValidAbs())
                return LinkIsBad;
            Merge(codebaseUrl);
        } else {
            // Base is already in this variable
            // see SetProperty() for details
            Merge(base);
        }

        // check result: must be correct absolute URL
        if (!IsValidAbs())
            return LinkBadAbs;

        if (!host.empty()) {
            //  - we don't care about different ports for the same server
            //  - we don't care about win|www|koi|etc. preffixes for the same server
            if (GetPort() != base.GetPort() || !EqualNoCase(host, base.GetHost()))
                return LinkIsGlobal;
        }

        // find out if it is link to itself then ignore it
        if (!Compare(base, FlagPath | FlagQuery))
            return LinkIsFragment;

        return LinkIsLocal;
    }

    /********************************************************/

    size_t TUri::PrintSize(ui32 flags) const {
        size_t len = 10;
        flags &= FieldsSet; // can't output what we don't have
        if (flags & FlagHostAscii)
            flags &= ~FlagHost; // don't want to print both of them
        ui32 opt = 1;
        for (int fld = 0; opt <= flags && fld < FieldAllMAX; ++fld, opt <<= 1) {
            if (opt & flags) {
                const TStringBuf& v = Fields[fld];
                if (v.IsInited()) {
                    if (opt & FlagAuth)
                        len += 3 * v.length() + 1;
                    else
                        len += v.length() + 1;
                }
            }
        }

        return len;
    }

    IOutputStream& TUri::PrintImpl(IOutputStream& out, int flags) const {
        TStringBuf v;

        const int wantFlags = flags; // save the original
        flags &= FieldsSet;          // can't print what we don't have
        if (flags & FlagHostAscii)
            flags |= FlagHost; // to make host checks simpler below

        if (flags & FlagScheme) {
            v = Fields[FieldScheme];
            if (!v.empty())
                out << v << ':';
        }

        TStringBuf host;
        if (flags & FlagHost) {
            const EField fldhost =
                flags & FlagHostAscii ? FieldHostAscii : FieldHost;
            host = Fields[fldhost];
        }

        TStringBuf port;
        if ((flags & FlagPort) && 0 != Port && Port != DefaultPort)
            port = Fields[FieldPort];

        if (host) {
            if (wantFlags & FlagScheme)
                out << "//";

            if (flags & FlagAuth) {
                if (flags & FlagUser) {
                    v = Fields[FieldUser];
                    if (!v.empty())
                        TEncoder::EncodeNotAlnum(out, v);
                }

                if (flags & FlagPass) {
                    v = Fields[FieldPass];
                    if (v.IsInited()) {
                        out << ':';
                        TEncoder::EncodeAll(out, v);
                    }
                }

                out << '@';
            }

            out << host;

            if (port)
                out << ':';
        }
        if (port)
            out << port;

        if (flags & FlagPath) {
            v = Fields[FieldPath];
            // for relative, empty path is not the same as missing
            if (v.empty() && 0 == (flags & FlagHost))
                v = TStringBuf(".");
            out << v;
        }

        if (flags & FlagQuery) {
            v = Fields[FieldQuery];
            if (v.IsInited())
                out << '?' << v;
        }

        if (flags & FlagFrag) {
            v = Fields[FieldFrag];
            if (v.IsInited())
                out << '#' << v;
        }

        return out;
    }

    /********************************************************/
    int TUri::CompareField(EField fld, const TUri& url) const {
        const TStringBuf& v0 = GetField(fld);
        const TStringBuf& v1 = url.GetField(fld);
        switch (fld) {
            case FieldScheme:
            case FieldHost:
                return CompareNoCase(v0, v1);
            default:
                return v0.compare(v1);
        }
    }

    /********************************************************/
    int TUri::Compare(const TUri& url, int flags) const {
        // first compare fields with default values
        if (flags & FlagPort) {
            const int ret = GetPort() - url.GetPort();
            if (ret)
                return ret;
            flags &= ~FlagPort;
        }

        // compare remaining sets of available fields
        const int rtflags = flags & url.FieldsSet;
        flags &= FieldsSet;
        const int fldcmp = flags - rtflags;
        if (fldcmp)
            return fldcmp;

        // field sets are the same, compare the fields themselves
        for (int i = 0; i < FieldAllMAX; ++i) {
            const EField fld = EField(i);
            if (flags & FldFlag(fld)) {
                const int ret = CompareField(fld, url);
                if (ret)
                    return ret;
            }
        }

        return 0;
    }

    /********************************************************/
    bool TUri::PathOperation(char*& pathPtr, char*& pathEnd, int correctAbs) {
        if (!pathPtr)
            return false;
        if (pathPtr == pathEnd)
            return true;

        if ((pathEnd - pathPtr) >= 2 && *(pathEnd - 2) == '/' && *(pathEnd - 1) == '.') {
            --pathEnd;
        }

        char* p_wr = pathEnd;
        int upCount = 0;

        char* p_prev = pathEnd;
        Y_ASSERT(p_prev > pathPtr);
        while (p_prev > pathPtr && *(p_prev - 1) == '/')
            p_prev--;

        for (char* p_rd = p_prev; p_rd; p_rd = p_prev) {
            Y_ASSERT(p_rd == pathEnd || p_rd[0] == '/');
            p_prev = nullptr;

            char* p = p_rd;

            if (p > pathPtr) {
                for (p--; *p != '/'; p--) {
                    if (p == pathPtr)
                        break;
                }
                if (*p == '/') {
                    p_prev = p++;
                    if ((p_prev - pathPtr >= 6 && !strnicmp(p_prev - 6, "http://", 7)) ||
                        (p_prev - pathPtr >= 7 && !strnicmp(p_prev - 7, "https://", 8))) {
                        --p_prev;
                        --p;
                    } else {
                        //skip multiple from head '/'
                        while (p_prev > pathPtr && *(p_prev - 1) == '/')
                            p_prev--;
                    }
                }
            }

            Y_ASSERT(p_prev == nullptr || p_prev[0] == '/');
            //and the first symbol !='/' after p_prev is p

            if (p == p_rd) {
                //empty block:
                if (p_prev) { //either tail:
                    Y_ASSERT(p_rd == p_wr && *(p - 1) == '/');
                    --p_wr;
                    continue;
                } else { //or head of abs path
                    *(--p_wr) = '/';
                    break;
                }
            }

            if (p[0] == '.') {
                if (p + 1 == p_rd) {
                    if (correctAbs || p_prev > pathPtr || pathPtr[0] != '/')
                        // ignore "./"
                        continue;
                } else {
                    if ((p[1] == '.') && (p + 2 == p_rd)) {
                        // register "../" but not print
                        upCount++;
                        continue;
                    }
                }
            }

            if (upCount) {
                //unregister "../" and not print
                upCount--;
                continue;
            }

            // print
            Y_ASSERT(p < p_rd);
            Y_ASSERT(!p_prev || *(p - 1) == '/');
            if (p_wr == p_rd) { //just skip
                p_wr = p;
            } else { //copy
                int l = p_rd - p + 1;
                p_wr -= l;
                memmove(p_wr, p, l);
            }
        }

        if (upCount) {
            if (*pathPtr != '/') {
                if (pathEnd == p_wr && *(p_wr - 1) == '.') {
                    Y_ASSERT(*(p_wr - 2) == '.');
                    p_wr -= 2;
                    upCount--;
                }
                for (; upCount > 0; upCount--) {
                    *(--p_wr) = '/';
                    *(--p_wr) = '.';
                    *(--p_wr) = '.';
                }
            } else {
                if (correctAbs > 0)
                    return false;
                if (correctAbs == 0) {
                    //Bad path but present in RFC:
                    // "Similarly, parsers must avoid treating "." and ".."
                    // as special when they are not complete components of
                    // a relative path. "
                    for (; upCount > 0; upCount--) {
                        *(--p_wr) = '.';
                        *(--p_wr) = '.';
                        *(--p_wr) = '/';
                    }
                } else {
                    upCount = false;
                }
            }
        }

        Y_ASSERT(p_wr >= pathPtr);

        if (upCount)
            return false;
        pathPtr = p_wr;
        return true;
    }

    /********************************************************/
    const char* LinkTypeToString(const TUri::TLinkType& t) {
        switch (t) {
            case TUri::LinkIsBad:
                return "LinkIsBad";
            case TUri::LinkBadAbs:
                return "LinkBadAbs";
            case TUri::LinkIsFragment:
                return "LinkIsFragment";
            case TUri::LinkIsLocal:
                return "LinkIsLocal";
            case TUri::LinkIsGlobal:
                return "LinkIsGlobal";
        }
        Y_ASSERT(0);
        return "";
    }

}
