/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * 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 com.badlogic.gdx.graphics.g3d.particles;

import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.reflect.ArrayReflection;

/** This class represents an group of elements like an array, but the properties of the elements are stored as separate arrays.
 * These arrays are called {@link Channel} and are represented by {@link ChannelDescriptor}. It's not necessary to store primitive
 * types in the channels but doing so will "exploit" data locality in the JVM, which is ensured for primitive types. Use
 * {@link FloatChannel}, {@link IntChannel}, {@link ObjectChannel} to store the data.
 * @author inferno */
public class ParallelArray {

	/** This class describes the content of a {@link Channel} */
	public static class ChannelDescriptor {
		public int id;
		public Class<?> type;
		public int count;

		public ChannelDescriptor (int id, Class<?> type, int count) {
			this.id = id;
			this.type = type;
			this.count = count;
		}
	}

	/** This class represents a container of values for all the elements for a given property */
	public abstract class Channel {
		public int id;
		public Object data;
		public int strideSize;

		public Channel (int id, Object data, int strideSize) {
			this.id = id;
			this.strideSize = strideSize;
			this.data = data;
		}

		public abstract void add (int index, Object... objects);

		public abstract void swap (int i, int k);

		protected abstract void setCapacity (int requiredCapacity);
	}

	/** This interface is used to provide custom initialization of the {@link Channel} data */
	public static interface ChannelInitializer<T extends Channel> {
		public void init (T channel);
	}

	public class FloatChannel extends Channel {
		public float[] data;

		public FloatChannel (int id, int strideSize, int size) {
			super(id, new float[size * strideSize], strideSize);
			this.data = (float[])super.data;
		}

		@Override
		public void add (int index, Object... objects) {
			for (int i = strideSize * size, c = i + strideSize, k = 0; i < c; ++i, ++k) {
				data[i] = (Float)objects[k];
			}
		}

		@Override
		public void swap (int i, int k) {
			float t;
			i = strideSize * i;
			k = strideSize * k;
			for (int c = i + strideSize; i < c; ++i, ++k) {
				t = data[i];
				data[i] = data[k];
				data[k] = t;
			}
		}

		@Override
		public void setCapacity (int requiredCapacity) {
			float[] newData = new float[strideSize * requiredCapacity];
			System.arraycopy(data, 0, newData, 0, Math.min(data.length, newData.length));
			super.data = data = newData;
		}
	}

	public class IntChannel extends Channel {
		public int[] data;

		public IntChannel (int id, int strideSize, int size) {
			super(id, new int[size * strideSize], strideSize);
			this.data = (int[])super.data;
		}

		@Override
		public void add (int index, Object... objects) {
			for (int i = strideSize * size, c = i + strideSize, k = 0; i < c; ++i, ++k) {
				data[i] = (Integer)objects[k];
			}
		}

		@Override
		public void swap (int i, int k) {
			int t;
			i = strideSize * i;
			k = strideSize * k;
			for (int c = i + strideSize; i < c; ++i, ++k) {
				t = data[i];
				data[i] = data[k];
				data[k] = t;
			}
		}

		@Override
		public void setCapacity (int requiredCapacity) {
			int[] newData = new int[strideSize * requiredCapacity];
			System.arraycopy(data, 0, newData, 0, Math.min(data.length, newData.length));
			super.data = data = newData;
		}
	}

	@SuppressWarnings("unchecked")
	public class ObjectChannel<T> extends Channel {
		Class<T> componentType;
		public T[] data;

		public ObjectChannel (int id, int strideSize, int size, Class<T> type) {
			super(id, ArrayReflection.newInstance(type, size * strideSize), strideSize);
			componentType = type;
			this.data = (T[])super.data;
		}

		@Override
		public void add (int index, Object... objects) {
			for (int i = strideSize * size, c = i + strideSize, k = 0; i < c; ++i, ++k) {
				this.data[i] = (T)objects[k];
			}
		}

		@Override
		public void swap (int i, int k) {
			T t;
			i = strideSize * i;
			k = strideSize * k;
			for (int c = i + strideSize; i < c; ++i, ++k) {
				t = data[i];
				data[i] = data[k];
				data[k] = t;
			}
		}

		@Override
		public void setCapacity (int requiredCapacity) {
			T[] newData = (T[])ArrayReflection.newInstance(componentType, strideSize * requiredCapacity);
			System.arraycopy(data, 0, newData, 0, Math.min(data.length, newData.length));
			super.data = data = newData;
		}
	}

	/** the channels added to the array */
	Array<Channel> arrays;
	/** the maximum amount of elements that this array can hold */
	public int capacity;
	/** the current amount of defined elements, do not change manually unless you know what you are doing. */
	public int size;

	public ParallelArray (int capacity) {
		arrays = new Array<Channel>(false, 2, Channel.class);
		this.capacity = capacity;
		size = 0;
	}

	/** Adds and returns a channel described by the channel descriptor parameter. If a channel with the same id already exists, no
	 * allocation is performed and that channel is returned. */
	public <T extends Channel> T addChannel (ChannelDescriptor channelDescriptor) {
		return addChannel(channelDescriptor, null);
	}

	/** Adds and returns a channel described by the channel descriptor parameter. If a channel with the same id already exists, no
	 * allocation is performed and that channel is returned. Otherwise a new channel is allocated and initialized with the
	 * initializer. */
	public <T extends Channel> T addChannel (ChannelDescriptor channelDescriptor, ChannelInitializer<T> initializer) {
		T channel = getChannel(channelDescriptor);
		if (channel == null) {
			channel = allocateChannel(channelDescriptor);
			if (initializer != null) initializer.init(channel);
			arrays.add(channel);
		}
		return channel;
	}

	@SuppressWarnings({"unchecked", "rawtypes"})
	private <T extends Channel> T allocateChannel (ChannelDescriptor channelDescriptor) {
		if (channelDescriptor.type == float.class) {
			return (T)new FloatChannel(channelDescriptor.id, channelDescriptor.count, capacity);
		} else if (channelDescriptor.type == int.class) {
			return (T)new IntChannel(channelDescriptor.id, channelDescriptor.count, capacity);
		} else {
			return (T)new ObjectChannel(channelDescriptor.id, channelDescriptor.count, capacity, channelDescriptor.type);
		}
	}

	/** Removes the channel with the given id */
	public <T> void removeArray (int id) {
		arrays.removeIndex(findIndex(id));
	}

	private int findIndex (int id) {
		for (int i = 0; i < arrays.size; ++i) {
			Channel array = arrays.items[i];
			if (array.id == id) return i;
		}
		return -1;
	}

	/** Adds an element considering the values in the same order as the current channels in the array. The n_th value must have the
	 * same type and stride of the given channel at position n */
	public void addElement (Object... values) {
		/* FIXME make it grow... */
		if (size == capacity) throw new GdxRuntimeException("Capacity reached, cannot add other elements");

		int k = 0;
		for (Channel strideArray : arrays) {
			strideArray.add(k, values);
			k += strideArray.strideSize;
		}
		++size;
	}

	/** Removes the element at the given index and swaps it with the last available element */
	public void removeElement (int index) {
		int last = size - 1;
		// Swap
		for (Channel strideArray : arrays) {
			strideArray.swap(index, last);
		}
		size = last;
	}

	/** @return the channel with the same id as the one in the descriptor */
	@SuppressWarnings("unchecked")
	public <T extends Channel> T getChannel (ChannelDescriptor descriptor) {
		for (Channel array : arrays) {
			if (array.id == descriptor.id) return (T)array;
		}
		return null;
	}

	/** Removes all the channels and sets size to 0 */
	public void clear () {
		arrays.clear();
		size = 0;
	}

	/** Sets the capacity. Each contained channel will be resized to match the required capacity and the current data will be
	 * preserved. */
	public void setCapacity (int requiredCapacity) {
		if (capacity != requiredCapacity) {
			for (Channel channel : arrays) {
				channel.setCapacity(requiredCapacity);
			}
			capacity = requiredCapacity;
		}
	}

}
