// bsearch-based array element check
function contains(array, elem) {
	let min = 0, max = array.length;

	while (min < max) {
		const i = min + Math.floor((max-min)/2);
		const cur = array[i];

		if (cur === elem)
			return true;
		else if (cur < elem)
			min = i + 1;
		else
			max = i;
	}

	return false;
}

const signKinds = {
	sign: {
		iconSize: [26, 28],
		popupAnchor: [0, -20],
	},
	wall_sign: {
		iconSize: [26, 18],
		popupAnchor: [0, -15],
	},
	hanging_sign: {
		iconSize: [28, 24],
		popupAnchor: [0, -18],
	},
	hanging_wall_sign: {
		iconSize: [28, 28],
		popupAnchor: [0, -20],
	},
}

const params = {};
const signIcons = {};
const markers = {};

let updateHash = () => {};

function coordKey(coords) {
	if (!coords)
		return null;

	return `${coords[0]},${coords[1]}`;
}

function getMarker(coords) {
	return markers[coordKey(coords)];
}

function signIcon(material, kind) {
	function createSignIcon(material, kind) {
		const {iconSize, popupAnchor} = signKinds[kind];

		return L.icon({
			iconUrl: `images/icon/${material}_${kind}.png`,
			iconSize,
			popupAnchor,
			shadowUrl: `images/icon/shadow_${kind}.png`,
			shadowSize: [iconSize[0]+8, iconSize[1]+8],
			className: 'overzoomed',
		});
	}


	let icons = signIcons[material] ??= {};
	return icons[kind] ??= createSignIcon(material, kind);
}

const MinedMapLayer = L.TileLayer.extend({
	initialize: function (mipmaps, layer, tile_extension) {
		L.TileLayer.prototype.initialize.call(this, '', {
			detectRetina: true,
			tileSize: 512,
			zoomReverse: true,
			minZoom: -(mipmaps.length-1),
			maxZoom: 0,
			attribution: 'Generated by <a href="https://github.com/neocturne/MinedMap">MinedMap</a>',
		});

		this.options.maxNativeZoom = this.options.maxZoom;
		this.options.maxZoom = undefined;

		this.mipmaps = mipmaps;
		this.layer = layer;
		this.ext = tile_extension;
	},

	createTile: function (coords, done) {
		const tile = L.TileLayer.prototype.createTile.call(this, coords, done);

		if (coords.z - this.options.zoomOffset >= 0)
			L.DomUtil.addClass(tile, 'overzoomed');

		return tile;
	},

	getTileUrl: function (coords) {
		let z = -coords.z + this.options.zoomOffset;
		if (z < 0)
			z = 0;

		const mipmap = this.mipmaps[z];

		if (coords.x < mipmap.bounds.minX || coords.x > mipmap.bounds.maxX ||
		    coords.y < mipmap.bounds.minZ || coords.y > mipmap.bounds.maxZ ||
		    !contains(mipmap.regions[coords.y] || [], coords.x))
			return L.Util.emptyImageUrl;


		return `data/${this.layer}/${z}/r.${coords.x}.${coords.y}.${this.ext}`;
	},
});


const CoordControl = L.Control.extend({
	initialize: function () {
		this.options.position = 'bottomleft';
	},

	onAdd: function (map) {
		this._container = L.DomUtil.create('div', 'leaflet-control-attribution');

		return this._container;
	},

	update: function (x, z) {
		if (!this._map) { return; }

		this._container.innerHTML = 'X: ' + x + '&nbsp;&nbsp;&nbsp;Z: ' + z;
	}
});


const parseHash = function () {
	const args = {};

	if (window.location.hash) {
		const parts = window.location.hash.substring(1).split('&');

		for (const part of parts) {
			const key_value = part.split('=');
			const key = key_value[0], value = key_value.slice(1).join('=');

			args[key] = value;
		}
	}

	return args;
}

function formatSignLine(line) {
	const el = document.createElement('span');
	el.style.whiteSpace = 'pre';

	for (const span of line) {
		const child = document.createElement('span');
		child.textContent = span.text;

		let color = span.color ?? '';
		if (color[0] !== '#')
			color = '#000000';

		if (span.bold)
			child.style.fontWeight = 'bold';
		if (span.italic)
			child.style.fontStyle = 'italic';

		child.style.textDecoration = '';
		if (span.underlined)
			child.style.textDecoration += ' underline';
		if (span.strikethrough)
			child.style.textDecoration += ' line-through';

		child.style.color = color;
		if (span.obfuscated) {
			child.style.backgroundColor = color;
			child.className = 'obfuscated';
		}

		el.appendChild(child);
	}
	return el;
}

function createSign(sign, back) {
	// standing signs
	function px(base) {
		const scale = 11;
		return (base*scale)+'px';
	}
	// hanging signs
	function pxh(base) {
		const scale = 16;
		return (base*scale)+'px';
	}

	const sizes = {
		sign: {
			width: px(24),
			height: px(12),
			paddingTop: px(0),
			paddingBottom: px(14),
		},
		wall_sign: {
			width: px(24),
			height: px(12),
			paddingTop: px(0),
			paddingBottom: px(0),
		},
		hanging_sign: {
			width: pxh(16),
			height: pxh(10),
			paddingTop: pxh(4),
			paddingBottom: pxh(0),
		},
		hanging_wall_sign: {
			width: pxh(16),
			height: pxh(10),
			paddingTop: pxh(6),
			paddingBottom: pxh(0),
		},
	};
	const size = sizes[sign.kind];

	const wrapper = document.createElement('div');
	wrapper.classList = 'sign-wrapper';

	const title = document.createElement('div');
	title.classList = 'sign-title'
	title.textContent = `Sign at ${sign.x}/${sign.y}/${sign.z}`;
	if (back)
		title.textContent += ' (back)';
	title.textContent += ':';

	wrapper.appendChild(title);

	const container = document.createElement('div');
	container.style.width = size.width;
	container.style.height = size.height;
	container.style.paddingTop = size.paddingTop;
	container.style.paddingBottom = size.paddingBottom;
	container.style.backgroundImage = `url(images/bg/${sign.material}_${sign.kind}.png)`;
	container.classList = 'sign-container overzoomed';

	const content = document.createElement('div');
	content.classList = 'sign-content';

	let text = [];
	if (!back && sign.front_text)
		text = sign.front_text;
	else if (back && sign.back_text)
		text = sign.back_text;

	for (const line of text) {
		content.appendChild(formatSignLine(line));
		content.appendChild(document.createElement('br'));
	}

	container.appendChild(content);
	wrapper.appendChild(container);

	return wrapper;
}

async function loadSigns(signLayer) {
	const response = await fetch('data/entities.json', {cache: 'no-store'});
	const res = await response.json();

	const groups = {};

	// Group signs by x,z coordinates
	for (const sign of res.signs) {
		const key = coordKey([sign.x, sign.z]);
		const group = groups[key] ??= [];
		group.push(sign);
	}

	for (const [key, group] of Object.entries(groups)) {
		const el = document.createElement('div');

		let material;
		let kind;

		// Sort from top to bottom
		group.sort((a, b) => b.y - a.y);

		for (const sign of group) {
			el.appendChild(createSign(sign, false));

			if (sign.back_text)
				el.appendChild(createSign(sign, true));

			material ??= sign.material;
			kind ??= sign.kind;
		}

		// Default material
		material ??= 'oak';

		const [x, z] = key.split(',').map((i) => +i);

		const popup = L.popup().setContent(el);

		popup.on('add', () => {
			params.marker = [x, z];
			updateHash();
		});
		popup.on('remove', () => {
			params.marker = null;
			updateHash();
		});

		const marker = L.marker([-z-0.5, x+0.5], {
			icon: signIcon(material, kind),
		}).addTo(signLayer).bindPopup(popup);

		markers[coordKey([x, z])] = marker;

		if (params.marker && x === params.marker[0] && z === params.marker[1])
			marker.openPopup();
	}
}

window.createMap = function () {
	(async function () {
		const response = await fetch('data/info.json', {cache: 'no-store'});
		const res = await response.json();
		const {mipmaps, spawn} = res;
		const features = res.features || {};
		const tile_extension = res.tile_extension || 'png';

		const updateParams = function () {
			const args = parseHash();

			params.zoom = parseInt(args['zoom']);
			params.x = parseFloat(args['x']);
			params.z = parseFloat(args['z']);
			params.light = parseInt(args['light']);
			params.signs = parseInt(args['signs'] ?? '1');
			params.marker = (args['marker'] ?? '').split(',').map((i) => +i);

			if (isNaN(params.zoom))
				params.zoom = 0;
			if (isNaN(params.x))
				params.x = spawn.x;
			if (isNaN(params.z))
				params.z = spawn.z;
			if (!features.signs || isNaN(params.marker[0]) || isNaN(params.marker[1]))
				params.marker = null;
		};

		updateParams();

		const map = L.map('map', {
			center: [-params.z, params.x],
			zoom: params.zoom,
			minZoom: -(mipmaps.length-1),
			maxZoom: 5,
			crs: L.CRS.Simple,
			maxBounds: [
				[-512*(mipmaps[0].bounds.maxZ+1), 512*mipmaps[0].bounds.minX],
				[-512*mipmaps[0].bounds.minZ, 512*(mipmaps[0].bounds.maxX+1)],
			],
		});

		const overlayMaps = {};

		const mapLayer = new MinedMapLayer(mipmaps, 'map', tile_extension);
		mapLayer.addTo(map);

		const lightLayer = new MinedMapLayer(mipmaps, 'light', tile_extension);
		overlayMaps['Illumination'] = lightLayer;
		if (params.light)
			map.addLayer(lightLayer);

		let signLayer;
		if (features.signs) {
			signLayer = L.layerGroup();
			loadSigns(signLayer);
			if (params.signs)
				map.addLayer(signLayer);

			overlayMaps['Signs'] = signLayer;
		}

		L.control.layers({}, overlayMaps).addTo(map);

		const coordControl = new CoordControl();
		coordControl.addTo(map);

		map.on('mousemove', function(e) {
			coordControl.update(Math.round(e.latlng.lng), Math.round(-e.latlng.lat));
		});

		const makeHash = function () {
			let ret = '#x='+params.x+'&z='+params.z;

			if (params.zoom != 0)
				ret += '&zoom='+params.zoom;

			if (map.hasLayer(lightLayer))
				ret += '&light=1';
			if (features.signs && !map.hasLayer(signLayer))
				ret += '&signs=0';
			if (params.marker) {
				ret += `&marker=${params.marker[0]},${params.marker[1]}`;
			}

			return ret;
		};

		updateHash = function () {
			window.location.hash = makeHash();
		};

		const refreshHash = function (ev) {
			if (ev.type === 'layeradd' || ev.type === 'layerremove') {
				if (ev.layer !== lightLayer && ev.layer !== signLayer)
					return;
			}

			const center = map.getCenter();

			params.zoom = map.getZoom();
			params.x = Math.round(center.lng);
			params.z = Math.round(-center.lat);

			updateHash();
		}

		updateHash();

		map.on('moveend', refreshHash);
		map.on('zoomend', refreshHash);
		map.on('layeradd', refreshHash);
		map.on('layerremove', refreshHash);

		window.onhashchange = function () {
			if (window.location.hash === makeHash())
				return;

			const prevMarkerCoords = params.marker;

			updateParams();

			if (params.light)
				map.addLayer(lightLayer);
			else
				map.removeLayer(lightLayer);

			if (features.signs) {
				if (params.signs)
					map.addLayer(signLayer);
				else
					map.removeLayer(signLayer);

				if (coordKey(prevMarkerCoords) !== coordKey(params.marker))
					getMarker(params.marker)?.openPopup();
			}

			map.setView([-params.z, params.x], params.zoom);

			updateHash();
		};

	})();
}