package com.laytonsmith.core.exceptions.CRE;

import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery;
import com.laytonsmith.PureUtilities.Common.ReflectionUtils;
import com.laytonsmith.annotations.seealso;
import com.laytonsmith.annotations.typeof;
import com.laytonsmith.core.Documentation;
import com.laytonsmith.core.Static;
import com.laytonsmith.core.constructs.CArray;
import com.laytonsmith.core.constructs.CClassType;
import com.laytonsmith.core.constructs.CNull;
import com.laytonsmith.core.constructs.Construct;
import com.laytonsmith.core.constructs.NativeTypeList;
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
import com.laytonsmith.core.exceptions.StackTraceManager;
import com.laytonsmith.core.natives.interfaces.ArrayAccess;
import com.laytonsmith.core.natives.interfaces.Mixed;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * Exceptions should extend this class, which provides default implementations of various utility methods.
 */
public abstract class AbstractCREException extends ConfigRuntimeException implements Documentation, Mixed, ArrayAccess {

	private final static Class[] EMPTY_CLASS = new Class[0];

	private List<StackTraceElement> stackTrace = null;

	public AbstractCREException(String msg, Target t){
		super(msg, t);
	}

	public AbstractCREException(String msg, Target t, Throwable cause) {
		super(msg, t, cause);
	}

	@Override
	public Class<? extends Documentation>[] seeAlso() {
		seealso see = this.getClass().getAnnotation(seealso.class);
		if(see == null){
			return EMPTY_CLASS;
		} else {
			return see.value();
		}
	}

	@Override
	public URL getSourceJar() {
		return ClassDiscovery.GetClassContainer(this.getClass());
	}

	@Override
	public String getName() {
		typeof to = this.getClass().getAnnotation(typeof.class);
		if(to == null){
			throw new Error("ConfigRuntimeException subtypes must annotate themselves with @typeof, if they are instantiateable.");
		} else {
			return to.value();
		}
	}

	/**
	 * Alias for {@link #getName() }
	 * @return
	 */
	public String getExceptionType(){
		return getName();
	}

	/**
	 * Returns the name of the exception. If the exception is an instanceof AbstractCREException,
	 * this is equivalent to calling {@link #getName() }. Otherwise, the java class name is returned.
	 * @param ex
	 * @return
	 */
	public static String getExceptionName(ConfigRuntimeException ex){
		if(ex instanceof AbstractCREException){
			return ((AbstractCREException)ex).getName();
		} else {
			return ex.getClass().getName();
		}
	}

	private CArray exceptionObject = null;

	/**
	 * Returns a standardized CArray given this exception.
	 * @return
	 */
	public CArray getExceptionObject(){
		CArray ret = new CArray(Target.UNKNOWN);
		ret.set("classType", new CClassType(this.getName(), Target.UNKNOWN), Target.UNKNOWN);
		ret.set("message", this.getMessage());
		CArray stackTrace = new CArray(Target.UNKNOWN);
		ret.set("stackTrace", stackTrace, Target.UNKNOWN);
		for(StackTraceElement e : this.getCREStackTrace()){
			CArray element = e.getObjectFor();
			stackTrace.push(element, Target.UNKNOWN);
		}
		ret.set("causedBy", getCausedBy(this.getCause()), Target.UNKNOWN);
		return ret;
	}
	
	@SuppressWarnings({"ThrowableInstanceNotThrown", "ThrowableInstanceNeverThrown"})
	public static AbstractCREException getFromCArray(CArray exception, Target t) throws ClassNotFoundException {
		String classType = exception.get("classType", t).val();
		Class<Mixed> clzz = NativeTypeList.getNativeClass(classType);
		Throwable cause = null;
		if(exception.get("causedBy", t) instanceof CArray){
			// It has a cause
			cause = new CRECausedByWrapper((CArray)exception.get("causedBy", t));
		}
		String message = exception.get("message", t).val();
		List<StackTraceElement> st = new ArrayList<>();
		for(Construct consStElement : Static.getArray(exception.get("stackTrace", t), t).asList()){
			CArray stElement = Static.getArray(consStElement, t);
			int line = Static.getInt32(stElement.get("line", t), t);
			File f = new File(stElement.get("file", t).val());
			int col = 0; // 
			st.add(new StackTraceElement(stElement.get("id", t).val(), new Target(line, f, col)));
		}
		// Now we have parsed everything into POJOs
		Class[] types = new Class[]{String.class, Target.class, Throwable.class};
		Object[] args = new Object[]{message, t, cause};
		AbstractCREException ex = (AbstractCREException) ReflectionUtils.newInstance(clzz, types, args);
		ex.stackTrace = st;
		return ex;
	}
	
	private static Construct getCausedBy(Throwable causedBy){
		if(causedBy == null){
			return CNull.NULL;
		}
		CRECausedByWrapper cre = (CRECausedByWrapper) causedBy;
		CArray ret = cre.getException();
		return ret;
	}

	/**
	 * Casts the CRE to an AbstractCREException, or throws an error if it is not convertable.
	 * @param ex
	 * @return
	 */
	public static AbstractCREException getAbstractCREException(ConfigRuntimeException ex){
		if(ex instanceof AbstractCREException){
			return (AbstractCREException)ex;
		}
		throw new Error("Unexpected CRE exception that isn't convertable to AbstractCREException");
	}

	@Override
	public String val() {
		return getName() + ":" + getMessage();
	}


	/**
	 * These methods are required because we don't actually want to extend CArray, we want to be our own
	 * class of Object (at least for now). These basically just call through to our underlying CArray though.
	 * @param index
	 * @param t
	 * @return
	 * @throws ConfigRuntimeException
	 */
	@Override
	public Construct get(String index, Target t) throws ConfigRuntimeException {
		return exceptionObject.get(index, t);
	}

	@Override
	public Construct get(int index, Target t) throws ConfigRuntimeException {
		return exceptionObject.get(index, t);
	}

	@Override
	public Construct get(Construct index, Target t) throws ConfigRuntimeException {
		return exceptionObject.get(index, t);
	}

	@Override
	public Set<Construct> keySet() {
		return exceptionObject.keySet();
	}

	@Override
	public long size() {
		return exceptionObject.size();
	}

	@Override
	public boolean isAssociative() {
		return exceptionObject.isAssociative();
	}

	@Override
	public boolean canBeAssociative() {
		return exceptionObject.canBeAssociative();
	}

	@Override
	public Construct slice(int begin, int end, Target t) {
		return exceptionObject.slice(begin, end, t);
	}

	@Override
	public AbstractCREException clone() throws CloneNotSupportedException {
		AbstractCREException obj = (AbstractCREException)super.clone();

		return obj;
	}
	
	public void freezeStackTraceElements(StackTraceManager manager){
		if(this.stackTrace == null){
			this.stackTrace = manager.getCurrentStackTrace();
		}
	}
	
	public List<StackTraceElement> getCREStackTrace(){
		if(this.stackTrace == null){
			return new ArrayList<>();
		}
		return new ArrayList<>(this.stackTrace);
	}

}

@@ -4668,32 +4668,77 @@
 causedBy == null
+ %7C%7C !(causedBy instanceof CRECausedByWrapper)
 )%7B%0A%09%09%09return CNu

Fix core error when exception cause is not a CRE