/*
 *   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-2014 Peter Güttinger
 * 
 */

package ch.njol.skript.effects;

import java.util.Arrays;
import java.util.logging.Level;

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

import ch.njol.skript.Skript;
import ch.njol.skript.SkriptConfig;
import ch.njol.skript.classes.Changer;
import ch.njol.skript.classes.Changer.ChangeMode;
import ch.njol.skript.classes.ClassInfo;
import ch.njol.skript.doc.Description;
import ch.njol.skript.doc.Examples;
import ch.njol.skript.doc.Name;
import ch.njol.skript.doc.Since;
import ch.njol.skript.lang.Effect;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.SkriptParser;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.lang.Variable;
import ch.njol.skript.log.CountingLogHandler;
import ch.njol.skript.log.ErrorQuality;
import ch.njol.skript.log.ParseLogHandler;
import ch.njol.skript.log.SkriptLogger;
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.util.Patterns;
import ch.njol.skript.util.Utils;
import ch.njol.util.Kleenean;

/**
 * @author Peter Güttinger
 */
@Name("Change: Set/Add/Remove/Delete/Reset")
@Description("A very general effect that can change many <a href='../expressions'>expressions</a>. Many expressions can only be set and/or deleted, while some can have things added to or removed from them.")
@Examples({"# set:",
		"Set the player's display name to \"<red>%name of player%\"",
		"set the block above the victim to lava",
		"# add:",
		"add 2 to the player's health # preferably use '<a href='#heal'>heal</a>' for this",
		"add argument to {blacklist::*}",
		"give a diamond pickaxe of efficiency 5 to the player",
		"increase the data value of the clicked block by 1",
		"# remove:",
		"remove 2 pickaxes from the victim",
		"subtract 2.5 from {points.%player%}",
		"# remove all:",
		"remove every iron tool from the player",
		"remove all minecarts from {entitylist::*}",
		"# delete:",
		"delete the block below the player",
		"clear drops",
		"delete {variable}",
		"# reset:",
		"reset walk speed of player",
		"reset chunk at the targeted block"})
@Since("1.0 (set, add, remove, delete), 2.0 (remove all)")
public class EffChange extends Effect {
	private static Patterns<ChangeMode> patterns = new Patterns<ChangeMode>(new Object[][] {
			{"(add|give) %objects% to %~objects%", ChangeMode.ADD},
			{"increase %~objects% by %objects%", ChangeMode.ADD},
			{"give %~objects% %objects%", ChangeMode.ADD},
			
			{"set %~objects% to %objects%", ChangeMode.SET},
			
			{"remove (all|every) %objects% from %~objects%", ChangeMode.REMOVE_ALL},
			
			{"(remove|subtract) %objects% from %~objects%", ChangeMode.REMOVE},
			{"reduce %~objects% by %objects%", ChangeMode.REMOVE},
			
			{"(delete|clear) %~objects%", ChangeMode.DELETE},
			
			{"reset %~objects%", ChangeMode.RESET}
	});
	
	static {
		Skript.registerEffect(EffChange.class, patterns.getPatterns());
	}
	
	@SuppressWarnings("null")
	private Expression<?> changed;
	@Nullable
	private Expression<?> changer = null;
	
	@SuppressWarnings("null")
	private ChangeMode mode;
	
	private boolean single;
	
//	private Changer<?, ?> c = null;
	
	@SuppressWarnings({"unchecked", "null"})
	@Override
	public boolean init(final Expression<?>[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parser) {
		mode = patterns.getInfo(matchedPattern);
		
		switch (mode) {
			case ADD:
				if (matchedPattern == 0) {
					changer = exprs[0];
					changed = exprs[1];
				} else {
					changer = exprs[1];
					changed = exprs[0];
				}
				break;
			case SET:
				changer = exprs[1];
				changed = exprs[0];
				break;
			case REMOVE_ALL:
				changer = exprs[0];
				changed = exprs[1];
				break;
			case REMOVE:
				if (matchedPattern == 5) {
					changer = exprs[0];
					changed = exprs[1];
				} else {
					changer = exprs[1];
					changed = exprs[0];
				}
				break;
			case DELETE:
				changed = exprs[0];
				break;
			case RESET:
				changed = exprs[0];
		}
		
		final CountingLogHandler h = SkriptLogger.startLogHandler(new CountingLogHandler(Level.SEVERE));
		final Class<?>[] rs;
		final String what;
		try {
			rs = changed.acceptChange(mode);
			final ClassInfo<?> c = Classes.getSuperClassInfo(changed.getReturnType());
			final Changer<?> changer = c.getChanger();
			what = changer == null || !Arrays.equals(changer.acceptChange(mode), rs) ? changed.toString(null, false) : c.getName().withIndefiniteArticle();
		} finally {
			h.stop();
		}
		if (rs == null) {
			if (h.getCount() > 0)
				return false;
			switch (mode) {
				case SET:
					Skript.error(what + " can't be set to anything", ErrorQuality.SEMANTIC_ERROR);
					break;
				case DELETE:
					if (changed.acceptChange(ChangeMode.RESET) != null)
						Skript.error(what + " can't be deleted/cleared. It can however be reset which might result in the desired effect.", ErrorQuality.SEMANTIC_ERROR);
					else
						Skript.error(what + " can't be deleted/cleared", ErrorQuality.SEMANTIC_ERROR);
					break;
				case REMOVE_ALL:
					if (changed.acceptChange(ChangeMode.REMOVE) != null) {
						Skript.error(what + " can't have 'all of something' removed from it. Use 'remove' instead of 'remove all' to fix this.", ErrorQuality.SEMANTIC_ERROR);
						break;
					}
					//$FALL-THROUGH$
				case ADD:
				case REMOVE:
					Skript.error(what + " can't have anything " + (mode == ChangeMode.ADD ? "added to" : "removed from") + " it", ErrorQuality.SEMANTIC_ERROR);
					break;
				case RESET:
					if (changed.acceptChange(ChangeMode.DELETE) != null)
						Skript.error(what + " can't be reset. It can however be deleted which might result in the desired effect.", ErrorQuality.SEMANTIC_ERROR);
					else
						Skript.error(what + " can't be reset", ErrorQuality.SEMANTIC_ERROR);
			}
			return false;
		}
		
		final Class<?>[] rs2 = new Class<?>[rs.length];
		for (int i = 0; i < rs.length; i++)
			rs2[i] = rs[i].isArray() ? rs[i].getComponentType() : rs[i];
		final boolean allSingle = Arrays.equals(rs, rs2);
		
		Expression<?> ch = changer;
		if (ch != null) {
			Expression<?> v = null;
			final ParseLogHandler log = SkriptLogger.startParseLogHandler();
			try {
				for (final Class<?> r : rs) {
					log.clear();
					if ((r.isArray() ? r.getComponentType() : r).isAssignableFrom(ch.getReturnType())) {
						v = ch.getConvertedExpression(Object.class);
						break; // break even if v == null as it won't convert to Object apparently
					}
				}
				if (v == null)
					v = ch.getConvertedExpression((Class<Object>[]) rs2);
				if (v == null) {
					if (log.hasError()) {
						log.printError();
						return false;
					}
					log.clear();
					log.printLog();
					final Class<?>[] r = new Class[rs.length];
					for (int i = 0; i < rs.length; i++)
						r[i] = rs[i].isArray() ? rs[i].getComponentType() : rs[i];
					if (rs.length == 1 && rs[0] == Object.class)
						Skript.error("Can't understand this expression: " + changer, ErrorQuality.NOT_AN_EXPRESSION);
					else if (mode == ChangeMode.SET)
						Skript.error(what + " can't be set to " + changer + " because the latter is " + SkriptParser.notOfType(r), ErrorQuality.SEMANTIC_ERROR);
					else
						Skript.error(changer + " can't be " + (mode == ChangeMode.ADD ? "added to" : "removed from") + " " + what + " because the former is " + SkriptParser.notOfType(r), ErrorQuality.SEMANTIC_ERROR);
					return false;
				}
				log.printLog();
			} finally {
				log.stop();
			}
			
			Class<?> x = Utils.getSuperType(rs2);
			single = allSingle;
			for (int i = 0; i < rs.length; i++) {
				if (rs2[i].isAssignableFrom(v.getReturnType())) {
					single = !rs[i].isArray();
					x = rs2[i];
					break;
				}
			}
			assert x != null;
			changer = ch = v;
			
			if (!ch.isSingle() && single) {
				if (mode == ChangeMode.SET)
					Skript.error(changed + " can only be set to one " + Classes.getSuperClassInfo(x).getName() + ", not more", ErrorQuality.SEMANTIC_ERROR);
				else
					Skript.error("only one " + Classes.getSuperClassInfo(x).getName() + " can be " + (mode == ChangeMode.ADD ? "added to" : "removed from") + " " + changed + ", not more", ErrorQuality.SEMANTIC_ERROR);
				return false;
			}
			
			if (changed instanceof Variable && !((Variable<?>) changed).isLocal() && (mode == ChangeMode.SET || ((Variable<?>) changed).isList() && mode == ChangeMode.ADD)) {
				final ClassInfo<?> ci = Classes.getSuperClassInfo(ch.getReturnType());
				if (ci.getC() != Object.class && ci.getSerializer() == null && ci.getSerializeAs() == null && !SkriptConfig.disableObjectCannotBeSavedWarnings.value())
					Skript.warning(ci.getName().withIndefiniteArticle() + " cannot be saved, i.e. the contents of the variable " + changed + " will be lost when the server stops.");
			}
		}
		return true;
	}
	
	@Override
	protected void execute(final Event e) {
		final Expression<?> changer = this.changer;
		final Object[] delta = changer == null ? null : changer.getArray(e);
		if (delta != null && delta.length == 0)
			return;
		changed.change(e, delta, mode); // REMIND use a random element out of delta if changed only supports changing a single instance
//		changed.change(e, new Changer2<Object>() {
//			@Override
//			public Object change(Object o) {
//				return delta;
//			}
//		}, mode);
	}
	
	@Override
	public String toString(final @Nullable Event e, final boolean debug) {
		final Expression<?> changer = this.changer;
		switch (mode) {
			case ADD:
				assert changer != null;
				return "add " + changer.toString(e, debug) + " to " + changed.toString(e, debug);
			case SET:
				assert changer != null;
				return "set " + changed.toString(e, debug) + " to " + changer.toString(e, debug);
			case REMOVE:
				assert changer != null;
				return "remove " + changer.toString(e, debug) + " from " + changed.toString(e, debug);
			case REMOVE_ALL:
				assert changer != null;
				return "remove all " + changer.toString(e, debug) + " from " + changed.toString(e, debug);
			case DELETE:
				return "delete/clear " + changed.toString(e, debug);
			case RESET:
				return "reset " + changed.toString(e, debug);
		}
		assert false;
		return "";
	}
	
}
