/*
 * This file is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2,
 * as published by the Free Software Foundation.
 *
 * In addition to the permissions in the GNU General Public License,
 * the authors give you unlimited permission to link the compiled
 * version of this file into combinations with other programs,
 * and to distribute those combinations without any restriction
 * coming from the use of this file.  (The General Public License
 * restrictions do apply in other respects; for example, they cover
 * modification of the file, and distribution when not linked into
 * a combined executable.)
 *
 * This file is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#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"

enum last_cb {
	NONE,
	FIELD,
	VALUE
};

typedef struct {
	git_transport parent;
	git_vector refs;
	int socket;
	git_buf buf;
	git_remote_head **heads;
	int error;
	int transfer_finished :1,
		ct_found :1,
		ct_finished :1;
	enum last_cb last_cb;
	char *content_type;
	char *service;
} transport_http;

static int gen_request(git_buf *buf, const char *url, const char *host, const char *service)
{
	const char *path = url;

	path = strchr(path, '/');
	if (path == NULL) /* Is 'git fetch http://host.com/' valid? */
		path = "/";

	git_buf_printf(buf, "GET %s/info/refs?service=git-%s HTTP/1.1\r\n", 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);
	git_buf_puts(buf, "Accept: */*\r\n" "Pragma: no-cache\r\n\r\n");

	if (git_buf_oom(buf))
		return GIT_ENOMEM;

	return GIT_SUCCESS;
}

static int do_connect(transport_http *t, const char *service)
{
	git_buf request = GIT_BUF_INIT;
	int s = -1, error;
	const char *url, *prefix;
	char *host = NULL, *port = NULL;

	url = t->parent.url;
	prefix = "http://";

	if (!git__prefixcmp(url, prefix))
		url += strlen(prefix);

	error = gitno_extract_host_and_port(&host, &port, url, "80");
	if (error < GIT_SUCCESS)
		goto cleanup;

	t->service = git__strdup(service);
	if (t->service == NULL) {
		error = GIT_ENOMEM;
		goto cleanup;
	}

	s = gitno_connect(host, port);
	if (s < GIT_SUCCESS) {
	    error = git__throw(error, "Failed to connect to host");
	}
	t->socket = s;

	/* Generate and send the HTTP request */
	error = gen_request(&request, url, host, service);
	if (error < GIT_SUCCESS) {
		error = git__throw(error, "Failed to generate request");
		goto cleanup;
	}
	error = gitno_send(s, git_buf_cstr(&request), strlen(git_buf_cstr(&request)), 0);
	if (error < GIT_SUCCESS)
		error = git__rethrow(error, "Failed to send the HTTP request");

cleanup:
	git_buf_free(&request);
	free(host);
	free(port);

	return error;
}

/*
 * 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));
		if (t->content_type == NULL)
			return t->error = GIT_ENOMEM;
		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;

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

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

	if (strcmp(t->content_type, git_buf_cstr(buf)))
		return t->error = git__throw(GIT_EOBJCORRUPTED, "Content-Type '%s' is wrong", t->content_type);

	git_buf_clear(buf);
	return 0;
}

static int on_body_store_refs(http_parser *parser, const char *str, size_t len)
{
	transport_http *t = (transport_http *) parser->data;
	git_buf *buf = &t->buf;
	git_vector *refs = &t->refs;
	int error;
	const char *line_end, *ptr;
	static int first_pkt = 1;

	if (len == 0) { /* EOF */
		if (buf->size != 0)
			return t->error = git__throw(GIT_ERROR, "EOF and unprocessed data");
		else
			return 0;
	}

	git_buf_put(buf, str, len);
	ptr = buf->ptr;
	while (1) {
		git_pkt *pkt;

		if (buf->size == 0)
			return 0;

		error = git_pkt_parse_line(&pkt, ptr, &line_end, buf->size);
		if (error == GIT_ESHORTBUFFER)
			return 0; /* Ask for more */
		if (error < GIT_SUCCESS)
			return t->error = git__rethrow(error, "Failed to parse pkt-line");

		git_buf_consume(buf, line_end);

		if (first_pkt) {
			first_pkt = 0;
			if (pkt->type != GIT_PKT_COMMENT)
				return t->error = git__throw(GIT_EOBJCORRUPTED, "Not a valid smart HTTP response");
		}

		error = git_vector_insert(refs, pkt);
		if (error < GIT_SUCCESS)
			return t->error = git__rethrow(error, "Failed to add pkt to list");
	}

	return error;
}

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

	t->transfer_finished = 1;
	return 0;
}

static int store_refs(transport_http *t)
{
	int error = GIT_SUCCESS;
	http_parser parser;
	http_parser_settings settings;
	char buffer[1024];
	gitno_buffer buf;

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

	gitno_buffer_setup(&buf, buffer, sizeof(buffer), t->socket);

	while(1) {
		size_t parsed;

		error = gitno_recv(&buf);
		if (error < GIT_SUCCESS)
			return git__rethrow(error, "Error receiving data from network");

		parsed = http_parser_execute(&parser, &settings, buf.data, buf.offset);
		/* Both should happen at the same time */
		if (parsed != buf.offset || t->error < GIT_SUCCESS)
			return git__rethrow(t->error, "Error parsing HTTP data");

		gitno_consume_n(&buf, parsed);

		if (error == 0 || t->transfer_finished)
			return GIT_SUCCESS;
	}

	return error;
}

static int http_connect(git_transport *transport, int direction)
{
	transport_http *t = (transport_http *) transport;
	int error;

	if (direction == GIT_DIR_PUSH)
		return git__throw(GIT_EINVALIDARGS, "Pushing over HTTP is not supported");

	t->parent.direction = direction;
	error = git_vector_init(&t->refs, 16, NULL);
	if (error < GIT_SUCCESS)
		return git__rethrow(error, "Failed to init refs vector");

	error = do_connect(t, "upload-pack");
	if (error < GIT_SUCCESS) {
		error = git__rethrow(error, "Failed to connect to host");
		goto cleanup;
	}

	error = store_refs(t);

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

	return error;
}

static int http_ls(git_transport *transport, git_headarray *array)
{
	transport_http *t = (transport_http *) transport;
	git_vector *refs = &t->refs;
	unsigned int i;
	int len = 0;
	git_pkt_ref *p;

	array->heads = git__calloc(refs->length, sizeof(git_remote_head*));
	if (array->heads == NULL)
		return GIT_ENOMEM;

	git_vector_foreach(refs, i, p) {
		if (p->type != GIT_PKT_REF)
			continue;

		array->heads[len] = &p->head;
		len++;
	}

	array->len = len;
	t->heads = array->heads;

	return GIT_SUCCESS;
}

static int http_close(git_transport *transport)
{
	transport_http *t = (transport_http *) transport;
	int error;

	error = close(t->socket);
	if (error < 0)
		return git__throw(GIT_EOSERR, "Failed to close the socket: %s", strerror(errno));

	return GIT_SUCCESS;
}


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

	git_vector_foreach(refs, i, p) {
		git_pkt_free(p);
	}
	git_vector_free(refs);
	free(t->heads);
	free(t->content_type);
	free(t->service);
	free(t->parent.url);
	free(t);
}


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

	t = git__malloc(sizeof(transport_http));
	if (t == NULL)
		return GIT_ENOMEM;

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

	t->parent.connect = http_connect;
	t->parent.ls = http_ls;
	t->parent.close = http_close;
	t->parent.free = http_free;

	*out = (git_transport *) t;

	return GIT_SUCCESS;
}
