/**
 * Tencent is pleased to support the open source community by making Tars available.
 *
 * Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
 *
 * Licensed under the BSD 3-Clause License (the "License"); you may not use this file except 
 * in compliance with the License. You may obtain a copy of the License at
 *
 * https://opensource.org/licenses/BSD-3-Clause
 *
 * Unless required by applicable law or agreed to in writing, software distributed 
 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 
 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 
 * specific language governing permissions and limitations under the License.
 */

#if TARS_HTTP2

#include <string>
#include <algorithm>
#include <iostream>
#include "util/tc_http2.h"
#include "nghttp2/nghttp2.h"

namespace tars
{

TC_Http2::TC_Http2() : _session(NULL)
{

}

TC_Http2::~TC_Http2()
{
	if(_session)
	{
		nghttp2_session_del(_session);
		_session = NULL;
	}
}

int TC_Http2::settings(unsigned int maxCurrentStreams)
{
	nghttp2_settings_entry iv[2] = {
		{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, maxCurrentStreams},
		{NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, 1024 * 1024},
	};

	/* 24 bytes magic string also will be sent*/
	nghttp2_submit_settings(_session,
	                        NGHTTP2_FLAG_NONE,
	                        iv,
	                        sizeof(iv)/sizeof(iv[0]));
	_err = nghttp2_session_send(_session);

	return _err;
}

const char *TC_Http2::getErrMsg()
{
	return nghttp2_strerror(_err);
}

///////////////////////////////////////////////////////////////////////////////////
namespace server
{

static ssize_t str_read_callback(nghttp2_session *session, int32_t stream_id,
                                  uint8_t *buf, size_t length,
                                  uint32_t *data_flags,
                                  nghttp2_data_source *source,
                                  void *user_data) 
{
    TC_Http2::DataPack *dataPack = (TC_Http2::DataPack*)(source->ptr);
	if(dataPack->_readPos == dataPack->_length)
	{
		*data_flags |= NGHTTP2_DATA_FLAG_EOF;
		return 0;
	}
    size_t size = std::min(dataPack->_length - dataPack->_readPos, length);

    memcpy(buf, dataPack->_dataBuf + dataPack->_readPos, size);

    dataPack->_readPos += size;
	
    if(dataPack->_readPos == dataPack->_length)
    {
        *data_flags |= NGHTTP2_DATA_FLAG_EOF;
    }
  
    return size;
}

static ssize_t send_callback(nghttp2_session *session, const uint8_t *data,
                             size_t length, int flags, void *user_data) 
{
    TC_Http2Server *ptr = (TC_Http2Server*)user_data;
	ptr->insertBuff((const char*)data, length);

    return (ssize_t)length;
}

static int on_header_callback(nghttp2_session *session,
                              const nghttp2_frame *frame, const uint8_t *name,
                              size_t namelen, const uint8_t *value,
                              size_t valuelen, uint8_t flags, void *user_data)
{
    TC_Http2Server *ptr = (TC_Http2Server*)user_data;
    ptr->onHeaderCallback(frame->hd.stream_id, string((char*)name, namelen), string((char*)value, valuelen));
    return 0;
}

static int on_begin_headers_callback(nghttp2_session *session,
                                     const nghttp2_frame *frame,
                                     void *user_data)
{
    TC_Http2Server *ptr = (TC_Http2Server*)user_data;

    if (frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
        return 0;
    }

    ptr->onHeaderCallback(frame->hd.stream_id);
    return 0;
}

static int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data)
{
    TC_Http2Server *ptr = (TC_Http2Server*)user_data;

    switch (frame->hd.type)
    {
    case NGHTTP2_DATA:
    case NGHTTP2_HEADERS:
        /* Check that the client request has finished */
        if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM)
        {
            ptr->onFrameRecvCallback(frame->hd.stream_id);
            return 0;
        }
        break;
    default:
        break;
    }
    return 0;
}

static int on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags,
                                       int32_t stream_id, const uint8_t *data,
                                       size_t len, void *user_data)
{
    TC_Http2Server *ptr = (TC_Http2Server*)user_data;

    ptr->onDataChunkRecvCallback(stream_id, (const char*)data, len);

    return 0;
}

static int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, uint32_t error_code, void *user_data)
{
    TC_Http2Server *ptr = (TC_Http2Server*)user_data;
    ptr->onStreamCloseCallback(stream_id);

    return 0;
}

}

void TC_Http2Server::onHeaderCallback(int streamId)
{
	_context[streamId] = std::make_shared<Http2Context>(streamId);
}

void TC_Http2Server::onHeaderCallback(int32_t streamId, const string &skey, const string &svalue)
{
	shared_ptr<Http2Context> context = getContext(streamId);

    if (TC_Port::strcasecmp(skey.c_str(), ":method") == 0)
    {
	    context->request.setMethod(svalue.c_str());
    }
    else if (TC_Port::strcasecmp(skey.c_str(), ":path") == 0)
    {
    	context->request.setPath(svalue.c_str());
    }
    else if (TC_Port::strcasecmp(skey.c_str(), ":scheme") == 0)
    {
	    context->request.setScheme(svalue.c_str());
    }
    else if (TC_Port::strcasecmp(skey.c_str(), ":authority") == 0)
    {
	    context->request.setDomain(svalue.c_str());
    }

	context->request.setHeader(skey, svalue);
}

void TC_Http2Server::onFrameRecvCallback(int32_t streamId)
{
	shared_ptr<Http2Context> context = getContext(streamId);

    if(context->request.getHeaders().find(":method") != context->request.getHeaders().end() ||
        context->request.getHeaders().find(":path") != context->request.getHeaders().end() ||
        context->request.getHeaders().find(":scheme") != context->request.getHeaders().end())
    {

	    _contextFinished.push_back(context);

        _reqout.insert(_reqout.end(), (char*)&streamId, (char*)&streamId + sizeof(int32_t));

	    deleteContext(streamId);
    }
}

void TC_Http2Server::onDataChunkRecvCallback(int32_t streamId, const char *data, size_t len)
{
	shared_ptr<Http2Context> context = getContext(streamId);

	context->request.getContent().append(data, len);
}

void TC_Http2Server::onStreamCloseCallback(int32_t streamId)
{
	deleteContext(streamId);
}

TC_Http2Server::TC_Http2Server()
{
    nghttp2_session_callbacks *callbacks;

    nghttp2_session_callbacks_new(&callbacks);

    nghttp2_session_callbacks_set_send_callback(callbacks, server::send_callback);

    nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, server::on_frame_recv_callback);

    nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, server::on_data_chunk_recv_callback);

    nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, server::on_stream_close_callback);

    nghttp2_session_callbacks_set_on_header_callback(callbacks, server::on_header_callback);

    nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks, server::on_begin_headers_callback);

    nghttp2_session_server_new(&_session, callbacks, ((void*)this));

    nghttp2_session_callbacks_del(callbacks);
}

TC_Http2Server::~TC_Http2Server()
{
}

TC_NetWorkBuffer::PACKET_TYPE TC_Http2Server::parse(TC_NetWorkBuffer&in, vector<char> &out)
{
    in.mergeBuffers();

    auto buff = in.getBufferPointer();

	TC_LockT<TC_ThreadMutex> lock(_nghttpLock);

	int readlen = nghttp2_session_mem_recv(_session, (uint8_t *) buff.first, buff.second);

    if(readlen < 0)
    {
        return TC_NetWorkBuffer::PACKET_ERR;
    }
    else
    {
        in.moveHeader(readlen);

	    if (_reqout.empty())
        {
            return TC_NetWorkBuffer::PACKET_LESS;
        }

        out.swap(_reqout);
        _reqout.clear();
    }

    return TC_NetWorkBuffer::PACKET_FULL;
}

shared_ptr<TC_Http2Server::Http2Context> TC_Http2Server::getContext(int32_t streamId)
{
	auto it = _context.find(streamId);
	if (it != _context.end())
	{
		return it->second;
	}

	assert(false);
	return NULL;
}

void TC_Http2Server::deleteContext(int32_t streamId)
{
	_context.erase(streamId);
}

vector<shared_ptr<TC_Http2Server::Http2Context>> TC_Http2Server::decodeRequest()
{
	vector<shared_ptr<TC_Http2Server::Http2Context>> contexts;

	TC_LockT<TC_ThreadMutex> lock(_nghttpLock);

	_contextFinished.swap(contexts);

	return contexts;
}

int TC_Http2Server::encodeResponse(const shared_ptr<TC_Http2Server::Http2Context> &context, vector<char> &out)
{
	string sstatus = TC_Common::tostr(context->response.getStatus());

	const char* strstatus = ":status";

	nghttp2_nv *hdrs    = new nghttp2_nv[context->response.getHeaders().size() + 1];
	hdrs[0].flags       = NGHTTP2_NV_FLAG_NONE;
	hdrs[0].name        = (uint8_t*)strstatus;
	hdrs[0].namelen     = 7;
	hdrs[0].value       = (uint8_t*)sstatus.c_str();
	hdrs[0].valuelen    = sstatus.size();

	TC_Http::http_header_type::const_iterator it = context->response.getHeaders().begin();
	for (int n = 1; it != context->response.getHeaders().end(); n++, it++)
	{
		hdrs[n].flags       = NGHTTP2_NV_FLAG_NONE;
		hdrs[n].name        = (uint8_t*)it->first.c_str();
		hdrs[n].namelen     = it->first.size();
		hdrs[n].value       = (uint8_t*)it->second.c_str();
		hdrs[n].valuelen    = it->second.size();
	}

	const string &data = context->response.getContent();
	DataPack dataPack(data.c_str(), data.size());

	nghttp2_data_provider data_prd;
	data_prd.source.ptr     = (void*)&dataPack;
	data_prd.read_callback  = server::str_read_callback;
	{
		TC_LockT<TC_ThreadMutex> lock(_nghttpLock);

		_err = nghttp2_submit_response(_session, context->reqId, hdrs, context->response.getHeaders().size()+1, &data_prd);
		if (_err != 0 ) {
			delete [] hdrs;

			return _err;
		}

		while (nghttp2_session_want_write(_session)) {
			_err = nghttp2_session_send(_session);
			if (_err != 0) {
				delete [] hdrs;

				return _err;
			}
		}

		this->swap(out);
	}

	delete [] hdrs;

	return 0;
}

///////////////////////////////////////////////////////////////////////////////////////

namespace client
{
static ssize_t send_callback(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data)
{
    TC_Http2Client* nghttp2 = (TC_Http2Client* )user_data;
    nghttp2->insertBuff((const char*)data, length);

    return length;
}

static int on_begin_headers_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
{
    TC_Http2Client* nghttp2 = (TC_Http2Client* )user_data;

    if (frame->hd.type == NGHTTP2_HEADERS)
    {
        if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE)
        {
            nghttp2->responses()[frame->hd.stream_id] = std::make_shared<TC_HttpResponse>();
        }
    }

    return 0;
}

static int on_header_callback(nghttp2_session* session, const nghttp2_frame* frame,
                         const uint8_t* name, size_t namelen,
                         const uint8_t* value, size_t valuelen,
                         uint8_t flags, void* user_data)
{
    TC_Http2Client* nghttp2 = (TC_Http2Client* )user_data;

    int streamId = frame->hd.stream_id;
    auto it = nghttp2->responses().find(streamId);
    if (it == nghttp2->responses().end())
    {
        return NGHTTP2_ERR_CALLBACK_FAILURE;
    }

    std::string n((const char*)name, namelen);
    std::string v((const char*)value, valuelen);

    it->second->setHeader(n, v);

    return 0;
}

static int on_frame_recv_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data)
{
    TC_Http2Client* nghttp2 = (TC_Http2Client* )user_data;

    int streamId = frame->hd.stream_id;
    if (streamId == 0)
        return 0;

    auto it = nghttp2->responses().find(streamId);
    if (it == nghttp2->responses().end())
    {
        return NGHTTP2_ERR_CALLBACK_FAILURE;
    }

    switch (frame->hd.type)
    {
        case NGHTTP2_HEADERS:
            if (frame->hd.flags & NGHTTP2_FLAG_END_HEADERS)
            {
            	;
            }
            return 0;

        default:
            break;
    }

    return 0;
}

static int on_data_chunk_recv_callback(nghttp2_session* session, uint8_t flags, int32_t stream_id, const uint8_t* data, size_t len, void* user_data)
{
    TC_Http2Client* nghttp2 = (TC_Http2Client* )user_data;

    auto it = nghttp2->responses().find(stream_id);
    if (it == nghttp2->responses().end())
    {
        return NGHTTP2_ERR_CALLBACK_FAILURE;
    }

    it->second->appendContent((const char* )data, len);
    return 0;
}

static int on_stream_close_callback(nghttp2_session* session, int32_t stream_id, uint32_t error_code, void* user_data)
{
    TC_Http2Client* nghttp2 = (TC_Http2Client* )user_data;

    auto it = nghttp2->responses().find(stream_id);
    if (it == nghttp2->responses().end())
    {
        return NGHTTP2_ERR_CALLBACK_FAILURE;
    }

	nghttp2->doneResponses()[stream_id] = it->second;
	nghttp2->responses().erase(it);

    return 0;
}

}

#define MAKE_STRING_NV(NAME, VALUE) {(uint8_t*)(NAME.c_str()), (uint8_t*)(VALUE.c_str()), NAME.size(), VALUE.size(), NGHTTP2_NV_FLAG_NONE};

TC_Http2Client::TC_Http2Client()
{
    nghttp2_session_callbacks* callbacks;
    nghttp2_session_callbacks_new(&callbacks);
    nghttp2_session_callbacks_set_send_callback(callbacks, client::send_callback);
    nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks, client::on_begin_headers_callback);
    nghttp2_session_callbacks_set_on_header_callback(callbacks, client::on_header_callback);
    nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, client::on_frame_recv_callback);
    nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, client::on_data_chunk_recv_callback);
    nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, client::on_stream_close_callback);

    nghttp2_session_client_new(&_session, callbacks, this);

    nghttp2_session_callbacks_del(callbacks);
}

TC_Http2Client::~TC_Http2Client()
{
}

int TC_Http2Client::submit(const TC_HttpRequest &request)
{
	std::vector<nghttp2_nv> nva;

	const std::string smethod(":method");
	nghttp2_nv nv1 = MAKE_STRING_NV(smethod, request.getMethod());
	nva.push_back(nv1);

	const std::string spath(":path");
	nghttp2_nv nv2 = MAKE_STRING_NV(spath, request.getRequest());
	nva.push_back(nv2);

	const std::string sauthority(":authority");
	nghttp2_nv nv3 = MAKE_STRING_NV(sauthority, request.getURL().getDomain());
	nva.push_back(nv3);

	const std::string sscheme(":scheme");
	nghttp2_nv nv4 = MAKE_STRING_NV(sscheme, request.getURL().getScheme());
	nva.push_back(nv4);

// cout << "submit:" << request.getMethod() << ", " << request.getRequest() << ", " << request.getURL().getDomain() << ", " << request.getURL().getScheme() << endl;

	const TC_Http::http_header_type &header = request.getHeaders();
	for (auto it = header.begin(); it != header.end(); ++ it)
	{
		if(TC_Port::strcasecmp(it->first.c_str(), "Content-Length") == 0 || it->second.empty())
			continue;

		nghttp2_nv nv = MAKE_STRING_NV(it->first, it->second);
		nva.push_back(nv);
	}

	nghttp2_data_provider* pData = NULL;
	nghttp2_data_provider data;

	DataPack dataPack(request.getContent().c_str(), request.getContent().size());

	if (!request.getContent().empty())
	{
		pData = &data;
		data.source.ptr = (void*)&dataPack;
		data.read_callback = server::str_read_callback;
	}

	_err = nghttp2_submit_request(_session,
	                              NULL,
	                              nva.data(),
	                              nva.size(),
	                              pData,
	                              NULL);
	if (_err < 0)
	{
		return _err;
	}

	int sid = _err;

	_err = nghttp2_session_send(_session);
	if (_err != 0) {
		return _err;
	}

	return sid;
}

TC_NetWorkBuffer::PACKET_TYPE TC_Http2Client::parseResponse(TC_NetWorkBuffer &in, pair<int, shared_ptr<TC_HttpResponse>> &out)
{
	if(_doneResponses.empty() && in.empty())
	{
		return TC_NetWorkBuffer::PACKET_LESS;
	}

	if(!in.empty())
	{
		//merge to one buffer
		in.mergeBuffers();

		pair<const char *, size_t> buffer = in.getBufferPointer();

		int readlen = nghttp2_session_mem_recv(_session, (const uint8_t *) buffer.first, buffer.second);
		if (readlen < 0) {
			return TC_NetWorkBuffer::PACKET_ERR;
		}

		in.moveHeader(readlen);
	}

	if(_doneResponses.empty())
	{
		return TC_NetWorkBuffer::PACKET_LESS;
	}

	auto it = _doneResponses.begin();
	out.first = it->first;
	out.second = it->second;

	_doneResponses.erase(it);
	return TC_NetWorkBuffer::PACKET_FULL;

}

// void TC_Http2Client::onNegotiateDone(bool succ)
// {
//     // assert (_state == Negotiating);
//     _state = succ ? Http2: Http1;
//     if (succ) 
//     {
//         int rv = nghttp2_session_upgrade(_session,
//                                          (const uint8_t*)_settings.data(),
//                                          _settings.size(),
//                                          NULL);
//         if (rv)
//             cerr << "nghttp2_session_upgrade error: " << nghttp2_strerror(rv) << endl;
//     }
// }

// string& TC_Http2Client::_buffer()
// {
//     return _sendBuf;
// }

// nghttp2_session* TC_Http2Client::session() const
// {
//     return _session;
// }

} // end namespace tars

#endif // end #if TARS_HTTP2

