/*******************************************************************************
 * 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;

import java.util.Comparator;
import java.util.Iterator;

import com.badlogic.gdx.utils.Array;

public class Attributes implements Iterable<Attribute>, Comparator<Attribute>, Comparable<Attributes> {
	protected long mask;
	protected final Array<Attribute> attributes = new Array<Attribute>();

	protected boolean sorted = true;

	/** Sort the attributes by their ID */
	public final void sort () {
		if (!sorted) {
			attributes.sort(this);
			sorted = true;
		}
	}

	/** @return Bitwise mask of the ID's of all the containing attributes */
	public final long getMask () {
		return mask;
	}

	/** Example usage: ((BlendingAttribute)material.get(BlendingAttribute.ID)).sourceFunction;
	 * @return The attribute (which can safely be cast) if any, otherwise null */
	public final Attribute get (final long type) {
		if (has(type)) for (int i = 0; i < attributes.size; i++)
			if (attributes.get(i).type == type) return attributes.get(i);
		return null;
	}

	/** Example usage: ((BlendingAttribute)material.get(BlendingAttribute.ID)).sourceFunction;
	 * @return The attribute if any, otherwise null */
	public final <T extends Attribute> T get (Class<T> clazz, final long type) {
		return (T)get(type);
	}

	/** Get multiple attributes at once. Example: material.get(out, ColorAttribute.Diffuse | ColorAttribute.Specular |
	 * TextureAttribute.Diffuse); */
	public final Array<Attribute> get (final Array<Attribute> out, final long type) {
		for (int i = 0; i < attributes.size; i++)
			if ((attributes.get(i).type & type) != 0) out.add(attributes.get(i));
		return out;
	}

	/** Removes all attributes */
	public void clear () {
		mask = 0;
		attributes.clear();
	}

	/** @return The amount of attributes this material contains. */
	public int size () {
		return attributes.size;
	}

	private final void enable (final long mask) {
		this.mask |= mask;
	}

	private final void disable (final long mask) {
		this.mask &= ~mask;
	}

	/** Add a attribute to this material. If the material already contains an attribute of the same type it is overwritten. */
	public final void set (final Attribute attribute) {
		final int idx = indexOf(attribute.type);
		if (idx < 0) {
			enable(attribute.type);
			attributes.add(attribute);
			sorted = false;
		} else {
			attributes.set(idx, attribute);
		}
		sort(); //FIXME: See #4186
	}

	/** Add multiple attributes to this material. If the material already contains an attribute of the same type it is overwritten. */
	public final void set (final Attribute attribute1, final Attribute attribute2) {
		set(attribute1);
		set(attribute2);
	}

	/** Add multiple attributes to this material. If the material already contains an attribute of the same type it is overwritten. */
	public final void set (final Attribute attribute1, final Attribute attribute2, final Attribute attribute3) {
		set(attribute1);
		set(attribute2);
		set(attribute3);
	}

	/** Add multiple attributes to this material. If the material already contains an attribute of the same type it is overwritten. */
	public final void set (final Attribute attribute1, final Attribute attribute2, final Attribute attribute3,
		final Attribute attribute4) {
		set(attribute1);
		set(attribute2);
		set(attribute3);
		set(attribute4);
	}

	/** Add an array of attributes to this material. If the material already contains an attribute of the same type it is
	 * overwritten. */
	public final void set (final Attribute... attributes) {
		for (final Attribute attr : attributes)
			set(attr);
	}

	/** Add an array of attributes to this material. If the material already contains an attribute of the same type it is
	 * overwritten. */
	public final void set (final Iterable<Attribute> attributes) {
		for (final Attribute attr : attributes)
			set(attr);
	}

	/** Removes the attribute from the material, i.e.: material.remove(BlendingAttribute.ID); Can also be used to remove multiple
	 * attributes also, i.e. remove(AttributeA.ID | AttributeB.ID); */
	public final void remove (final long mask) {
		for (int i = attributes.size - 1; i >= 0; i--) {
			final long type = attributes.get(i).type;
			if ((mask & type) == type) {
				attributes.removeIndex(i);
				disable(type);
				sorted = false;
			}
		}
		sort(); //FIXME: See #4186
	}

	/** @return True if this collection has the specified attribute, i.e. attributes.has(ColorAttribute.Diffuse); Or when multiple
	 *         attribute types are specified, true if this collection has all specified attributes, i.e. attributes.has(out,
	 *         ColorAttribute.Diffuse | ColorAttribute.Specular | TextureAttribute.Diffuse); */
	public final boolean has (final long type) {
		return type != 0 && (this.mask & type) == type;
	}

	/** @return the index of the attribute with the specified type or negative if not available. */
	protected int indexOf (final long type) {
		if (has(type)) for (int i = 0; i < attributes.size; i++)
			if (attributes.get(i).type == type) return i;
		return -1;
	}

	/** Check if this collection has the same attributes as the other collection. If compareValues is true, it also compares the
	 * values of each attribute.
	 * @param compareValues True to compare attribute values, false to only compare attribute types
	 * @return True if this collection contains the same attributes (and optionally attribute values) as the other. */
	public final boolean same (final Attributes other, boolean compareValues) {
		if (other == this) return true;
		if ((other == null) || (mask != other.mask)) return false;
		if (!compareValues) return true;
		sort();
		other.sort();
		for (int i = 0; i < attributes.size; i++)
			if (!attributes.get(i).equals(other.attributes.get(i))) return false;
		return true;
	}

	/** See {@link #same(Attributes, boolean)}
	 * @return True if this collection contains the same attributes (but not values) as the other. */
	public final boolean same (final Attributes other) {
		return same(other, false);
	}

	/** Used for sorting attributes by type (not by value) */
	@Override
	public final int compare (final Attribute arg0, final Attribute arg1) {
		return (int)(arg0.type - arg1.type);
	}

	/** Used for iterating through the attributes */
	@Override
	public final Iterator<Attribute> iterator () {
		return attributes.iterator();
	}

	/** @return A hash code based on only the attribute values, which might be different compared to {@link #hashCode()} because the latter
	 * might include other properties as well, i.e. the material id. */
	public int attributesHash () {
		sort();
		final int n = attributes.size;
		long result = 71 + mask;
		int m = 1;
		for (int i = 0; i < n; i++)
			result += mask * attributes.get(i).hashCode() * (m = (m * 7) & 0xFFFF);
		return (int)(result ^ (result >> 32));
	}

	@Override
	public int hashCode () {
		return attributesHash();
	}

	@Override
	public boolean equals (Object other) {
		if (!(other instanceof Attributes)) return false;
		if (other == this) return true;
		return same((Attributes)other, true);
	}

	@Override
	public int compareTo (Attributes other) {
		if (other == this)
			return 0;
		if (mask != other.mask)
			return mask < other.mask ? -1 : 1;
		sort();
		other.sort();
		for (int i = 0; i < attributes.size; i++) {
			final int c = attributes.get(i).compareTo(other.attributes.get(i));
			if (c != 0)
				return c < 0 ? -1 : (c > 0 ? 1 : 0);
		}
		return 0;
	}
}
