import './fastClick.mjs';
import { HORIZONTAL, VERTICAL } from './split.mjs';
import {
	getDroppedFile,
	getFileContent,
	hasDroppedFile,
} from './fileHelpers.mjs';
import CodeEditor from './CodeEditor.mjs';
import DOMWrapper from '../../../scripts/core/DOMWrapper.mjs';
import URLExporter from './URLExporter.mjs';
import VoidStorage from '../storage/VoidStorage.mjs';

const DELAY_AGENTCHANGE = 500;
const DELAY_STAGECHANGE = 250;
const PNG_RESOLUTION = 4;

function simplifyPreview(code) {
	return 'headers fade\nterminators fade\n' + code
		.replace(/\{Agent([0-9]*)\}/g, (match, num) => {
			if(typeof num === 'undefined') {
				return 'A';
			} else {
				return String.fromCharCode('A'.charCodeAt(0) + Number(num) - 1);
			}
		})
		.replace(/[{}]/g, '');
}

function fetchResource(path) {
	if(typeof fetch === 'undefined') {
		return Promise.reject(new Error());
	}
	return fetch(path)
		.then((response) => {
			if(!response.ok) {
				throw new Error(response.statusText);
			}
			return response;
		});
}

function setDocumentTitle(diagramTitle) {
	let title = diagramTitle.trim();
	if(title.length > 20) {
		title = title.substr(0, 18).trim() + '\u2026';
	}
	document.title = (title ? `${title} \u2014 ` : '') + 'Sequence Diagram';
}

export default class Interface {
	constructor({
		sequenceDiagram,
		defaultCode = '',
		library = [],
		links = [],
		storage = new VoidStorage(),
		touchUI = false,
	}) {
		this.diagram = sequenceDiagram;
		this.defaultCode = defaultCode;
		this.storage = storage;
		this.library = library;
		this.links = links;
		this.minScale = 1.5;
		this.touchUI = touchUI;

		this.debounced = null;
		this.latestSeq = null;
		this.renderedSeq = null;
		this.pngDirty = true;
		this.updatingPNG = false;

		this.marker = null;

		this._downloadSVGClick = this._downloadSVGClick.bind(this);
		this._downloadPNGClick = this._downloadPNGClick.bind(this);
		this._downloadPNGFocus = this._downloadPNGFocus.bind(this);
		this._downloadURLClick = this._downloadURLClick.bind(this);
		this._hideDropStyle = this._hideDropStyle.bind(this);

		this.diagram
			.on('render', () => {
				this.updateMinSize(this.diagram.getSize());
				setDocumentTitle(this.diagram.getTitle());
				this.pngDirty = true;
			})
			.on('mouseover', (element) => this.code.markLineHover(element.ln))
			.on('mouseout', () => this.code.unmarkLineHover())
			.on('click', (element) => {
				this.code.unmarkLineHover();
				this.code.selectLine(element.ln);
				this._hideURLBuilder();
			})
			.on('dblclick', (element) => {
				this.diagram.toggleCollapsed(element.ln);
				this._hideURLBuilder();
			});
	}

	buildURLBuilder() {
		const copied = this.dom.el('div').setClass('copied')
			.add('Copied to Clipboard');
		this.urlOutput = this.dom.el('input').setClass('output')
			.attr('readonly', 'readonly')
			.on('focus', () => {
				this.urlOutput.select(0, this.urlOutput.element.value.length);
			});

		const copy = this.dom.el('button').setClass('copy')
			.attr('title', 'Copy to clipboard')
			.fastClick()
			.on('click', () => {
				if(this.touchUI) {
					this.urlOutput.styles({display: 'block'});
				}
				this.urlOutput
					.focus()
					.select(0, this.urlOutput.element.value.length)
					.element.ownerDocument.execCommand('copy');
				copy.focus();
				this.container.delClass('keyinput');
				if(this.touchUI) {
					this.urlOutput.styles({display: 'none'});
				}
				copied.styles({
					'display': 'block',
					'opacity': 1,
					'transition': 'none',
				});
				setTimeout(() => copied.styles({
					'opacity': 0,
					'transition': 'opacity 0.5s linear',
				}), 1000);
				setTimeout(() => copied.styles({'display': 'none'}), 1500);
			});

		const updateMode = () => {
			this.renderOpts.styles({
				'display': this.modeEdit.element.checked ? 'none' : 'block',
			});
			this._refreshURL();
		};

		this.modeRender = this.dom.el('input').attrs({
			'checked': 'checked',
			'name': 'export-mode',
			'type': 'radio',
			'value': 'render',
		}).on('change', updateMode);

		this.modeEdit = this.dom.el('input').attrs({
			'name': 'export-mode',
			'type': 'radio',
			'value': 'edit',
		}).on('change', updateMode);

		this.modeMarkdown = this.dom.el('input').attrs({
			'name': 'export-mode',
			'type': 'radio',
			'value': 'markdown',
		}).on('change', updateMode);

		this.urlWidth = this.dom.el('input').attrs({
			'min': 0,
			'placeholder': 'auto',
			'step': 'any',
			'type': 'number',
		}).on('input', () => {
			this.urlZoom.val('1');
			this._refreshURL();
		});

		this.urlHeight = this.dom.el('input').attrs({
			'min': 0,
			'placeholder': 'auto',
			'step': 'any',
			'type': 'number',
		}).on('input', () => {
			this.urlZoom.val('1');
			this._refreshURL();
		});

		this.urlZoom = this.dom.el('input').attrs({
			'min': 0,
			'step': 'any',
			'type': 'number',
			'value': 1,
		}).on('input', () => {
			this.urlWidth.val('');
			this.urlHeight.val('');
			this._refreshURL();
		});

		this.renderOpts = this.dom.el('div').add(
			this.dom.el('label').add('width ', this.urlWidth),
			', ',
			this.dom.el('label').add('height ', this.urlHeight),
			this.dom.el('span').setClass('or').add('or'),
			this.dom.el('label').add('zoom ', this.urlZoom),
		);

		const urlOpts = this.dom.el('div').setClass('config').add(
			this.dom.el('div').setClass('export-mode').add(
				this.dom.el('label').add(this.modeRender, 'View'),
				this.dom.el('label').add(this.modeEdit, 'Edit'),
				this.dom.el('label').add(this.modeMarkdown, 'Markdown'),
			),
			this.renderOpts,
			this.urlOutput,
			copy,
			copied,
		);

		const urlBuilder = this.dom.el('div').setClass('urlbuilder')
			.styles({'display': 'none'})
			.add(
				this.dom.el('div').setClass('message')
					.add('Loading\u2026'),
			);

		const ownURL = (typeof window === 'undefined') ? 'http://localhost' : window.location.href;
		this.renderService = new URLExporter();
		this.renderService.setEditBase(new URL('.', ownURL).href);

		const relativePath = 'render/';
		fetchResource(relativePath)
			.then((response) => response.text())
			.then((content) => {
				let path = content.trim();
				if(!path || path.startsWith('<')) {
					path = relativePath;
				}
				this.renderService.setRenderBase(new URL(path, ownURL).href);
				urlBuilder.empty().add(urlOpts);
				this._refreshURL();
			})
			.catch(() => {
				urlBuilder.empty().add(
					this.dom.el('div').setClass('message')
						.add('No online rendering service available.'),
				);
			});

		return urlBuilder;
	}

	_refreshURL() {
		const code = this.code.value();
		const viewOpts = {
			height: Number.parseFloat(this.urlHeight.element.value),
			width: Number.parseFloat(this.urlWidth.element.value),
			zoom: Number.parseFloat(this.urlZoom.element.value || '1'),
		};

		let url = '';
		if(this.modeMarkdown.element.checked) {
			const edit = this.renderService.getEditURL(code);
			const view = this.renderService.getRenderURL(code, viewOpts);
			const title = this.diagram.getTitle()
				.replace(/[^a-zA-Z0-9 \-_'"]/g, '')
				.trim();
			url = `[![${title}](${view})](${edit})`;
		} else if(this.modeEdit.element.checked) {
			url = this.renderService.getEditURL(code);
		} else {
			url = this.renderService.getRenderURL(code, viewOpts);
		}
		this.urlOutput.val(url);
	}

	_showURLBuilder() {
		if(this.builderVisible) {
			return;
		}
		this.builderVisible = true;
		if(this.touchUI) {
			this.urlBuilder.styles({
				'bottom': '-210px',
				'display': 'block',
			});
		} else {
			this.urlBuilder.styles({
				'display': 'block',
				'height': '0px',
				'padding': '0px',
				'width': this.optsHold.element.clientWidth + 'px',
			});
		}
		clearTimeout(this.builderTm);
		this.builderTm = setTimeout(() => {
			if(this.touchUI) {
				this.urlBuilder.styles({'bottom': 0});
			} else {
				this.urlBuilder.styles({
					'height': '150px',
					'padding': '10px',
					'width': '400px',
				});
				this.optsHold.styles({
					'box-shadow': '10px 10px 25px 12px rgba(0,0,0,0.3)',
				});
			}
		}, 0);

		this._refreshURL();
	}

	_hideURLBuilder() {
		if(!this.builderVisible) {
			return;
		}
		this.builderVisible = false;
		if(this.touchUI) {
			this.urlBuilder.styles({
				'bottom': (-this.urlBuilder.element.clientHeight - 60) + 'px',
			});
		} else {
			this.urlBuilder.styles({
				'height': '0px',
				'padding': '0px',
				'width': '0px',
			});
			this.optsHold.styles({
				'box-shadow': 'none',
			});
		}
		this.container.delClass('keyinput');
		clearTimeout(this.builderTm);
		this.builderTm = setTimeout(() => {
			this.urlBuilder.styles({'display': 'none'});
		}, 200);
	}

	buildOptionsDownloads() {
		this.downloadPNG = this.dom.el('a')
			.text('Export PNG')
			.attrs({
				'download': 'SequenceDiagram.png',
				'href': '#',
			})
			.on(['focus', 'mouseover', 'mousedown'], this._downloadPNGFocus)
			// Exploit delay between touchend and click on mobile
			.on('touchend', this._downloadPNGFocus)
			.on('click', this._downloadPNGClick);

		this.downloadSVG = this.dom.el('a')
			.text('SVG')
			.attrs({
				'download': 'SequenceDiagram.svg',
				'href': '#',
			})
			.fastClick()
			.on('click', this._downloadSVGClick);

		this.downloadURL = this.dom.el('a')
			.text('URL')
			.attrs({'href': '#'})
			.fastClick()
			.on('click', this._downloadURLClick);

		this.urlBuilder = this.buildURLBuilder();

		this.optsHold = this.dom.el('div').setClass('options downloads').add(
			this.downloadPNG,
			this.downloadSVG,
			this.downloadURL,
			this.urlBuilder,
		);

		return this.optsHold;
	}

	buildLibrary(container) {
		const diagrams = this.library.map((lib) => {
			const holdInner = this.dom.el('div')
				.attr('title', lib.title || lib.code);

			const hold = this.dom.el('div')
				.setClass('library-item')
				.add(holdInner)
				.fastClick()
				.on('click', () => this.code.addCodeBlock(
					lib.code,
					lib.surround,
				))
				.attach(container);

			return this.diagram.clone({
				code: simplifyPreview(lib.preview || lib.code),
				container: holdInner.element,
				render: false,
			}).on('error', (sd, e) => {
				window.console.warn('Failed to render preview', e);
				hold.attr('class', 'library-item broken');
				holdInner.text(lib.code);
			});
		});

		try {
			this.diagram.renderAll(diagrams);
		} catch(ignore) {
			// Ignore
		}

		return container;
	}

	buildCodePane() {
		const container = this.dom.el('div').setClass('pane-code');

		this.code = new CodeEditor(this.dom, container, {
			mode: 'sequence',
			value: this.storage.get() || this.defaultCode,
		});

		this.code
			.on('enhance', (CM, globals) => {
				this.diagram.registerCodeMirrorMode(CM);
				globals.themes = this.diagram.getThemeNames();
			})
			.on('change', () => this.update(false))
			.on('cursorActivity', (from, to) => {
				this.diagram.setHighlight(Math.min(from.line, to.line));
			})
			.on('focus', () => this._hideURLBuilder());

		return container;
	}

	buildLibPane() {
		if(this.library.length === 0) {
			return null;
		}

		return this.dom.el('div').setClass('pane-library')
			.add(this.dom.el('div').setClass('pane-library-scroller')
				.add(this.buildLibrary(
					this.dom.el('div').setClass('pane-library-inner'),
				)));
	}

	buildViewPane() {
		this.viewPaneInner = this.dom.el('div').setClass('pane-view-inner')
			.add(this.diagram.dom())
			.on('touchstart', () => this._hideURLBuilder(), {passive: true})
			.on('mousedown', () => this._hideURLBuilder());

		this.errorMsg = this.dom.el('div').setClass('msg-error');

		return this.dom.el('div').setClass('pane-view')
			.add(
				this.dom.el('div').setClass('pane-view-scroller')
					.add(this.viewPaneInner),
				this.errorMsg,
			);
	}

	build(container) {
		this.dom = new DOMWrapper(container.ownerDocument);

		this.container = this.dom.wrap(container)
			.on('dragover', (event) => {
				event.preventDefault();
				if(hasDroppedFile(event, 'image/svg+xml')) {
					event.dataTransfer.dropEffect = 'copy';
					this._showDropStyle();
				} else {
					event.dataTransfer.dropEffect = 'none';
				}
			})
			.on('dragleave', this._hideDropStyle)
			.on('dragend', this._hideDropStyle)
			.on('drop', (event) => {
				event.preventDefault();
				this._hideDropStyle();
				const file = getDroppedFile(event, 'image/svg+xml');
				if(file) {
					this.loadFile(file);
				}
			})
			.on('focusin', () => this.container.addClass('keyinput'))
			.on('focusout', () => this.container.delClass('keyinput'));

		const codePane = this.buildCodePane();
		const libPane = this.buildLibPane();
		const viewPane = this.buildViewPane();

		const links = this.links.map((link) => {
			const label = this.touchUI ? link.touchLabel : link.label;
			return label && this.dom.el('a')
				.attrs({
					'href': link.href,
					'rel': 'noopener',
					'target': link.target || '',
				})
				.text(label);
		}).filter((x) => x);

		if(this.touchUI) {
			this.buildOptionsDownloads();
			this.container
				.addClass('touch')
				.add(
					this.dom.el('div').setClass('pane-hold')
						.split(this.dom, VERTICAL, { snap: 20 }, [
							{initialSize: 80, minSize: 10, node: viewPane},
							{initialSize: 20, minSize: 10, node: codePane},
						]),
					libPane.styles({'display': 'none', 'top': '100%'}),
					this.urlBuilder,
					this.dom.el('div').setClass('optbar')
						.add(
							...links,
							this.downloadPNG.text('PNG'),
							this.downloadSVG.text('SVG'),
							this.downloadURL.text('URL'),
						),
				);
		} else {
			const side = this.dom.el('div').setClass('pane-side')
				.split(this.dom, VERTICAL, { snap: 5 }, [
					{initialSize: 70, minSize: 100, node: codePane},
					{initialSize: 30, minSize: 5, node: libPane},
				]);
			this.container
				.add(
					this.dom.el('div').setClass('pane-hold')
						.split(this.dom, HORIZONTAL, { snap: 70 }, [
							{initialSize: 30, minSize: 10, node: side},
							{initialSize: 70, minSize: 10, node: viewPane},
						]),
					this.dom.el('div').setClass('options links').add(links),
					this.buildOptionsDownloads(),
				);
		}

		if(typeof window !== 'undefined') {
			window.addEventListener('keydown', (e) => {
				if(e.keyCode === 27) {
					this._hideURLBuilder();
				}
			});
		}

		// Delay first update 1 frame to ensure render target is ready
		// (prevents initial incorrect font calculations for custom fonts)
		setTimeout(this.update.bind(this), 0);
	}

	updateMinSize({width, height}) {
		this.viewPaneInner.styles({
			'minHeight': Math.ceil(height * this.minScale) + 'px',
			'minWidth': Math.ceil(width * this.minScale) + 'px',
		});
	}

	redrawDebounced(sequence, delay) {
		if(delay <= 0) {
			this.redraw(sequence);
		} else {
			clearTimeout(this.debounced);
			this.latestSeq = sequence;
			this.debounced = setTimeout(() => this.redraw(sequence), delay);
		}
	}

	redraw(sequence) {
		clearTimeout(this.debounced);
		this.debounced = null;
		this.renderedSeq = sequence;
		this.diagram.render(sequence);
	}

	markError(error) {
		if(typeof error === 'object' && error.message) {
			this.errorMsg.text(error.message);
		} else {
			this.errorMsg.text(error);
		}
		this.errorMsg.addClass('error');
	}

	markOK() {
		this.errorMsg.text('').delClass('error');
	}

	loadFile(file) {
		return getFileContent(file).then((svg) => {
			const code = this.diagram.extractCodeFromSVG(svg);
			if(code) {
				this.code.setValue(code);
				this.diagram.expandAll({render: false});
				this.update(true);
				this.diagram.setHighlight(null);
			}
		});
	}

	update(immediate = true) {
		this._hideURLBuilder();
		const src = this.code.value();
		this.storage.set(src);
		let sequence = null;
		try {
			sequence = this.diagram.process(src);
		} catch(e) {
			this.markError(e);
			return;
		}
		this.markOK();

		let delay = 0;
		if(!immediate && this.renderedSeq) {
			const old = this.renderedSeq;
			if(sequence.agents.length !== old.agents.length) {
				delay = DELAY_AGENTCHANGE;
			} else if(sequence.stages.length !== old.stages.length) {
				delay = DELAY_STAGECHANGE;
			}
		}

		this.redrawDebounced(sequence, delay);
	}

	forceRender() {
		if(this.debounced) {
			clearTimeout(this.debounced);
			this.redraw(this.latestSeq);
		}
	}

	updatePNGLink() {
		this.forceRender();
		if(this.updatingPNG || !this.pngDirty) {
			return false;
		}
		this.pngDirty = false;
		this.updatingPNG = true;
		this.diagram.getPNG({resolution: PNG_RESOLUTION})
			.then(({url, latest}) => {
				if(latest) {
					this.downloadPNG.attr('href', url);
					this.updatingPNG = false;
				}
			});
		return true;
	}

	_showDropStyle() {
		this.container.addClass('drop-target');
	}

	_hideDropStyle() {
		this.container.delClass('drop-target');
	}

	_downloadPNGFocus() {
		this.updatePNGLink();
	}

	_downloadPNGClick(e) {
		if(this.updatingPNG) {
			e.preventDefault();
		} else if(this.updatePNGLink()) {
			e.preventDefault();
		}
		this._hideURLBuilder();
	}

	_downloadSVGClick() {
		this.forceRender();
		const url = this.diagram.getSVGSynchronous();
		this.downloadSVG.attr('href', url);
		this._hideURLBuilder();
	}

	_downloadURLClick(e) {
		e.preventDefault();

		if(this.builderVisible) {
			this._hideURLBuilder();
		} else {
			this._showURLBuilder();
		}
	}
}
