/* *******************************************************************
 * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     PARC     initial implementation
 * ******************************************************************/

package org.aspectj.weaver;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.weaver.tools.Trace;
import org.aspectj.weaver.tools.TraceFactory;

public class Lint {
	Map<String, Lint.Kind> kinds = new HashMap<>();
	/* private */World world;

	public final Kind invalidAbsoluteTypeName = new Kind("invalidAbsoluteTypeName", "no match for this type name: {0}");

	public final Kind invalidWildcardTypeName = new Kind("invalidWildcardTypeName", "no match for this type pattern: {0}");

	public final Kind unresolvableMember = new Kind("unresolvableMember", "can not resolve this member: {0}");

	public final Kind typeNotExposedToWeaver = new Kind("typeNotExposedToWeaver",
			"this affected type is not exposed to the weaver: {0}");

	public final Kind shadowNotInStructure = new Kind("shadowNotInStructure",
			"the shadow for this join point is not exposed in the structure model: {0}");

	public final Kind unmatchedSuperTypeInCall = new Kind("unmatchedSuperTypeInCall",
			"does not match because declaring type is {0}, if match desired use target({1})");

	public final Kind unmatchedTargetKind = new Kind("unmatchedTargetKind", "does not match because annotation {0} has @Target{1}");

	public final Kind canNotImplementLazyTjp = new Kind("canNotImplementLazyTjp",
			"can not implement lazyTjp on this joinpoint {0} because around advice is used");

	public final Kind multipleAdviceStoppingLazyTjp = new Kind("multipleAdviceStoppingLazyTjp",
			"can not implement lazyTjp at joinpoint {0} because of advice conflicts, see secondary locations to find conflicting advice");

	public final Kind needsSerialVersionUIDField = new Kind("needsSerialVersionUIDField",
			"serialVersionUID of type {0} needs to be set because of {1}");

	public final Kind serialVersionUIDBroken = new Kind("brokeSerialVersionCompatibility",
			"serialVersionUID of type {0} is broken because of added field {1}");

	public final Kind noInterfaceCtorJoinpoint = new Kind("noInterfaceCtorJoinpoint",
			"no interface constructor-execution join point - use {0}+ for implementing classes");

	public final Kind noJoinpointsForBridgeMethods = new Kind(
			"noJoinpointsForBridgeMethods",
			"pointcut did not match on the method call to a bridge method.  Bridge methods are generated by the compiler and have no join points");

	public final Kind enumAsTargetForDecpIgnored = new Kind("enumAsTargetForDecpIgnored",
			"enum type {0} matches a declare parents type pattern but is being ignored");

	public final Kind annotationAsTargetForDecpIgnored = new Kind("annotationAsTargetForDecpIgnored",
			"annotation type {0} matches a declare parents type pattern but is being ignored");

	public final Kind cantMatchArrayTypeOnVarargs = new Kind("cantMatchArrayTypeOnVarargs",
			"an array type as the last parameter in a signature does not match on the varargs declared method: {0}");

	public final Kind adviceDidNotMatch = new Kind("adviceDidNotMatch", "advice defined in {0} has not been applied");

	public final Kind invalidTargetForAnnotation = new Kind("invalidTargetForAnnotation",
			"{0} is not a valid target for annotation {1}, this annotation can only be applied to {2}");

	public final Kind elementAlreadyAnnotated = new Kind("elementAlreadyAnnotated",
			"{0} - already has an annotation of type {1}, cannot add a second instance");

	public final Kind runtimeExceptionNotSoftened = new Kind("runtimeExceptionNotSoftened",
			"{0} will not be softened as it is already a RuntimeException");

	public final Kind uncheckedArgument = new Kind("uncheckedArgument",
			"unchecked match of {0} with {1} when argument is an instance of {2} at join point {3}");

	public final Kind uncheckedAdviceConversion = new Kind("uncheckedAdviceConversion",
			"unchecked conversion when advice applied at shadow {0}, expected {1} but advice uses {2}");

	public final Kind noGuardForLazyTjp = new Kind("noGuardForLazyTjp",
			"can not build thisJoinPoint lazily for this advice since it has no suitable guard");

	public final Kind noExplicitConstructorCall = new Kind("noExplicitConstructorCall",
			"inter-type constructor does not contain explicit constructor call: field initializers in the target type will not be executed");

	public final Kind aspectExcludedByConfiguration = new Kind("aspectExcludedByConfiguration",
			"aspect {0} exluded for class loader {1}");

	public final Kind unorderedAdviceAtShadow = new Kind("unorderedAdviceAtShadow",
			"at this shadow {0} no precedence is specified between advice applying from aspect {1} and aspect {2}");

	public final Kind swallowedExceptionInCatchBlock = new Kind("swallowedExceptionInCatchBlock",
			"exception swallowed in catch block");

	public final Kind calculatingSerialVersionUID = new Kind("calculatingSerialVersionUID",
			"calculated SerialVersionUID for type {0} to be {1}");

	public final Kind nonReweavableTypeEncountered = new Kind("nonReweavableTypeEncountered",
			"class {0} is already woven and has not been built in reweavable mode");

	// there are a lot of messages in the cant find type family - I'm defining an umbrella lint warning that
	// allows a user to control their severity (for e.g. ltw or binary weaving)
	public final Kind cantFindType = new Kind("cantFindType", "{0}");

	public final Kind cantFindTypeAffectingJoinPointMatch = new Kind("cantFindTypeAffectingJPMatch", "{0}");

	public final Kind advisingSynchronizedMethods = new Kind("advisingSynchronizedMethods",
			"advice matching the synchronized method shadow ''{0}'' will be executed outside the lock rather than inside (compiler limitation)");

	public final Kind mustWeaveXmlDefinedAspects = new Kind(
			"mustWeaveXmlDefinedAspects",
			"XML Defined aspects must be woven in cases where cflow pointcuts are involved. Currently the include/exclude patterns exclude ''{0}''");

	public final Kind cannotAdviseJoinpointInInterfaceWithAroundAdvice = new Kind(
			"cannotAdviseJoinpointInInterfaceWithAroundAdvice",
			"The joinpoint ''{0}'' cannot be advised and is being skipped as the compiler implementation will lead to creation of methods with bodies in an interface (compiler limitation)");

	/**
	 * Indicates an aspect could not be found when attempting reweaving.
	 */
	public final Kind missingAspectForReweaving = new Kind("missingAspectForReweaving",
			"aspect {0} cannot be found when reweaving {1}");

	public final Kind arrayCannotBeVoid = new Kind("arrayCannotBeVoid", "arrays cannot have a void type, but found ''{0}'' in pointcut");

	private static final Trace trace = TraceFactory.getTraceFactory().getTrace(Lint.class);

	public Lint(World world) {
		if (trace.isTraceEnabled())
			trace.enter("<init>", this, world);
		this.world = world;
		if (trace.isTraceEnabled())
			trace.exit("<init>");
	}

	public void setAll(String messageKind) {
		if (trace.isTraceEnabled())
			trace.enter("setAll", this, messageKind);
		setAll(getMessageKind(messageKind));
		if (trace.isTraceEnabled())
			trace.exit("setAll");
	}

	private void setAll(IMessage.Kind messageKind) {
		for (Kind kind : kinds.values())
			kind.setKind(messageKind);
	}

	public void setFromMap(Map<String,String> lintOptionsMap) {
		for (String key: lintOptionsMap.keySet()) {
			String value = lintOptionsMap.get(key);
			Kind kind = kinds.get(key);
			if (kind == null)
				MessageUtil.error(world.getMessageHandler(), WeaverMessages.format(WeaverMessages.XLINT_KEY_ERROR, key));
			else
				kind.setKind(getMessageKind(value));
		}
	}

	public void setFromProperties(File file) {
		if (trace.isTraceEnabled())
			trace.enter("setFromProperties", this, file);
		try (InputStream s = Files.newInputStream(file.toPath())) {
			setFromProperties(s);
		}
		catch (IOException ioe) {
			MessageUtil.error(
				world.getMessageHandler(),
				WeaverMessages.format(WeaverMessages.XLINT_LOAD_ERROR, file.getPath(), ioe.getMessage())
			);
		}

		if (trace.isTraceEnabled())
			trace.exit("setFromProperties");
	}

	public void loadDefaultProperties() {
		InputStream s = getClass().getResourceAsStream("XlintDefault.properties");
		if (s == null) {
			MessageUtil.warn(world.getMessageHandler(), WeaverMessages.format(WeaverMessages.XLINTDEFAULT_LOAD_ERROR));
			return;
		}
		try {
			setFromProperties(s);
		}
		catch (IOException ioe) {
			MessageUtil.error(world.getMessageHandler(),
					WeaverMessages.format(WeaverMessages.XLINTDEFAULT_LOAD_PROBLEM, ioe.getMessage()));
		} finally {
			try {
				s.close();
			} catch (IOException ioe) {
				// ignore
			}
		}

	}

	private void setFromProperties(InputStream s) throws IOException {
		Properties p = new Properties();
		p.load(s);
		setFromProperties(p);
	}

	public void setFromProperties(Properties properties) {
		for (Map.Entry<Object, Object> entry : properties.entrySet()) {
			Kind kind = kinds.get(entry.getKey());
			if (kind == null)
				MessageUtil.error(world.getMessageHandler(), WeaverMessages.format(WeaverMessages.XLINT_KEY_ERROR, entry.getKey()));
			else
				kind.setKind(getMessageKind((String) entry.getValue()));
		}
	}

	public Collection<Kind> allKinds() {
		return kinds.values();
	}

	public Kind getLintKind(String name) {
		return kinds.get(name);
	}

	// temporarily suppress the given lint messages
	public void suppressKinds(Collection<Kind> lintKind) {
		if (lintKind.isEmpty())
			return;
		for (Kind k : lintKind)
			k.setSuppressed(true);
	}

	// remove any suppression of lint warnings in place
	public void clearAllSuppressions() {
		for (Kind k : kinds.values())
			k.setSuppressed(false);
	}

	public void clearSuppressions(Collection<Lint.Kind> lintKinds) {
		for (Kind k : lintKinds)
			k.setSuppressed(false);
	}

	private IMessage.Kind getMessageKind(String v) {
		switch (v) {
			case "ignore":
				return null;
			case "warning":
				return IMessage.WARNING;
			case "error":
				return IMessage.ERROR;
		}

		MessageUtil.error(world.getMessageHandler(), WeaverMessages.format(WeaverMessages.XLINT_VALUE_ERROR, v));
		return null;
	}

	public Kind fromKey(String lintkey) {
		return kinds.get(lintkey);
	}

	public class Kind {
		private final String name;
		private final String message;
		private IMessage.Kind kind = IMessage.WARNING;
		private boolean isSupressed = false; // by SuppressAjWarnings

		public Kind(String name, String message) {
			this.name = name;
			this.message = message;
			kinds.put(this.name, this);
		}

		public void setSuppressed(boolean shouldBeSuppressed) {
			this.isSupressed = shouldBeSuppressed;
		}

		public boolean isEnabled() {
			return (kind != null) && !isSupressed();
		}

		private boolean isSupressed() {
			// can't suppress errors!
			return isSupressed && (kind != IMessage.ERROR);
		}

		public String getName() {
			return name;
		}

		public IMessage.Kind getKind() {
			return kind;
		}

		public void setKind(IMessage.Kind kind) {
			this.kind = kind;
		}

		public void signal(String info, ISourceLocation location) {
			if (kind == null)
				return;

			String text = MessageFormat.format(message, info);
			text += " [Xlint:" + name + "]";
			world.getMessageHandler().handleMessage(new LintMessage(text, kind, location, null, getLintKind(name)));
		}

		public void signal(String[] infos, ISourceLocation location, ISourceLocation[] extraLocations) {
			if (kind == null)
				return;

			String text = MessageFormat.format(message, (Object[]) infos);
			text += " [Xlint:" + name + "]";
			world.getMessageHandler().handleMessage(new LintMessage(text, kind, location, extraLocations, getLintKind(name)));
		}

	}

}
