// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.proxetta;

import jodd.io.FileUtil;
import jodd.proxetta.asm.TargetClassInfoReader;
import jodd.proxetta.asm.WorkData;
import jodd.util.StringUtil;
import jodd.asm5.ClassReader;
import jodd.asm5.ClassWriter;
import jodd.util.ClassLoaderUtil;
import jodd.io.StreamUtil;
import jodd.log.Logger;
import jodd.log.LoggerFactory;

import java.io.File;
import java.io.InputStream;
import java.io.IOException;

/**
 * Proxetta builder. While {@link Proxetta} only holds aspects and
 * configuration, <code>ProxettaBuilder</code> deals with the
 * actually building proxies and wrappers over provided target.
 */
public abstract class ProxettaBuilder {

	Logger log = LoggerFactory.getLogger(ProxettaBuilder.class);

	protected final Proxetta proxetta;

	/**
	 * Creates new builder.
	 */
	protected ProxettaBuilder(Proxetta proxetta) {
		this.proxetta = proxetta;
	}
	// ---------------------------------------------------------------- IN

	/**
	 * Main target source.
	 */
	private InputStream targetInputStream;

	/**
	 * Target class, when available.
	 */
	private Class targetClass;

	/**
	 * Target class name, when available.
	 */
	private String targetClassName;

	/**
	 * Requested proxy class name (or class name template).
	 */
	protected String requestedProxyClassName;

	/**
	 * Sets requested proxy class name.
	 */
	public void setTargetProxyClassName(String targetProxyClassName) {
		this.requestedProxyClassName = targetProxyClassName;
	}

	// ---------------------------------------------------------------- IN targets

	/**
	 * Defines class input stream as a target.
	 */
	protected void setTarget(InputStream target) {
		checkTarget();

		targetInputStream = target;
		targetClass = null;
		targetClassName = null;
	}

	/**
	 * Defines class name as a target.
	 * Class will not be loaded by classloader!
	 */
	protected void setTarget(String targetName) {
		checkTarget();

		try {
			targetInputStream = ClassLoaderUtil.getClassAsStream(targetName);
			if (targetInputStream == null) {
				throw new ProxettaException("Target class not found: " + targetName);
			}
			targetClassName = targetName;
			targetClass = null;
		}
		catch (IOException ioex) {
			StreamUtil.close(targetInputStream);
			throw new ProxettaException("Unable to get stream class name: " + targetName, ioex);
		}
	}

	/**
	 * Defines class as a target.
	 */
	protected void setTarget(Class target) {
		checkTarget();

		try {
			targetInputStream = ClassLoaderUtil.getClassAsStream(target);
			if (targetInputStream == null) {
				throw new ProxettaException("Target class not found: " + target.getName());
			}
			targetClass = target;
			targetClassName = target.getName();
		}
		catch (IOException ioex) {
			StreamUtil.close(targetInputStream);
			throw new ProxettaException("Unable to stream class: " + target.getName(), ioex);
		}
	}

	/**
	 * Checks if target is not defined yet.
	 */
	private void checkTarget() {
		if (targetInputStream != null) {
			throw new ProxettaException("Target already defined");
		}

	}

	// ---------------------------------------------------------------- IN naming

	/**
	 * Number appended to proxy class name, incremented on each use to make classnames unique
	 * in the system (e.g. classloader).
	 *
	 * @see Proxetta#setVariableClassName(boolean)
 	 */
	protected static int suffixCounter;


	/**
	 * Returns new suffix or <code>null</code> if suffix is not in use.
	 */
	protected String resolveClassNameSuffix() {
		String classNameSuffix = proxetta.getClassNameSuffix();

		if (classNameSuffix == null) {
			return null;
		}

		if (!proxetta.isVariableClassName()) {
			return classNameSuffix;
		}

		suffixCounter++;
		return classNameSuffix + suffixCounter;
	}

	// ---------------------------------------------------------------- PROCESS

	/**
	 * Creates custom class builder and process the target class with it.
	 */
	protected abstract WorkData process(ClassReader cr, TargetClassInfoReader targetClassInfoReader);

	// ---------------------------------------------------------------- ACCEPT

	protected ClassWriter destClassWriter;			// destination class writer
	protected boolean proxyApplied;
	protected String proxyClassName;

	/**
	 * Reads the target and creates destination class.
	 */
	protected void process() {
		if (targetInputStream == null) {
			throw new ProxettaException("Target missing");
		}
		// create class reader
		ClassReader classReader;
		try {
			classReader = new ClassReader(targetInputStream);
		} catch (IOException ioex) {
			throw new ProxettaException("Error reading class input stream", ioex);
		}

		// reads information
		TargetClassInfoReader targetClassInfoReader = new TargetClassInfoReader(proxetta.getClassLoader());
		classReader.accept(targetClassInfoReader, 0);

		this.destClassWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);

		// create proxy
		if (log.isDebugEnabled()) {
			log.debug("processing: " + classReader.getClassName());
		}
		WorkData wd = process(classReader, targetClassInfoReader);

		// store important data
		proxyApplied = wd.proxyApplied;
		proxyClassName = wd.thisReference.replace('/', '.');
	}

	/**
	 * Returns byte array of created class.
	 */
	public byte[] create() {
		process();

		byte[] result = toByteArray();

		dumpClass(result);

		if ((!proxetta.isForced()) && (!isProxyApplied())) {
			if (log.isDebugEnabled()) {
				log.debug("proxy not applied " + StringUtil.toSafeString(targetClassName));
			}
			return null;
		}

		if (log.isDebugEnabled()) {
			log.debug("proxy created " + StringUtil.toSafeString(targetClassName));
		}

		return result;
	}

	/**
	 * Defines class.
	 */
	public Class define() {
		process();

		if ((!proxetta.isForced()) && (!isProxyApplied())) {
			if (log.isDebugEnabled()) {
				log.debug("proxy not applied " + StringUtil.toSafeString(targetClassName));
			}

			if (targetClass != null) {
				return targetClass;
			} else if (targetClassName != null) {
				try {
					return ClassLoaderUtil.loadClass(targetClassName);
				} catch (ClassNotFoundException cnfex) {
					throw new ProxettaException(cnfex);
				}
			}
		}

		if (log.isDebugEnabled()) {
			log.debug("proxy created " + StringUtil.toSafeString(targetClassName));
		}

		try {
			ClassLoader classLoader = proxetta.getClassLoader();

			if (classLoader == null) {

				if (targetClass != null) {
					classLoader = targetClass.getClassLoader();
				}

				if (classLoader == null) {
					classLoader = ClassLoaderUtil.getDefaultClassLoader();
				}
			}

			byte[] bytes = toByteArray();

			dumpClass(bytes);

			return ClassLoaderUtil.defineClass(getProxyClassName(), bytes, classLoader);
		} catch (Exception ex) {
			throw new ProxettaException("Class definition failed", ex);
		}
	}

	/**
	 * Creates new instance of created class.
	 * Assumes default no-arg constructor.
	 */
	public Object newInstance() {
		Class type = define();
		try {
			return type.newInstance();
		} catch (Exception ex) {
			throw new ProxettaException("Invalid Proxetta class", ex);
		}
	}


	// ---------------------------------------------------------------- debug

	/**
	 * Writes created class content to output folder for debugging purposes.
	 */
	protected void dumpClass(byte[] bytes) {
		String debugFolder = proxetta.getDebugFolder();
		if (debugFolder == null) {
			return;
		}

		File folder = new File(debugFolder);
		if (!folder.exists()) {
			folder.mkdirs();
		}

		String fileName = proxyClassName;
		if (fileName == null) {
			fileName = "proxetta-" + System.currentTimeMillis();
		}

		fileName += ".class";

		File file = new File(folder, fileName);
		try {
			FileUtil.writeBytes(file, bytes);
		} catch (IOException ioex) {
			log.warn("Error dumping class", ioex);
		}
	}

	// ---------------------------------------------------------------- OUT

	/**
	 * Checks if proxy is created and throws an exception if not.
	 */
	protected void checkAccepted() {
		if (destClassWriter == null) {
			throw new ProxettaException("Target not accepted yet!");
		}
	}

	/**
	 * Returns raw bytecode.
	 */
	protected byte[] toByteArray() {
		checkAccepted();
		return destClassWriter.toByteArray();
	}

	/**
	 * Returns <code>true</code> if at least one method was wrapped.
	 */
	public boolean isProxyApplied() {
		checkAccepted();
		return proxyApplied;
	}

	/**
	 * Returns proxy class name.
	 */
	public String getProxyClassName() {
		checkAccepted();
		return proxyClassName;
	}

}
