'use strict';

var Tour = (function(){
	var walkPoints = [];
	var walk = [];
	var active = false;
	var walkDistance = 0;
	var wholeWalkDistance = 0;
	var goButton = null;
	var pauseButton = null;
	var walkInfo = null;
	var lastInfoTime = 0;
	var graph;
	var texts;
	var speed = 10.0; // m/s
	var tourDialog = null;

	// constructor - create graph structure
	function Init(mesh, dialog){
		// create graph
		graph = CreateGraph(mesh.vertices, mesh.indicies);

		var svg = InitMap(mesh, dialog);
		GoButton(svg, dialog);
		PauseButton();
		MapPoints(svg);
		TextSearch(svg, dialog);
		ResetButton(svg, dialog);

		texts = ParseTexts(mesh.texts);

		tourDialog = dialog;
	}

	// add new neightbour to node
	function AddNeighbour(graph, node, neighbourId, distance, position){
		// check if node exists
		if(!graph[node]){
			graph[node] = {
				position: position,
				nodes: [],
			};
		}

		// add neigbour
		graph[node].nodes.push({
			id: neighbourId,
			distance: distance,
		});
	}

	function CreateGraph(vertices, edges){
		var graph = [];
		var len = edges.length;
		for(var i = 0; i < len; i += 2){
			var nodeA = edges[i];
			var nodeB = edges[i + 1];
			var vertexA = new THREE.Vector3(vertices[nodeA * 3], vertices[nodeA * 3 + 2], -vertices[nodeA * 3 + 1]);
			var vertexB = new THREE.Vector3(vertices[nodeB * 3], vertices[nodeB * 3 + 2], -vertices[nodeB * 3 + 1]);
			var distance = vertexA.distanceTo(vertexB);

			AddNeighbour(graph, nodeA, nodeB, distance, vertexA);
			AddNeighbour(graph, nodeB, nodeA, distance, vertexB);
		}

		return graph;
	}

	function NearestNode(distance, unvisited){
		var nearestId = false;
		var nearestDistance = false;
		for(var i = unvisited.length - 1; i >= 0; i --){
			var d = distance[unvisited[i]];
			if(d < nearestDistance || nearestDistance === false){
				nearestDistance = d;
				nearestId = unvisited[i];
			}
		}

		return nearestId;
	}

	function CreateWalk(end, distance, previous, graph){
		// create walk
		var walk = [];
		var nodeId = end|0;
		while(typeof graph[nodeId] != 'undefined'){
			walk.unshift(nodeId);
			nodeId = previous[nodeId];
		}

		return {
			distance: distance[end],
			walk: walk,
		}
	}

	function ParseTexts(texts){
		if(typeof texts === 'undefined'){
			return [];
		}

		return texts;
	}

	function Dijkstra(graph, begin, end){
		// initialization
		var len = graph.length;
		var distance = [];
		var previous = [];
		var unvisited = [begin|0];
		var closed = [];
		for(var i = 0; i < len; i ++){
			distance.push(100000); // infinity
			previous.push(false);
			closed.push(false);
		}
		distance[begin] = 0;

		// search graph
		while(unvisited.length){
			var nodeId = NearestNode(distance, unvisited);
			var nodes = graph[nodeId].nodes;

			// final node
			if(nodeId === end){
				break;
			}

			// close this node
			closed[nodeId] = true;
			unvisited.splice(unvisited.indexOf(nodeId), 1);

			for(var i = nodes.length - 1; i >= 0; i --){
				var neighbourId = nodes[i].id;
				var alt = distance[nodeId] + nodes[i].distance;

				if(alt < distance[neighbourId]){
					distance[neighbourId] = alt;
					previous[neighbourId] = nodeId;
				}

				// open this neighbour
				if(!closed[neighbourId]){
					unvisited.push(neighbourId);
				}
			}
		}

		return CreateWalk(end, distance, previous, graph);
	}

	function AddWalkPoint(svg, target){
		var walkId = target.attr('data-walk-id');
		var svgActive = svg.find('[data-walk-id="' + walkId + '"]');
		if(!svgActive.length){
			return;
		}

		var isActive = svgActive.attr('data-active') == 1;
		if(isActive){
			svgActive.attr('data-active', '0');
			svgActive.attr('r', '2.5');
			walkPoints.splice(walkPoints.indexOf(walkId), 1);
		}
		else{
			svgActive.attr('data-active', '1');
			svgActive.attr('r', '5.5');
			walkPoints.push(walkId);
		}

		FindWalk();
		pauseButton.toggle(walkPoints.length < 2);
		goButton.prop('disabled', walkPoints.length < 2);
		MarkWalk(svg);
	}

	// set edges on route to active
	function MarkWalk(svg){
		// clear old
		svg.find('line[data-active="1"]').attr('data-active', '0');
		svg.find('text').remove();

		// set edges
		for(var i = 0; i < walk.length - 1; i ++){
			var j = i + 1;
			var nodeA = walk[i];
			var nodeB = walk[j];
			var points = Math.min(nodeA, nodeB) + '_' + Math.max(nodeA, nodeB);
			svg.find('line[data-points="' + points + '"]').attr('data-active', '1');
		}

		// set numbers to selected circles and move them to front
		for(var i = 0; i < walkPoints.length; i ++){
			var circle = svg.find('[data-walk-id="' + walkPoints[i] + '"]');
			var x = parseFloat(circle.attr('cx')) - 2.5 - (i > 8 ? 2 : 0);
			var y = parseFloat(circle.attr('cy')) + 2.5;
			var node = document.createElementNS('http://www.w3.org/2000/svg', 'text'); // we can't use jQuery contructor to create SVG nodes
			node.setAttribute('x', x);
			node.setAttribute('y', y);
			node.setAttribute('data-walk-id', circle.attr('data-walk-id'));
			node.innerHTML = i + 1;
			svg.append(circle); // move circle node to front by appending it to the end of the SVG element.
			svg.append(node); // append number after to the end after the circle
		}
	}

	// walk created by clicking on points in map
	function MapPoints(svg){
		Stop();

		// select points from map
		svg.click(function(event){
			AddWalkPoint(svg, $(event.target), walkPoints);
		});
	}

	function ResetSelect(select, closeButton, input){
		select.html('');
		closeButton.removeClass('active');
		input.val('');
	}

	// search point by text
	function TextSearch(svg, dialog){
		// search
		var input = dialog.find('.search-point');
		var select = dialog.find('.search-select');
		var closeButton = dialog.find('.search-close');
		input.keyup(function(){
			var value = $(this).val();
			var points = SearchText(value);
			var str = '';
			var count = 0;
			for(var name in points) {
				if(++ count > 10) {
					break;
				}
				var id = points[name];
				str += '<div class="search-tip" data-walk-id=' + id + '>' + name + '</div>';
			}

			// no points found
			if (value && !str) {
				str = '<div class="search-tip search-no-results">No results found</div>';
			}

			select.html(str);
			if(str != ''){
				closeButton.addClass('active');
			}
		});

		// click
		select.click(function(event){
			AddWalkPoint(svg, $(event.target), walkPoints);
		});

		// close button
		closeButton.click(function () {
			ResetSelect(select, closeButton, input)
		});

		// close by clicking anywhere else
		$(document).click(function (event) {
			if(!$(event.target).parents('.search-wrapper').length){
				ResetSelect(select, closeButton, input);
			}
		});
	}

	function ResetButton(svg, dialog) {
		dialog.find('button.reset').click(function () {
			// reset datastructure
			walkPoints = [];
			walk = [];
			Stop();
			HideWalkInfo();

			// clear SVG map
			svg.find('[data-active="1"]').attr('data-active', '0')
			svg.find('[r="5.5"]').attr('r', '2.5');
			svg.find('text').remove();

			// disable Go button
			goButton.prop('disabled', true);

			// diable pause button
			pauseButton.disable();
		});
	}

	function GoButton(svg, dialog){
		goButton = dialog.find('button.go');
		goButton.click(function(){
			Go();
			dialog.dialog('close');
			dialog.parents('.window-open').removeClass('window-open');
		});
	}

	function PauseButton(){
		pauseButton = $('#buttonOnscreenTour_pause');
		pauseButton.hide();

		pauseButton.click(function(){
			TogglePause();
		});

		pauseButton.disable = function(){
			pauseButton.prop('disabled', true);
			pauseButton.hide(300);
		}

		pauseButton.enable = function(){
			pauseButton.prop('disabled', false);
			pauseButton.addClass('active');
			pauseButton.fadeIn(300);
		}
	}

	function InitMap(mesh, dialog){
		var svg = dialog.find('#tour-map');
		var str = '';
		// var box = {x1: 0, y1: 0, x2: 0, y2: 0};
		for(var i = 0; i < mesh.indicies.length; i += 2){
			var nodeA = mesh.indicies[i];
			var nodeB = mesh.indicies[i + 1];
			var x1 = mesh.vertices[nodeA * 3];
			var y1 = -mesh.vertices[nodeA * 3 + 1];
			var x2 = mesh.vertices[nodeB * 3];
			var y2 = -mesh.vertices[nodeB * 3 + 1];
			var points = Math.min(nodeA, nodeB) + '_' + Math.max(nodeA, nodeB);
			str += '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" data-points="' + points + '"/>';
		}
		for(var i = 0; i < mesh.vertices.length; i += 3){
			var cx = mesh.vertices[i];
			var cy = -mesh.vertices[i + 1];
			str += '<circle data-walk-id="' + (i / 3) + '" cx="' + cx + '" cy="' + cy + '" r="2.5"/>';
		}
		svg.html(str);
		return svg;
	}

	function FindWalk(){
		walk = [];
		wholeWalkDistance = 0;
		for(var i = 1, len = walkPoints.length; i < len; i ++){
			var begin = walkPoints[i - 1];
			var end = walkPoints[i];
			var walkObj = Dijkstra(graph, begin, end);
			walk = walk.concat(walkObj.walk);
			wholeWalkDistance += walkObj.distance;
		}
		walkDistance = 0;
	}

	// return position of the point which lies @distance from the beginning of the walk
	function WalkPosition(distance){
		var i = 0;
		var edgesSum = 0;
		var positionA, positionB, edgeLength;

		// find edge where distance lies
		while(i + 1 < walk.length){
			positionA = graph[walk[i]].position.clone(); // distanceTo method change vector itsel. :-(
			positionB = graph[walk[i + 1]].position;
			edgeLength = positionA.distanceTo(positionB);

			if(edgesSum + edgeLength < distance){
				edgesSum += edgeLength;
				i ++;
			}
			else{
				break;
			}
		}

		// we are at the end
		if(i + 1 >= walk.length){
			return false;
		}

		// compute new position from distance
		var direction = new THREE.Vector3();
		direction.subVectors(positionB, positionA);
		var position = positionA;
		position.addScaledVector(direction, (distance - edgesSum) / edgeLength)

		return position;
	}

	function HideWalkInfo () {
		if (walkInfo !== null) {
			walkInfo.fadeOut(200);
		}
	}

	function UpdateWalkInfo (delta) {
		if (lastInfoTime > 0.33) {

			var wholeTime = Math.round(wholeWalkDistance / speed);
			var timeNeed = Math.round(wholeTime - walkDistance / speed);

			var str = '';
			str += '<div class="distance">';
			str += '<span class="need">' + Math.round(wholeWalkDistance - walkDistance) + '</span>';
			str += '<span class="delimiter">/</span>';
			str += '<span class="sum">' + Math.round(wholeWalkDistance) + 'm</span>';
			str += '</div>';
			str += '<div class="time">';
			str += '<span class="need">' + timeNeed + '</span>';
			str += '<span class="delimiter">/</span>';
			str += '<span class="sum">' + wholeTime + 's</span>';
			str += '</div>';

			if (walkInfo === null) {
				walkInfo = $('<div class="walk-info onscreenGui"></div>');

				// open tour window
				walkInfo.click(function () {
					tourDialog.dialog('open');
					tourDialog.parents('.window-full').addClass('window-open');
				});

				$('#onscreenGui').append(walkInfo);
			}

			walkInfo.html(str);
			walkInfo.fadeIn(200);

			lastInfoTime = 0;
		}

		lastInfoTime += delta;
	}

	// return camera position based on delta time and already walked distance (walkDistance)
	function CameraPosition(delta, camera) {
		walkDistance += speed * delta; // move
		var position = WalkPosition(walkDistance); // find position

		// end of the walk
		if (position === false) {
			Stop();
			HideWalkInfo();
			pauseButton.disable();
			walkDistance = 0; // reset walk
			return;
		}

		camera.setPosition(position, true); // set camera position

		// set direction for few first steps
		if (walkDistance <= 1.0) {
			var viewPosition = WalkPosition(walkDistance + 1.0);
			var d = new THREE.Vector3();
			d.subVectors(viewPosition, position).normalize();
			var angle = Math.acos(d.z) * (d.x < 0 ? -1 : 1);
			camera.angleHorizontal = angle;
			camera.angleVertical = 0;
		}

		UpdateWalkInfo(delta);
	}

	function Go(){
		active = true;
		pauseButton.enable();
	}

	// return array of searched points
	function SearchText(search){
		var found = {};
		if(search == ''){
			return {};
		}
		for(var name in texts){
			if(name.toLowerCase().search(search.toLowerCase()) !== -1){
				found[name] = texts[name];
			}
		}

		return found;
	}

	function Stop(){
		active = false;
	}

	function IsActive(){
		return active === true;
	}

	function TogglePause(){
		active = !active;

		pauseButton.toggleClass('active', active);
	}

	// interface
	return {
		Init: Init,
		CameraPosition: CameraPosition,
		Go: Go,
		Stop: Stop,
		IsActive: IsActive,
		TogglePause: TogglePause,
	}
})();
