/*******************************************************************************
 * Copyright (c) 2005 Contributors.
 * 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:
 *   Alexandre Vasseur         initial implementation
 *******************************************************************************/
package org.aspectj.weaver.loadtime;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringTokenizer;

import org.aspectj.bridge.context.CompilationAndWeavingContext;
import org.aspectj.weaver.Dump;
import org.aspectj.weaver.tools.Trace;
import org.aspectj.weaver.tools.TraceFactory;
import org.aspectj.weaver.tools.WeavingAdaptor;
import org.aspectj.weaver.tools.cache.SimpleCache;
import org.aspectj.weaver.tools.cache.SimpleCacheFactory;

/**
 * Adapter between the generic class pre processor interface and the AspectJ weaver Load time weaving consistency relies on
 * Bcel.setRepository
 *
 * @author Alexandre Vasseur (alex AT gnilux DOT com)
 */
public class Aj implements ClassPreProcessor {

	private IWeavingContext weavingContext;
	public static SimpleCache laCache=SimpleCacheFactory.createSimpleCache();

	/**
	 * References are added to this queue when their associated classloader is removed, and once on here that indicates that we
	 * should tidy up the adaptor map and remove the adaptor (weaver) from the map we are maintaining from adaptorkey &gt; adaptor
	 * (weaver)
	 */
	private static ReferenceQueue<ClassLoader> adaptorQueue = new ReferenceQueue<>();

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

	public Aj() {
		this(null);
	}

	public Aj(IWeavingContext context) {
		if (trace.isTraceEnabled())
			trace.enter("<init>", this, new Object[] { context, getClass().getClassLoader() });
		this.weavingContext = context;
		if (trace.isTraceEnabled())
			trace.exit("<init>");
	}

	/**
	 * Initialization
	 */
	@Override
	public void initialize() {

	}

	private final static String deleLoader = "sun.reflect.DelegatingClassLoader";
	private final static String deleLoader2 = "jdk.internal.reflect.DelegatingClassLoader"; // On JDK11+

	@Override
	public byte[] preProcess(String className, final byte[] bytes, ClassLoader classLoader, ProtectionDomain protectionDomain) {

		// Implementation note: Try to return null, whenever it is clear that the original byte code was not changed by the
		// weaver. While not strictly necessary, the changes from commit 3e3c83712e are defensive programming, which helps
		// to avoid problems like https://bugs.openjdk.org/browse/JDK-8325536, even if unfixed in the JDK.

		if (classLoader == null || className == null ||
			classLoader.getClass().getName().equals(deleLoader) || classLoader.getClass().getName().equals(deleLoader2)) {
			// skip boot loader, null classes (hibernate), or those from a reflection loader
			return null;
		}

		if (loadersToSkip != null) {
			// Check whether to reject it
			if (loadersToSkip.contains(classLoader.getClass().getName())) {
//				System.out.println("debug: no weaver created for loader '"+loader.getClass().getName()+"'");
				return null;
			}
		}

		if (trace.isTraceEnabled())
			trace.enter("preProcess", this, new Object[] { className, bytes, classLoader });
		if (trace.isTraceEnabled())
			trace.event("preProcess", this, new Object[] { classLoader.getParent(), Thread.currentThread().getContextClassLoader() });

		try {
			synchronized (classLoader) {

				if (SimpleCacheFactory.isEnabled()) {
					Optional<byte[]> cacheBytes = laCache.getAndInitialize(className, bytes, classLoader, protectionDomain);
					if (cacheBytes != null){
						return cacheBytes.orElse(null);
					}
				}

				WeavingAdaptor weavingAdaptor = WeaverContainer.getWeaver(classLoader, weavingContext);
				if (weavingAdaptor == null) {
					if (trace.isTraceEnabled())
						trace.exit("preProcess");
					return null;
				}
				try {
					weavingAdaptor.setActiveProtectionDomain(protectionDomain);
					byte[] newBytes = weavingAdaptor.weaveClass(className, bytes, false);
					Dump.dumpOnExit(weavingAdaptor.getMessageHolder(), true);
					if (trace.isTraceEnabled())
						trace.exit("preProcess", newBytes);
					if (SimpleCacheFactory.isEnabled()) {
						laCache.put(className, bytes, newBytes);
					}
					return newBytes;
				} finally {
					weavingAdaptor.setActiveProtectionDomain(null);
				}
			}

			/* Don't like to do this but JVMTI swallows all exceptions */
		} catch (Throwable th) {
			trace.error(className, th);
			Dump.dumpWithException(th);
			// FIXME AV wondering if we should have the option to fail (throw runtime exception) here
			// would make sense at least in test f.e. see TestHelper.handleMessage()
			if (trace.isTraceEnabled())
				trace.exit("preProcess", th);
			return null;
		} finally {
			CompilationAndWeavingContext.resetForThread();
		}
	}

	/**
	 * An AdaptorKey is a WeakReference wrapping a classloader reference that will enqueue to a specified queue when the classloader
	 * is GC'd. Since the AdaptorKey is used as a key into a hashmap we need to give it a non-varying hashcode/equals
	 * implementation, and we need that hashcode not to vary even when the internal referent has been GC'd. The hashcode is
	 * calculated on creation of the AdaptorKey based on the loader instance that it is wrapping. This means even when the referent
	 * is gone we can still use the AdaptorKey and it will 'point' to the same place as it always did.
	 */
	private static class AdaptorKey extends WeakReference {

		private final int loaderHashCode, sysHashCode, hashValue;
		private final String loaderClass;

		public AdaptorKey(ClassLoader loader) {
			super(loader, adaptorQueue);
			loaderHashCode = loader.hashCode();
			sysHashCode = System.identityHashCode(loader);
			loaderClass = loader.getClass().getName();
			hashValue = loaderHashCode + sysHashCode + loaderClass.hashCode();
		}

		public ClassLoader getClassLoader() {
			ClassLoader instance = (ClassLoader) get();
			// Assert instance!=null - shouldn't be asked for after a GC of the referent has occurred !
			return instance;
		}

		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof AdaptorKey)) {
				return false;
			}
			AdaptorKey other = (AdaptorKey) obj;
			return (other.loaderHashCode == loaderHashCode)
					&&  (other.sysHashCode == sysHashCode)
					&& loaderClass.equals(other.loaderClass);
		}

		@Override
		public int hashCode() {
			return hashValue;
		}

	}

	/**
	 * The reference queue is only processed when a request is made for a weaver adaptor. This means there can be one or two stale
	 * weavers left around. If the user knows they have finished all their weaving, they might wish to call removeStaleAdaptors
	 * which will process anything left on the reference queue containing adaptorKeys for garbage collected classloaders.
	 *
	 * @param displayProgress produce System.err info on the tidying up process
	 * @return number of stale weavers removed
	 */
	public static int removeStaleAdaptors(boolean displayProgress) {
		int removed = 0;
		synchronized (WeaverContainer.weavingAdaptors) {
			if (displayProgress) {
				System.err.println("Weaver adaptors before queue processing:");
				Map<AdaptorKey,ExplicitlyInitializedClassLoaderWeavingAdaptor> m = WeaverContainer.weavingAdaptors;
				Set<AdaptorKey> keys = m.keySet();
				for (AdaptorKey object : keys) {
					System.err.println(object + " = " + WeaverContainer.weavingAdaptors.get(object));
				}
			}
			Reference<?> o = adaptorQueue.poll();
			while (o != null) {
				if (displayProgress)
					System.err.println("Processing referencequeue entry " + o);
				AdaptorKey wo = (AdaptorKey) o;
				boolean didit = WeaverContainer.weavingAdaptors.remove(wo) != null;
				if (didit) {
					removed++;
				} else {
					throw new RuntimeException("Eh?? key=" + wo);
				}
				if (displayProgress)
					System.err.println("Removed? " + didit);
				o = adaptorQueue.poll();
			}
			if (displayProgress) {
				System.err.println("Weaver adaptors after queue processing:");
				Map<AdaptorKey,ExplicitlyInitializedClassLoaderWeavingAdaptor> m = WeaverContainer.weavingAdaptors;
				Set<AdaptorKey> keys = m.keySet();
				for (AdaptorKey object : keys) {
					System.err.println(object + " = " + WeaverContainer.weavingAdaptors.get(object));
				}
			}
		}
		return removed;
	}

	/**
	 * @return the number of entries still in the weavingAdaptors map
	 */
	public static int getActiveAdaptorCount() {
		return WeaverContainer.weavingAdaptors.size();
	}

	/**
	 * Process the reference queue that contains stale AdaptorKeys - the keys are put on the queue when their classloader referent
	 * is garbage collected and so the associated adaptor (weaver) should be removed from the map
	 */
	public static void checkQ() {
		synchronized (adaptorQueue) {
			Reference<?> o = adaptorQueue.poll();
			while (o != null) {
				AdaptorKey wo = (AdaptorKey) o;
				// boolean removed =
				WeaverContainer.weavingAdaptors.remove(wo);
				// DBG System.err.println("Evicting key " + wo + " = " + didit);
				o = adaptorQueue.poll();
			}
		}
	}

	public static List<String> loadersToSkip = null;

	static {
		// pr271840 - touch the types early and outside the locks
		new ExplicitlyInitializedClassLoaderWeavingAdaptor(new ClassLoaderWeavingAdaptor());
		try {
			String loadersToSkipProperty = System.getProperty("aj.weaving.loadersToSkip","");
			StringTokenizer st = new StringTokenizer(loadersToSkipProperty, ",");
			if (loadersToSkipProperty != null && loadersToSkip == null) {
				if (st.hasMoreTokens()) {
//					System.out.println("aj.weaving.loadersToSkip is set. Skipping loaders: '"+loadersToSkipProperty+"'");
					loadersToSkip = new ArrayList<>();
				}
				while (st.hasMoreTokens()) {
					String nextLoader = st.nextToken();
					loadersToSkip.add(nextLoader);
				}
			}
		} catch (Exception e) {
			// Likely security issue related to property access...
		}
	}

	/**
	 * Cache of weaver There is one weaver per classloader
	 */
	static class WeaverContainer {

		final static Map<AdaptorKey,ExplicitlyInitializedClassLoaderWeavingAdaptor> weavingAdaptors =
				Collections.synchronizedMap(new HashMap<>());

		static WeavingAdaptor getWeaver(ClassLoader loader, IWeavingContext weavingContext) {
			ExplicitlyInitializedClassLoaderWeavingAdaptor adaptor = null;
			AdaptorKey adaptorKey = new AdaptorKey(loader);

			String loaderClassName = loader.getClass().getName();

			synchronized (weavingAdaptors) {
				checkQ();
                if (loader.equals(myClassLoader)){
                    adaptor = myClassLoaderAdaptor;
                } else {
                	adaptor = weavingAdaptors.get(adaptorKey);
                }
				if (adaptor == null) {
					// create it and put it back in the weavingAdaptors map but avoid any kind of instantiation
					// within the synchronized block
					ClassLoaderWeavingAdaptor weavingAdaptor = new ClassLoaderWeavingAdaptor();
					adaptor = new ExplicitlyInitializedClassLoaderWeavingAdaptor(weavingAdaptor);
					  if(myClassLoaderAdaptor == null && loader.equals(myClassLoader)){
	                        myClassLoaderAdaptor = adaptor;
					  } else {
	                    	weavingAdaptors.put(adaptorKey, adaptor);
	                  }
				}
			}
			// perform the initialization
			return adaptor.getWeavingAdaptor(loader, weavingContext);


		}
		private static final ClassLoader myClassLoader = WeavingAdaptor.class.getClassLoader();
		private static ExplicitlyInitializedClassLoaderWeavingAdaptor myClassLoaderAdaptor;
	}


	static class ExplicitlyInitializedClassLoaderWeavingAdaptor {
		private final ClassLoaderWeavingAdaptor weavingAdaptor;
		private boolean isInitialized;

		public ExplicitlyInitializedClassLoaderWeavingAdaptor(ClassLoaderWeavingAdaptor weavingAdaptor) {
			this.weavingAdaptor = weavingAdaptor;
			this.isInitialized = false;
		}

		private void initialize(ClassLoader loader, IWeavingContext weavingContext) {
			if (!isInitialized) {
				isInitialized = true;
				weavingAdaptor.initialize(loader, weavingContext);
			}
		}

		public ClassLoaderWeavingAdaptor getWeavingAdaptor(ClassLoader loader, IWeavingContext weavingContext) {
			initialize(loader, weavingContext);
			return weavingAdaptor;
		}
	}

	/**
	 * Returns a namespace based on the contest of the aspects available
	 */
	public String getNamespace(ClassLoader loader) {
		ClassLoaderWeavingAdaptor weavingAdaptor = (ClassLoaderWeavingAdaptor) WeaverContainer.getWeaver(loader, weavingContext);
		return weavingAdaptor.getNamespace();
	}

	/**
	 * Check to see if any classes have been generated for a particular classes loader. Calls
	 * ClassLoaderWeavingAdaptor.generatedClassesExist()
	 *
	 * @param loader the class cloder
	 * @return true if classes have been generated.
	 */
	public boolean generatedClassesExist(ClassLoader loader) {
		return ((ClassLoaderWeavingAdaptor) WeaverContainer.getWeaver(loader, weavingContext)).generatedClassesExistFor(null);
	}

	public void flushGeneratedClasses(ClassLoader loader) {
		((ClassLoaderWeavingAdaptor) WeaverContainer.getWeaver(loader, weavingContext)).flushGeneratedClasses();
	}

	@Override
	public void prepareForRedefinition(ClassLoader loader, String className) {
		((ClassLoaderWeavingAdaptor) WeaverContainer.getWeaver(loader, weavingContext)).flushGeneratedClassesFor(className);
	}

}
