package com.baselet.element;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;

import com.baselet.control.SharedUtils;
import com.baselet.control.basics.geom.Rectangle;
import com.baselet.control.constants.SharedConstants;
import com.baselet.element.sticking.PointChange;
import com.baselet.element.sticking.Stickable;

public class UndoInformation {

	private final Rectangle diffRect;
	private final Map<Stickable, List<PointChange>> stickableMoves;
	private final String oldAdditionalAttributes;
	private final String newAdditionalAttributes;

	private UndoInformation(Rectangle diffRect, Map<Stickable, List<PointChange>> stickableMoves, String oldAdditionalAttributes, String newAdditionalAttributes) {
		this.diffRect = diffRect;
		this.stickableMoves = stickableMoves;
		this.oldAdditionalAttributes = oldAdditionalAttributes;
		this.newAdditionalAttributes = newAdditionalAttributes;
	}

	public UndoInformation(Rectangle newRect, Rectangle oldRect, Map<Stickable, List<PointChange>> stickableMoves, int gridSize, String oldAdditionalAttributes, String newAdditionalAttributes) {
		this(toMinZoom(newRect.subtract(oldRect), gridSize), stickableMoves, oldAdditionalAttributes, newAdditionalAttributes);
	}

	private static Map<Stickable, List<PointChange>> invertStickableMoves(Map<Stickable, List<PointChange>> stickableMoves) {
		Map<Stickable, List<PointChange>> invertedMap = new HashMap<Stickable, List<PointChange>>();
		for (Entry<Stickable, List<PointChange>> entry : stickableMoves.entrySet()) {
			List<PointChange> invList = new ArrayList<PointChange>();
			for (PointChange p : entry.getValue()) {
				invList.add(new PointChange(p.getIndex(), -p.getDiffX(), -p.getDiffY()));
			}
			invertedMap.put(entry.getKey(), invList);
		}
		return invertedMap;
	}

	public Rectangle getDiffRectangle(int gridSize, boolean undo) {
		Rectangle returnRect = undo ? diffRect.copyInverted() : diffRect;
		return toCurrentZoom(returnRect, gridSize);
	}

	public Map<Stickable, List<PointChange>> getStickableMoves(boolean undo) {
		return undo ? invertStickableMoves(stickableMoves) : stickableMoves;
	}

	public String getAdditionalAttributes(boolean undo) {
		if (undo) {
			return oldAdditionalAttributes;
		}
		else {
			return newAdditionalAttributes;
		}
	}

	private static Rectangle toMinZoom(Rectangle rectangle, int gridSize) {
		int xBefore = toMinZoom(rectangle.getX(), gridSize);
		int yBefore = toMinZoom(rectangle.getY(), gridSize);
		int wBefore = toMinZoom(rectangle.getWidth(), gridSize);
		int hBefore = toMinZoom(rectangle.getHeight(), gridSize);
		return new Rectangle(xBefore, yBefore, wBefore, hBefore);
	}

	private static int toMinZoom(int val, int gridSize) {
		return val / gridSize;
	}

	private static Rectangle toCurrentZoom(Rectangle rectangle, int gridSize) {
		int xBefore = toCurrentZoom(rectangle.getX(), gridSize);
		int yBefore = toCurrentZoom(rectangle.getY(), gridSize);
		int wBefore = toCurrentZoom(rectangle.getWidth(), gridSize);
		int hBefore = toCurrentZoom(rectangle.getHeight(), gridSize);
		return new Rectangle(xBefore, yBefore, wBefore, hBefore);
	}

	private static int toCurrentZoom(int val, int gridSize) {
		return val * gridSize;
	}

	public UndoInformation merge(UndoInformation other) {
		Rectangle mergedUndoDiffRect = diffRect.add(other.diffRect);
		Map<Stickable, List<PointChange>> mergedMap = new HashMap<Stickable, List<PointChange>>();
		mergeStickableMoves(mergedMap, stickableMoves);
		mergeStickableMoves(mergedMap, other.stickableMoves);
		return new UndoInformation(mergedUndoDiffRect, mergedMap, other.oldAdditionalAttributes, newAdditionalAttributes);

	}

	private void mergeStickableMoves(Map<Stickable, List<PointChange>> targetMap, Map<Stickable, List<PointChange>> sourceMap) {
		for (Entry<Stickable, List<PointChange>> sourceEntry : sourceMap.entrySet()) {
			Stickable sourceStickable = sourceEntry.getKey();
			List<PointChange> sourceChangeList = sourceEntry.getValue();
			List<PointChange> targetPointChanges = targetMap.get(sourceStickable);
			if (targetPointChanges == null) { // stickable was not moved before in targetMap
				targetMap.put(sourceStickable, sourceChangeList);
			}
			else { // stickable was already moved and these moves must be merged
				mergeSourceToTarget(sourceChangeList, targetPointChanges);
			}
		}
	}

	private void mergeSourceToTarget(List<PointChange> sourceChangeList, List<PointChange> targetPointChanges) {
		for (PointChange sourceChange : sourceChangeList) {
			mergePoint(targetPointChanges, sourceChange);
		}
	}

	private void mergePoint(List<PointChange> targetPointChanges, PointChange sourceChange) {
		for (ListIterator<PointChange> iter = targetPointChanges.listIterator(); iter.hasNext();) {
			PointChange targetChange = iter.next();
			if (sourceChange.getIndex().equals(targetChange.getIndex())) {
				iter.set(new PointChange(targetChange.getIndex(), sourceChange.getDiffX() + targetChange.getDiffX(), sourceChange.getDiffY() + targetChange.getDiffY()));
				return; // index already in targetList and successfully updated
			}
		}
		targetPointChanges.add(sourceChange); // index not in targetList, therefore added here
	}

	public String toString(boolean undo) {
		return "UndoInformation [diffRect=" + getDiffRectangle(SharedConstants.DEFAULT_GRID_SIZE, undo) + ", stickableMoves=" + SharedUtils.mapToString(getStickableMoves(undo)) + ", additionalAttributes=" + getAdditionalAttributes(undo) + "]";
	}

}
