import { Annotation } from 'delete-p-cus-brace-jan';
import * as PropTypes from 'prop-types';
import * as React from 'react';
const isEqual = require('lodash.isequal');
import { debounce, editorEvents, editorOptions, getAceInstance } from './editorOptions';
const ace = getAceInstance();
const { Range } = ace.acequire('ace/range');

import { AceEditorClass } from './AceEditorClass';
import { IAceOptions, ICommand, IEditorProps, IMarker } from './types';
/**
 * See https://github.com/ajaxorg/ace/wiki/Configuring-Ace
 */

import 'delete-p-cus-brace-jan/ext/language_tools';
import 'delete-p-cus-brace-jan/mode/hcpgrammar';
import 'delete-p-cus-brace-jan/mode/jsx';
import 'delete-p-cus-brace-jan/snippets/hcpgrammar';
import 'delete-p-cus-brace-jan/theme/monokai';

import { HCPExpression } from './HCPExpression';

export interface IAceEditorProps {
	name?: string;
	style?: React.CSSProperties;
	/** For available modes see https://github.com/thlorenz/brace/tree/master/mode */
	mode?: string;
	/** For available themes see https://github.com/thlorenz/brace/tree/master/theme */
	theme?: string;
	height?: string;
	width?: string;
	className?: string;
	fontSize?: number | string;
	showGutter?: boolean;
	showPrintMargin?: boolean;
	highlightActiveLine?: boolean;
	focus?: boolean;
	cursorStart?: number;
	wrapEnabled?: boolean;
	readOnly?: boolean;
	minLines?: number;
	maxLines?: number;
	navigateToFileEnd?: boolean;
	debounceChangePeriod?: number;
	enableBasicAutocompletion?: boolean | string[];
	enableLiveAutocompletion?: boolean | string[];
	tabSize?: number;
	value?: string;
	placeholder?: string;
	defaultValue?: string;
	scrollMargin?: number[];
	enableSnippets?: boolean;
	onSelectionChange?: (value: any, event?: any) => void;
	onCursorChange?: (value: any, event?: any) => void;
	onInput?: (event?: any) => void;
	onLoad?: (editor: IEditorProps) => void;
	onValidate?: (annotations: Annotation[]) => void;
	onBeforeLoad?: (ace: any) => void;
	onChange?: (value: string, event?: any) => void;
	onSelection?: (selectedText: string, event?: any) => void;
	onCopy?: (value: string) => void;
	onPaste?: (value: string) => void;
	onFocus?: (event: any, editor?: AceEditorClass) => void;
	onBlur?: (event: any, editor?: AceEditorClass) => void;
	onScroll?: (editor: IEditorProps) => void;
	editorProps?: IEditorProps;
	setOptions?: IAceOptions;
	keyboardHandler?: string;
	commands?: ICommand[];
	annotations?: Annotation[];
	markers?: IMarker[];
}

export class ReactAce extends React.Component<IAceEditorProps> {
	public static propTypes: PropTypes.ValidationMap<IAceEditorProps> = {
		mode: PropTypes.string,
		focus: PropTypes.bool,
		theme: PropTypes.string,
		name: PropTypes.string,
		className: PropTypes.string,
		height: PropTypes.string,
		width: PropTypes.string,
		fontSize: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]),
		showGutter: PropTypes.bool,
		onChange: PropTypes.func,
		onCopy: PropTypes.func,
		onPaste: PropTypes.func,
		onFocus: PropTypes.func,
		onInput: PropTypes.func,
		onBlur: PropTypes.func,
		onScroll: PropTypes.func,
		value: PropTypes.string,
		defaultValue: PropTypes.string,
		onLoad: PropTypes.func,
		onSelectionChange: PropTypes.func,
		onCursorChange: PropTypes.func,
		onBeforeLoad: PropTypes.func,
		onValidate: PropTypes.func,
		minLines: PropTypes.number,
		maxLines: PropTypes.number,
		readOnly: PropTypes.bool,
		highlightActiveLine: PropTypes.bool,
		tabSize: PropTypes.number,
		showPrintMargin: PropTypes.bool,
		cursorStart: PropTypes.number,
		debounceChangePeriod: PropTypes.number,
		editorProps: PropTypes.object,
		setOptions: PropTypes.object,
		style: PropTypes.object,
		scrollMargin: PropTypes.array,
		annotations: PropTypes.array,
		markers: PropTypes.array,
		keyboardHandler: PropTypes.string,
		wrapEnabled: PropTypes.bool,
		enableSnippets: PropTypes.bool,
		enableBasicAutocompletion: PropTypes.oneOfType([ PropTypes.bool, PropTypes.array ]),
		enableLiveAutocompletion: PropTypes.oneOfType([ PropTypes.bool, PropTypes.array ]),
		navigateToFileEnd: PropTypes.bool,
		commands: PropTypes.array,
		placeholder: PropTypes.string
	};
	public static defaultProps: Partial<IAceEditorProps> = {
		name: 'brace-editor',
		focus: false,
		mode: '',
		theme: '',
		height: '500px',
		width: "500px",
		value: "",
		fontSize: 12,
		enableSnippets: false,
		showGutter: true,
		onChange: null,
		onPaste: null,
		onLoad: null,
		onScroll: null,
		minLines: null,
		maxLines: null,
		readOnly: false,
		highlightActiveLine: true,
		showPrintMargin: true,
		tabSize: 4,
		cursorStart: 1,
		editorProps: {},
		style: {},
		scrollMargin: [ 0, 0, 0, 0 ],
		setOptions: {},
		wrapEnabled: false,
		enableBasicAutocompletion: false,
		enableLiveAutocompletion: false,
		placeholder: null,
		navigateToFileEnd: true
	};
	public editor: AceEditorClass;
	public refEditor: HTMLElement;
	public debounce: (fn: any, delay: number) => (...args: any) => void;
	// [index: string]: any;
	public silent: boolean;
	constructor(props: IAceEditorProps) {
		super(props);
		editorEvents.forEach((method) => {
			this[method] = this[method].bind(this);
		});
		this.debounce = debounce;
	}

	public componentDidMount() {
		const {
			className,
			onBeforeLoad,
			onValidate,
			mode,
			focus,
			theme,
			fontSize,
			value,
			defaultValue,
			cursorStart,
			showGutter,
			wrapEnabled,
			showPrintMargin,
			scrollMargin = [ 0, 0, 0, 0 ],
			keyboardHandler,
			onLoad,
			commands,
			annotations,
			markers,
			placeholder
		} = this.props;

		this.editor = ace.edit(this.refEditor);

		if (onBeforeLoad) {
			onBeforeLoad(ace);
		}

		const editorProps = Object.keys(this.props.editorProps);
		for (let i = 0; i < editorProps.length; i++) {
			this.editor[editorProps[i]] = this.props.editorProps[editorProps[i]];
		}
		if (this.props.debounceChangePeriod) {
			this.onChange = this.debounce(this.onChange, this.props.debounceChangePeriod);
		}
		this.editor.renderer.setScrollMargin(scrollMargin[0], scrollMargin[1], scrollMargin[2], scrollMargin[3]);
		this.editor.getSession().setMode(`ace/mode/${mode}`);
		this.editor.setTheme(`ace/theme/${theme}`);
		this.editor.setFontSize(fontSize);
		this.editor.getSession().setValue(!defaultValue ? value : defaultValue, cursorStart);
		if (this.props.navigateToFileEnd) {
			this.editor.navigateFileEnd();
		}
		this.editor.renderer.setShowGutter(showGutter);
		this.editor.getSession().setUseWrapMode(wrapEnabled);
		this.editor.setShowPrintMargin(showPrintMargin);
		this.editor.on("focus", this.onFocus);
		this.editor.on("blur", this.onBlur);
		this.editor.on("copy", this.onCopy);
		this.editor.on("paste", this.onPaste);
		this.editor.on("change", this.onChange);
		this.editor.on("input", this.onInput);
		if (placeholder) {
			this.updatePlaceholder();
		}
		this.editor.getSession().selection.on("changeSelection", this.onSelectionChange);
		this.editor.getSession().selection.on("changeCursor", this.onCursorChange);
		if (onValidate) {
			this.editor.getSession().on("changeAnnotation", () => {
				// tslint:disable-next-line:no-shadowed-variable
				const annotations = this.editor.getSession().getAnnotations();
				this.props.onValidate(annotations);
			});
		}
		this.editor.session.on("changeScrollTop", this.onScroll);
		this.editor.getSession().setAnnotations(annotations || []);
		if (markers && markers.length > 0) {
			this.handleMarkers(markers);
		}

		// get a list of possible options to avoid 'misspelled option errors'
		const availableOptions = this.editor.$options;
		editorOptions.forEach((option) => {
			if (availableOptions.hasOwnProperty(option)) {
				this.editor.setOption(option, this.props[option]);
			} else if (this.props[option]) {
				console.warn(
					`ReactAce: editor option ${option} was activated but not found. Did you need to import a related tool or did you possibly mispell the option?`
				);
			}
		});

		this.handleOptions(this.props);

		if (Array.isArray(commands)) {
			commands.forEach((command) => {
				if (typeof command.exec === "string") {
					this.editor.commands.bindKey(command.bindKey, command.exec);
				} else {
					this.editor.commands.addCommand(command);
				}
			});
		}

		if (keyboardHandler) {
			this.editor.setKeyboardHandler("ace/keyboard/" + keyboardHandler);
		}

		if (className) {
			this.refEditor.className += " " + className;
		}

		if (onLoad) {
			onLoad(this.editor);
		}

		this.editor.resize();

		if (focus) {
			this.editor.focus();
		}
	}

	public componentDidUpdate(prevProps: IAceEditorProps) {
		const oldProps = prevProps;
		const nextProps = this.props;

		for (let i = 0; i < editorOptions.length; i++) {
			const option = editorOptions[i];
			if (nextProps[option] !== oldProps[option]) {
				this.editor.setOption(option, nextProps[option]);
			}
		}

		if (nextProps.className !== oldProps.className) {
			const appliedClasses = this.refEditor.className;
			const appliedClassesArray = appliedClasses.trim().split(" ");
			const oldClassesArray = oldProps.className.trim().split(" ");
			oldClassesArray.forEach((oldClass) => {
				const index = appliedClassesArray.indexOf(oldClass);
				appliedClassesArray.splice(index, 1);
			});
			this.refEditor.className = " " + nextProps.className + " " + appliedClassesArray.join(" ");
		}

		// First process editor value, as it may create a new session (see issue #300)
		if (this.editor && this.editor.getValue() !== nextProps.value) {
			// editor.setValue is a synchronous function call, change event is emitted before setValue return.
			this.silent = true;
			const pos = this.editor.session.selection.toJSON();
			this.editor.setValue(nextProps.value, nextProps.cursorStart);
			this.editor.session.selection.fromJSON(pos);
			this.silent = false;
		}

		if (nextProps.placeholder !== oldProps.placeholder) {
			this.updatePlaceholder();
		}
		if (nextProps.mode !== oldProps.mode) {
			this.editor.getSession().setMode("ace/mode/" + nextProps.mode);
		}
		if (nextProps.theme !== oldProps.theme) {
			this.editor.setTheme("ace/theme/" + nextProps.theme);
		}
		if (nextProps.keyboardHandler !== oldProps.keyboardHandler) {
			if (nextProps.keyboardHandler) {
				this.editor.setKeyboardHandler("ace/keyboard/" + nextProps.keyboardHandler);
			} else {
				this.editor.setKeyboardHandler(null);
			}
		}
		if (nextProps.fontSize !== oldProps.fontSize) {
			this.editor.setFontSize(nextProps.fontSize);
		}
		if (nextProps.wrapEnabled !== oldProps.wrapEnabled) {
			this.editor.getSession().setUseWrapMode(nextProps.wrapEnabled);
		}
		if (nextProps.showPrintMargin !== oldProps.showPrintMargin) {
			this.editor.setShowPrintMargin(nextProps.showPrintMargin);
		}
		if (nextProps.showGutter !== oldProps.showGutter) {
			this.editor.renderer.setShowGutter(nextProps.showGutter);
		}
		if (!isEqual(nextProps.setOptions, oldProps.setOptions)) {
			this.handleOptions(nextProps);
		}
		if (!isEqual(nextProps.annotations, oldProps.annotations)) {
			this.editor.getSession().setAnnotations(nextProps.annotations || []);
		}
		if (!isEqual(nextProps.markers, oldProps.markers) && Array.isArray(nextProps.markers)) {
			this.handleMarkers(nextProps.markers);
		}

		// this doesn't look like it works at all....
		if (!isEqual(nextProps.scrollMargin, oldProps.scrollMargin)) {
			this.handleScrollMargins(nextProps.scrollMargin);
		}

		if (prevProps.height !== this.props.height || prevProps.width !== this.props.width) {
			this.editor.resize();
		}
		if (this.props.focus && !prevProps.focus) {
			this.editor.focus();
		}
	}

	public handleScrollMargins(margins = [ 0, 0, 0, 0 ]) {
		this.editor.renderer.setScrollMargins(margins[0], margins[1], margins[2], margins[3]);
	}

	public componentWillUnmount() {
		this.editor.destroy();
		this.editor = null;
	}

	public onChange(event: any) {
		if (this.props.onChange && !this.silent) {
			const value = this.editor.getValue();
			this.props.onChange(value, event);
		}
	}

	public onSelectionChange(event: any) {
		if (this.props.onSelectionChange) {
			const value = this.editor.getSelection();
			this.props.onSelectionChange(value, event);
		}
	}
	public onCursorChange(event: any) {
		if (this.props.onCursorChange) {
			const value = this.editor.getSelection();
			this.props.onCursorChange(value, event);
		}
	}
	public onInput(event: any) {
		if (this.props.onInput) {
			this.props.onInput(event);
		}
		if (this.props.placeholder) {
			this.updatePlaceholder();
		}
	}
	public onFocus(event: any) {
		if (this.props.onFocus) {
			this.props.onFocus(event, this.editor);
		}
	}

	public onBlur(event: any) {
		if (this.props.onBlur) {
			this.props.onBlur(event, this.editor);
		}
	}

	public onCopy(text: string) {
		if (this.props.onCopy) {
			this.props.onCopy(text);
		}
	}

	public onPaste(text: string) {
		if (this.props.onPaste) {
			this.props.onPaste(text);
		}
	}

	public onScroll() {
		if (this.props.onScroll) {
			this.props.onScroll(this.editor);
		}
	}

	public handleOptions(props: IAceEditorProps) {
		const setOptions = Object.keys(props.setOptions);
		for (let y = 0; y < setOptions.length; y++) {
			this.editor.setOption(setOptions[y], props.setOptions[setOptions[y]]);
		}
	}

	public handleMarkers(markers: IMarker[]) {
		// remove foreground markers
		let currentMarkers = this.editor.getSession().getMarkers(true);
		for (const i in currentMarkers) {
			if (currentMarkers.hasOwnProperty(i)) {
				this.editor.getSession().removeMarker(currentMarkers[i].id);
			}
		}
		// remove background markers except active line marker and selected word marker
		currentMarkers = this.editor.getSession().getMarkers(false);
		for (const i in currentMarkers) {
			if (
				currentMarkers.hasOwnProperty(i) &&
				currentMarkers[i].clazz !== "ace_active-line" &&
				currentMarkers[i].clazz !== "ace_selected-word"
			) {
				this.editor.getSession().removeMarker(currentMarkers[i].id);
			}
		}
		// add new markers
		markers.forEach(({ startRow, startCol, endRow, endCol, className, type, inFront = false }) => {
			const range = new Range(startRow, startCol, endRow, endCol);
			this.editor.getSession().addMarker(range, className, type, inFront);
		});
	}

	public updatePlaceholder() {
		// Adapted from https://stackoverflow.com/questions/26695708/how-can-i-add-placeholder-text-when-the-editor-is-empty

		const editor = this.editor;
		const { placeholder } = this.props;

		const showPlaceholder = !editor.session.getValue().length;
		let node = editor.renderer.placeholderNode;
		if (!showPlaceholder && node) {
			editor.renderer.scroller.removeChild(editor.renderer.placeholderNode);
			editor.renderer.placeholderNode = null;
		} else if (showPlaceholder && !node) {
			node = editor.renderer.placeholderNode = document.createElement("div");
			node.textContent = placeholder || "";
			node.className = "ace_comment ace_placeholder";
			node.style.padding = "0 9px";
			node.style.position = "absolute";
			node.style.zIndex = "3";
			editor.renderer.scroller.appendChild(node);
		} else if (showPlaceholder && node) {
			node.textContent = placeholder;
		}
	}

	public updateRef(item: HTMLElement) {
		this.refEditor = item;
	}

	public render() {
		const { name, width, height, style } = this.props;
		const divStyle = { width, height, ...style };
		return <div ref={this.updateRef} id={name} style={divStyle} />;
	}
}

export class HcpCustomEditor extends ReactAce {
	private currentpos: any;

	private overriddenProps: any = {
		mode: "hcpgrammar",
		theme: "textmate",
		name: "UNIQUE_ID_OF_DIV",
		editorProps: {
			$blockScrolling: true
		},
		enableBasicAutocompletion: true,
		enableLiveAutocompletion: true,
		enableSnippets: true
	};

	public validateCode = () => {
		const editor = getAceInstance().edit("UNIQUE_ID_OF_DIV");
		const expression = editor.getValue();
		const hcpExpression = new HCPExpression();
		const results = hcpExpression.ValidateExpression(expression);
		const annotations = [];

		if (results.length > 0) {
			for (let i = 0; i < results.length; i++) {
				const error = results[i];
				if (!error) {
					continue;
				}

				annotations.push({
					row: error.LineNumber - 1,
					column: error.CharPositionInLine,
					text: error.Message,
					type: "error",
					raw: error.Message
				});
			}
		}
		editor.getSession().setAnnotations(annotations);
		return results;
	}

	public onChange = (event: any) => {
		super.onChange(event);
		if (event.action === "insert") {
			if (event.lines.length > 1 || (event.lines.length === 1 && event.lines[0] === " ")) {
				this.validateCode();
			}
		}
	}

	public onCursorChange = (event: any) => {
		super.onCursorChange(event);

		const editor = getAceInstance().edit("UNIQUE_ID_OF_DIV");
		const newcursorpos = editor.getCursorPosition();

		if (this.currentpos && this.currentpos.row !== newcursorpos.row) {
			this.validateCode();
		}
		this.currentpos = newcursorpos;
	}
}

export default class HcpCalcEditor extends React.Component<IAceEditorProps> {
	public render() {
		return (
			<div className="hcp-custom-calc-editor-container" style={{ width: "100%", height: "100%" }}>
				<HcpCustomEditor
					mode="hcpgrammar"
					theme="textmate"
					name="UNIQUE_ID_OF_DIV"
					editorProps={{
						$blockScrolling: true
					}}
					enableBasicAutocompletion={true}
					enableLiveAutocompletion={true}
					enableSnippets={true}
					{...this.props}
				/>
			</div>
		);
	}
}
