// mercurial.js - JavaScript utility functions
//
// Rendering of branch DAGs on the client side
// Display of elapsed time
// Show or hide diffstat
//
// Copyright 2008 Dirkjan Ochtman <dirkjan AT ochtman DOT nl>
// Copyright 2006 Alexander Schremmer <alex AT alexanderweb DOT de>
//
// derived from code written by Scott James Remnant <scott@ubuntu.com>
// Copyright 2005 Canonical Ltd.
//
// This software may be used and distributed according to the terms
// of the GNU General Public License, incorporated herein by reference.

var colors = [
	[ 1.0, 0.0, 0.0 ],
	[ 1.0, 1.0, 0.0 ],
	[ 0.0, 1.0, 0.0 ],
	[ 0.0, 1.0, 1.0 ],
	[ 0.0, 0.0, 1.0 ],
	[ 1.0, 0.0, 1.0 ]
];

function Graph() {

	this.canvas = document.getElementById('graph');
	this.ctx = this.canvas.getContext('2d');
	this.ctx.strokeStyle = 'rgb(0, 0, 0)';
	this.ctx.fillStyle = 'rgb(0, 0, 0)';
	this.bg = [0, 4];
	this.cell = [2, 0];
	this.columns = 0;

}

Graph.prototype = {
	reset: function() {
		this.bg = [0, 4];
		this.cell = [2, 0];
		this.columns = 0;
	},

	scale: function(height) {
		this.bg_height = height;
		this.box_size = Math.floor(this.bg_height / 1.2);
		this.cell_height = this.box_size;
	},

	setColor: function(color, bg, fg) {

		// Set the colour.
		//
		// If color is a string, expect an hexadecimal RGB
		// value and apply it unchanged. If color is a number,
		// pick a distinct colour based on an internal wheel;
		// the bg parameter provides the value that should be
		// assigned to the 'zero' colours and the fg parameter
		// provides the multiplier that should be applied to
		// the foreground colours.
		var s;
		if(typeof color === "string") {
			s = "#" + color;
		} else { //typeof color === "number"
			color %= colors.length;
			var red = (colors[color][0] * fg) || bg;
			var green = (colors[color][1] * fg) || bg;
			var blue = (colors[color][2] * fg) || bg;
			red = Math.round(red * 255);
			green = Math.round(green * 255);
			blue = Math.round(blue * 255);
			s = 'rgb(' + red + ', ' + green + ', ' + blue + ')';
		}
		this.ctx.strokeStyle = s;
		this.ctx.fillStyle = s;
		return s;

	},

	edge: function(x0, y0, x1, y1, color, width) {

		this.setColor(color, 0.0, 0.65);
		if(width >= 0)
			 this.ctx.lineWidth = width;
		this.ctx.beginPath();
		this.ctx.moveTo(x0, y0);
		this.ctx.lineTo(x1, y1);
		this.ctx.stroke();

	},

	graphNodeCurrent: function(x, y, radius) {
		this.ctx.lineWidth = 2;
		this.ctx.beginPath();
		this.ctx.arc(x, y, radius * 1.75, 0, Math.PI * 2, true);
		this.ctx.stroke();
	},

	graphNodeClosing: function(x, y, radius) {
		this.ctx.fillRect(x - radius, y - 1.5, radius * 2, 3);
	},

	graphNodeUnstable: function(x, y, radius) {
		var x30 = radius * Math.cos(Math.PI / 6);
		var y30 = radius * Math.sin(Math.PI / 6);
		this.ctx.lineWidth = 2;
		this.ctx.beginPath();
		this.ctx.moveTo(x, y - radius);
		this.ctx.lineTo(x, y + radius);
		this.ctx.moveTo(x - x30, y - y30);
		this.ctx.lineTo(x + x30, y + y30);
		this.ctx.moveTo(x - x30, y + y30);
		this.ctx.lineTo(x + x30, y - y30);
		this.ctx.stroke();
	},

	graphNodeObsolete: function(x, y, radius) {
		var p45 = radius * Math.cos(Math.PI / 4);
		this.ctx.lineWidth = 3;
		this.ctx.beginPath();
		this.ctx.moveTo(x - p45, y - p45);
		this.ctx.lineTo(x + p45, y + p45);
		this.ctx.moveTo(x - p45, y + p45);
		this.ctx.lineTo(x + p45, y - p45);
		this.ctx.stroke();
	},

	graphNodeNormal: function(x, y, radius) {
		this.ctx.beginPath();
		this.ctx.arc(x, y, radius, 0, Math.PI * 2, true);
		this.ctx.fill();
	},

	vertex: function(x, y, radius, color, parity, cur) {
		this.ctx.save();
		this.setColor(color, 0.25, 0.75);
		if (cur.graphnode[0] === '@') {
			this.graphNodeCurrent(x, y, radius);
		}
		switch (cur.graphnode.substr(-1)) {
			case '_':
				this.graphNodeClosing(x, y, radius);
				break;
			case '*':
				this.graphNodeUnstable(x, y, radius);
				break;
			case 'x':
				this.graphNodeObsolete(x, y, radius);
				break;
			default:
				this.graphNodeNormal(x, y, radius);
		}
		this.ctx.restore();

		var left = (this.bg_height - this.box_size) + (this.columns + 1) * this.box_size;
		var item = document.querySelector('[data-node="' + cur.node + '"]');
		if (item) {
			item.style.paddingLeft = left + 'px';
		}
	},

	render: function(data) {

		var i, j, cur, line, start, end, color, x, y, x0, y0, x1, y1, column, radius;

		var cols = 0;
		for (i = 0; i < data.length; i++) {
			cur = data[i];
			for (j = 0; j < cur.edges.length; j++) {
				line = cur.edges[j];
				cols = Math.max(cols, line[0], line[1]);
			}
		}
		this.canvas.width = (cols + 1) * this.bg_height;
		this.canvas.height = (data.length + 1) * this.bg_height - 27;

		for (i = 0; i < data.length; i++) {

			var parity = i % 2;
			this.cell[1] += this.bg_height;
			this.bg[1] += this.bg_height;

			cur = data[i];
			var fold = false;

			var prevWidth = this.ctx.lineWidth;
			for (j = 0; j < cur.edges.length; j++) {

				line = cur.edges[j];
				start = line[0];
				end = line[1];
				color = line[2];
				var width = line[3];
				if(width < 0)
					 width = prevWidth;
				var branchcolor = line[4];
				if(branchcolor)
					color = branchcolor;

				if (end > this.columns || start > this.columns) {
					this.columns += 1;
				}

				if (start === this.columns && start > end) {
					fold = true;
				}

				x0 = this.cell[0] + this.box_size * start + this.box_size / 2;
				y0 = this.bg[1] - this.bg_height / 2;
				x1 = this.cell[0] + this.box_size * end + this.box_size / 2;
				y1 = this.bg[1] + this.bg_height / 2;

				this.edge(x0, y0, x1, y1, color, width);

			}
			this.ctx.lineWidth = prevWidth;

			// Draw the revision node in the right column

			column = cur.vertex[0];
			color = cur.vertex[1];

			radius = this.box_size / 8;
			x = this.cell[0] + this.box_size * column + this.box_size / 2;
			y = this.bg[1] - this.bg_height / 2;
			this.vertex(x, y, radius, color, parity, cur);

			if (fold) this.columns -= 1;

		}

	}

};


function process_dates(parentSelector){

	// derived from code from mercurial/templatefilter.py

	var scales = {
		'year':  365 * 24 * 60 * 60,
		'month':  30 * 24 * 60 * 60,
		'week':    7 * 24 * 60 * 60,
		'day':    24 * 60 * 60,
		'hour':   60 * 60,
		'minute': 60,
		'second': 1
	};

	function format(count, string){
		var ret = count + ' ' + string;
		if (count > 1){
			ret = ret + 's';
		}
 		return ret;
 	}

	function shortdate(date){
		var ret = date.getFullYear() + '-';
		// getMonth() gives a 0-11 result
		var month = date.getMonth() + 1;
		if (month <= 9){
			ret += '0' + month;
		} else {
			ret += month;
		}
		ret += '-';
		var day = date.getDate();
		if (day <= 9){
			ret += '0' + day;
		} else {
			ret += day;
		}
		return ret;
	}

 	function age(datestr){
 		var now = new Date();
 		var once = new Date(datestr);
		if (isNaN(once.getTime())){
			// parsing error
			return datestr;
		}

		var delta = Math.floor((now.getTime() - once.getTime()) / 1000);

		var future = false;
		if (delta < 0){
			future = true;
			delta = -delta;
			if (delta > (30 * scales.year)){
				return "in the distant future";
			}
		}

		if (delta > (2 * scales.year)){
			return shortdate(once);
		}

		for (var unit in scales){
			if (!scales.hasOwnProperty(unit)) { continue; }
			var s = scales[unit];
			var n = Math.floor(delta / s);
			if ((n >= 2) || (s === 1)){
				if (future){
					return format(n, unit) + ' from now';
				} else {
					return format(n, unit) + ' ago';
				}
			}
		}
	}

	var nodes = document.querySelectorAll((parentSelector || '') + ' .age');
	var dateclass = new RegExp('\\bdate\\b');
	for (var i=0; i<nodes.length; ++i){
		var node = nodes[i];
		var classes = node.className;
		var agevalue = age(node.textContent);
		if (dateclass.test(classes)){
			// We want both: date + (age)
			node.textContent += ' ('+agevalue+')';
		} else {
			node.title = node.textContent;
			node.textContent = agevalue;
		}
	}
}

function toggleDiffstat() {
    var curdetails = document.getElementById('diffstatdetails').style.display;
    var curexpand = curdetails === 'none' ? 'inline' : 'none';
    document.getElementById('diffstatdetails').style.display = curexpand;
    document.getElementById('diffstatexpand').style.display = curdetails;
}

function toggleLinewrap() {
    function getLinewrap() {
        var nodes = document.getElementsByClassName('sourcelines');
        // if there are no such nodes, error is thrown here
        return nodes[0].classList.contains('wrap');
    }

    function setLinewrap(enable) {
        var nodes = document.getElementsByClassName('sourcelines');
        var i;
        for (i = 0; i < nodes.length; i++) {
            if (enable) {
                nodes[i].classList.add('wrap');
            } else {
                nodes[i].classList.remove('wrap');
            }
        }

        var links = document.getElementsByClassName('linewraplink');
        for (i = 0; i < links.length; i++) {
            links[i].innerHTML = enable ? 'on' : 'off';
        }
    }

    setLinewrap(!getLinewrap());
}

function format(str, replacements) {
    return str.replace(/%(\w+)%/g, function(match, p1) {
        return String(replacements[p1]);
    });
}

function makeRequest(url, method, onstart, onsuccess, onerror, oncomplete) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            try {
                if (xhr.status === 200) {
                    onsuccess(xhr.responseText);
                } else {
                    throw 'server error';
                }
            } catch (e) {
                onerror(e);
            } finally {
                oncomplete();
            }
        }
    };

    xhr.open(method, url);
    xhr.overrideMimeType("text/xhtml; charset=" + document.characterSet.toLowerCase());
    xhr.send();
    onstart();
    return xhr;
}

function removeByClassName(className) {
    var nodes = document.getElementsByClassName(className);
    while (nodes.length) {
        nodes[0].parentNode.removeChild(nodes[0]);
    }
}

function docFromHTML(html) {
    var doc = document.implementation.createHTMLDocument('');
    doc.documentElement.innerHTML = html;
    return doc;
}

function appendFormatHTML(element, formatStr, replacements) {
    element.insertAdjacentHTML('beforeend', format(formatStr, replacements));
}

function adoptChildren(from, to) {
    var nodes = from.children;
    var curClass = 'c' + Date.now();
    while (nodes.length) {
        var node = nodes[0];
        node = document.adoptNode(node);
        node.classList.add(curClass);
        to.appendChild(node);
    }
    process_dates('.' + curClass);
}

function ajaxScrollInit(urlFormat,
                        nextPageVar,
                        nextPageVarGet,
                        containerSelector,
                        messageFormat,
                        mode) {
    var updateInitiated = false;
    var container = document.querySelector(containerSelector);

    function scrollHandler() {
        if (updateInitiated) {
            return;
        }

        var scrollHeight = document.documentElement.scrollHeight;
        var clientHeight = document.documentElement.clientHeight;
        var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;

        if (scrollHeight - (scrollTop + clientHeight) < 50) {
            updateInitiated = true;
            removeByClassName('scroll-loading-error');
            container.lastElementChild.classList.add('scroll-separator');

            if (!nextPageVar) {
                var message = {
                    'class': 'scroll-loading-info',
                    text: 'No more entries'
                };
                appendFormatHTML(container, messageFormat, message);
                return;
            }

            makeRequest(
                format(urlFormat, {next: nextPageVar}),
                'GET',
                function onstart() {
                    var message = {
                        'class': 'scroll-loading',
                        text: 'Loading...'
                    };
                    appendFormatHTML(container, messageFormat, message);
                },
                function onsuccess(htmlText) {
                    var doc = docFromHTML(htmlText);

                    if (mode === 'graph') {
                        var graph = window.graph;
                        var dataStr = htmlText.match(/^\s*var data = (.*);$/m)[1];
                        var data = JSON.parse(dataStr);
                        graph.reset();
                        adoptChildren(doc.querySelector('#graphnodes'), container.querySelector('#graphnodes'));
                        graph.render(data);
                    } else {
                        adoptChildren(doc.querySelector(containerSelector), container);
                    }

                    nextPageVar = nextPageVarGet(htmlText);
                },
                function onerror(errorText) {
                    var message = {
                        'class': 'scroll-loading-error',
                        text: 'Error: ' + errorText
                    };
                    appendFormatHTML(container, messageFormat, message);
                },
                function oncomplete() {
                    removeByClassName('scroll-loading');
                    updateInitiated = false;
                    scrollHandler();
                }
            );
        }
    }

    window.addEventListener('scroll', scrollHandler);
    window.addEventListener('resize', scrollHandler);
    scrollHandler();
}

function renderDiffOptsForm() {
    // We use URLSearchParams for query string manipulation. Old browsers don't
    // support this API.
    if (!("URLSearchParams" in window)) {
        return;
    }

    var form = document.getElementById("diffopts-form");

    var KEYS = [
        "ignorews",
        "ignorewsamount",
        "ignorewseol",
        "ignoreblanklines",
    ];

    var urlParams = new window.URLSearchParams(window.location.search);

    function updateAndRefresh(e) {
        var checkbox = e.target;
        var name = checkbox.id.substr(0, checkbox.id.indexOf("-"));
        urlParams.set(name, checkbox.checked ? "1" : "0");
        window.location.search = urlParams.toString();
    }

    var allChecked = form.getAttribute("data-ignorews") === "1";

    for (var i = 0; i < KEYS.length; i++) {
        var key = KEYS[i];

        var checkbox = document.getElementById(key + "-checkbox");
        if (!checkbox) {
            continue;
        }

        var currentValue = form.getAttribute("data-" + key);
        checkbox.checked = currentValue !== "0";

        // ignorews implies ignorewsamount and ignorewseol.
        if (allChecked && (key === "ignorewsamount" || key === "ignorewseol")) {
            checkbox.checked = true;
            checkbox.disabled = true;
        }

        checkbox.addEventListener("change", updateAndRefresh, false);
    }

    form.style.display = 'block';
}

document.addEventListener('DOMContentLoaded', function() {
   process_dates();
}, false);
