/*
 * Copyright (c) 2013 Red Hat, Inc. and/or its affiliates.
 *  
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  which accompanies this distribution, and is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *  
 *  Contributors:
 *      Brad Davis - bradsdavis@gmail.com - Initial API and implementation
*/
package org.jboss.windup.interrogator.impl;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javassist.ClassPool;
import javassist.CtClass;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jboss.windup.decorator.java.decompiler.DecompilerAdapter;
import org.jboss.windup.metadata.type.FileMetadata;
import org.jboss.windup.metadata.type.JavaMetadata;
import org.jboss.windup.metadata.type.ZipEntryMetadata;
import org.jboss.windup.metadata.type.archive.ZipMetadata;
import org.jboss.windup.util.BlacklistPackageResolver;
import org.jboss.windup.util.CustomerPackageResolver;
import org.jboss.windup.util.FatalWindupException;


public class ClassInterrogator extends ExtensionInterrogator<JavaMetadata> {
	private static final Logger LOG = LoggerFactory.getLogger(ClassInterrogator.class);
	private DecompilerAdapter decompiler;

	private BlacklistPackageResolver blacklistPackageResolver;
	private CustomerPackageResolver customerPackageResolver;

	public void setBlacklistPackageResolver(BlacklistPackageResolver blacklistPackageResolver) {
		this.blacklistPackageResolver = blacklistPackageResolver;
	}
	
	public void setDecompiler(DecompilerAdapter decompiler) {
		this.decompiler = decompiler;
	}

	public void setCustomerPackageResolver(CustomerPackageResolver customerPackageResolver) {
		this.customerPackageResolver = customerPackageResolver;
	}

	@Override
	public JavaMetadata archiveEntryToMeta(ZipEntryMetadata archiveEntry) {
		String className = extractClassName(archiveEntry.getZipEntry().getName());
		if (LOG.isDebugEnabled()) {
			LOG.debug("Class: " + className + " " + customerPackageResolver.isCustomerPkg(className));
		}

		// first, is this a customer's package?
		if (!customerPackageResolver.isCustomerPkg(className)) {
			if (LOG.isTraceEnabled()) {
				LOG.trace("Class: " + className + " is not a customer package.");
			}
			return null;
		}

		// extract imports if it is a customer class.
		Set<String> clzImports = extractImports(archiveEntry);

		// second, is this generated?
		if (blacklistPackageResolver.containsGenerated(clzImports)) {
			LOG.trace("Class is generated.  Skip profiling.");
			return null;
		}

		// third does the class contain any blacklists?
		if (!blacklistPackageResolver.containsBlacklist(clzImports)) {
			LOG.trace("Class does not contain blacklists.");
			return null;
		}

		/*
		 * ok; it contains blacklists... we need to process it. get the Java for the class by either
		 * 1) finding it in the Zip archive... or
		 * 2) decompiling.
		 */
		return extractJavaFile(className, clzImports, archiveEntry);
	}

	private JavaMetadata extractJavaFile(String className, Set<String> clzImports, ZipEntryMetadata archiveEntry) {
		JavaMetadata javaMeta = new JavaMetadata();
		try {
			File clzFile;
			File javaFile;

			//TODO: make this work for directorymeta too.
			ZipMetadata zipMeta = (ZipMetadata)archiveEntry.getArchiveMeta();
			ZipFile zipFile = zipMeta.getZipFile();
			ZipEntry entry = archiveEntry.getZipEntry();
			// check to see whether the Java version is packaged with the archive...
			String javaZipEntry = StringUtils.removeEnd(entry.getName(), ".class") + ".java";

			if (zipFile.getEntry(javaZipEntry) != null) {
				if (LOG.isDebugEnabled()) {
					LOG.debug("Found Java in archive: " + className);
				}
				ZipEntry javaEntry = zipFile.getEntry(javaZipEntry);
				archiveEntry.setZipEntry(javaEntry);
				javaFile = archiveEntry.getFilePointer();
			}
			else {
				clzFile = archiveEntry.getFilePointer();
				javaFile = new File(StringUtils.substringBeforeLast(clzFile.getAbsolutePath(), ".class") + ".java");
				File javaPathFile = new File(StringUtils.substringBeforeLast(clzFile.getAbsolutePath(), File.separator));

				if (LOG.isDebugEnabled()) {
					LOG.debug("Did not find class in archive. Decompiling class: " + className);
				}
				decompiler.decompile(className, clzFile, javaPathFile);

				if (LOG.isDebugEnabled()) {
					LOG.debug("Unzipped class: " + className);
				}
			}

			javaMeta.setFilePointer(javaFile);
			javaMeta.setArchiveMeta(archiveEntry.getArchiveMeta());
			javaMeta.setClassDependencies(clzImports);
			javaMeta.setQualifiedClassName(className);
			javaMeta.setBlackListedDependencies(blacklistPackageResolver.extractBlacklist(clzImports));

			return javaMeta;
		}
		catch (Exception e) {
			if(e instanceof FatalWindupException) {
				throw (FatalWindupException)e;
			}
			
			LOG.error("Error extracting java file: " + e.getMessage(), e);
			return null;
		}
	}

	protected Set<String> extractImports(ZipEntryMetadata archiveEntry) {
		try {
			// otherwise, load the class and get its imports.

			//TODO: make this work for directorymeta too.
			ZipMetadata zipMeta = (ZipMetadata)archiveEntry.getArchiveMeta();
			ZipFile zipFile = zipMeta.getZipFile();
			ZipEntry entry = archiveEntry.getZipEntry();
			CtClass ctClz = new ClassPool().makeClass(zipFile.getInputStream(entry));
			if (LOG.isDebugEnabled()) {
				for (String clz : ctClz.getClassFile2().getInterfaces()) {
					LOG.debug("Interfaces: " + clz);
				}
				LOG.debug("Super Class: " + ctClz.getClassFile2().getSuperclass());
			}
			Set<String> clzImports = new HashSet<String>(castList(String.class, ctClz.getRefClasses()));
			return clzImports;
		}
		catch (Exception e) {
			LOG.error("Error extractine imports: " + e.getMessage(), e);
			return null;
		}
	}

	public static <T> List<T> castList(Class<? extends T> aclass, Collection<?> c) {
		List<T> r = new ArrayList<T>(c.size());
		for (Object o : c) {
			r.add(aclass.cast(o));
		}
		return r;
	}

	protected String extractClassName(String entryName) {
		String className = StringUtils.replace(entryName, "\\", "/");
		className = StringUtils.removeStart(className, "/");
		className = StringUtils.replace(className, "/", ".");
		className = StringUtils.removeEnd(className, ".class");
		className = StringUtils.removeEnd(className, ".java");
		className = StringUtils.substringBefore(className, "$");

		// account for WAR classes.
		if (StringUtils.contains(className, "WEB-INF.classes.")) {
			className = StringUtils.substringAfter(className, "WEB-INF.classes.");
		}
		return className;
	}

	@Override
	public JavaMetadata fileEntryToMeta(FileMetadata entry) {
		JavaMetadata javaMeta = new JavaMetadata();
		javaMeta.setArchiveMeta(entry.getArchiveMeta());
		
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(entry.getFilePointer());
			javaMeta.setFilePointer(entry.getFilePointer());
			//javaMeta.setArchiveMeta(archiveEntry.getArchiveMeta());
			CtClass ctClazz = new ClassPool().makeClass(fis);

			String className = ctClazz.getName();
			if (!customerPackageResolver.isCustomerPkg(className)) {
				if (LOG.isTraceEnabled()) {
					LOG.trace("Class: " + className + " is not a customer package.");
				}
				return null;
			}
			
			Set<String> clzImports = new HashSet<String>(castList(String.class, ctClazz.getRefClasses()));

			// second, is this generated?
			if (blacklistPackageResolver.containsGenerated(clzImports)) {
				LOG.trace("Class is generated.  Skip profiling.");
				return null;
			}

			// third does the class contain any blacklists?
			if (!blacklistPackageResolver.containsBlacklist(clzImports)) {
				LOG.trace("Class does not contain blacklists.");
				return null;
			}
			
			javaMeta.setClassDependencies(clzImports);
			javaMeta.setQualifiedClassName(ctClazz.getName());
			javaMeta.setBlackListedDependencies(blacklistPackageResolver.extractBlacklist(clzImports));
			
			return javaMeta;
		}
		catch(Exception e) {
			LOG.error("Exception processing file: "+entry.getFilePointer().getAbsolutePath(), e);
		}
		finally { 
			org.apache.commons.io.IOUtils.closeQuietly(fis);
		}
		
		return null;
	}
}
