/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.openjpa.persistence.proxy;

import java.io.PrintStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Version;

import org.apache.openjpa.persistence.DetachedState;
import org.apache.openjpa.persistence.ElementDependent;
import org.apache.openjpa.persistence.jdbc.ElementJoinColumn;
import org.apache.openjpa.persistence.jdbc.OrderColumn;

/**
 * Persistent entity for testing adding/removing elements of collection valued
 * field while in detached state.
 *
 * Node refers to a list of Nodes as children.
 *
 * Contains recursive methods to create or modify uniform subtree. Uniform
 * subtree implies that each child at a level L has equal number of
 * grand children at level L+1.
 *
 * @author Pinaki Poddar
 *
 */
@Entity
@DetachedState
public class TreeNode implements Serializable {
	
    private static final long serialVersionUID = 1L;

    @Id
	@GeneratedValue
	private long id;

	private String name;

	@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
	@ElementJoinColumn(name = "ParentID")
	@OrderColumn(name = "Sequence")
	@ElementDependent
	private List<TreeNode> childern = new ArrayList<>();

	@Version
	private int version;

	public long getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Add a child node at the end of the current list of children.
	 */
	public void addNode(TreeNode node) {
		addNode(node, childern.size());
	}

	/**
     * Insert a child node at the specified position in the list of children.
	 */
	public void addNode(TreeNode node, int position) {
		checkSequenceRange(position);
		childern.add(position, node);
	}

	public boolean removeNode(TreeNode node) {
		return childern.remove(node);
	}

	public TreeNode removeNode(int sequence) {
		checkSequenceRange(sequence);
		return childern.remove(sequence);
	}

	public TreeNode getNode(int sequence) {
		checkSequenceRange(sequence);
		return childern.get(sequence);
	}

	public List<TreeNode> getNodes() {
		return childern;
	}

	public void clearNodes() {
		childern.clear();
	}

	public boolean isLeaf() {
		return childern.isEmpty();
	}

	protected void checkSequenceRange(int sequence)
			throws IllegalArgumentException {
		int size = childern.size();
		if (sequence < 0 || sequence > size)
            throw new IllegalArgumentException("Sequence number is beyond "
					+ "range of 0 to " + size + ".");
	}

	public int getVersion() {
		return version;
	}

	/**
     * Create a uniform subtree below the receiver. Uniform subtree implies that
     * each child at a level L has equal number of grand children at level L+1.
	 *
	 * @param fanOuts
	 *            array of fan outs for children at every level.
	 */
	public void createTree(int[] fanOuts) {
		if (fanOuts.length == 0)
			return;
		int[] nextFanOuts = new int[fanOuts.length];
        System.arraycopy(fanOuts, 1, nextFanOuts, 0, fanOuts.length - 1);
		for (int j = 0; j < fanOuts[0]; j++) {
			TreeNode child = new TreeNode();
			child.setName(getName() + "." + j);
			addNode(child);
			child.createTree(nextFanOuts);
		}
	}

	/**
	 * Add or remove subtree of the receiver to match the given fanOut.
	 */
	public void modify(int[] fanOuts) {
		if (fanOuts == null || fanOuts.length == 0)
			return;
		int n = fanOuts[0];
		int[] nextFanOuts = new int[fanOuts.length];
        System.arraycopy(fanOuts, 1, nextFanOuts, 0, fanOuts.length - 1);
		List<TreeNode> children = getNodes();
		int diff = children.size() - n;
		if (diff < 0) {
			for (int i = 0; i < -diff; i++) {
				TreeNode newChild = new TreeNode();
				int position = getNodes().size();
				newChild.setName(getName() + "." + position);
				addNode(newChild);
			}
		} else if (diff > 0) {
			for (int i = 0; i < diff; i++) {
				int position = getNodes().size() - 1;
				removeNode(position);
			}
		}
		children = getNodes();
		for (TreeNode child : children) {
			child.modify(nextFanOuts);
		}
	}

	/**
	 * Get the fan outs of the given receiver. Assumes that the subtree is
	 * uniform. Otherwise throws exception.
	 */
	public int[] getFanOuts() {
		return getFanOuts(new int[] {});
	}

	private int[] getFanOuts(int[] list) {
		List<TreeNode> children = getNodes();
		if (children.isEmpty())
			return list;
		int[] fanOuts = new int[children.size()];
		int i = 0;
		for (TreeNode child : children) {
			fanOuts[i++] = child.getNodes().size();
		}
		for (int j = 0; j < fanOuts.length - 1; j++)
			if (fanOuts[j] != fanOuts[j + 1])
                throw new RuntimeException("non-uniform fanouts for children "
						+ " of " + getName());

		int[] newList = new int[list.length + 1];
		System.arraycopy(list, 0, newList, 0, list.length);
		newList[list.length] = children.size();
		return children.get(0).getFanOuts(newList);
	}

	/**
	 * Prints this receiver and its subtree.
	 */
	public void print(PrintStream out) {
		print(2, out);
	}

	private void print(int tab, PrintStream out) {
		for (int i = 0; i < tab; i++)
			out.print(" ");
		out.println(getName());
		for (TreeNode child : getNodes()) {
			child.print(tab + 2, out);
		}
	}

}
