/* *******************************************************************
 * Copyright (c) 1999-2001 Xerox Corporation, 
 *               2002 Palo Alto Research Center, Incorporated (PARC).
 * All rights reserved. 
 * This program and the accompanying materials are made available 
 * under the terms of the Common Public License v1.0 
 * which accompanies this distribution and is available at 
 * http://www.eclipse.org/legal/cpl-v10.html 
 *  
 * Contributors: 
 *     Xerox/PARC     initial implementation 
 * ******************************************************************/

package org.aspectj.testing.harness.bridge;

import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.IMessageHolder;
import org.aspectj.bridge.MessageHandler;
import org.aspectj.bridge.MessageUtil;
//import org.aspectj.bridge.IMessage.Kind;
import org.aspectj.testing.util.BridgeUtil;
import org.aspectj.testing.util.Diffs;
import org.aspectj.util.LangUtil;

/**
 * Handle messages during test and calculate differences
 * between expected and actual messages.
 */
public class AjcMessageHandler extends MessageHandler {

	/** Comparator for enclosed IMessage diffs */
	public static final Comparator COMP_IMessage =
		BridgeUtil.Comparators.MEDIUM_IMessage;

	/** Comparator for enclosed File diffs */
	public static final Comparator COMP_File = BridgeUtil.Comparators.WEAK_File;

	/** unmodifiable list of IMessage messages of any type */
	private final List expectedMessagesAsList;

	/** IMessageHolder variant of expectedMessagesAsList */
	private final IMessageHolder expectedMessages;

	/** number of messages FAIL or worse */
	private final int numExpectedFailed;

	/** true if there were no error or worse messages expected */
	private final boolean expectingCommandTrue;

	/** unmodifiable list of File expected to be recompiled */
	private final List expectedRecompiled;
	// Unused now, but reinstate when supported

	/** if true, ignore warnings when calculating diffs and passed() */
	private final boolean ignoreWarnings;

	/** list of File actually recompiled */
	private List actualRecompiled;

	/** cache expected/actual diffs, nullify if any new message */
	private transient CompilerDiffs diffs;

	AjcMessageHandler(IMessageHolder expectedMessages) {
		this(expectedMessages, false);
	}
	/** 
	 * @param messages the (constant) IMessageHolder with expected messages
	 */
	AjcMessageHandler(
		IMessageHolder expectedMessages,
		boolean ignoreWarnings) {
		LangUtil.throwIaxIfNull(messages, "messages");
		this.expectedMessages = expectedMessages;
		expectedMessagesAsList = expectedMessages.getUnmodifiableListView();
		expectedRecompiled = Collections.EMPTY_LIST;
		this.ignoreWarnings = ignoreWarnings;
		int fails = 0;
		int errors = 0;
		for (Iterator iter = expectedMessagesAsList.iterator();
			iter.hasNext();
			) {
			IMessage m = (IMessage) iter.next();
			IMessage.Kind kind = m.getKind();
			if (IMessage.FAIL.isSameOrLessThan(kind)) {
				fails++;
			} else if (m.isError()) {
				errors++;
			}
		}
		expectingCommandTrue = (0 == (errors + fails));
		numExpectedFailed = fails;
	}

	/** clear out any actual values to be re-run */
	public void init() {
		super.init();
		actualRecompiled = null;
		diffs = null;
	}

	/**
	 * Return true if we have this kind of
	 * message for the same line and store all messages.
	 * @see bridge.tools.impl.ErrorHandlerAdapter#doShowMessage(IMessage)
	 * @return true if message handled (processing should abort)
	 */
	public boolean handleMessage(IMessage message) {
		if (null == message) {
			throw new IllegalArgumentException("null message");
		}
		super.handleMessage(message);
		return expecting(message);
	}

	/** 
	 * Set the actual files recompiled.
	 * @param List of File recompiled - may be null; adopted but not modified
	 * @throws IllegalStateException if they have been set already.
	 */
	public void setRecompiled(List list) {
		if (null != actualRecompiled) {
			throw new IllegalStateException("actual recompiled already set");
		}
		this.actualRecompiled = LangUtil.safeList(list);
	}

	/** Generate differences between expected and actual errors and warnings */
	public CompilerDiffs getCompilerDiffs() {
		if (null == diffs) {
			final List expected;
			final List actual;
			if (!ignoreWarnings) {
				expected = expectedMessages.getUnmodifiableListView();
				actual = this.getUnmodifiableListView();
			} else {
				expected =
					Arrays.asList(
						expectedMessages.getMessages(IMessage.ERROR, true));
				actual = Arrays.asList(this.getMessages(IMessage.ERROR, true));
			}
			// we ignore unexpected info messages,
			// but we do test for expected ones
			final Diffs messages;
            boolean usingNew = true; // XXX extract old API's after shake-out period
            if (usingNew) {
                final IMessage.Kind[] NOSKIPS = new IMessage.Kind[0];
                IMessage.Kind[] skipActual =  new IMessage.Kind[] { IMessage.INFO };
                int expectedInfo 
                    = MessageUtil.numMessages(expected, IMessage.INFO, false);
                if (0 < expectedInfo) {
                    // fyi, when expecting any info messages, have to expect all
                    skipActual = NOSKIPS;
                }
                messages = Diffs.makeDiffs(
                    "message",
                    (IMessage[]) expected.toArray(new IMessage[0]),
                    (IMessage[]) actual.toArray(new IMessage[0]),
                    NOSKIPS,
                    skipActual);
            } else {
                messages = Diffs.makeDiffs(
                    "message", 
                    expected, 
                    actual, 
                    COMP_IMessage, 
                    Diffs.ACCEPT_ALL, 
                    CompilerDiffs.SKIP_UNEXPECTED_INFO);
            }
			Diffs recompiled =
				Diffs.makeDiffs(
					"recompiled",
					expectedRecompiled,
					actualRecompiled,
					COMP_File);
			diffs = new CompilerDiffs(messages, recompiled);
		}
		return diffs;
	}

	/**
	 * Get the (current) result of this run,
	 * ignoring differences in warnings on request.
	 * Note it may return passed (true) when there are expected error messages.
	 * @return false 
	 * if there are any fail or abort messages,
	 * or if the expected errors, warnings, or recompiled do not match actual.
	 */
	public boolean passed() {
		return !getCompilerDiffs().different;
	}

	/** @return true if we are expecting the command to fail - i.e., any expected errors */
	public boolean expectingCommandTrue() {
		return expectingCommandTrue;
	}

	/**
	 * Report results to a handler,
	 * adding all messages
	 * and creating fail messages for each diff.
	 */
	public void report(IMessageHandler handler) {
		if (null == handler) {
			MessageUtil.debug(this, "report got null handler");
		}
		// Report all messages except expected fail+ messages,
		// which will cause the reported-to handler client to gack.
		// XXX need some verbose way to report even expected fail+
		final boolean fastFail = false; // do all messages
		if (0 == numExpectedFailed) {
			MessageUtil.handleAll(handler, this, fastFail);
		} else {
			IMessage[] ra = getMessagesWithoutExpectedFails();
			MessageUtil.handleAll(handler, ra, fastFail);
		}

		CompilerDiffs diffs = getCompilerDiffs();
		if (diffs.different) {
			diffs.messages.report(handler, IMessage.FAIL);
			diffs.recompiled.report(handler, IMessage.FAIL);
		}
	}

	/** @return String consisting of differences and any other messages */
	public String toString() {
		CompilerDiffs diffs = getCompilerDiffs();
		StringBuffer sb = new StringBuffer(super.toString());
		final String EOL = "\n";
		sb.append(EOL);
		render(sb, " unexpected message ", EOL, diffs.messages.unexpected);
		render(sb, "    missing message ", EOL, diffs.messages.missing);
		render(sb, "               fail ", EOL, getList(IMessage.FAIL));
		render(sb, "              abort ", EOL, getList(IMessage.ABORT));
		render(sb, "               info ", EOL, getList(IMessage.INFO));
		return sb.toString(); // XXX cache toString
	}

	/** 
	 * Check if the message was expected, and clear diffs if not.
	 * @return true if we expect a message of this kind with this line number 
	 */
	private boolean expecting(IMessage message) {
		boolean match = false;
		if (null != message) {
			for (Iterator iter = expectedMessagesAsList.iterator();
				iter.hasNext();
				) {
				// amc - we have to compare against all messages to consume multiple
				// text matches on same line. Return true if any matches.
				if (0 == COMP_IMessage.compare(message, iter.next())) {
					match = true;
				}
			}
		}
		if (!match) {
			diffs = null;
		}
		return match;
	}

	private IMessage[] getMessagesWithoutExpectedFails() {
		IMessage[] result = super.getMessages(null, true);
		// remove all expected fail+ (COSTLY)
		ArrayList list = new ArrayList();
		int leftToFilter = numExpectedFailed;
		for (int i = 0; i < result.length; i++) {
			if ((0 == leftToFilter)
				|| !IMessage.FAIL.isSameOrLessThan(result[i].getKind())) {
				list.add(result[i]);
			} else {
				// see if this failure was expected
				if (expectedMessagesHasMatchFor(result[i])) {
					leftToFilter--; // ok, don't add
				} else {
					list.add(result[i]);
				}
			}
		}
		result = (IMessage[]) list.toArray(new IMessage[0]);
		return result;
	}

	/**
	 * @param actual the actual IMessage to seek a match for in expected messages
	 * @return true if actual message is matched in the expected messages
	 */
	private boolean expectedMessagesHasMatchFor(IMessage actual) {
		for (Iterator iter = expectedMessagesAsList.iterator();
			iter.hasNext();
			) {
			IMessage expected = (IMessage) iter.next();
			if (0 == COMP_IMessage.compare(expected, actual)) {
				return true;
			}
		}
		return false;
	}

	/** @return immutable list of a given kind - use  null for all kinds */
	private List getList(IMessage.Kind kind) {
		if ((null == kind) || (0 == numMessages(kind, IMessageHolder.EQUAL))) {
			return Collections.EMPTY_LIST;
		}
		return Arrays.asList(getMessages(kind, IMessageHolder.EQUAL));
	}

	/** @return "" if no items or {prefix}{item}{suffix}... otherwise */
	private void render(// LangUtil instead?
	StringBuffer result, String prefix, String suffix, List items) {
		if ((null != items)) {
			for (Iterator iter = items.iterator(); iter.hasNext();) {
				result.append(prefix + iter.next() + suffix);
			}
		}
	}

	/** compiler results for errors, warnings, and recompiled files */
	public static class CompilerDiffs {
		/** Skip info messages when reporting unexpected messages */
		static final Diffs.Filter SKIP_UNEXPECTED_INFO = new Diffs.Filter() {
			public boolean accept(Object o) {
				return ((o instanceof IMessage) && !((IMessage) o).isInfo());
			}
		};
		public final Diffs messages;
		public final Diffs recompiled;
		public final boolean different;

		public CompilerDiffs(Diffs messages, Diffs recompiled) {
			this.recompiled = recompiled;
			this.messages = messages;
			different = (messages.different || recompiled.different);
		}
	}
}
