/**
 *   This file is part of Skript.
 *
 *  Skript is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Skript is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Skript.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Copyright 2011-2017 Peter Güttinger and contributors
 */
package ch.njol.skript.lang;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;

import org.bukkit.event.Event;
import org.eclipse.jdt.annotation.Nullable;

import ch.njol.skript.classes.Changer.ChangeMode;
import ch.njol.skript.conditions.CondCompare;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.lang.util.SimpleLiteral;
import ch.njol.skript.util.Utils;
import ch.njol.util.Checker;
import ch.njol.util.Kleenean;
import ch.njol.util.coll.CollectionUtils;

/**
 * A list of expressions.
 * 
 * @author Peter Güttinger
 */
public class ExpressionList<T> implements Expression<T> {
	
	protected final Expression<? extends T>[] expressions;
	protected boolean and;
	private final boolean single;
	private final Class<T> returnType;
	@Nullable
	private ExpressionList<?> source;
	
	public ExpressionList(final Expression<? extends T>[] expressions, final Class<T> returnType, final boolean and) {
		this(expressions, returnType, and, null);
	}
	
	protected ExpressionList(final Expression<? extends T>[] expressions, final Class<T> returnType, final boolean and, final @Nullable ExpressionList<?> source) {
		assert expressions != null && expressions.length > 1;
		this.expressions = expressions;
		this.returnType = returnType;
		this.and = and;
		if (and) {
			single = false;
		} else {
			boolean single = true;
			for (final Expression<?> e : expressions) {
				if (!e.isSingle()) {
					single = false;
					break;
				}
			}
			this.single = single;
		}
		this.source = source;
	}
	
	@Override
	public boolean init(final Expression<?>[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parseResult) {
		throw new UnsupportedOperationException();
	}
	
	@Override
	@Nullable
	public T getSingle(final Event e) {
		if (!single)
			throw new UnsupportedOperationException();
		for (final int i : CollectionUtils.permutation(expressions.length)) {
			final T t = expressions[i].getSingle(e);
			if (t != null)
				return t;
		}
		return null;
	}
	
	@Override
	public T[] getArray(final Event e) {
		if (and)
			return getAll(e);
		for (final int i : CollectionUtils.permutation(expressions.length)) {
			final T[] t = expressions[i].getArray(e);
			if (t.length > 0)
				return t;
		}
		@SuppressWarnings("unchecked")
		final T[] r = (T[]) Array.newInstance(returnType, 0);
		assert r != null;
		return r;
	}
	
	@SuppressWarnings({"null", "unchecked"})
	@Override
	public T[] getAll(final Event e) {
		final ArrayList<T> r = new ArrayList<>();
		for (final Expression<? extends T> expr : expressions)
			r.addAll(Arrays.asList(expr.getAll(e)));
		return r.toArray((T[]) Array.newInstance(returnType, r.size()));
	}
	
	@Override
	@Nullable
	public Iterator<? extends T> iterator(final Event e) {
		if (!and) {
			for (final int i : CollectionUtils.permutation(expressions.length)) {
				final Iterator<? extends T> t = expressions[i].iterator(e);
				if (t != null && t.hasNext())
					return t;
			}
			return null;
		}
		return new Iterator<T>() {
			private int i = 0;
			@Nullable
			private Iterator<? extends T> current = null;
			
			@Override
			public boolean hasNext() {
				Iterator<? extends T> c = current;
				while (i < expressions.length && (c == null || !c.hasNext()))
					current = c = expressions[i++].iterator(e);
				return c != null && c.hasNext();
			}
			
			@Override
			public T next() {
				if (!hasNext())
					throw new NoSuchElementException();
				final Iterator<? extends T> c = current;
				if (c == null)
					throw new NoSuchElementException();
				final T t = c.next();
				assert t != null : current;
				return t;
			}
			
			@Override
			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}
	
	@Override
	public boolean isSingle() {
		return single;
	}
	
	@Override
	public boolean check(final Event e, final Checker<? super T> c, final boolean negated) {
		return negated ^ check(e, c);
	}
	
	@Override
	public boolean check(final Event e, final Checker<? super T> c) {
		for (final Expression<? extends T> expr : expressions) {
			final Boolean b = expr.check(e, c);
			if (and && !b)
				return false;
			if (!and && b)
				return true;
		}
		return and;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	@Nullable
	public <R> Expression<? extends R> getConvertedExpression(final Class<R>... to) {
		final Expression<? extends R>[] exprs = new Expression[expressions.length];
		for (int i = 0; i < exprs.length; i++)
			if ((exprs[i] = expressions[i].getConvertedExpression(to)) == null)
				return null;
		return new ExpressionList<>(exprs, (Class<R>) Utils.getSuperType(to), and, this);
	}
	
	@Override
	public Class<T> getReturnType() {
		return returnType;
	}
	
	@Override
	public boolean getAnd() {
		return and;
	}
	
	/**
	 * For use in {@link CondCompare} only.
	 * 
	 * @return The old 'and' value
	 */
	public boolean setAnd(final boolean and) {
		final boolean r = and;
		this.and = and;
		return r;
	}
	
	/**
	 * For use in {@link CondCompare} only.
	 */
	public void invertAnd() {
		and = !and;
	}
	
	@Override
	@Nullable
	public Class<?>[] acceptChange(final ChangeMode mode) {
		Class<?>[] l = expressions[0].acceptChange(mode);
		if (l == null)
			return null;
		final ArrayList<Class<?>> r = new ArrayList<>(Arrays.asList(l));
		for (int i = 1; i < expressions.length; i++) {
			l = expressions[i].acceptChange(mode);
			if (l == null)
				return null;
			r.retainAll(Arrays.asList(l));
			if (r.isEmpty())
				return null;
		}
		return r.toArray(new Class[r.size()]);
	}
	
	@Override
	public void change(final Event e, final @Nullable Object[] delta, final ChangeMode mode) throws UnsupportedOperationException {
		if (delta == null || delta.length < expressions.length) {
			for (Expression<?> expr : expressions) {
				expr.change(e, delta, mode);
			}
		} else {
			for (int i = 0; i < expressions.length; i++) {
				Expression<?> expr = expressions[i];
				expr.change(e, new Object[] {delta[i]}, mode);
			}
		}
	}
	
	private int time = 0;
	
	@Override
	public boolean setTime(final int time) {
		boolean ok = false;
		for (final Expression<?> e : expressions) {
			ok |= e.setTime(time);
		}
		if (ok)
			this.time = time;
		return ok;
	}
	
	@Override
	public int getTime() {
		return time;
	}
	
	@Override
	public boolean isDefault() {
		return false;
	}
	
	@Override
	public boolean isLoopOf(final String s) {
		for (final Expression<?> e : expressions)
			if (e.isLoopOf(s))
				return true;
		return false;
	}
	
	@Override
	public Expression<?> getSource() {
		final ExpressionList<?> s = source;
		return s == null ? this : s;
	}
	
	@Override
	public String toString(final @Nullable Event e, final boolean debug) {
		final StringBuilder b = new StringBuilder("(");
		for (int i = 0; i < expressions.length; i++) {
			if (i != 0) {
				if (i == expressions.length - 1)
					b.append(and ? " and " : " or ");
				else
					b.append(", ");
			}
			b.append(expressions[i].toString(e, debug));
		}
		b.append(")");
		if (debug)
			b.append("[").append(returnType).append("]");
		return "" + b;
	}
	
	@Override
	public String toString() {
		return toString(null, false);
	}
	
	/**
	 * @return The internal list of expressions. Can be modified with care.
	 */
	public Expression<? extends T>[] getExpressions() {
		return expressions;
	}
	
	@Override
	public Expression<T> simplify() {
		boolean isLiteralList = true;
		boolean isSimpleList = true;
		for (int i = 0; i < expressions.length; i++) {
			expressions[i] = expressions[i].simplify();
			isLiteralList &= expressions[i] instanceof Literal;
			isSimpleList &= expressions[i].isSingle();
		}
		if (isLiteralList && isSimpleList) {
			@SuppressWarnings("unchecked")
			final T[] values = (T[]) Array.newInstance(returnType, expressions.length);
			for (int i = 0; i < values.length; i++)
				values[i] = ((Literal<? extends T>) expressions[i]).getSingle();
			return new SimpleLiteral<>(values, returnType, and);
		}
		if (isLiteralList) {
			final Literal<? extends T>[] ls = Arrays.copyOf(expressions, expressions.length, Literal[].class);
			assert ls != null;
			return new LiteralList<>(ls, returnType, and);
		}
		return this;
	}
	
}
