import * as THREE from 'three';
import * as Nodes from 'three/nodes';
import { Canvas, CircleMenu, ButtonInput, StringInput, ContextMenu, Tips, Search, Loader, Node, TreeViewNode, TreeViewInput, Element } from 'flow';
import { FileEditor } from './editors/FileEditor.js';
import { exportJSON } from './NodeEditorUtils.js';
import { init, ClassLib, getNodeEditorClass, getNodeList } from './NodeEditorLib.js';
import { SplitscreenManager } from './SplitscreenManager.js';

init();

Element.icons.unlink = 'ti ti-unlink';

export class NodeEditor extends THREE.EventDispatcher {

	constructor( scene = null, renderer = null, composer = null ) {

		super();

		const domElement = document.createElement( 'flow' );
		const canvas = new Canvas();

		domElement.append( canvas.dom );

		this.scene = scene;
		this.renderer = renderer;

		const { global } = Nodes;

		global.set( 'THREE', THREE );
		global.set( 'TSL', Nodes );

		global.set( 'scene', scene );
		global.set( 'renderer', renderer );
		global.set( 'composer', composer );

		this.nodeClasses = [];

		this.canvas = canvas;
		this.domElement = domElement;

		this._preview = false;
		this._splitscreen = false;

		this.search = null;

		this.menu = null;
		this.previewMenu = null;

		this.nodesContext = null;
		this.examplesContext = null;

		this._initSplitview();
		this._initUpload();
		this._initTips();
		this._initMenu();
		this._initSearch();
		this._initNodesContext();
		this._initExamplesContext();
		this._initShortcuts();
		this._initParams();
	}

	setSize( width, height ) {

		this.canvas.setSize( width, height );

		return this;

	}

	centralizeNode( node ) {

		const canvas = this.canvas;
		const nodeRect = node.dom.getBoundingClientRect();

		node.setPosition(
			( ( canvas.width / 2 ) - canvas.scrollLeft ) - nodeRect.width,
			( ( canvas.height / 2 ) - canvas.scrollTop ) - nodeRect.height
		);

		return this;

	}

	add( node ) {

		const onRemove = () => {

			node.removeEventListener( 'remove', onRemove );

			node.setEditor( null );

		};

		node.setEditor( this );
		node.addEventListener( 'remove', onRemove );

		this.canvas.add( node );

		this.dispatchEvent( { type: 'add', node } );

		return this;

	}

	get nodes() {

		return this.canvas.nodes;

	}

	set preview( value ) {

		if ( this._preview === value ) return;

		if ( value ) {

			this._wasSplitscreen = this.splitscreen;

			this.splitscreen = false;

			this.menu.dom.remove();
			this.canvas.dom.remove();
			this.search.dom.remove();

			this.domElement.append( this.previewMenu.dom );

		} else {

			this.canvas.focusSelected = false;

			this.domElement.append( this.menu.dom );
			this.domElement.append( this.canvas.dom );
			this.domElement.append( this.search.dom );

			this.previewMenu.dom.remove();

			if ( this._wasSplitscreen == true ) {

				this.splitscreen = true;

			}

		}

		this._preview = value;

	}

	get preview() {

		return this._preview;

	}

	set splitscreen( value ) {

		if ( this._splitscreen === value ) return;

		this.splitview.setSplitview( value );

		this._splitscreen = value;

	}

	get splitscreen() {

		return this._splitscreen;

	}

	newProject() {

		const canvas = this.canvas;
		canvas.clear();
		canvas.scrollLeft = 0;
		canvas.scrollTop = 0;
		canvas.zoom = 1;

		this.dispatchEvent( { type: 'new' } );

	}

	async loadURL( url ) {

		const loader = new Loader( Loader.OBJECTS );
		const json = await loader.load( url, ClassLib );

		this.loadJSON( json );

	}

	loadJSON( json ) {

		const canvas = this.canvas;

		canvas.clear();

		canvas.deserialize( json );

		for ( const node of canvas.nodes ) {

			this.add( node );

		}

		this.dispatchEvent( { type: 'load' } );

	}

	_initSplitview() {

		this.splitview = new SplitscreenManager( this );

	}

	_initUpload() {

		const canvas = this.canvas;

		canvas.onDrop( () => {

			for ( const item of canvas.droppedItems ) {

				const { relativeClientX, relativeClientY } = canvas;

				const file = item.getAsFile();
				const reader = new FileReader();

				reader.onload = () => {

					const fileEditor = new FileEditor( reader.result, file.name );

					fileEditor.setPosition(
						relativeClientX - ( fileEditor.getWidth() / 2 ),
						relativeClientY - 20
					);

					this.add( fileEditor );

				};

				reader.readAsArrayBuffer( file );

			}

		} );

	}

	_initTips() {

		this.tips = new Tips();

		this.domElement.append( this.tips.dom );

	}

	_initMenu() {

		const menu = new CircleMenu();
		const previewMenu = new CircleMenu();

		menu.setAlign( 'top left' );
		previewMenu.setAlign( 'top left' );

		const previewButton = new ButtonInput().setIcon( 'ti ti-brand-threejs' ).setToolTip( 'Preview' );
		const splitscreenButton = new ButtonInput().setIcon( 'ti ti-layout-sidebar-right-expand' ).setToolTip( 'Splitscreen' );
		const menuButton = new ButtonInput().setIcon( 'ti ti-apps' ).setToolTip( 'Add' );
		const examplesButton = new ButtonInput().setIcon( 'ti ti-file-symlink' ).setToolTip( 'Examples' );
		const newButton = new ButtonInput().setIcon( 'ti ti-file' ).setToolTip( 'New' );
		const openButton = new ButtonInput().setIcon( 'ti ti-upload' ).setToolTip( 'Open' );
		const saveButton = new ButtonInput().setIcon( 'ti ti-download' ).setToolTip( 'Save' );

		const editorButton = new ButtonInput().setIcon( 'ti ti-subtask' ).setToolTip( 'Editor' );

		previewButton.onClick( () => this.preview = true );
		editorButton.onClick( () => this.preview = false );

		splitscreenButton.onClick( () => {

			this.splitscreen = ! this.splitscreen;
			splitscreenButton.setIcon( this.splitscreen ? 'ti ti-layout-sidebar-right-collapse' : 'ti ti-layout-sidebar-right-expand' );

		});

		menuButton.onClick( () => this.nodesContext.open() );
		examplesButton.onClick( () => this.examplesContext.open() );

		newButton.onClick( () => {

			if ( confirm( 'Are you sure?' ) === true ) {

				this.newProject();

			}

		} );

		openButton.onClick( () => {

			const input = document.createElement( 'input' );
			input.type = 'file';

			input.onchange = e => {

				const file = e.target.files[ 0 ];

				const reader = new FileReader();
				reader.readAsText( file, 'UTF-8' );

				reader.onload = readerEvent => {

					const loader = new Loader( Loader.OBJECTS );
					const json = loader.parse( JSON.parse( readerEvent.target.result ), ClassLib );

					this.loadJSON( json );

				};

			};

			input.click();

		} );

		saveButton.onClick( () => {

			exportJSON( this.canvas.toJSON(), 'node_editor' );

		} );

		menu.add( previewButton )
			.add( splitscreenButton )
			.add( newButton )
			.add( examplesButton )
			.add( openButton )
			.add( saveButton )
			.add( menuButton );

		previewMenu.add( editorButton );

		this.domElement.appendChild( menu.dom );

		this.menu = menu;
		this.previewMenu = previewMenu;

	}

	_initExamplesContext() {

		const context = new ContextMenu();

		//**************//
		// MAIN
		//**************//

		const onClickExample = async ( button ) => {

			this.examplesContext.hide();

			const filename = button.getExtra();

			this.loadURL( `./examples/${filename}.json` );

		};

		const addExamples = ( category, names ) => {

			const subContext = new ContextMenu();

			for ( const name of names ) {

				const filename = name.replaceAll( ' ', '-' ).toLowerCase();

				subContext.add( new ButtonInput( name )
					.setIcon( 'ti ti-file-symlink' )
					.onClick( onClickExample )
					.setExtra( category.toLowerCase() + '/' + filename )
				);

			}

			context.add( new ButtonInput( category ), subContext );

			return subContext;

		};

		//**************//
		// EXAMPLES
		//**************//

		addExamples( 'Basic', [
			'Teapot',
			'Matcap',
			'Fresnel',
			'Particles'
		] );

		this.examplesContext = context;

	}

	_initShortcuts() {

		document.addEventListener( 'keydown', ( e ) => {

			if ( e.target === document.body ) {

				const key = e.key;

				if ( key === 'Tab' ) {

					this.search.inputDOM.focus();

					e.preventDefault();
					e.stopImmediatePropagation();

				} else if ( key === ' ' ) {

					this.preview = ! this.preview;

				} else if ( key === 'Delete' ) {

					if ( this.canvas.selected ) this.canvas.selected.dispose();

				} else if ( key === 'Escape' ) {

					this.canvas.select( null );

				}

			}

		} );

	}

	_initParams() {

		const urlParams = new URLSearchParams( window.location.search );

		const example = urlParams.get( 'example' ) || 'basic/teapot';

		this.loadURL( `./examples/${example}.json` );

	}

	addClass( nodeData ) {

		this.removeClass( nodeData );

		this.nodeClasses.push( nodeData );

		ClassLib[ nodeData.name ] = nodeData.nodeClass;

		return this;

	}

	removeClass( nodeData ) {

		const index = this.nodeClasses.indexOf( nodeData );

		if ( index !== - 1 ) {

			this.nodeClasses.splice( index, 1 );

			delete ClassLib[ nodeData.name ];

		}

		return this;

	}

	_initSearch() {

		const traverseNodeEditors = ( item ) => {

			if ( item.children ) {

				for ( const subItem of item.children ) {

					traverseNodeEditors( subItem );

				}

			} else {

				const button = new ButtonInput( item.name );
				button.setIcon( `ti ti-${item.icon}` );
				button.addEventListener( 'complete', async () => {

					const nodeClass = await getNodeEditorClass( item );

					const node = new nodeClass();

					this.add( node );

					this.centralizeNode( node );
					this.canvas.select( node );

				} );

				search.add( button );

				if ( item.tags !== undefined ) {

					search.setTag( button, item.tags );

				}

			}



		};

		const search = new Search();
		search.forceAutoComplete = true;

		search.onFilter( async () => {

			search.clear();

			const nodeList = await getNodeList();

			for ( const item of nodeList.nodes ) {

				traverseNodeEditors( item );

			}

			for ( const item of this.nodeClasses ) {

				traverseNodeEditors( item );

			}

		} );

		search.onSubmit( () => {

			if ( search.currentFiltered !== null ) {

				search.currentFiltered.button.dispatchEvent( new Event( 'complete' ) );

			}

		} );

		this.search = search;

		this.domElement.append( search.dom );

	}

	async _initNodesContext() {

		const context = new ContextMenu( this.canvas.canvas ).setWidth( 300 );

		let isContext = false;
		const contextPosition = {};

		const add = ( node ) => {

			context.hide();

			this.add( node );

			if ( isContext ) {

				node.setPosition(
					Math.round( contextPosition.x ),
					Math.round( contextPosition.y )
				);

			} else {

				this.centralizeNode( node );

			}

			this.canvas.select( node );

			isContext = false;

		};

		context.onContext( () => {

			isContext = true;

			const { relativeClientX, relativeClientY } = this.canvas;

			contextPosition.x = Math.round( relativeClientX );
			contextPosition.y = Math.round( relativeClientY );

		} );

		context.addEventListener( 'show', () => {

			reset();
			focus();

		} );

		//**************//
		// INPUTS
		//**************//

		const nodeButtons = [];

		let nodeButtonsVisible = [];
		let nodeButtonsIndex = - 1;

		const focus = () => requestAnimationFrame( () => search.inputDOM.focus() );
		const reset = () => {

			search.setValue( '', false );

			for ( const button of nodeButtons ) {

				button.setOpened( false ).setVisible( true ).setSelected( false );

			}

		};

		const node = new Node();
		context.add( node );

		const search = new StringInput().setPlaceHolder( 'Search...' ).setIcon( 'ti ti-list-search' );

		search.inputDOM.addEventListener( 'keydown', e => {

			const key = e.key;

			if ( key === 'ArrowDown' ) {

				const previous = nodeButtonsVisible[ nodeButtonsIndex ];
				if ( previous ) previous.setSelected( false );

				const current = nodeButtonsVisible[ nodeButtonsIndex = ( nodeButtonsIndex + 1 ) % nodeButtonsVisible.length ];
				if ( current ) current.setSelected( true );

				e.preventDefault();
				e.stopImmediatePropagation();

			} else if ( key === 'ArrowUp' ) {

				const previous = nodeButtonsVisible[ nodeButtonsIndex ];
				if ( previous ) previous.setSelected( false );

				const current = nodeButtonsVisible[ nodeButtonsIndex > 0 ? -- nodeButtonsIndex : ( nodeButtonsIndex = nodeButtonsVisible.length - 1 ) ];
				if ( current ) current.setSelected( true );

				e.preventDefault();
				e.stopImmediatePropagation();

			} else if ( key === 'Enter' ) {

				if ( nodeButtonsVisible[ nodeButtonsIndex ] !== undefined ) {

					nodeButtonsVisible[ nodeButtonsIndex ].dom.click();

				} else {

					context.hide();

				}

				e.preventDefault();
				e.stopImmediatePropagation();

			} else if ( key === 'Escape' ) {

				context.hide();

			}

		} );

		search.onChange( () => {

			const value = search.getValue().toLowerCase();

			if ( value.length === 0 ) return reset();

			nodeButtonsVisible = [];
			nodeButtonsIndex = 0;

			for ( const button of nodeButtons ) {

				const buttonLabel = button.getLabel().toLowerCase();

				button.setVisible( false ).setSelected( false );

				const visible = buttonLabel.indexOf( value ) !== - 1;

				if ( visible && button.children.length === 0 ) {

					nodeButtonsVisible.push( button );

				}

			}

			for ( const button of nodeButtonsVisible ) {

				let parent = button;

				while ( parent !== null ) {

					parent.setOpened( true ).setVisible( true );

					parent = parent.parent;

				}

			}

			if ( nodeButtonsVisible[ nodeButtonsIndex ] !== undefined ) {

				nodeButtonsVisible[ nodeButtonsIndex ].setSelected( true );

			}

		} );

		const treeView = new TreeViewInput();
		node.add( new Element().setHeight( 30 ).add( search ) );
		node.add( new Element().setHeight( 200 ).add( treeView ) );

		const addNodeEditorElement = ( nodeData ) => {

			const button = new TreeViewNode( nodeData.name );
			button.setIcon( `ti ti-${nodeData.icon}` );

			if ( nodeData.children === undefined ) {

				button.isNodeClass = true;
				button.onClick( async () => {

					const nodeClass = await getNodeEditorClass( nodeData );

					add( new nodeClass() );

				} );

			}

			if ( nodeData.tip ) {

				//button.setToolTip( item.tip );

			}

			nodeButtons.push( button );

			if ( nodeData.children ) {

				for ( const subItem of nodeData.children ) {

					const subButton = addNodeEditorElement( subItem );

					button.add( subButton );

				}

			}

			return button;

		};

		//

		const nodeList = await getNodeList();

		for ( const node of nodeList.nodes ) {

			const button = addNodeEditorElement( node );

			treeView.add( button );

		}

		this.nodesContext = context;

	}

}
