package it.cavallium.warppi.gui.expression.containers;

import it.cavallium.warppi.device.display.DisplayOutputDevice;
import it.cavallium.warppi.event.KeyboardEventListener;
import it.cavallium.warppi.gui.GraphicalElement;
import it.cavallium.warppi.gui.expression.Caret;
import it.cavallium.warppi.gui.expression.CaretState;
import it.cavallium.warppi.gui.expression.ExtraMenu;
import it.cavallium.warppi.gui.expression.InputContext;
import it.cavallium.warppi.gui.expression.blocks.*;
import it.cavallium.warppi.gui.expression.layouts.InputLayout;
import it.cavallium.warppi.gui.graphicengine.Renderer;
import it.cavallium.warppi.math.Function;
import it.cavallium.warppi.math.MathContext;
import it.cavallium.warppi.util.Error;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;

public abstract class InputContainer implements GraphicalElement, InputLayout {
	protected BlockContainer root;
	protected Caret caret;
	private static final float CARET_DURATION = 0.5f;
	private float caretTime;
	private boolean parsed = false;
	private ExtraMenu<?> extra;
	protected InputContext inputContext;

	public synchronized InputContext getInputContext() {
		return inputContext;
	}

	@Deprecated()
	/**
	 * Use InputContainer(InputContext) instead
	 */
	public InputContainer() {
		this(new InputContext());
	}

	public InputContainer(final InputContext ic) {
		this(ic, false);
	}

	public InputContainer(final InputContext ic, final boolean small) {
		this(ic, small, 0, 0);
	}

	public InputContainer(final InputContext ic, final boolean small, final int minWidth, final int minHeight) {
		inputContext = ic;
		root = new BlockContainer(null, small, false);
		caret = new Caret(CaretState.VISIBLE_ON, new BlockPosition(root));
	}
	
	/**
	 * Copy
	 * @param old
	 * @param ic
	 */
	protected InputContainer(InputContainer old, InputContext ic) {
		this.caretTime = old.caretTime;
		this.extra = old.extra == null ? null : old.extra.clone(null, ic);
		this.inputContext = ic;
		this.root = old.root == null ? null : old.root.clone(null, ic);
		this.caret = old.caret == null ? null : new Caret(old.caret, root);
		this.parsed = old.parsed;
	}

	public void typeChar(final char c) {
		final Block b = parseChar(c);
		typeBlock(b);
	}

	public void typeBlock(final Block b) {
		if (b != null) {
			if (root.appendBlock(caret, b)) {
				var innerPos = b.getFirstInnerPosition();
				caret.setPosition(innerPos == null ? new BlockPosition(b) : innerPos);
				root.recomputeDimensions();
			}
			closeExtra();
		}
		caretTime = 0;
		caret.turnOn();
	}

	public void typeChar(final String c) {
		typeChar(c.charAt(0));
	}

	public void del() {
		var pos = caret.getPos();
		BlockOrContainer currentElementToRecompute;
		boolean removed;
		if (pos.isBlock()) {
			var block = pos.getBlock();
			var lastInnerPosition = block.getLastInnerPosition();
			if (lastInnerPosition != null) {
				caret.setPosition(lastInnerPosition);
				currentElementToRecompute = null;
				removed = false;
			} else {
				caret.setPosition(pos.getPrevious());
				var parent = block.getParentContainer();
				parent.removeBlockUnsafe(block);
				currentElementToRecompute = parent;
				removed = true;
			}
		} else if (pos.isContainer()) {
			var container = pos.getContainer();
			if (container == root) {
				currentElementToRecompute = null;
				removed = false;
			} else if (!container.hasParent()) {
				throw new IllegalStateException();
			} else {
				var block = container.getParentBlock();
				var containers = block.getInnerContainers();
				BlockContainer prevContainer;
				if (containers == null) {
					prevContainer = null;
				} else {
					var containerIt = containers.listIterator(containers.indexOf(container));
					if (containerIt.hasPrevious()) {
						prevContainer = containerIt.previous();
					} else {
						prevContainer = null;
					}
				}
				if (prevContainer != null) {
					var lastPosition = prevContainer.getLastPosition();
                    caret.setPosition(lastPosition);
                    currentElementToRecompute = null;
                    removed = false;
                } else {
					caret.setPosition(pos.getPrevious());
					var parentContainer = block.getParentContainer();
					parentContainer.removeBlockUnsafe(block);
					currentElementToRecompute = parentContainer;
					removed = true;
				}
			}
		} else {
			throw new UnsupportedOperationException();
		}
		if (removed) {
			while (currentElementToRecompute != null) {
				currentElementToRecompute.recomputeDimensions();
				currentElementToRecompute = currentElementToRecompute.getParent();
			}
		}
		caret.turnOn();
		caretTime = 0;
		closeExtra();
	}

	public BlockReference<?> getSelectedBlock() {
		var pos = caret.getPos();
		if (pos.isBlock()) {
			return pos.getBlock().getReference();
		} else {
			var container = pos.getContainer();
			if (container.hasParent()) {
				return container.getParentBlock().getReference();
			} else {
				return null;
			}
		}
	}

	public void moveLeft() {
		var prev = caret.getPos().getPrevious();
		if (prev != null) {
			caret.setPosition(prev);
		} else {
			caret.setPosition(root.getLastPosition());
		}
		caret.turnOn();
		caretTime = 0;
		closeExtra();
	}

	public void moveRight() {
		var prev = caret.getPos().getNext();
		if (prev != null) {
			caret.setPosition(prev);
		} else {
			caret.setPosition(new BlockPosition(root));
		}
		caret.turnOn();
		caretTime = 0;
		closeExtra();
	}

	public void moveTo(final BlockPosition position) {
		caret.setPosition(position);
		caret.turnOn();
		caretTime = 0;
		closeExtra();
	}

	@Override
	public void recomputeDimensions() {
		root.recomputeDimensions();
	}

	@Override
	public int getWidth() {
		return root.getWidth();
	}

	@Override
	public int getHeight() {
		return root.getHeight();
	}

	@Override
	public int getLine() {
		return root.getLine();
	}

	/**
	 *
	 * @param delta
	 *            Time, in seconds
	 * @return true if something changed
	 */
	public boolean beforeRender(final float delta) {
		boolean somethingChanged = false;
		caretTime += delta;
		if (caretTime >= InputContainer.CARET_DURATION) {
			while (caretTime >= InputContainer.CARET_DURATION) {
				caretTime -= InputContainer.CARET_DURATION;
				caret.flipState();
				somethingChanged = true;
			}
		}

		if (extra != null) {
			somethingChanged = somethingChanged | extra.beforeRender(delta, caret);
		}

		return somethingChanged;
	}

	/**
	 *
	 * @param ge
	 *            Graphic Engine class.
	 * @param r
	 *            Graphic Renderer class of <b>ge</b>.
	 * @param x
	 *            Position relative to the window.
	 * @param y
	 *            Position relative to the window.
	 */
	public void draw(final DisplayOutputDevice ge, final Renderer r, final int x, final int y) {
		root.draw(ge, r, x, y, caret);
		if (extra != null) {
			extra.draw(ge, r, caret);
		}
	}

	public void clear() {
		caret = new Caret(CaretState.VISIBLE_ON, new BlockPosition(root));
		root.clear();
		recomputeDimensions();
	}

	public boolean isEmpty() {
		return root.getSize() == 0;
	}

	public void setParsed(final boolean parsed) {
		this.parsed = parsed;
	}

	public boolean isAlreadyParsed() {
		return parsed;
	}

	/**
	 * <strong>WARNING! DO NOT MODIFY THIS ARRAY!!!</strong>
	 *
	 * @return an arraylist representing the content
	 */
	public ObjectArrayList<Block> getContent() {
		return root.getContent();
	}

	public void toggleExtra() {
		if (extra == null) {
			final BlockReference<?> selectedBlock = getSelectedBlock();
			if (selectedBlock != null) {
				extra = selectedBlock.get().getExtraMenu();
				if (extra != null) {
					extra.open();
				}
			}
		} else {
			extra.close();
			extra = null;
		}
	}

	public void closeExtra() {
		if (extra != null) {
			extra.close();
			extra = null;
		}
	}

	public boolean isExtraOpened() {
		return extra != null;
	}

	public KeyboardEventListener getExtraKeyboardEventListener() {
		return extra;
	}

	public Function toFunction(final MathContext context) throws Error {
		return root.toFunction(context);
	}

}
