/* *******************************************************************
 * 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;

import org.aspectj.bridge.MessageUtil;
import org.aspectj.bridge.Version;
import org.aspectj.util.FileUtil;
import org.aspectj.weaver.patterns.Declare;
import org.aspectj.weaver.patterns.IScope;
import org.aspectj.weaver.patterns.PerClause;
import org.aspectj.weaver.patterns.Pointcut;

/**
 * These attributes are written to and read from .class files (see the JVM spec).
 *
 * <p>
 * Each member or type can have a number of AjAttributes. Each such attribute is in 1-1 correspondence with an Unknown bcel
 * attribute. Creating one of these does NOTHING to the underlying thing, so if you really want to add an attribute to a particular
 * thing, well, you'd better actually do that.
 *
 * @author Erik Hilsdale
 * @author Jim Hugunin
 */
public abstract class AjAttribute {

	public static final String AttributePrefix = "org.aspectj.weaver";

	protected abstract void write(CompressingDataOutputStream s) throws IOException;

	public abstract String getNameString();

	public char[] getNameChars() {
		return getNameString().toCharArray();
	}

	/**
	 * Just writes the contents
	 */
	public byte[] getBytes(ConstantPoolWriter compressor) {
		try {
			ByteArrayOutputStream b0 = new ByteArrayOutputStream();
			CompressingDataOutputStream s0 = new CompressingDataOutputStream(b0, compressor);
			write(s0);
			s0.close();
			return b0.toByteArray();
		} catch (IOException e) {
			// shouldn't happen with ByteArrayOutputStreams
			throw new RuntimeException("sanity check");
		}
	}

	/**
	 * Writes the full attribute, i.e. name_index, length, and contents
	 *
	 * @param dataCompressor
	 */
	public byte[] getAllBytes(short nameIndex, ConstantPoolWriter dataCompressor) {
		try {
			byte[] bytes = getBytes(dataCompressor);

			ByteArrayOutputStream b0 = new ByteArrayOutputStream();
			DataOutputStream s0 = new DataOutputStream(b0);

			s0.writeShort(nameIndex);
			s0.writeInt(bytes.length);
			s0.write(bytes);
			s0.close();
			return b0.toByteArray();
		} catch (IOException e) {
			// shouldn't happen with ByteArrayOutputStreams
			throw new RuntimeException("sanity check");
		}
	}

	public static AjAttribute read(AjAttribute.WeaverVersionInfo v, String name, byte[] bytes, ISourceContext context, World w,
			ConstantPoolReader dataDecompressor) {
		try {
			if (bytes == null) {
				bytes = new byte[0];
			}

			VersionedDataInputStream s = new VersionedDataInputStream(new ByteArrayInputStream(bytes), dataDecompressor);
			s.setVersion(v);
			if (name.equals(Aspect.AttributeName)) {
				return new Aspect(PerClause.readPerClause(s, context));
			} else if (name.equals(MethodDeclarationLineNumberAttribute.AttributeName)) {
				return MethodDeclarationLineNumberAttribute.read(s);
			} else if (name.equals(WeaverState.AttributeName)) {
				return new WeaverState(WeaverStateInfo.read(s, context));
			} else if (name.equals(WeaverVersionInfo.AttributeName)) {
				return WeaverVersionInfo.read(s);
			} else if (name.equals(AdviceAttribute.AttributeName)) {
				AdviceAttribute aa = AdviceAttribute.read(s, context);
				aa.getPointcut().check(context, w);
				return aa;
			} else if (name.equals(PointcutDeclarationAttribute.AttributeName)) {
				PointcutDeclarationAttribute pda = new PointcutDeclarationAttribute(ResolvedPointcutDefinition.read(s, context));
				pda.pointcutDef.getPointcut().check(context, w);
				return pda;
			} else if (name.equals(TypeMunger.AttributeName)) {
				return new TypeMunger(ResolvedTypeMunger.read(s, context));
			} else if (name.equals(AjSynthetic.AttributeName)) {
				return new AjSynthetic();
			} else if (name.equals(DeclareAttribute.AttributeName)) {
				return new DeclareAttribute(Declare.read(s, context));
			} else if (name.equals(PrivilegedAttribute.AttributeName)) {
				return PrivilegedAttribute.read(s, context);
			} else if (name.equals(SourceContextAttribute.AttributeName)) {
				return SourceContextAttribute.read(s);
			} else if (name.equals(EffectiveSignatureAttribute.AttributeName)) {
				return EffectiveSignatureAttribute.read(s, context);
			} else {
				// We have to tell the user about this...
				if (w == null || w.getMessageHandler() == null) {
					throw new BCException("unknown attribute" + name);
				}
				w.getMessageHandler().handleMessage(MessageUtil.warn("unknown attribute encountered " + name));
				return null;
			}
		} catch (BCException e) {
			throw new BCException("malformed " + name + " attribute (length:" + bytes.length + ")" + e);
		} catch (IOException e) {
			throw new BCException("malformed " + name + " attribute (length:" + bytes.length + ")" + e);
		}
	}

	// ----

	/**
	 * Synthetic members should have NO advice put on them or on their contents. This attribute is currently unused as we consider
	 * all members starting with NameMangler.PREFIX to automatically be synthetic. As we use this we might find that we want
	 * multiple kinds of synthetic. In particular, if we want to treat the call to a synthetic getter (say, of an introduced field)
	 * as a field reference itself, then a method might want a particular kind of AjSynthetic attribute that also includes a
	 * signature of what it stands for.
	 */
	public static class AjSynthetic extends AjAttribute {
		public static final String AttributeName = "org.aspectj.weaver.AjSynthetic";

		@Override
		public String getNameString() {
			return AttributeName;
		}

		// private ResolvedTypeMunger munger;
		public AjSynthetic() {
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
		}
	}

	public static class TypeMunger extends AjAttribute {
		public static final String AttributeName = "org.aspectj.weaver.TypeMunger";

		@Override
		public String getNameString() {
			return AttributeName;
		}

		private final ResolvedTypeMunger munger;

		public TypeMunger(ResolvedTypeMunger munger) {
			this.munger = munger;
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			munger.write(s);
		}

		public ConcreteTypeMunger reify(World world, ResolvedType aspectType) {
			return world.getWeavingSupport().concreteTypeMunger(munger, aspectType);
		}
	}

	public static class WeaverState extends AjAttribute {
		public static final String AttributeName = "org.aspectj.weaver.WeaverState";

		@Override
		public String getNameString() {
			return AttributeName;
		}

		private final WeaverStateInfo kind;

		public WeaverState(WeaverStateInfo kind) {
			this.kind = kind;
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			kind.write(s);
		}

		public WeaverStateInfo reify() {
			return kind;
		}
	}

	public static class WeaverVersionInfo extends AjAttribute {
		public static final String AttributeName = "org.aspectj.weaver.WeaverVersion";

		// If you change the format of an AspectJ class file, you have two
		// options:
		// - changing the minor version means you have not added anything that
		// prevents
		// previous versions of the weaver from operating (e.g.
		// MethodDeclarationLineNumber attribute)
		// - changing the major version means you have added something that
		// prevents previous
		// versions of the weaver from operating correctly.
		//
		// The user will get a warning for any org.aspectj.weaver attributes the
		// weaver does
		// not recognize.

		// When we don't know ... (i.e. pre 1.2.1)
		public final static short WEAVER_VERSION_MAJOR_UNKNOWN = 0;
		public final static short WEAVER_VERSION_MINOR_UNKNOWN = 0;

		// These are the weaver major/minor numbers for AspectJ 1.2.1
		public final static short WEAVER_VERSION_MAJOR_AJ121 = 1;
		public final static short WEAVER_VERSION_MINOR_AJ121 = 0;

		// These are the weaver major/minor numbers for AspectJ 1.5.0
		public final static short WEAVER_VERSION_MAJOR_AJ150M4 = 3;
		public final static short WEAVER_VERSION_MAJOR_AJ150 = 2;
		public final static short WEAVER_VERSION_MINOR_AJ150 = 0;

		// These are the weaver major/minor numbers for AspectJ 1.6.0
		public final static short WEAVER_VERSION_MAJOR_AJ160M2 = 5;
		public final static short WEAVER_VERSION_MAJOR_AJ160 = 4;
		public final static short WEAVER_VERSION_MINOR_AJ160 = 0;

		// These are the weaver major/minor numbers for AspectJ 1.6.1
		// added annotation value binding
		public final static short WEAVER_VERSION_MAJOR_AJ161 = 6;
		public final static short WEAVER_VERSION_MINOR_AJ161 = 0;

		// 1.6.9 adds new style ITDs. This is used to see what version of AJ was used to
		// build the ITDs so we know id the generated get/set dispatchers are using old
		// or new style (new style will be get/setters for private ITD fields)
		public final static short WEAVER_VERSION_AJ169 = 7;

		// These are the weaver major/minor versions for *this* weaver
		private final static short CURRENT_VERSION_MAJOR = WEAVER_VERSION_AJ169;
		private final static short CURRENT_VERSION_MINOR = 0;

		public final static WeaverVersionInfo UNKNOWN = new WeaverVersionInfo(WEAVER_VERSION_MAJOR_UNKNOWN,
				WEAVER_VERSION_MINOR_UNKNOWN);
		public final static WeaverVersionInfo CURRENT = new WeaverVersionInfo(CURRENT_VERSION_MAJOR, CURRENT_VERSION_MINOR);

		// These are the versions read in from a particular class file.
		private final short major_version;
		private final short minor_version;

		private long buildstamp = Version.NOTIME;

		@Override
		public String getNameString() {
			return AttributeName;
		}

		// Default ctor uses the current version numbers
		public WeaverVersionInfo() {
			major_version = CURRENT_VERSION_MAJOR;
			minor_version = CURRENT_VERSION_MINOR;
		}

		public WeaverVersionInfo(short major, short minor) {
			major_version = major;
			minor_version = minor;
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			s.writeShort(CURRENT_VERSION_MAJOR);
			s.writeShort(CURRENT_VERSION_MINOR);
			s.writeLong(Version.getTime()); // build used to construct the
			// class...
		}

		public static WeaverVersionInfo read(VersionedDataInputStream s) throws IOException {
			short major = s.readShort();
			short minor = s.readShort();
			WeaverVersionInfo wvi = new WeaverVersionInfo(major, minor);
			if (s.getMajorVersion() >= WEAVER_VERSION_MAJOR_AJ150M4) {
				long stamp = 0;
				try {
					stamp = s.readLong();
					wvi.setBuildstamp(stamp);
				} catch (EOFException eof) {
					// didnt find that build stamp - its not the end of the
					// world
				}
			}
			return wvi;
		}

		public short getMajorVersion() {
			return major_version;
		}

		public short getMinorVersion() {
			return minor_version;
		}

		public static short getCurrentWeaverMajorVersion() {
			return CURRENT_VERSION_MAJOR;
		}

		public static short getCurrentWeaverMinorVersion() {
			return CURRENT_VERSION_MINOR;
		}

		public void setBuildstamp(long stamp) {
			buildstamp = stamp;
		}

		public long getBuildstamp() {
			return buildstamp;
		}

		@Override
		public String toString() {
			return major_version + "." + minor_version;
		}

		public static String toCurrentVersionString() {
			return CURRENT_VERSION_MAJOR + "." + CURRENT_VERSION_MINOR;
		}

	}

	public static class SourceContextAttribute extends AjAttribute {
		public static final String AttributeName = "org.aspectj.weaver.SourceContext";

		@Override
		public String getNameString() {
			return AttributeName;
		}

		private final String sourceFileName;
		private final int[] lineBreaks;

		public SourceContextAttribute(String sourceFileName, int[] lineBreaks) {
			this.sourceFileName = sourceFileName;
			this.lineBreaks = lineBreaks;
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			if (s.canCompress()) {
				s.writeCompressedPath(sourceFileName);
			} else {
				s.writeUTF(sourceFileName);
			}
			s.writeInt(lineBreaks.length);
			int previous = 0;
			for (int lineBreak : lineBreaks) {
				s.writeShort(lineBreak - previous);
				previous = lineBreak;
			}
		}

		public static SourceContextAttribute read(VersionedDataInputStream s) throws IOException {
			String sourceFileName = s.isAtLeast169() ? s.readPath() : s.readUTF();
			int lineBreaks = s.readInt();
			int[] lines = new int[lineBreaks];
			int previous = 0;
			for (int i = 0; i < lineBreaks; i++) {
				if (s.isAtLeast169()) {
					lines[i] = s.readShort() + previous;
					previous = lines[i];
				} else {
					lines[i] = s.readInt();
				}
			}
			return new SourceContextAttribute(sourceFileName, lines);
		}

		public int[] getLineBreaks() {
			return lineBreaks;
		}

		public String getSourceFileName() {
			return sourceFileName;
		}
	}

	public static class MethodDeclarationLineNumberAttribute extends AjAttribute {

		public static final String AttributeName = "org.aspectj.weaver.MethodDeclarationLineNumber";

		@Override
		public String getNameString() {
			return AttributeName;
		}

		private final int lineNumber;

		// AV: added in 1.5 M3 thus handling cases where we don't have that
		// information
		private final int offset;

		public MethodDeclarationLineNumberAttribute(int line, int offset) {
			lineNumber = line;
			this.offset = offset;
		}

		public int getLineNumber() {
			return lineNumber;
		}

		public int getOffset() {
			return offset;
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			s.writeInt(lineNumber);
			s.writeInt(offset);
		}

		public static MethodDeclarationLineNumberAttribute read(VersionedDataInputStream s) throws IOException {
			int line = s.readInt();
			int offset = 0;
			if (s.available() > 0) {
				offset = s.readInt();
			}
			return new MethodDeclarationLineNumberAttribute(line, offset);
		}

		@Override
		public String toString() {
			return AttributeName + ": " + lineNumber + ":" + offset;
		}
	}

	public static class PointcutDeclarationAttribute extends AjAttribute {
		public static final String AttributeName = "org.aspectj.weaver.PointcutDeclaration";

		@Override
		public String getNameString() {
			return AttributeName;
		}

		private final ResolvedPointcutDefinition pointcutDef;

		public PointcutDeclarationAttribute(ResolvedPointcutDefinition pointcutDef) {
			this.pointcutDef = pointcutDef;
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			pointcutDef.write(s);
		}

		public ResolvedPointcutDefinition reify() {
			return pointcutDef;
		}
	}

	public static class DeclareAttribute extends AjAttribute {
		public static final String AttributeName = "org.aspectj.weaver.Declare";

		@Override
		public String getNameString() {
			return AttributeName;
		}

		private final Declare declare;

		public DeclareAttribute(Declare declare) {
			this.declare = declare;
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			declare.write(s);
		}

		public Declare getDeclare() {
			return declare;
		}
	}

	public static class AdviceAttribute extends AjAttribute {
		public static final String AttributeName = "org.aspectj.weaver.Advice";

		@Override
		public String getNameString() {
			return AttributeName;
		}

		private final AdviceKind kind;
		private final Pointcut pointcut;
		private final int extraParameterFlags;
		private final int start;
		private final int end;
		private final ISourceContext sourceContext;

		// these are only used by around advice
		private boolean proceedInInners;
		private ResolvedMember[] proceedCallSignatures; // size == # of proceed
		// calls in body
		private boolean[] formalsUnchangedToProceed; // size == formals.size
		private UnresolvedType[] declaredExceptions;

		/**
		 * @param start must be greater than the start of any advice declared before this one in an aspect,
		 *        otherwise, it can be any value.
		 */
		public AdviceAttribute(AdviceKind kind, Pointcut pointcut, int extraArgumentFlags, int start, int end,
				ISourceContext sourceContext) {
			this.kind = kind;
			this.pointcut = pointcut;
			extraParameterFlags = extraArgumentFlags;
			this.start = start;
			this.end = end;
			this.sourceContext = sourceContext;

			// XXX put this back when testing works better (or fails better)
			// if (kind == AdviceKind.Around) throw new
			// IllegalArgumentException("not for around");
		}

		public AdviceAttribute(AdviceKind kind, Pointcut pointcut, int extraArgumentFlags, int start, int end,
				ISourceContext sourceContext, boolean proceedInInners, ResolvedMember[] proceedCallSignatures,
				boolean[] formalsUnchangedToProceed, UnresolvedType[] declaredExceptions) {
			this.kind = kind;
			this.pointcut = pointcut;
			extraParameterFlags = extraArgumentFlags;
			this.start = start;
			this.end = end;
			this.sourceContext = sourceContext;

			if (kind != AdviceKind.Around) {
				throw new IllegalArgumentException("only for around");
			}

			this.proceedInInners = proceedInInners;
			this.proceedCallSignatures = proceedCallSignatures;
			this.formalsUnchangedToProceed = formalsUnchangedToProceed;
			this.declaredExceptions = declaredExceptions;
		}

		public static AdviceAttribute read(VersionedDataInputStream s, ISourceContext context) throws IOException {
			AdviceKind kind = AdviceKind.read(s);
			if (kind == AdviceKind.Around) {
				return new AdviceAttribute(kind, Pointcut.read(s, context), s.readByte(), s.readInt(), s.readInt(), context,
						s.readBoolean(), ResolvedMemberImpl.readResolvedMemberArray(s, context), FileUtil.readBooleanArray(s),
						UnresolvedType.readArray(s));
			} else {
				return new AdviceAttribute(kind, Pointcut.read(s, context), s.readByte(), s.readInt(), s.readInt(), context);
			}
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			kind.write(s);
			pointcut.write(s);
			s.writeByte(extraParameterFlags);
			s.writeInt(start);
			s.writeInt(end);

			if (kind == AdviceKind.Around) {
				s.writeBoolean(proceedInInners);
				ResolvedMemberImpl.writeArray(proceedCallSignatures, s);
				FileUtil.writeBooleanArray(formalsUnchangedToProceed, s);
				UnresolvedType.writeArray(declaredExceptions, s);
			}
		}

		public Advice reify(Member signature, World world, ResolvedType concreteAspect) {
			return world.getWeavingSupport().createAdviceMunger(this, pointcut, signature, concreteAspect);
		}

		@Override
		public String toString() {
			return "AdviceAttribute(" + kind + ", " + pointcut + ", " + extraParameterFlags + ", " + start + ")";
		}

		public int getExtraParameterFlags() {
			return extraParameterFlags;
		}

		public AdviceKind getKind() {
			return kind;
		}

		public Pointcut getPointcut() {
			return pointcut;
		}

		public UnresolvedType[] getDeclaredExceptions() {
			return declaredExceptions;
		}

		public boolean[] getFormalsUnchangedToProceed() {
			return formalsUnchangedToProceed;
		}

		public ResolvedMember[] getProceedCallSignatures() {
			return proceedCallSignatures;
		}

		public boolean isProceedInInners() {
			return proceedInInners;
		}

		public int getEnd() {
			return end;
		}

		public ISourceContext getSourceContext() {
			return sourceContext;
		}

		public int getStart() {
			return start;
		}

	}

	public static class Aspect extends AjAttribute {
		public static final String AttributeName = "org.aspectj.weaver.Aspect";

		@Override
		public String getNameString() {
			return AttributeName;
		}

		private final PerClause perClause;
		private IScope resolutionScope;

		public Aspect(PerClause perClause) {
			this.perClause = perClause;
		}

		public PerClause reify(ResolvedType inAspect) {
			// XXXperClause.concretize(inAspect);
			return perClause;
		}

		public PerClause reifyFromAtAspectJ(ResolvedType inAspect) {
			perClause.resolve(resolutionScope);
			return perClause;
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			perClause.write(s);
		}

		public void setResolutionScope(IScope binding) {
			resolutionScope = binding;
		}
	}

	public static class PrivilegedAttribute extends AjAttribute {

		public static final String AttributeName = "org.aspectj.weaver.Privileged";

		private final ResolvedMember[] accessedMembers;

		public PrivilegedAttribute(ResolvedMember[] accessedMembers) {
			this.accessedMembers = accessedMembers;
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			ResolvedMemberImpl.writeArray(accessedMembers, s);
		}

		public ResolvedMember[] getAccessedMembers() {
			return accessedMembers;
		}

		public static PrivilegedAttribute read(VersionedDataInputStream stream, ISourceContext context) throws IOException {
			PrivilegedAttribute pa = new PrivilegedAttribute(ResolvedMemberImpl.readResolvedMemberArray(stream, context));
			return pa;
		}

		@Override
		public String getNameString() {
			return AttributeName;
		}
	}

	public static class EffectiveSignatureAttribute extends AjAttribute {
		public static final String AttributeName = "org.aspectj.weaver.EffectiveSignature";

		@Override
		public String getNameString() {
			return AttributeName;
		}

		private final ResolvedMember effectiveSignature;
		private final Shadow.Kind shadowKind;
		private final boolean weaveBody;

		public EffectiveSignatureAttribute(ResolvedMember effectiveSignature, Shadow.Kind shadowKind, boolean weaveBody) {
			this.effectiveSignature = effectiveSignature;
			this.shadowKind = shadowKind;
			this.weaveBody = weaveBody;
		}

		@Override
		public void write(CompressingDataOutputStream s) throws IOException {
			effectiveSignature.write(s);
			shadowKind.write(s);
			s.writeBoolean(weaveBody);
		}

		public static EffectiveSignatureAttribute read(VersionedDataInputStream s, ISourceContext context) throws IOException {
			ResolvedMember member = ResolvedMemberImpl.readResolvedMember(s, context);
			return new EffectiveSignatureAttribute(member, Shadow.Kind.read(s), s.readBoolean());
		}

		public ResolvedMember getEffectiveSignature() {
			return effectiveSignature;
		}

		@Override
		public String toString() {
			return "EffectiveSignatureAttribute(" + effectiveSignature + ", " + shadowKind + ")";
		}

		public Shadow.Kind getShadowKind() {
			return shadowKind;
		}

		public boolean isWeaveBody() {
			return weaveBody;
		}

	}

}
