#include "slang-http.h"

#include "slang-process.h"
#include "slang-string-util.h"

namespace Slang
{

static const UnownedStringSlice g_headerEnd = UnownedStringSlice::fromLiteral("\r\n\r\n");
static const UnownedStringSlice g_contentLength = UnownedStringSlice::fromLiteral("Content-Length");
static const UnownedStringSlice g_contentType = UnownedStringSlice::fromLiteral("Content-Type");

/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! HTTPHeader !!!!!!!!!!!!!!!!!!!!!!! */

void HTTPHeader::reset()
{
    const UnownedStringSlice empty;

    m_contentLength = 0;
    m_mimeType = empty;
    m_encoding = empty;
    m_valuePairs.clear();
    m_header = empty;

    m_arena.deallocateAll();
}

/* static */ SlangResult HTTPHeader::readHeaderText(BufferedReadStream* stream, Index& outEndIndex)
{
    // https://microsoft.github.io/language-server-protocol/specifications/specification-current/

    while (true)
    {
        SLANG_RETURN_ON_FAIL(stream->update());

        const Index index = findHeaderEnd(stream);
        if (index >= 0)
        {
            outEndIndex = index;
            return SLANG_OK;
        }

        if (stream->isEnd())
        {
            return SLANG_FAIL;
        }

        Process::sleepCurrentThread(0);
    }
}

/* static */ Index HTTPHeader::findHeaderEnd(BufferedReadStream* stream)
{
    // This could be more efficient - it just searches until there are enough bytes to have
    // termination
    auto bytes = stream->getView();
    UnownedStringSlice input((const char*)bytes.begin(), (const char*)bytes.end());

    const Index index = input.indexOf(g_headerEnd);
    return (index >= 0) ? (index + g_headerEnd.getLength()) : index;
}

/* static */ SlangResult HTTPHeader::parse(const UnownedStringSlice& inSlice, HTTPHeader& out)
{
    out.reset();

    {
        auto slice = inSlice;
        // If has termination at end, remove so we don't have empty lines
        if (slice.endsWith(g_headerEnd))
        {
            slice = slice.head(slice.getLength() - g_headerEnd.getLength());
        }
        // Allocate on on the arena, so when we reference other slices, they are part of this
        // allocation.
        out.m_header = UnownedStringSlice(
            out.m_arena.allocateString(slice.begin(), slice.getLength()),
            slice.getLength());
    }

    // Okay, we need to split into lines, and then examine the contents
    for (auto line : LineParser(out.m_header))
    {
        // Examine the line for :
        Index index = line.indexOf(':');
        if (index < 0)
        {
            return SLANG_FAIL;
        }

        const UnownedStringSlice key = line.head(index).trim();
        const UnownedStringSlice value = line.tail(index + 1).trim();

        // Add the pair
        Pair pair{key, value};

        // We could check if key is already used. Some values can be repeated I believe.
        // So we just allow for now.

        out.m_valuePairs.add(pair);

        if (key == g_contentLength)
        {
            Index length;
            if (SLANG_FAILED(StringUtil::parseInt(value, length)) || length < 0)
            {
                return SLANG_FAIL;
            }

            out.m_contentLength = length;
        }
        else if (key == g_contentType)
        {
            List<UnownedStringSlice> slices;

            // text/html; charset=UTF-8
            StringUtil::split(value, ';', slices);

            if (slices.getCount() < 1)
            {
                return SLANG_FAIL;
            }
            // set the mime type
            out.m_mimeType = slices[0].trim();

            // Look for other parameters, in particular charset
            for (Index i = 1; i < slices.getCount(); ++i)
            {
                auto slice = slices[i];
                Index equalIndex = slice.indexOf('=');
                if (equalIndex >= 0)
                {
                    auto paramName = slice.head(equalIndex).trim();
                    auto paramValue = slice.tail(equalIndex + 1).trim();

                    if (paramName == UnownedStringSlice::fromLiteral("charset"))
                    {
                        out.m_encoding = paramValue;
                    }
                }
            }
        }
    }

    return SLANG_OK;
}

/* static */ SlangResult HTTPHeader::read(BufferedReadStream* stream, HTTPHeader& out)
{
    Index endIndex;
    SLANG_RETURN_ON_FAIL(readHeaderText(stream, endIndex));

    // Get header into a slice
    UnownedStringSlice headerText((const char*)stream->getBuffer(), endIndex);

    // Parse the slice into the out HttpHeader
    SLANG_RETURN_ON_FAIL(parse(headerText, out));

    // Can consume these bytes from the stream.
    stream->consume(endIndex);

    return SLANG_OK;
}

void HTTPHeader::append(StringBuilder& out) const
{
    // Output the content length
    out << g_contentLength << ": " << SlangSizeT(m_contentLength) << "\r\n";

    // If either is set construct a content type
    if (m_mimeType.getLength() || m_encoding.getLength())
    {
        out << g_contentType << ": ";

        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types

        auto mimeType =
            m_mimeType.getLength() ? m_mimeType : UnownedStringSlice::fromLiteral("text/plain");
        auto encoding =
            m_encoding.getLength() ? m_encoding : UnownedStringSlice::fromLiteral("UTF-8");

        out << mimeType << "; ";
        out << "charset=" << encoding;

        out << "\r\n";
    }

    // Output any other data
    for (auto pair : m_valuePairs)
    {
        auto key = pair.key;
        // Ignore these types, as already output from data we already have
        if (key == g_contentType || key == g_contentLength)
        {
            continue;
        }

        out << key << ": " << pair.value << "\r\n";
    }

    // Add termination
    out << "\r\n";
}

/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! HTTPPacketConnection !!!!!!!!!!!!!!!!!!!!!!! */

HTTPPacketConnection::HTTPPacketConnection(BufferedReadStream* readStream, Stream* writeStream)
    : m_readStream(readStream)
    , m_writeStream(writeStream)
    , m_readState(ReadState::Header)
    , m_readError(ReadError::None)
    , m_readResult(SLANG_OK)
{
}

SlangResult HTTPPacketConnection::_handleHeader()
{
    SLANG_ASSERT(m_readState == ReadState::Header);

    const Index index = HTTPHeader::findHeaderEnd(m_readStream);
    if (index < 0)
    {
        // Don't have the full header yet
        return SLANG_OK;
    }

    // Okay we can parse the header
    UnownedStringSlice slice((const char*)m_readStream->getBuffer(), size_t(index));
    SLANG_RETURN_ON_FAIL(
        _setReadError(HTTPHeader::parse(slice, m_readHeader), ReadError::InvalidHeader));

    // Consume the header
    m_readStream->consume(index);

    // We are now consuming content
    m_readState = ReadState::Content;
    return SLANG_OK;
}

SlangResult HTTPPacketConnection::_handleContent()
{
    SLANG_ASSERT(m_readState == ReadState::Content);
    // Do we have enough content, mark as done
    if (m_readStream->getCount() >= m_readHeader.m_contentLength)
    {
        m_readState = ReadState::Done;
    }
    return SLANG_OK;
}

SlangResult HTTPPacketConnection::update()
{
    switch (m_readState)
    {
    case ReadState::Closed:
        return SLANG_OK;
    case ReadState::Error:
        return m_readResult;
    default:
        break;
    }

    SLANG_RETURN_ON_FAIL(_setReadError(m_readStream->update(), ReadError::Stream));

    switch (m_readState)
    {
    case ReadState::Header:
        {
            SLANG_RETURN_ON_FAIL(_handleHeader());
            // We might be able to progress through content, if we have the header
            if (m_readState == ReadState::Content)
            {
                _handleContent();
            }
            break;
        }
    case ReadState::Content:
        {
            _handleContent();
            break;
        }
    default:
        break;
    }

    // Process the buffered bytes before checking EOF: the peer may have closed immediately after
    // writing one complete final packet. Use the backing-stream state rather than
    // BufferedReadStream::isEnd(), which stays false while partial packet bytes remain buffered.
    if (m_readState != ReadState::Done && m_readStream->isSourceEnd())
    {
        if (m_readState == ReadState::Header && m_readStream->getCount() == 0)
        {
            m_readState = ReadState::Closed;
        }
        else
        {
            // The buffered bytes cannot complete once the source has ended.
            m_readState = ReadState::Error;
            m_readResult = SLANG_FAIL;
            m_readError = ReadError::UnexpectedEnd;
        }
    }

    return m_readResult;
}


namespace
{ // anonymous

// Handles binary backoff like sleeping mechanism.
struct SleepState
{
    void sleep()
    {
        Process::sleepCurrentThread(m_intervalInMs);
        _update();
    }
    void reset()
    {
        m_intervalInMs = 0;
        m_count = 0;
    }
    void _update()
    {
        const Int maxIntervalInMs = 32;
        const Int initialCountThreshold = 4;

        ++m_count;

        const Int countThreshold = (m_intervalInMs == 0) ? initialCountThreshold : 1;

        // If we hit the count change the interval
        if (m_count >= countThreshold)
        {
            m_intervalInMs =
                (m_intervalInMs == 0) ? 1 : Math::Min(m_intervalInMs * 2, maxIntervalInMs);
            // Reset the count
            m_count = 0;
        }
    }

    Int m_intervalInMs = 0;
    Int m_count = 0;
};

} // namespace

SlangResult HTTPPacketConnection::waitForResult(Int timeOutInMs)
{
    m_readResult = SLANG_OK;

    int64_t startTick = 0;
    int64_t timeOutInTicks = -1;

    if (timeOutInMs >= 0)
    {
        timeOutInTicks = timeOutInMs * (Process::getClockFrequency() / 1000);
        startTick = Process::getClockTick();
    }

    SleepState sleepState;

    while (m_readState == ReadState::Header || m_readState == ReadState::Content)
    {
        const auto prevCount = m_readStream->getCount();

        SLANG_RETURN_ON_FAIL(update());

        if (m_readState == ReadState::Done)
        {
            break;
        }

        // We timed out
        if (timeOutInTicks >= 0 && int64_t(Process::getClockTick()) - startTick >= timeOutInTicks)
        {
            break;
        }

        if (prevCount == m_readStream->getCount())
        {
            sleepState.sleep();
        }
        else
        {
            sleepState.reset();
        }
    }

    // If we exited the loop without reaching Done state (e.g., due to timeout,
    // connection closure, or error), report failure so callers don't mistakenly
    // treat an incomplete/missing response as success.
    if (SLANG_SUCCEEDED(m_readResult) && m_readState != ReadState::Done)
    {
        return SLANG_FAIL;
    }

    return m_readResult;
}

void HTTPPacketConnection::consumeContent()
{
    SLANG_ASSERT(m_readState == ReadState::Done);
    if (m_readState == ReadState::Done)
    {
        // Consume the content
        m_readStream->consume(Index(m_readHeader.m_contentLength));
        // Back looking for the header again
        m_readState = ReadState::Header;
        m_readError = ReadError::None;
    }
}

SlangResult HTTPPacketConnection::write(const void* content, size_t sizeInBytes)
{
    // Write the header
    {
        HTTPHeader header;
        header.m_contentLength = sizeInBytes;

        StringBuilder buf;
        header.append(buf);

        SLANG_RETURN_ON_FAIL(m_writeStream->write(buf.getBuffer(), buf.getLength()));
    }

    // Write the content
    SLANG_RETURN_ON_FAIL(m_writeStream->write(content, sizeInBytes));

    return SLANG_OK;
}

} // namespace Slang
