#include <u.h>
#include <libc.h>

#include "git.h"

typedef struct Map	Map;

struct Map {
	char	*ref;
	Hash	ours;
	Hash	theirs;
};

int sendall;
int force;
int nbranch;
char **branch;
char *removed[128];
int nremoved;
int npacked;
int nsent;

int
findref(char **r, int nr, char *ref)
{
	int i;
	for(i = 0; i < nr; i++)
		if(strcmp(r[i], ref) == 0)
			return i;
	return -1;
}

int
findkey(Map *m, int nm, char *ref)
{
	int i;
	for(i = 0; i < nm; i++)
		if(strcmp(m[i].ref, ref) == 0)
			return i;
	return -1;
}

int
readours(Hash **tailp, char ***refp)
{
	int nu, i, idx;
	char *r, *pfx, **ref;
	Hash *tail;

	if(sendall)
		return listrefs(tailp, refp);
	nu = 0;
	tail = eamalloc((nremoved + nbranch), sizeof(Hash));
	ref = eamalloc((nremoved + nbranch), sizeof(char*));
	for(i = 0; i < nbranch; i++){
		ref[nu] = estrdup(branch[i]);
		if(resolveref(&tail[nu], branch[i]) == -1)
			sysfatal("broken branch %s", branch[i]);
		nu++;
	}
	for(i = 0; i < nremoved; i++){
		pfx = "refs/heads/";
		if(strstr(removed[i], "heads/") == removed[i])
			pfx = "refs/";
		if(strstr(removed[i], "refs/heads/") == removed[i])
			pfx = "";
		if((r = smprint("%s%s", pfx, removed[i])) == nil)
			sysfatal("smprint: %r");
		if((idx = findref(ref, nu, r)) == -1)
			idx = nu++;
		else
			free(ref[idx]);
		assert(idx < nremoved + nbranch);
		memcpy(&tail[idx], &Zhash, sizeof(Hash));
		ref[idx] = r;
	}
	dprint(1, "nu: %d\n", nu);
	for(i = 0; i < nu; i++)
		dprint(1, "update: %H %s\n", tail[i], ref[i]);
	*tailp = tail;
	*refp = ref;
	return nu;	
}

int
sendpack(Conn *c)
{
	int i, n, idx, nsp, send, first;
	int nours, ntheirs, nmap;
	char buf[Pktmax], *sp[3];
	Hash h, *theirs, *ours;
	Object *a, *b, *p, *o;
	char **refs;
	Map *map, *m;

	first = 1;
	nours = readours(&ours, &refs);
	theirs = nil;
	ntheirs = 0;
	nmap = nours;
	map = eamalloc(nmap, sizeof(Map));
	for(i = 0; i < nmap; i++){
		map[i].theirs = Zhash;
		map[i].ours = ours[i];
		map[i].ref = refs[i];
	}
	while(1){
		n = readpkt(c, buf, sizeof(buf));
		if(n == -1)
			return -1;
		if(n == 0)
			break;
		if(first && n > strlen(buf))
			parsecaps(buf + strlen(buf) + 1, c);
		first = 0;

		if(getfields(buf, sp, nelem(sp), 1, " \t\r\n") != 2)
			sysfatal("invalid ref line %.*s", utfnlen(buf, n), buf);
		theirs = earealloc(theirs, ntheirs+1, sizeof(Hash));
		if(hparse(&theirs[ntheirs], sp[0]) == -1)
			sysfatal("invalid hash %s", sp[0]);
		if((idx = findkey(map, nmap, sp[1])) != -1)
			map[idx].theirs = theirs[ntheirs];
		/*
		 * we only keep their ref if we can read the object to add it
		 * to our reachability; otherwise, discard it; we only care
		 * that we don't have it, so we can tell whether we need to
		 * bail out of pushing.
		 */
		if((o = readobject(theirs[ntheirs])) != nil){
			ntheirs++;
			unref(o);
		}
	}

	if(writephase(c) == -1)
		return -1;
	send = 0;
	if(force)
		send=1;
	for(i = 0; i < nmap; i++){
		m = &map[i];
		a = readobject(m->theirs);
		if(hasheq(&m->ours, &Zhash))
			b = nil;
		else
			b = readobject(m->ours);
		p = nil;
		if(a != nil && b != nil)
			p = ancestor(a, b);
		if(!force
		&& !hasheq(&m->theirs, &Zhash)
		&& !hasheq(&m->ours, &Zhash)
		&& (a == nil || p != a)){
			fprint(2, "remote has diverged\n");
			werrstr("remote diverged");
			flushpkt(c);
			return -1;
		}
		unref(a);
		unref(b);
		unref(p);
		if(hasheq(&m->theirs, &m->ours)){
			print("uptodate %s\n", m->ref);
			continue;
		}
		print("update %s %H %H\n", m->ref, m->theirs, m->ours);
		n = snprint(buf, sizeof(buf), "%H %H %s", m->theirs, m->ours, m->ref);

		/*
		 * Workaround for github.
		 *
		 * Github will accept the pack but fail to update the references
		 * if we don't have capabilities advertised. Report-status seems
		 * harmless to add, so we add it.
		 *
		 * Github doesn't advertise any capabilities, so we can't check
		 * for compatibility. We just need to add it blindly.
		 */
		if(i == 0 && c->report){
			buf[n++] = '\0';
			n += snprint(buf + n, sizeof(buf) - n, " report-status");
		}
		if(writepkt(c, buf, n) == -1)
			sysfatal("unable to send update pkt");
		send = 1;
	}
	flushpkt(c);
	if(!send){
		fprint(2, "nothing to send\n");
		return 0;
	}

	if(writepack(c->wfd, ours, nours, theirs, ntheirs, &h) == -1)
		return -1;
	if(!c->report)
		return 0;

	if(readphase(c) == -1)
		return -1;
	/* We asked for a status report, may as well use it. */
	while((n = readpkt(c, buf, sizeof(buf))) > 0){
 		buf[n] = 0;
		if(chattygit)
			fprint(2, "done sending pack, status %s\n", buf);
		nsp = getfields(buf, sp, nelem(sp), 1, " \t\n\r");
		if(nsp < 2)
			continue;
		if(nsp < 3)
			sp[2] = "";
		/*
		 * Only report errors; successes will be reported by
		 * surrounding scripts.
		 */
		if(strcmp(sp[0], "unpack") == 0 && strcmp(sp[1], "ok") != 0)
			werrstr("unpack %s", sp[1]);
		else if(strcmp(sp[0], "ng") == 0)
			werrstr("failed update: %s %s", sp[1], sp[2]);
		else
			continue;
		return -1;
	}
	return 0;
}

void
usage(void)
{
	fprint(2, "usage: %s remote [reponame]\n", argv0);
	exits("usage");
}

void
main(int argc, char **argv)
{
	char *br;
	Conn c;

	ARGBEGIN{
	default:
		usage();
		break;
	case 'd':
		chattygit++;
		break;
	case 'f':
		force++;
		break;
	case 'r':
		if(nremoved == nelem(removed))
			sysfatal("too many deleted branches");
		removed[nremoved++] = EARGF(usage());
		break;
	case 'a':
		sendall++;
		break;
	case 'b':
		br = EARGF(usage());
		if(strncmp(br, "refs/heads/", strlen("refs/heads/")) == 0)
			br = smprint("%s", br);
		else if(strncmp(br, "heads/", strlen("heads/")) == 0)
			br = smprint("refs/%s", br);
		else
			br = smprint("refs/heads/%s", br);
		branch = erealloc(branch, (nbranch + 1)*sizeof(char*));
		branch[nbranch] = br;
		nbranch++;
		break;
	}ARGEND;

	gitinit();
	if(argc != 1)
		usage();
	if(gitconnect(&c, argv[0], "receive") == -1)
		sysfatal("git connect: %s: %r", argv[0]);
	if(sendpack(&c) == -1)
		sysfatal("send failed: %r");
	closeconn(&c);
	exits(nil);
}
