/*
 * Copyright (C) 2009-2012 the libgit2 contributors
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */

#include <stdlib.h>
#include "git2.h"
#include "http_parser.h"

#include "transport.h"
#include "common.h"
#include "netops.h"
#include "buffer.h"
#include "pkt.h"
#include "refs.h"
#include "pack.h"
#include "fetch.h"
#include "filebuf.h"
#include "repository.h"
#include "protocol.h"

enum last_cb {
	NONE,
	FIELD,
	VALUE
};

typedef struct {
	git_transport parent;
	http_parser_settings settings;
	git_buf buf;
	int error;
	int transfer_finished :1,
		ct_found :1,
		ct_finished :1,
		pack_ready :1;
	enum last_cb last_cb;
	http_parser parser;
	char *content_type;
	char *path;
	char *host;
	char *port;
	char *service;
	char buffer[4096];
#ifdef GIT_WIN32
	WSADATA wsd;
#endif
} transport_http;

static int gen_request(git_buf *buf, const char *path, const char *host, const char *op,
                       const char *service, ssize_t content_length, int ls)
{
	if (path == NULL) /* Is 'git fetch http://host.com/' valid? */
		path = "/";

	if (ls) {
		git_buf_printf(buf, "%s %s/info/refs?service=git-%s HTTP/1.1\r\n", op, path, service);
	} else {
		git_buf_printf(buf, "%s %s/git-%s HTTP/1.1\r\n", op, path, service);
	}
	git_buf_puts(buf, "User-Agent: git/1.0 (libgit2 " LIBGIT2_VERSION ")\r\n");
	git_buf_printf(buf, "Host: %s\r\n", host);
	if (content_length > 0) {
		git_buf_printf(buf, "Accept: application/x-git-%s-result\r\n", service);
		git_buf_printf(buf, "Content-Type: application/x-git-%s-request\r\n", service);
		git_buf_printf(buf, "Content-Length: %"PRIuZ "\r\n", content_length);
	} else {
		git_buf_puts(buf, "Accept: */*\r\n");
	}
	git_buf_puts(buf, "\r\n");

	if (git_buf_oom(buf))
		return -1;

	return 0;
}

static int do_connect(transport_http *t, const char *host, const char *port)
{
	if (t->parent.connected && http_should_keep_alive(&t->parser))
		return 0;

	if (gitno_connect((git_transport *) t, host, port) < 0)
		return -1;

	t->parent.connected = 1;

	return 0;
}

/*
 * The HTTP parser is streaming, so we need to wait until we're in the
 * field handler before we can be sure that we can store the previous
 * value. Right now, we only care about the
 * Content-Type. on_header_{field,value} should be kept generic enough
 * to work for any request.
 */

static const char *typestr = "Content-Type";

static int on_header_field(http_parser *parser, const char *str, size_t len)
{
	transport_http *t = (transport_http *) parser->data;
	git_buf *buf = &t->buf;

	if (t->last_cb == VALUE && t->ct_found) {
		t->ct_finished = 1;
		t->ct_found = 0;
		t->content_type = git__strdup(git_buf_cstr(buf));
		GITERR_CHECK_ALLOC(t->content_type);
		git_buf_clear(buf);
	}

	if (t->ct_found) {
		t->last_cb = FIELD;
		return 0;
	}

	if (t->last_cb != FIELD)
		git_buf_clear(buf);

	git_buf_put(buf, str, len);
	t->last_cb = FIELD;

	return git_buf_oom(buf);
}

static int on_header_value(http_parser *parser, const char *str, size_t len)
{
	transport_http *t = (transport_http *) parser->data;
	git_buf *buf = &t->buf;

	if (t->ct_finished) {
		t->last_cb = VALUE;
		return 0;
	}

	if (t->last_cb == VALUE)
		git_buf_put(buf, str, len);

	if (t->last_cb == FIELD && !strcmp(git_buf_cstr(buf), typestr)) {
		t->ct_found = 1;
		git_buf_clear(buf);
		git_buf_put(buf, str, len);
	}

	t->last_cb = VALUE;

	return git_buf_oom(buf);
}

static int on_headers_complete(http_parser *parser)
{
	transport_http *t = (transport_http *) parser->data;
	git_buf *buf = &t->buf;

	/* The content-type is text/plain for 404, so don't validate */
	if (parser->status_code == 404) {
		git_buf_clear(buf);
		return 0;
	}

	if (t->content_type == NULL) {
		t->content_type = git__strdup(git_buf_cstr(buf));
		if (t->content_type == NULL)
			return t->error = -1;
	}

	git_buf_clear(buf);
	git_buf_printf(buf, "application/x-git-%s-advertisement", t->service);
	if (git_buf_oom(buf))
		return t->error = -1;

	if (strcmp(t->content_type, git_buf_cstr(buf)))
		return t->error = -1;

	git_buf_clear(buf);
	return 0;
}

static int on_message_complete(http_parser *parser)
{
	transport_http *t = (transport_http *) parser->data;

	t->transfer_finished = 1;

	if (parser->status_code == 404) {
		giterr_set(GITERR_NET, "Remote error: %s", git_buf_cstr(&t->buf));
		t->error = -1;
	}

	return 0;
}

static int on_body_fill_buffer(http_parser *parser, const char *str, size_t len)
{
	git_transport *transport = (git_transport *) parser->data;
	transport_http *t = (transport_http *) parser->data;
	gitno_buffer *buf = &transport->buffer;

	if (buf->len - buf->offset < len) {
		giterr_set(GITERR_NET, "Can't fit data in the buffer");
		return t->error = -1;
	}

	memcpy(buf->data + buf->offset, str, len);
	buf->offset += len;

	return 0;
}

static int http_recv_cb(gitno_buffer *buf)
{
	git_transport *transport = (git_transport *) buf->cb_data;
	transport_http *t = (transport_http *) transport;
	size_t old_len;
	gitno_buffer inner;
	char buffer[2048];
	int error;

	if (t->transfer_finished)
		return 0;

	gitno_buffer_setup(transport, &inner, buffer, sizeof(buffer));

	if ((error = gitno_recv(&inner)) < 0)
		return -1;

	old_len = buf->offset;
	http_parser_execute(&t->parser, &t->settings, inner.data, inner.offset);
	if (t->error < 0)
		return t->error;

	return (int)(buf->offset - old_len);
}

/* Set up the gitno_buffer so calling gitno_recv() grabs data from the HTTP response */
static void setup_gitno_buffer(git_transport *transport)
{
	transport_http *t = (transport_http *) transport;

	http_parser_init(&t->parser, HTTP_RESPONSE);
	t->parser.data = t;
	t->transfer_finished = 0;
	memset(&t->settings, 0x0, sizeof(http_parser_settings));
	t->settings.on_header_field = on_header_field;
	t->settings.on_header_value = on_header_value;
	t->settings.on_headers_complete = on_headers_complete;
	t->settings.on_body = on_body_fill_buffer;
	t->settings.on_message_complete = on_message_complete;

	gitno_buffer_setup_callback(transport, &transport->buffer, t->buffer, sizeof(t->buffer), http_recv_cb, t);
}

static int http_connect(git_transport *transport, int direction)
{
	transport_http *t = (transport_http *) transport;
	int ret;
	git_buf request = GIT_BUF_INIT;
	const char *service = "upload-pack";
	const char *url = t->parent.url, *prefix_http = "http://", *prefix_https = "https://";
	const char *default_port;
	git_pkt *pkt;

	if (direction == GIT_DIR_PUSH) {
		giterr_set(GITERR_NET, "Pushing over HTTP is not implemented");
		return -1;
	}

	t->parent.direction = direction;

	if (!git__prefixcmp(url, prefix_http)) {
		url = t->parent.url + strlen(prefix_http);
		default_port = "80";
	}

	if (!git__prefixcmp(url, prefix_https)) {
		url += strlen(prefix_https);
		default_port = "443";
	}

	t->path = strchr(url, '/');

	if ((ret = gitno_extract_host_and_port(&t->host, &t->port, url, default_port)) < 0)
		goto cleanup;

	t->service = git__strdup(service);
	GITERR_CHECK_ALLOC(t->service);

	if ((ret = do_connect(t, t->host, t->port)) < 0)
		goto cleanup;

	/* Generate and send the HTTP request */
	if ((ret = gen_request(&request, t->path, t->host, "GET", service, 0, 1)) < 0) {
		giterr_set(GITERR_NET, "Failed to generate request");
		goto cleanup;
	}


	if (gitno_send(transport, request.ptr, request.size, 0) < 0)
		goto cleanup;

	setup_gitno_buffer(transport);
	if ((ret = git_protocol_store_refs(transport, 2)) < 0)
		goto cleanup;

	pkt = git_vector_get(&transport->refs, 0);
	if (pkt == NULL || pkt->type != GIT_PKT_COMMENT) {
		giterr_set(GITERR_NET, "Invalid HTTP response");
		return t->error = -1;
	} else {
		/* Remove the comment and flush pkts */
		git_vector_remove(&transport->refs, 0);
		git__free(pkt);
		pkt = git_vector_get(&transport->refs, 0);
		git_vector_remove(&transport->refs, 0);
		git__free(pkt);
	}

	if (git_protocol_detect_caps(git_vector_get(&transport->refs, 0), &transport->caps) < 0)
		return t->error = -1;

cleanup:
	git_buf_free(&request);
	git_buf_clear(&t->buf);

	return ret;
}

static int http_negotiation_step(struct git_transport *transport, void *data, size_t len)
{
	transport_http *t = (transport_http *) transport;
	git_buf request = GIT_BUF_INIT;
	int ret;

	/* First, send the data as a HTTP POST request */
	if ((ret = do_connect(t, t->host, t->port)) < 0)
		return -1;

	if ((ret = gen_request(&request, t->path, t->host, "POST", "upload-pack", len, 0)) < 0)
		goto on_error;

	if ((ret = gitno_send(transport, request.ptr, request.size, 0)) < 0)
		goto on_error;

	if ((ret = gitno_send(transport, data, len, 0)) < 0)
		goto on_error;

	git_buf_free(&request);

	/* Then we need to set up the buffer to grab data from the HTTP response */
	setup_gitno_buffer(transport);

	return 0;

on_error:
	git_buf_free(&request);
	return -1;
}

static int http_close(git_transport *transport)
{
	if (gitno_ssl_teardown(transport) < 0)
		return -1;

	if (gitno_close(transport->socket) < 0) {
		giterr_set(GITERR_OS, "Failed to close the socket: %s", strerror(errno));
		return -1;
	}

	transport->connected = 0;

	return 0;
}


static void http_free(git_transport *transport)
{
	transport_http *t = (transport_http *) transport;
	git_vector *refs = &transport->refs;
	git_vector *common = &transport->common;
	unsigned int i;
	git_pkt *p;

#ifdef GIT_WIN32
	/* cleanup the WSA context. note that this context
	 * can be initialized more than once with WSAStartup(),
	 * and needs to be cleaned one time for each init call
	 */
	WSACleanup();
#endif

	git_vector_foreach(refs, i, p) {
		git_pkt_free(p);
	}
	git_vector_free(refs);
	git_vector_foreach(common, i, p) {
		git_pkt_free(p);
	}
	git_vector_free(common);
	git_buf_free(&t->buf);
	git__free(t->content_type);
	git__free(t->host);
	git__free(t->port);
	git__free(t->service);
	git__free(t->parent.url);
	git__free(t);
}

int git_transport_http(git_transport **out)
{
	transport_http *t;

	t = git__malloc(sizeof(transport_http));
	GITERR_CHECK_ALLOC(t);

	memset(t, 0x0, sizeof(transport_http));

	t->parent.connect = http_connect;
	t->parent.negotiation_step = http_negotiation_step;
	t->parent.close = http_close;
	t->parent.free = http_free;
	t->parent.rpc = 1;

	if (git_vector_init(&t->parent.refs, 16, NULL) < 0) {
		git__free(t);
		return -1;
	}

#ifdef GIT_WIN32
	/* on win32, the WSA context needs to be initialized
	 * before any socket calls can be performed */
	if (WSAStartup(MAKEWORD(2,2), &t->wsd) != 0) {
		http_free((git_transport *) t);
		giterr_set(GITERR_OS, "Winsock init failed");
		return -1;
	}
#endif

	*out = (git_transport *) t;
	return 0;
}

int git_transport_https(git_transport **out)
{
#ifdef GIT_SSL
	transport_http *t;
	if (git_transport_http((git_transport **)&t) < 0)
		return -1;

	t->parent.use_ssl = 1;
	t->parent.check_cert = 1;
	*out = (git_transport *) t;

	return 0;
#else
	GIT_UNUSED(out);

	giterr_set(GITERR_NET, "HTTPS support not available");
	return -1;
#endif
}
