
import * as THREE from "../../libs/three.js/build/three.module.js";
import {Utils} from "../utils.js";
import {Points} from "../Points.js";
import {CSVExporter} from "../exporter/CSVExporter.js";
import {LASExporter} from "../exporter/LASExporter.js";
import { EventDispatcher } from "../EventDispatcher.js";
import {PointCloudTree} from "../PointCloudTree.js";
import {Renderer} from "../PotreeRenderer.js";
import {PointCloudMaterial} from "../materials/PointCloudMaterial.js";
import {PointSizeType} from "../defines.js";


function copyMaterial(source, target){

	for(let name of Object.keys(target.uniforms)){
		target.uniforms[name].value = source.uniforms[name].value;
	}

	target.gradientTexture = source.gradientTexture;
	target.visibleNodesTexture = source.visibleNodesTexture;
	target.classificationTexture = source.classificationTexture;
	target.matcapTexture = source.matcapTexture;

	target.activeAttributeName = source.activeAttributeName;
	target.ranges = source.ranges;

	//target.updateShaderSource();
}


class Batch{

	constructor(geometry, material){
		this.geometry = geometry;
		this.material = material;

		this.sceneNode = new THREE.Points(geometry, material);

		this.geometryNode = {
			estimatedSpacing: 1.0,
			geometry: geometry,
		};
	}

	getLevel(){
		return 0;
	}

}

class ProfileFakeOctree extends PointCloudTree{

	constructor(octree){
		super();

		this.trueOctree = octree;
		this.pcoGeometry = octree.pcoGeometry;
		this.points = [];
		this.visibleNodes = [];
		
		//this.material = this.trueOctree.material;
		this.material = new PointCloudMaterial();
		//this.material.copy(this.trueOctree.material);
		copyMaterial(this.trueOctree.material, this.material);
		this.material.pointSizeType = PointSizeType.FIXED;

		this.batchSize = 100 * 1000;
		this.currentBatch = null
	}

	getAttribute(name){
		return this.trueOctree.getAttribute(name);
	}

	dispose(){
		for(let node of this.visibleNodes){
			node.geometry.dispose();
		}

		this.visibleNodes = [];
		this.currentBatch = null;
		this.points = [];
	}

	addPoints(data){
		// since each call to addPoints can deliver very very few points,
		// we're going to batch them into larger buffers for efficiency.

		if(this.currentBatch === null){
			this.currentBatch = this.createNewBatch(data);
		}

		this.points.push(data);


		let updateRange = {
			start: this.currentBatch.geometry.drawRange.count,
			count: 0
		};
		let projectedBox = new THREE.Box3();

		let truePos = new THREE.Vector3();

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

			if(updateRange.start + updateRange.count >= this.batchSize){
				// current batch full, start new batch

				for(let key of Object.keys(this.currentBatch.geometry.attributes)){
					let attribute = this.currentBatch.geometry.attributes[key];
					attribute.updateRange.offset = updateRange.start;
					attribute.updateRange.count = updateRange.count;
					attribute.needsUpdate = true;
				}

				this.currentBatch.geometry.computeBoundingBox();
				this.currentBatch.geometry.computeBoundingSphere();

				this.currentBatch = this.createNewBatch(data);
				updateRange = {
					start: 0,
					count: 0
				};
			}

			truePos.set(
				data.data.position[3 * i + 0] + this.trueOctree.position.x,
				data.data.position[3 * i + 1] + this.trueOctree.position.y,
				data.data.position[3 * i + 2] + this.trueOctree.position.z,
			);

			let x = data.data.mileage[i];
			let y = 0;
			let z = truePos.z;

			projectedBox.expandByPoint(new THREE.Vector3(x, y, z));

			let index = updateRange.start + updateRange.count;
			let geometry = this.currentBatch.geometry;

			for(let attributeName of Object.keys(data.data)){
				let source = data.data[attributeName];
				let target = geometry.attributes[attributeName];
				let numElements = target.itemSize;
				
				for(let item = 0; item < numElements; item++){
					target.array[numElements * index + item] = source[numElements * i + item];
				}
			}

			{
				let position = geometry.attributes.position;

				position.array[3 * index + 0] = x;
				position.array[3 * index + 1] = y;
				position.array[3 * index + 2] = z;
			}

			updateRange.count++;
			this.currentBatch.geometry.drawRange.count++;
		}

		for(let key of Object.keys(this.currentBatch.geometry.attributes)){
			let attribute = this.currentBatch.geometry.attributes[key];
			attribute.updateRange.offset = updateRange.start;
			attribute.updateRange.count = updateRange.count;
			attribute.needsUpdate = true;
		}

		data.projectedBox = projectedBox;

		this.projectedBox = this.points.reduce( (a, i) => a.union(i.projectedBox), new THREE.Box3());
	}

	createNewBatch(data){
		let geometry = new THREE.BufferGeometry();

		// create new batches with batch_size elements of the same type as the attribute
		for(let attributeName of Object.keys(data.data)){
			let buffer = data.data[attributeName];
			let numElements = buffer.length / data.numPoints; // 3 for pos, 4 for col, 1 for scalars
			let constructor = buffer.constructor;
			let normalized = false;
			
			if(this.trueOctree.root.sceneNode){
				if(this.trueOctree.root.sceneNode.geometry.attributes[attributeName]){
					normalized = this.trueOctree.root.sceneNode.geometry.attributes[attributeName].normalized;
				}
			}
			

			let batchBuffer = new constructor(numElements * this.batchSize);

			let bufferAttribute = new THREE.BufferAttribute(batchBuffer, numElements, normalized);
			bufferAttribute.potree = {
				range: [0, 1],
			};

			geometry.setAttribute(attributeName, bufferAttribute);
		}

		geometry.drawRange.start = 0;
		geometry.drawRange.count = 0;

		let batch = new Batch(geometry, this.material);

		this.visibleNodes.push(batch);

		return batch;
	}
	
	computeVisibilityTextureData(){
		let data = new Uint8Array(this.visibleNodes.length * 4);
		let offsets = new Map();

		for(let i = 0; i < this.visibleNodes.length; i++){
			let node = this.visibleNodes[i];

			offsets[node] = i;
		}


		return {
			data: data,
			offsets: offsets,
		};
	}

}

export class ProfileWindow extends EventDispatcher {
	constructor (viewer) {
		super();

		this.viewer = viewer;
		this.elRoot = $('#profile_window');
		this.renderArea = this.elRoot.find('#profileCanvasContainer');
		this.svg = d3.select('svg#profileSVG');
		this.mouseIsDown = false;

		this.projectedBox = new THREE.Box3();
		this.pointclouds = new Map();
		this.numPoints = 0;
		this.lastAddPointsTimestamp = undefined;

		this.mouse = new THREE.Vector2(0, 0);
		this.scale = new THREE.Vector3(1, 1, 1);

		this.autoFitEnabled = true; // completely disable/enable
		this.autoFit = false; // internal

		let cwIcon = `${exports.resourcePath}/icons/arrow_cw.svg`;
		$('#potree_profile_rotate_cw').attr('src', cwIcon);

		let ccwIcon = `${exports.resourcePath}/icons/arrow_ccw.svg`;
		$('#potree_profile_rotate_ccw').attr('src', ccwIcon);
		
		let forwardIcon = `${exports.resourcePath}/icons/arrow_up.svg`;
		$('#potree_profile_move_forward').attr('src', forwardIcon);

		let backwardIcon = `${exports.resourcePath}/icons/arrow_down.svg`;
		$('#potree_profile_move_backward').attr('src', backwardIcon);

		let csvIcon = `${exports.resourcePath}/icons/file_csv_2d.svg`;
		$('#potree_download_csv_icon').attr('src', csvIcon);

		let lasIcon = `${exports.resourcePath}/icons/file_las_3d.svg`;
		$('#potree_download_las_icon').attr('src', lasIcon);

		let closeIcon = `${exports.resourcePath}/icons/close.svg`;
		$('#closeProfileContainer').attr("src", closeIcon);

		this.initTHREE();
		this.initSVG();
		this.initListeners();

		this.pRenderer = new Renderer(this.renderer);

		this.elRoot.i18n();
	}

	initListeners () {
		$(window).resize(() => {
			if (this.enabled) {
			this.render();
			}
		});

		this.renderArea.mousedown(e => {
			this.mouseIsDown = true;
		});

		this.renderArea.mouseup(e => {
			this.mouseIsDown = false;
		});

		let viewerPickSphereSizeHandler = () => {
			let camera = this.viewer.scene.getActiveCamera();
			let domElement = this.viewer.renderer.domElement;
			let distance = this.viewerPickSphere.position.distanceTo(camera.position);
			let pr = Utils.projectedRadius(1, camera, distance, domElement.clientWidth, domElement.clientHeight);
			let scale = (10 / pr);
			this.viewerPickSphere.scale.set(scale, scale, scale);
		};

		this.renderArea.mousemove(e => {
			if (this.pointclouds.size === 0) {
				return;
			}

			let rect = this.renderArea[0].getBoundingClientRect();
			let x = e.clientX - rect.left;
			let y = e.clientY - rect.top;

			let newMouse = new THREE.Vector2(x, y);

			if (this.mouseIsDown) {
				// DRAG
				this.autoFit = false;
				this.lastDrag = new Date().getTime();

				let cPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)];
				let ncPos = [this.scaleX.invert(newMouse.x), this.scaleY.invert(newMouse.y)];

				this.camera.position.x -= ncPos[0] - cPos[0];
				this.camera.position.z -= ncPos[1] - cPos[1];

				this.render();
			} else if (this.pointclouds.size > 0) {
				// FIND HOVERED POINT
				let radius = Math.abs(this.scaleX.invert(0) - this.scaleX.invert(40));
				let mileage = this.scaleX.invert(newMouse.x);
				let elevation = this.scaleY.invert(newMouse.y);

				let closest = this.selectPoint(mileage, elevation, radius);

				if (closest) {
					let point = closest.point;

					let position = new Float64Array([
						point.position[0] + closest.pointcloud.position.x,
						point.position[1] + closest.pointcloud.position.y,
						point.position[2] + closest.pointcloud.position.z
					]);

					this.elRoot.find('#profileSelectionProperties').fadeIn(200);
					this.pickSphere.visible = true;
					this.pickSphere.scale.set(0.5 * radius, 0.5 * radius, 0.5 * radius);
					this.pickSphere.position.set(point.mileage, 0, position[2]);

					this.viewerPickSphere.position.set(...position);
					
					if(!this.viewer.scene.scene.children.includes(this.viewerPickSphere)){
						this.viewer.scene.scene.add(this.viewerPickSphere);
						if(!this.viewer.hasEventListener("update", viewerPickSphereSizeHandler)){
							this.viewer.addEventListener("update", viewerPickSphereSizeHandler);
						}
					}
					

					let info = this.elRoot.find('#profileSelectionProperties');
					let html = '<table>';

					for (let attributeName of Object.keys(point)) {

						let value = point[attributeName];
						let attribute = closest.pointcloud.getAttribute(attributeName);

						let transform = value => value;
						if(attribute && attribute.type.size > 4){
							let range = attribute.initialRange;
							let scale = 1 / (range[1] - range[0]);
							let offset = range[0];
							transform = value => value / scale + offset;
						}

						

						

						if (attributeName === 'position') {
							let values = [...position].map(v => Utils.addCommas(v.toFixed(3)));
							html += `
								<tr>
									<td>x</td>
									<td>${values[0]}</td>
								</tr>
								<tr>
									<td>y</td>
									<td>${values[1]}</td>
								</tr>
								<tr>
									<td>z</td>
									<td>${values[2]}</td>
								</tr>`;
						} else if (attributeName === 'rgba') {
							html += `
								<tr>
									<td>${attributeName}</td>
									<td>${value.join(', ')}</td>
								</tr>`;
						} else if (attributeName === 'normal') {
							continue;
						} else if (attributeName === 'mileage') {
							html += `
								<tr>
									<td>${attributeName}</td>
									<td>${value.toFixed(3)}</td>
								</tr>`;
						} else {
							html += `
								<tr>
									<td>${attributeName}</td>
									<td>${transform(value)}</td>
								</tr>`;
						}
					}
					html += '</table>';
					info.html(html);

					this.selectedPoint = point;
				} else {
					// this.pickSphere.visible = false;
					// this.selectedPoint = null;

					this.viewer.scene.scene.add(this.viewerPickSphere);

					let index = this.viewer.scene.scene.children.indexOf(this.viewerPickSphere);
					if(index >= 0){
						this.viewer.scene.scene.children.splice(index, 1);
					}
					this.viewer.removeEventListener("update", viewerPickSphereSizeHandler);
					

				}
				this.render();
			}

			this.mouse.copy(newMouse);
		});

		let onWheel = e => {
			this.autoFit = false;

			let delta = 0;
			if (e.wheelDelta !== undefined) { // WebKit / Opera / Explorer 9
				delta = e.wheelDelta;
			} else if (e.detail !== undefined) { // Firefox
				delta = -e.detail;
			}

			let ndelta = Math.sign(delta);

			let cPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)];

			if (ndelta > 0) {
				// + 10%
				this.scale.multiplyScalar(1.1);
			} else {
				// - 10%
				this.scale.multiplyScalar(100 / 110);
			}

			this.updateScales();
			let ncPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)];

			this.camera.position.x -= ncPos[0] - cPos[0];
			this.camera.position.z -= ncPos[1] - cPos[1];

			this.render();
			this.updateScales();
		};
		$(this.renderArea)[0].addEventListener('mousewheel', onWheel, false);
		$(this.renderArea)[0].addEventListener('DOMMouseScroll', onWheel, false); // Firefox

		$('#closeProfileContainer').click(() => {
			this.hide();
		});

		let getProfilePoints = () => {
			let points = new Points();
			
			for(let [pointcloud, entry] of this.pointclouds){
				for(let pointSet of entry.points){

					let originPos = pointSet.data.position;
					let trueElevationPosition = new Float32Array(originPos);
					for(let i = 0; i < pointSet.numPoints; i++){
						trueElevationPosition[3 * i + 2] += pointcloud.position.z;
					}

					pointSet.data.position = trueElevationPosition;
					points.add(pointSet);
					pointSet.data.position = originPos;
				}
			}

			return points;
		};

		$('#potree_download_csv_icon').click(() => {
			
			let points = getProfilePoints();

			let string = CSVExporter.toString(points);

			let blob = new Blob([string], {type: "text/string"});
			$('#potree_download_profile_ortho_link').attr('href', URL.createObjectURL(blob));
		});

		$('#potree_download_las_icon').click(() => {

			let points = getProfilePoints();

			let buffer = LASExporter.toLAS(points);

			let blob = new Blob([buffer], {type: "application/octet-binary"});
			$('#potree_download_profile_link').attr('href', URL.createObjectURL(blob));
		});
	}

	selectPoint (mileage, elevation, radius) {
		let closest = {
			distance: Infinity,
			pointcloud: null,
			points: null,
			index: null
		};

		let pointBox = new THREE.Box2(
			new THREE.Vector2(mileage - radius, elevation - radius),
			new THREE.Vector2(mileage + radius, elevation + radius));

		let numTested = 0;
		let numSkipped = 0;
		let numTestedPoints = 0;
		let numSkippedPoints = 0;

		for (let [pointcloud, entry] of this.pointclouds) {
			for(let points of entry.points){

				let collisionBox = new THREE.Box2(
					new THREE.Vector2(points.projectedBox.min.x, points.projectedBox.min.z),
					new THREE.Vector2(points.projectedBox.max.x, points.projectedBox.max.z)
				);

				let intersects = collisionBox.intersectsBox(pointBox);

				if(!intersects){
					numSkipped++;
					numSkippedPoints += points.numPoints;
					continue;
				}

				numTested++;
				numTestedPoints += points.numPoints

				for (let i = 0; i < points.numPoints; i++) {

					let m = points.data.mileage[i] - mileage;
					let e = points.data.position[3 * i + 2] - elevation + pointcloud.position.z;
					let r = Math.sqrt(m * m + e * e);

					const withinDistance = r < radius && r < closest.distance;
					let unfilteredClass = true;

					if(points.data.classification){
						const classification = pointcloud.material.classification;

						const pointClassID = points.data.classification[i];
						const pointClassValue = classification[pointClassID];

						if(pointClassValue && (!pointClassValue.visible || pointClassValue.color.w === 0)){
							unfilteredClass = false;
						}
					}

					if (withinDistance && unfilteredClass) {
						closest = {
							distance: r,
							pointcloud: pointcloud,
							points: points,
							index: i
						};
					}
				}
			}
		}


		//console.log(`nodes: ${numTested}, ${numSkipped} || points: ${numTestedPoints}, ${numSkippedPoints}`);

		if (closest.distance < Infinity) {
			let points = closest.points;

			let point = {};

			let attributes = Object.keys(points.data);
			for (let attribute of attributes) {
				let attributeData = points.data[attribute];
				let itemSize = attributeData.length / points.numPoints;
				let value = attributeData.subarray(itemSize * closest.index, itemSize * closest.index + itemSize);

				if (value.length === 1) {
					point[attribute] = value[0];
				} else {
					point[attribute] = value;
				}
			}

			closest.point = point;

			return closest;
		} else {
			return null;
		}
	}

	initTHREE () {
		this.renderer = new THREE.WebGLRenderer({alpha: true, premultipliedAlpha: false});
		this.renderer.setClearColor(0x000000, 0);
		this.renderer.setSize(10, 10);
		this.renderer.autoClear = false;
		this.renderArea.append($(this.renderer.domElement));
		this.renderer.domElement.tabIndex = '2222';
		$(this.renderer.domElement).css('width', '100%');
		$(this.renderer.domElement).css('height', '100%');


		{
			let gl = this.renderer.getContext();

			if(gl.createVertexArray == null){
				let extVAO = gl.getExtension('OES_vertex_array_object');

				if(!extVAO){
					throw new Error("OES_vertex_array_object extension not supported");
				}

				gl.createVertexArray = extVAO.createVertexArrayOES.bind(extVAO);
				gl.bindVertexArray = extVAO.bindVertexArrayOES.bind(extVAO);
			}
			
		}

		this.camera = new THREE.OrthographicCamera(-1000, 1000, 1000, -1000, -1000, 1000);
		this.camera.up.set(0, 0, 1);
		this.camera.rotation.order = "ZXY";
		this.camera.rotation.x = Math.PI / 2.0;
	

		this.scene = new THREE.Scene();
		this.profileScene = new THREE.Scene();

		let sg = new THREE.SphereGeometry(1, 16, 16);
		let sm = new THREE.MeshNormalMaterial();
		this.pickSphere = new THREE.Mesh(sg, sm);
		this.scene.add(this.pickSphere);

		{
			const sg = new THREE.SphereGeometry(2);
			const sm = new THREE.MeshNormalMaterial();
			const s = new THREE.Mesh(sg, sm);

			s.position.set(589530.450, 231398.860, 769.735);

			this.scene.add(s);
		}

		this.viewerPickSphere = new THREE.Mesh(sg, sm);
	}

	initSVG () {
		let width = this.renderArea[0].clientWidth;
		let height = this.renderArea[0].clientHeight;
		let marginLeft = this.renderArea[0].offsetLeft;

		this.svg.selectAll('*').remove();

		this.scaleX = d3.scale.linear()
			.domain([this.camera.left + this.camera.position.x, this.camera.right + this.camera.position.x])
			.range([0, width]);
		this.scaleY = d3.scale.linear()
			.domain([this.camera.bottom + this.camera.position.z, this.camera.top + this.camera.position.z])
			.range([height, 0]);

		this.xAxis = d3.svg.axis()
			.scale(this.scaleX)
			.orient('bottom')
			.innerTickSize(-height)
			.outerTickSize(1)
			.tickPadding(10)
			.ticks(width / 50);

		this.yAxis = d3.svg.axis()
			.scale(this.scaleY)
			.orient('left')
			.innerTickSize(-width)
			.outerTickSize(1)
			.tickPadding(10)
			.ticks(height / 20);

		this.elXAxis = this.svg.append('g')
			.attr('class', 'x axis')
			.attr('transform', `translate(${marginLeft}, ${height})`)
			.call(this.xAxis);

		this.elYAxis = this.svg.append('g')
			.attr('class', 'y axis')
			.attr('transform', `translate(${marginLeft}, 0)`)
			.call(this.yAxis);
	}

	addPoints (pointcloud, points) {

		if(points.numPoints === 0){
			return;
		}

		let entry = this.pointclouds.get(pointcloud);
		if(!entry){
			entry = new ProfileFakeOctree(pointcloud);
			this.pointclouds.set(pointcloud, entry);
			this.profileScene.add(entry);

			let materialChanged = () => {
				this.render();
			};

			materialChanged();

			pointcloud.material.addEventListener('material_property_changed', materialChanged);
			this.addEventListener("on_reset_once", () => {
				pointcloud.material.removeEventListener('material_property_changed', materialChanged);
			});
		}

		entry.addPoints(points);
		this.projectedBox.union(entry.projectedBox);

		if (this.autoFit && this.autoFitEnabled) { 
			let width = this.renderArea[0].clientWidth;
			let height = this.renderArea[0].clientHeight;

			let size = this.projectedBox.getSize(new THREE.Vector3());

			let sx = width / size.x;
			let sy = height / size.z;
			let scale = Math.min(sx, sy);

			let center = this.projectedBox.getCenter(new THREE.Vector3());
			this.scale.set(scale, scale, 1);
			this.camera.position.copy(center);

			//console.log("camera: ", this.camera.position.toArray().join(", "));
		}

		//console.log(entry);

		this.render();

		let numPoints = 0;
		for (let [key, value] of this.pointclouds.entries()) {
			numPoints += value.points.reduce( (a, i) => a + i.numPoints, 0);
		}
		$(`#profile_num_points`).html(Utils.addCommas(numPoints));

	}

	reset () {
		this.lastReset = new Date().getTime();

		this.dispatchEvent({type: "on_reset_once"});
		this.removeEventListeners("on_reset_once");

		this.autoFit = true;
		this.projectedBox = new THREE.Box3();

		for(let [key, entry] of this.pointclouds){
			entry.dispose();
		}

		this.pointclouds.clear();
		this.mouseIsDown = false;
		this.mouse.set(0, 0);

		if(this.autoFitEnabled){
			this.scale.set(1, 1, 1);
		}
		this.pickSphere.visible = false;

		this.elRoot.find('#profileSelectionProperties').hide();

		this.render();
	}

	show () {
		this.elRoot.fadeIn();
		this.enabled = true;
	}

	hide () {
		this.elRoot.fadeOut();
		this.enabled = false;
	}

	updateScales () {

		let width = this.renderArea[0].clientWidth;
		let height = this.renderArea[0].clientHeight;

		let left = (-width / 2) / this.scale.x;
		let right = (+width / 2) / this.scale.x;
		let top = (+height / 2) / this.scale.y;
		let bottom = (-height / 2) / this.scale.y;

		this.camera.left = left;
		this.camera.right = right;
		this.camera.top = top;
		this.camera.bottom = bottom;
		this.camera.updateProjectionMatrix();

		this.scaleX.domain([this.camera.left + this.camera.position.x, this.camera.right + this.camera.position.x])
			.range([0, width]);
		this.scaleY.domain([this.camera.bottom + this.camera.position.z, this.camera.top + this.camera.position.z])
			.range([height, 0]);

		let marginLeft = this.renderArea[0].offsetLeft;

		this.xAxis.scale(this.scaleX)
			.orient('bottom')
			.innerTickSize(-height)
			.outerTickSize(1)
			.tickPadding(10)
			.ticks(width / 50);
		this.yAxis.scale(this.scaleY)
			.orient('left')
			.innerTickSize(-width)
			.outerTickSize(1)
			.tickPadding(10)
			.ticks(height / 20);


		this.elXAxis
			.attr('transform', `translate(${marginLeft}, ${height})`)
			.call(this.xAxis);
		this.elYAxis
			.attr('transform', `translate(${marginLeft}, 0)`)
			.call(this.yAxis);
	}

	requestScaleUpdate(){

		let threshold = 100;
		let allowUpdate = ((this.lastReset === undefined) || (this.lastScaleUpdate === undefined)) 
			|| ((new Date().getTime() - this.lastReset) > threshold && (new Date().getTime() - this.lastScaleUpdate) > threshold);

		if(allowUpdate){

			this.updateScales();

			this.lastScaleUpdate = new Date().getTime();

			

			this.scaleUpdatePending = false;
		}else if(!this.scaleUpdatePending) {
			setTimeout(this.requestScaleUpdate.bind(this), 100);
			this.scaleUpdatePending = true;
		}
		
	}

	render () {
		let width = this.renderArea[0].clientWidth;
		let height = this.renderArea[0].clientHeight;

		let {renderer, pRenderer, camera, profileScene, scene} = this;
		let {scaleX, pickSphere} = this;

		renderer.setSize(width, height);

		renderer.setClearColor(0x000000, 0);
		renderer.clear(true, true, false);

		for(let pointcloud of this.pointclouds.keys()){
			let source = pointcloud.material;
			let target = this.pointclouds.get(pointcloud).material;
			
			copyMaterial(source, target);
			target.size = 2;
		}
		
		pRenderer.render(profileScene, camera, null);

		let radius = Math.abs(scaleX.invert(0) - scaleX.invert(5));

		if (radius === 0) {
			pickSphere.visible = false;
		} else {
			pickSphere.scale.set(radius, radius, radius);
			pickSphere.visible = true;
		}
		
		renderer.render(scene, camera);

		this.requestScaleUpdate();
	}
};

export class ProfileWindowController {
	constructor (viewer) {
		this.viewer = viewer;
		this.profileWindow = viewer.profileWindow;
		this.profile = null;
		this.numPoints = 0;
		this.threshold = 60 * 1000;
		this.rotateAmount = 10;

		this.scheduledRecomputeTime = null;

		this.enabled = true;

		this.requests = [];

		this._recompute = () => { this.recompute(); };

		this.viewer.addEventListener("scene_changed", e => {
			e.oldScene.removeEventListener("pointcloud_added", this._recompute);
			e.scene.addEventListener("pointcloud_added", this._recompute);
		});
		this.viewer.scene.addEventListener("pointcloud_added", this._recompute);

		$("#potree_profile_rotate_amount").val(parseInt(this.rotateAmount));
		$("#potree_profile_rotate_amount").on("input", (e) => {
			const str = $("#potree_profile_rotate_amount").val();

			if(!isNaN(str)){
				const value = parseFloat(str);
				this.rotateAmount = value;
				$("#potree_profile_rotate_amount").css("background-color", "")
			}else{
				$("#potree_profile_rotate_amount").css("background-color", "#ff9999")
			}

		});

		const rotate = (radians) => {
			const profile = this.profile;
			const points = profile.points;
			const start = points[0];
			const end = points[points.length - 1];
			const center = start.clone().add(end).multiplyScalar(0.5);

			const mMoveOrigin = new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z);
			const mRotate = new THREE.Matrix4().makeRotationZ(radians);
			const mMoveBack = new THREE.Matrix4().makeTranslation(center.x, center.y, center.z);
			//const transform = mMoveOrigin.multiply(mRotate).multiply(mMoveBack);
			const transform = mMoveBack.multiply(mRotate).multiply(mMoveOrigin);

			const rotatedPoints = points.map( point => point.clone().applyMatrix4(transform) );

			this.profileWindow.autoFitEnabled = false;

			for(let i = 0; i < points.length; i++){
				profile.setPosition(i, rotatedPoints[i]);
			}
		}

		$("#potree_profile_rotate_cw").click( () => {
			const radians = THREE.Math.degToRad(this.rotateAmount);
			rotate(-radians);
		});

		$("#potree_profile_rotate_ccw").click( () => {
			const radians = THREE.Math.degToRad(this.rotateAmount);
			rotate(radians);
		});

		$("#potree_profile_move_forward").click( () => {
			const profile = this.profile;
			const points = profile.points;
			const start = points[0];
			const end = points[points.length - 1];

			const dir = end.clone().sub(start).normalize();
			const up = new THREE.Vector3(0, 0, 1);
			const forward = up.cross(dir);
			const move = forward.clone().multiplyScalar(profile.width / 2);

			this.profileWindow.autoFitEnabled = false;

			for(let i = 0; i < points.length; i++){
				profile.setPosition(i, points[i].clone().add(move));
			}
		});

		$("#potree_profile_move_backward").click( () => {
			const profile = this.profile;
			const points = profile.points;
			const start = points[0];
			const end = points[points.length - 1];

			const dir = end.clone().sub(start).normalize();
			const up = new THREE.Vector3(0, 0, 1);
			const forward = up.cross(dir);
			const move = forward.clone().multiplyScalar(-profile.width / 2);

			this.profileWindow.autoFitEnabled = false;

			for(let i = 0; i < points.length; i++){
				profile.setPosition(i, points[i].clone().add(move));
			}
		});
	}

	setProfile (profile) {
		if (this.profile !== null && this.profile !== profile) {
			this.profile.removeEventListener('marker_moved', this._recompute);
			this.profile.removeEventListener('marker_added', this._recompute);
			this.profile.removeEventListener('marker_removed', this._recompute);
			this.profile.removeEventListener('width_changed', this._recompute);
		}

		this.profile = profile;

		{
			this.profile.addEventListener('marker_moved', this._recompute);
			this.profile.addEventListener('marker_added', this._recompute);
			this.profile.addEventListener('marker_removed', this._recompute);
			this.profile.addEventListener('width_changed', this._recompute);
		}

		this.recompute();
	}

	reset () {
		this.profileWindow.reset();

		this.numPoints = 0;

		if (this.profile) {
			for (let request of this.requests) {
				request.cancel();
			}
		}
	}

	progressHandler (pointcloud, progress) {
		for (let segment of progress.segments) {
			this.profileWindow.addPoints(pointcloud, segment.points);
			this.numPoints += segment.points.numPoints;
		}
	}

	cancel () {
		for (let request of this.requests) {
			request.cancel();
			// request.finishLevelThenCancel();
		}

		this.requests = [];
	};

	finishLevelThenCancel(){
		for (let request of this.requests) {
			request.finishLevelThenCancel();
		}

		this.requests = [];
	}

	recompute () {
		if (!this.profile) {
			return;
		}

		if (this.scheduledRecomputeTime !== null && this.scheduledRecomputeTime > new Date().getTime()) {
			return;
		} else {
			this.scheduledRecomputeTime = new Date().getTime() + 100;
		}
		this.scheduledRecomputeTime = null;

		this.reset();

		for (let pointcloud of this.viewer.scene.pointclouds.filter(p => p.visible)) {
			let request = pointcloud.getPointsInProfile(this.profile, null, {
				'onProgress': (event) => {
					if (!this.enabled) {
						return;
					}

					this.progressHandler(pointcloud, event.points);

					if (this.numPoints > this.threshold) {
						this.finishLevelThenCancel();
					}
				},
				'onFinish': (event) => {
					if (!this.enabled) {

					}
				},
				'onCancel': () => {
					if (!this.enabled) {

					}
				}
			});

			this.requests.push(request);
		}
	}
};
