/**
 * MiniMap.js - MiniMap for the game Adventure.Land
 * 
 * Features:
 *  Event emitter
 *	Drag to reposition
 *	Numpad plus/minus to zoom
 *  Toggle visibility to show or hide
 *  Left click to interact with markers, right click to smart move
 *  Easily extensible
 * 
 * Tips:
 *   Toggle visibility: minimap.visible = !minimap.visible
 *   Toggle minimized: minimap.minimized = !minimap.minimized
 * 
 * Instantiation:
 * 
 * if (game.graphics) {
 *	const minimap = new MiniMap();
 *	parent.drawings.push(minimap);
 *	parent.stage.addChild(minimap);
 * }
 * 
 * Cleanup:
 *   None. Cleanup is handled by the game's clear_drawings call.
 * 
 * @todo mouse roll zoom via scale?
  */
'use strict';

const GUI_WINDOW_BORDER_COLOR = 0x47474F;
const GUI_WINDOW_INTERIOR_COLOR = 0x40420;
const GUI_WINDOW_TOOLBAR_HEIGHT = get('GUI_WINDOW_TOOLBAR_HEIGHT') || 25;
const GUI_WINDOW_TOOLBAR_BACKGROUND_COLOR = get('GUI_WINDOW_TOOLBAR_BACKGROUND_COLOR') || 0x00;
const GUI_WINDOW_TOOLTIP_FONT_SIZE = 11;
const GUI_WINDOW_BORDER_SIZE = get('MINIMAP_BORDER') || 6; // The radius of the outer line around the minimap
const GUI_WINDOW_DEFAULT_WIDTH = 240;
const GUI_WINDOW_DEFAULT_HEIGHT = 240;
const GUI_WINDOW_ORIGIN = get('GUI_WINDOW_ORIGIN') || { x: 175 + 50, y: 175 }; // Where the map is drawn on the screen

const MINIMAP_WALL_COLOR = 0x47474F;
const MINIMAP_NPC_COLOR = 0x2341DB;
const MINIMAP_DOOR_COLOR = 0xAAAAAA;

const MINIMAP_SCALE = get('MINIMAP_SCALE') || 6;
const MINIMAP_TRACKER_FREQUENCY = get('MINIMAP_TRACKER_FREQUENCY') || 500;
const MINIMAP_MAP_TRACKER_UPDATE_FREQUENCY = get('MINIMAP_ENTITY_FREQUENCY') || 100;
const MINIMAP_ENTITY_SIZE = get('MINIMAP_ENTITY_SIZE') || 12;
const MINIMAP_NPC_SIZE = get('MINIMAP_NPC_SIZE') || 15;

function clamp(low, value, high) {
	return Math.min(Math.max(low, value), high);
}

function truncate(str, len = 24) {
	if (!str) return null;
	let sub = str.slice(0, len - 3);
	if (str.length > sub.length)
		sub += '...';
	return sub;
}

/**
 * PIXI.js window with dragging
 */
class GuiWindow extends PIXI.Container {
	constructor(id, opts) {
		super();
		this.id = id; // Used for persistence
		this.restorePosition();
	}

	restorePosition() {
		const pos = get(`GUI_WINDOW_${this.id}_POS`) || {};
		const { x = GUI_WINDOW_ORIGIN.x, y = GUI_WINDOW_ORIGIN.y } = pos;
		const { width = GUI_WINDOW_DEFAULT_WIDTH, height = GUI_WINDOW_DEFAULT_HEIGHT } = pos;
		this.position.x = x;
		this.position.y = y;
		this.width = width;
		this.height = height;
		this.createViewport();
	}

	persist() { set(`GUI_WINDOW_${this.id}_POS`, { x: this.position.x, y: this.position.y, width: this.width, height: this.height }); }

	get minimized() { return !this.backdrop.visible; }
	set minimized(b) { this.backdrop.visible = !b; }

	createViewport() {
		const pos = get(`GUI_WINDOW_${this.id}_POS`) || {};
		const { x = GUI_WINDOW_ORIGIN.x, y = GUI_WINDOW_ORIGIN.y } = pos;
		const { width = GUI_WINDOW_DEFAULT_WIDTH, height = GUI_WINDOW_DEFAULT_HEIGHT } = pos;
		this.position.x = x;
		this.position.y = y;

		// Create main backdrop
		this.backdrop = this.addChild(new PIXI.Graphics());
		this.backdrop.lineStyle(GUI_WINDOW_BORDER_SIZE, GUI_WINDOW_BORDER_COLOR);
		this.backdrop.beginFill(GUI_WINDOW_INTERIOR_COLOR);
		this.backdrop.drawRect(0, 0, width - GUI_WINDOW_BORDER_SIZE, height - GUI_WINDOW_TOOLBAR_HEIGHT - GUI_WINDOW_BORDER_SIZE);
		this.backdrop.position.y = GUI_WINDOW_TOOLBAR_HEIGHT;
		this.backdrop.endFill();

		// Create mask (Apparently needs to be added to container to work properly)
		const mask = this.backdrop.addChild(new PIXI.Graphics());
		mask.lineStyle(0, 0x000000);	// Don't draw the border lines here, since we're drawing a mask.
		mask.beginFill(GUI_WINDOW_INTERIOR_COLOR);
		mask.drawRect(GUI_WINDOW_BORDER_SIZE / 2, GUI_WINDOW_BORDER_SIZE / 2, width - GUI_WINDOW_BORDER_SIZE * 2, height - GUI_WINDOW_TOOLBAR_HEIGHT - GUI_WINDOW_BORDER_SIZE * 2);
		mask.endFill();
		this.windowMask = mask;

		// Create toolbar and touch points last
		const t = this.addChild(new PIXI.Graphics());
		t.lineStyle(GUI_WINDOW_BORDER_SIZE, GUI_WINDOW_BORDER_COLOR);
		t.beginFill(GUI_WINDOW_TOOLBAR_BACKGROUND_COLOR);
		t.drawRect(0, 0, this.width - GUI_WINDOW_BORDER_SIZE, GUI_WINDOW_TOOLBAR_HEIGHT);
		t.interactive = true;
		this.toolbar = t;
		t
			.on('pointerdown', this.onDragStart.bind(this))
			.on('pointerup', this.onDragEnd.bind(this))
			.on('pointerupoutside', this.onDragEnd.bind(this))
			.on('pointermove', this.onDragMove.bind(this));
		// Title bar
		this.title = t.addChild(new PIXI.Text(null, { fontSize: 20, fill: 0xFFFFFF }));
		return this.backdrop;
	}

	onDragStart(e) {
		if (this.dragging) return;
		this.dragging = true;
		this.alpha *= 0.5;
		const { x, y } = e.data.getLocalPosition(this);
		this.pivot.set(x, y);
		this.position.set(e.data.global.x, e.data.global.y);
	}

	onDragEnd() {
		if (!this.dragging) return;
		this.dragging = false;
		this.alpha /= 0.5;
		set(`GUI_WINDOW_${this.id}_POS`, { x: this.x - this.pivot.x, y: this.y - this.pivot.y });
	}

	onDragMove(e) {
		if (!this.dragging) return;
		const { x, y } = e.data.getLocalPosition(this.parent);
		this.position.set(x, y);
	}
}

/**
 * Mark a point of interest in the game using in game cooridates
 */
export class MapMarker extends PIXI.Graphics {
	constructor(x = 0, y = 0, color = 0xFFFFFF, radius = MINIMAP_ENTITY_SIZE) {
		super();
		this.zIndex = 2000;
		this.radius = radius;
		this.color = color;
		this.position.x = x;
		this.position.y = y;
		this.interactive = true;
		this.redraw();
		this
			.on('pointerover', this.onMouseOver, this)
			.on('pointerout', this.onMouseOut, this)
	}

	getMap() {
		if (!this.map) {
			for (let item = this.parent; item; item = item.parent) {
				if (item instanceof MiniMap) {
					this.map = item;
					break;
				}
			}
		}
		return this.map;
	}

	onMouseOver(e) {
		this.mouse_in = true;
		console.log(`On entity mouse over`, e);
		const map = this.getMap();
		map.setTooltip(this.tag || this.name);
	}

	onMouseOut(e) {
		this.mouse_in = false;
		console.log(`On entity mouse out`, e);
		const map = this.getMap();
		map.setTooltip(null);
	}

	redraw() {
		this.clear();
		this.lineStyle(this.radius, this.color);
		this.beginFill(this.color);
		this.drawCircle(0, 0, this.radius);
		this.endFill();
	}
}

/**
 * Map marker that adjusts to track position
 * 
 * @todo move entity specific stuff out. MapTracker just interval tracks
 */
export class MapTracker extends MapMarker {
	constructor(color, size, entity, freq = MINIMAP_MAP_TRACKER_UPDATE_FREQUENCY) {
		super(0, 0, color, size);
		this.eid = entity.id || entity;
		this.visible = false;
		this.freq = freq;
		this.lastUpdate = 0;
		this.timer = setInterval(() => this.update(), freq);
	}

	destroy() {
		clearInterval(this.timer);
		super.destroy({ children: true });
	}
}

export class PlayerTracker extends MapMarker {
	constructor() {
		super(0, 0, 0xFFFFFF, MINIMAP_ENTITY_SIZE);
	}

	updateTransform() {
		this.position.x = character.real_x;
		this.position.y = character.real_y;
		return super.updateTransform();
	}
}

export class EntityTracker extends MapTracker {
	constructor(entity, freq) {
		super(0x00000, MINIMAP_ENTITY_SIZE, entity, freq);
		this.on('pointerup', this.onClick, this)
	}

	onClick(e) {
		const entity = parent.entities[this.eid];
		if (entity.mtype && !entity.dead && entity.visible !== false) {
			change_target(entity, true);
			game_log(`Attacking ${entity.name}`);
		}
	}

	updateTransform() {
		const entity = parent.entities[this.eid];
		if (!entity || entity.dead)
			return; //  We can't destroy it here or we break the renderer
		this.position.x = entity.real_x;
		this.position.y = entity.real_y;
		return super.updateTransform();
	}

	update() {
		const entity = parent.entities[this.eid];
		if (!entity || entity.dead)
			return this.destroy({ children: true });
		this.visible = true;
		if (entity.type === 'character') {
			if (parent.party_list.includes(entity.id)) {
				this.color = 0x1BD545;
			} else if (entity.npc == null) {
				this.color = 0xDCE20F;
			} else {
				this.color = 0x2341DB;
			}
		} else {
			if (entity.mtype != null && (parent.G.monsters[entity.mtype].respawn == -1 || parent.G.monsters[entity.mtype].respawn > 60 * 2)) {
				this.color = 0x40420;
			}
			if (entity.target)
				this.color = 0xff9900;
			else
				this.color = 0xEE190E;
		}
		this.redraw();
	}
}

export class MapTracking extends PIXI.Container {
	constructor(freq = MINIMAP_TRACKER_FREQUENCY) {
		super();
		this.zIndex = 1005;
		this.tracking = new WeakMap();
		this.timer = setInterval(() => this.update(), freq);
		this.update();
	}

	destroy() {
		clearInterval(this.timer);
		super.destroy({ children: true });
	}
}

class EntityTracking extends MapTracking {
	update() {
		for (const entity of Object.values(parent.entities)) {
			if (this.tracking.has(entity) || entity.dead)
				continue;
			const tracker = this.createTracker(entity);
			this.tracking.set(entity, tracker);
			this.addChild(tracker);
		}
	}

	createTracker(entity) {
		/*if (entity.type === 'monster') {
			const sprite = parent.new_sprite(entity.mtype, 'skin');
			sprite.position.set(entity.real_x,  entity.real_y);
			entity.scale = 2;
			return sprite;
		} */
		const tracker = new EntityTracker(entity, MINIMAP_MAP_TRACKER_UPDATE_FREQUENCY);
		tracker.tag = entity.name;
		return tracker;
	}
}

class NpcTracking extends MapTracking {
	update() {
		const npcs = G.maps[character.map].npcs;
		for (const npc of Object.values(npcs)) {
			if (npc.loop || npc.manual || !npc.position)
				continue;
			const [x, y] = npc.position;
			if (x === 0 && y === 0)
				continue;
			if (this.tracking.has(npc))
				continue;
			const marker = new MapMarker(x, y, MINIMAP_NPC_COLOR, MINIMAP_NPC_SIZE);
			marker.tag = npc.name || G.npcs[npc.id].name || npc.id;
			this.tracking.set(npc, marker);
			this.addChild(marker);
		}
	}
}

// Exists to encapsulates zoom and scale.
class ZoomLayer extends PIXI.Container {
	setZoom(fr = MINIMAP_SCALE) {
		if (fr <= 0 || fr > 10)
			return;
		this.fr = fr;
		this.scale.x = 1 / this.fr;
		this.scale.y = 1 / this.fr;
		console.debug(`Setting MINIMAP_SCALE to ${this.fr}`);
		set('MINIMAP_SCALE', this.fr);
	}

	incZoom() { this.setZoom(this.fr - 1); }
	decZoom() { this.setZoom(this.fr + 1); }
}


/**
 * The actual minimap
 */
export default class MiniMap extends GuiWindow {
	constructor() {
		super(`MINI_MAP`);
		this.zIndex = 999;
		this.map_listener = game.on('new_map', (e) => this.emit('new_map', e)); // Event forwarding
		this.on('new_map', (e) => this.onMapChange(e));
		// Because event listeners are written kind of poorly
		this.onKeyUpBound = this.onKeyUp.bind(this);
		parent.addEventListener('keyup', this.onKeyUpBound);
		this.onMapChange(null);
	}

	/**
	 * Cleanup
	 */
	destroy(opts) {
		try {
			game.remove(this.map_listener);
			parent.removeEventListener('keyup', this.onKeyUpBound);
		} finally {
			opts.chldren = true;
			super.destroy(opts);
		}
	}

	updateTransform() {
		this.recenter();
		return super.updateTransform();
	}

	recenter(force = false) {
		if (!this.dynamics)
			return;
		if (!is_moving(character) && !force) // @todo only recenter if moving, otherwise allow scrolling
			return;
		this.dynamics.pivot.x = character.real_x; // - this.width / 2;
		this.dynamics.pivot.y = character.real_y; // - this.height / 2;
		// this.zoomlayer.pivot.x = 0; // may be useful for map scrolling
		// this.zoomlayer.pivot.y = 0;
		const { min_x, min_y, max_x, max_y } = G.maps[character.map].data;
		this.dynamics.pivot.x = clamp(min_x, this.dynamics.pivot.x, max_x);
		this.dynamics.pivot.y = clamp(min_y, this.dynamics.pivot.y, max_y);
	}

	/**
	 * Event handling
	 */
	onKeyUp(e) {
		if (e.code === 'NumpadAdd')
			this.zoomlayer.incZoom();
		else if (e.code === 'NumpadSubtract')
			this.zoomlayer.decZoom();
	}

	onMapClick(e) {
		const { x, y } = e.data.getLocalPosition(this.dynamics);
		console.debug(`onMapClick`, x, y, e);
		if (is_moving(character))
			stop();
		smart_move({ map: character.map, x, y });
	}

	onMapChange(e) {
		console.debug(`onMapChange`, e);
		this.setTitle(G.maps[character.map].name);
		if (this.dynamics) {
			this.dynamics.destroy({ children: true });
			this.dynamics = null;
			this.interactables = null;
		}
		this.createDynamics();
		this.zoomlayer.addChildAt(this.dynamics, 0);
		this.dynamics.addChild(new PlayerTracker());
		this.recenter(true);
	}

	addSimpleMapMarker(name, pos, color = 0xAAAAAA) {
		const marker = this.interactables.addChild(new MapMarker(pos.x, pos.y, color));
		marker.tag = name;
	}

	addMapMarker(marker) {
		this.dynamics.addChild(marker);
	}

	setTitle(str) {
		this.title.text = truncate(str);
		this.title.position.x = (this.title.parent.width / 2) - (this.title.width / 2);
	}

	setTooltip(str) {
		this.tooltip.text = truncate(str);
	}

	createDynamics() {
		this.dynamics = new PIXI.Container();
		const map = this.dynamics.addChild(new PIXI.Graphics());
		const { min_x, min_y, max_x, max_y } = G.maps[character.map].data;
		const margin = 1000;  // Margin is useful so we have a clickable surface for drag		
		map.interactive = true;
		map.beginFill(GUI_WINDOW_INTERIOR_COLOR, 1.0); // Apparently this surface is required to register map clicks		
		map.drawRect(min_x - margin, min_y - margin, max_x - min_x + (margin * 2), max_y - min_y + (margin * 2));
		this.drawWalls(map);
		map.cacheAsBitmap = true;
		map.on('mousedown', this.onMapDragStart, this);		// Left click drag so we don't accidentally move while trying to click an entity
		map.on('mouseup', this.onMapDragEnd, this);
		map.on('mouseupoutside', this.onMapDragEnd, this);
		map.on('mouseout', this.onMapDragEnd, this);
		map.on('mousemove', this.onMapDragMove, this);
		map.on('rightup', this.onMapClick, this);

		this.interactables = this.dynamics.addChild(new PIXI.Container);
		// Dynamic objects container
		this.interactables.addChild(new EntityTracking());
		this.interactables.addChild(new NpcTracking());
		for (const door of G.maps[character.map].doors) {
			const [x, y, , , dest] = door;
			this.addSimpleMapMarker(G.maps[dest].name, { x, y }, MINIMAP_DOOR_COLOR);
		}
	}

	onMapDragStart(e) {
		if (is_moving(character) || this.draggingMap) return;
		this.draggingMap = true;
		this.dynamics.alpha *= 0.5;
		this.interactables.interactiveChildren = false;
		/* 		
		const { x, y } = e.data.getLocalPosition(this.dynamics);
		this.dynamics.pivot.set(x, y);  */
		const viewPosition = e.data.getLocalPosition(this.backdrop);
		const { x, y } = e.data.getLocalPosition(this.dynamics);
		//this.zoomlayer.pivot 
		// this.dynamics.position.set(e.data.global.x, e.data.global.y);
	}

	onMapDragEnd() {
		if (!this.draggingMap) return;
		this.dynamics.alpha /= 0.5;
		this.draggingMap = false;
		this.interactables.interactiveChildren = true;
	}

	onMapDragMove(e) {
		if (!this.dynamics.dragging)
			return;
		// Adjust for container coordinates?
		// const newPosition = e.data.getLocalPosition(this.dynamics.parent);
		const viewPosition = e.data.getLocalPosition(this.backdrop);
		const newPosition = e.data.getLocalPosition(this.dynamics);
		const { x, y } = newPosition;
		console.log('Drag event', viewPosition, newPosition, e);

		this.zoomlayer.pivot.x = (this.backdrop.width / 2) - viewPosition.x;
		this.zoomlayer.pivot.y = (this.backdrop.height / 2) - viewPosition.y;
		// this.dynamics.pivot.x = newPosition.x;
		// this.dynamics.pivot.y = newPosition.y;
		// this.dynamics.pivot.x = newPosition.x / this.zoomlayer.fr * 2;
		// this.dynamics.pivot.y = newPosition.y / this.zoomlayer.fr * 2;		

		const { min_x, min_y, max_x, max_y } = G.maps[character.map].data;
		this.dynamics.pivot.x = clamp(min_x, this.dynamics.pivot.x, max_x);
		this.dynamics.pivot.y = clamp(min_y, this.dynamics.pivot.y, max_y);
	}

	/**
	 * Create elements that don't need to redraw
	 */
	createViewport() {
		super.createViewport();
		// Create camera / scaling leayer
		this.zoomlayer = this.backdrop.addChild(new ZoomLayer());
		this.zoomlayer.mask = this.windowMask;
		this.zoomlayer.position.x = this.backdrop.width / 2;
		this.zoomlayer.position.y = this.backdrop.height / 2;
		this.zoomlayer.setZoom(MINIMAP_SCALE);
		// This works, until we start scrolling
		// this.zoomlayer.addChild(new MapMarker(0, 0, 0xFFFFFF));

		this.tooltip = this.backdrop.addChild(new PIXI.Text(null, { fontSize: 16, fill: 0xFFFFFF }));
		this.tooltip.position.y = this.backdrop.height - this.tooltip.height - GUI_WINDOW_BORDER_SIZE - 5;
		this.tooltip.position.x = GUI_WINDOW_BORDER_SIZE + 1;
	}

	drawLine(e, [x1, y1], [x2, y2]) {
		e.moveTo(x1, y1);
		e.lineTo(x2, y2);
		e.endFill();
	}

	drawWalls(g) {
		const size = get('MINIMAP_LINE_SIZE') || 11;
		g.lineStyle(size, MINIMAP_WALL_COLOR);
		const map_data = parent.G.maps[character.map].data;
		for (const id in map_data.x_lines) {
			const line = map_data.x_lines[id];
			const x1 = line[0]; const y1 = line[1];
			const x2 = line[0]; const y2 = line[2];
			this.drawLine(g, [x1, y1], [x2, y2]);
		}

		for (const id in map_data.y_lines) {
			const line = map_data.y_lines[id];
			const x1 = line[1]; const y1 = line[0];
			const x2 = line[2]; const y2 = line[0];
			this.drawLine(g, [x1, y1], [x2, y2]);
		}
	}
}