/*
 * Copyright 2007 - 2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.jailer.ui.databrowser;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * Experimental layout optimization.
 * 
 * @author Ralf Wisser
 */
public class TreeLayoutOptimizer<T> {

	public static class Node<T> {
		private final T userObject;
		private final List<Node<T>> children = new ArrayList<Node<T>>();
		int level = 0;
		Node<T> parent = null;
		double position = 0;
		
		public T getUserObject() {
			return userObject;
		}
		
		public Node(T userObject) {
			this.userObject = userObject;
		}
		
		public void addChild(Node<T> child) {
			children.add(child);
			child.parent = this;
			child.level = level + 1;
		}
		
		public double getPosition() {
			return position;
		}

		public int getLevel() {
			return level;
		}

		public List<Node<T>> getChildren() {
			return children;
		}
		
		public int getDepth() {
			int maxChildDepth = 0;
			for (Node<T> child: children) {
				maxChildDepth = Math.max(maxChildDepth, child.getDepth());
			}
			return 1 + maxChildDepth;
		}

		public int getNodesCount() {
			int count = 1;
			for (Node<T> child: children) {
				count += child.getNodesCount();
			}
			return count;
		}

		public double getCompactness() {
			double compactness = 0;
			if (children.size() > 1) {
				compactness = children.get(children.size() - 1).position - children.get(0).position;
				compactness *= compactness;
				for (Node<T> child: children) {
					compactness += child.getCompactness();
				}
			}
			return compactness;
		}
	};

	public static <T> void optimizeTreeLayout(Node<T> root) {
		int numNodes = root.getNodesCount();
		optimizeChildrenOrder(root, System.currentTimeMillis(), MAX_OPTIM_TIME_MS, numNodes);
		layoutTree(root, numNodes);
		optimizeLeafs(root);
	}
	
	private static long MAX_OPTIM_TIME_MS = 1000;
	
	private static <T> double layoutTree(Node<T> root, int numNodes) {
		double[] maxPositionPerLevel = new double[root.getDepth()];
		for (int i = 0; i < maxPositionPerLevel.length; ++i) {
			maxPositionPerLevel[i] = -1;
		}
		root.position = 0.0;
		layoutNode(root, maxPositionPerLevel);

		double sumMaxPositionSqr = 0;
		for (int i = 0; i < maxPositionPerLevel.length; ++i) {
			if (maxPositionPerLevel[i] > 0) {
				sumMaxPositionSqr += maxPositionPerLevel[i] * maxPositionPerLevel[i];
			}
		}
		return sumMaxPositionSqr / numNodes + root.getCompactness();
	}
	
	private static <T> void layoutNode(Node<T> node, double[] maxPositionPerLevel) {
		for (Node<T> child: node.children) {
			child.position = Math.max(node.position - (node.children.size() - 1) / 2.0, maxPositionPerLevel[child.level] + 1);
			maxPositionPerLevel[child.level] = child.position;
			adjustParentPosition(node, maxPositionPerLevel);
			layoutNode(child, maxPositionPerLevel);
		}
	}
	
	private static <T> void adjustParentPosition(Node<T> node, double[] maxPositionPerLevel) {
		while (node != null) {
			double parentPos = node.children.get(0).position + (maxPositionPerLevel[node.level + 1] - node.children.get(0).position) / 2.0;
			if (node.position < parentPos) {
				node.position = parentPos;
				// node.position = (node.position + parentPos) / 2.0;
				maxPositionPerLevel[node.level] = parentPos;
			} else {
				break;
			}
			node = node.parent;
		}
	}
	
	private static <T> void optimizeLeafs(Node<T> node) {
		if (node.children.isEmpty() && node.parent != null && node.position < node.parent.position) {
			int i = node.parent.children.indexOf(node);
			if (i >= 0 && i < node.parent.children.size() - 1) {
				Node<T> brother = node.parent.children.get(i + 1);
				node.position = Math.min(node.parent.position, brother.position - 1);
			}
		}
		for (int i = node.children.size() - 1; i >= 0; --i) {
			optimizeLeafs(node.children.get(i));
		}
	}
	
	private static abstract class Modification {
		public abstract void doIt();
		public abstract void undoIt();
	}
	
	private static <T> void optimizeChildrenOrder(Node<T> root, long startTime, long maxTime, int numNodes) {
		Stack<Node<T>> toVisit = new Stack<TreeLayoutOptimizer.Node<T>>();
		toVisit.push(root);
		List<Modification> modifications = new ArrayList<TreeLayoutOptimizer.Modification>();
		
		while (!toVisit.isEmpty()) {
			final Node<T> node = toVisit.pop();
			for (Node<T> child: node.children) {
				toVisit.push(child);
			}
			for (int i = 0; i < node.children.size(); ++i) {
				for (int j = i + 1; j < node.children.size(); ++j) {
					final int a = i;
					final int b = j;
					modifications.add(new Modification() {
						@Override
						public void doIt() {
							Node<T> h = node.children.get(a);
							node.children.set(a, node.children.get(b));
							node.children.set(b, h);
						}
						@Override
						public void undoIt() {
							doIt();
						}
					});
				}
				
			}
		}
		
		// add modification pairs
		List<Modification> modificationPairs = new ArrayList<TreeLayoutOptimizer.Modification>();
		int l = modifications.size();
		int countDown = Math.max(100, 10 * l);
		for (int i = 0; i < l; ++i) {
			for (int j = i + 1; j < l; ++j) {
				final Modification a = modifications.get(i);
				final Modification b = modifications.get(j);
				modificationPairs.add(new Modification() {
					@Override
					public void doIt() {
						a.doIt();
						b.doIt();
					}
					@Override
					public void undoIt() {
						b.undoIt();
						a.undoIt();
					}
				});
				--countDown;
				if (countDown < 0) {
					break;
				}
			}
			if (countDown < 0) {
				break;
			}
		}
		modifications.addAll(modificationPairs);
		
		double currentMaxPosition = layoutTree(root, numNodes);
//		int steps = 0;
		while (System.currentTimeMillis() - startTime < maxTime) {
			Modification bestModification = null;
			double bestMaxPosition = 0;
			for (Modification modification: modifications) {
				modification.doIt();
				double maxPosition = layoutTree(root, numNodes);
				if (maxPosition < currentMaxPosition) {
					if (bestModification == null || maxPosition < bestMaxPosition) {
						bestMaxPosition = maxPosition;
						bestModification = modification;
					}
				}
				modification.undoIt();
//				++steps;
			}
			if (bestModification != null) {
				bestModification.doIt();
				currentMaxPosition = bestMaxPosition;
			} else {
				break;
			}
		}
		
//		System.out.println(System.currentTimeMillis() - startTime + " " + steps + " " + modifications.size());
	}
	
}
