/**
 *
 * Copyright (c) 2009-2012,
 *
 *  Adam Fuchs          <afuchs@cs.umd.edu>
 *  Avik Chaudhuri      <avik@cs.umd.edu>
 *  Steve Suh           <suhsteve@gmail.com>
 *  Galois, Inc. (Adam Foltzer <acfoltzer@galois.com)
 *
 * 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.
 *
 * 3. The names of the contributors may not be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * 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 OWNER 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 org.scandroid.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;

import org.scandroid.synthmethod.DefaultSCanDroidOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
import com.google.common.collect.Sets;
import com.ibm.wala.classLoader.IClass;
import com.ibm.wala.classLoader.Module;
import com.ibm.wala.dalvik.classLoader.DexFileModule;
import com.ibm.wala.dalvik.dex.util.config.DexAnalysisScopeReader;
import com.ibm.wala.dalvik.util.AndroidAnalysisScope;
import com.ibm.wala.ipa.callgraph.AnalysisCache;
import com.ibm.wala.ipa.callgraph.AnalysisOptions;
import com.ibm.wala.ipa.callgraph.AnalysisScope;
import com.ibm.wala.ipa.callgraph.ClassTargetSelector;
import com.ibm.wala.ipa.callgraph.ContextSelector;
import com.ibm.wala.ipa.callgraph.Entrypoint;
import com.ibm.wala.ipa.callgraph.MethodTargetSelector;
import com.ibm.wala.ipa.callgraph.impl.Util;
import com.ibm.wala.ipa.callgraph.propagation.SSAContextInterpreter;
import com.ibm.wala.ipa.callgraph.propagation.SSAPropagationCallGraphBuilder;
import com.ibm.wala.ipa.callgraph.propagation.cfa.ZeroXCFABuilder;
import com.ibm.wala.ipa.callgraph.propagation.cfa.ZeroXInstanceKeys;
import com.ibm.wala.ipa.cha.ClassHierarchy;
import com.ibm.wala.ipa.cha.ClassHierarchyException;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.ipa.summaries.BypassClassTargetSelector;
import com.ibm.wala.ipa.summaries.BypassMethodTargetSelector;
import com.ibm.wala.ipa.summaries.MethodSummary;
import com.ibm.wala.ipa.summaries.XMLMethodSummaryReader;
import com.ibm.wala.types.ClassLoaderReference;
import com.ibm.wala.types.MethodReference;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.CancelException;
import com.ibm.wala.util.io.FileProvider;
import com.ibm.wala.util.strings.Atom;
import com.ibm.wala.util.warnings.Warning;
import com.ibm.wala.util.warnings.Warnings;

public class AndroidAnalysisContext {
	private static final Logger logger = LoggerFactory
			.getLogger(AndroidAnalysisContext.class);
	static {
//		((ch.qos.logback.classic.Logger) logger).setLevel(Level.TRACE);
	}
	private static final String methodSpec = "MethodSummaries.xml";
	private static final String pathToSpec = "data";

	private final ISCanDroidOptions options;
	private final AnalysisScope scope;
	private final ClassHierarchy cha;
	
	public AndroidAnalysisContext() {
		throw new IllegalArgumentException();
	}

	public AndroidAnalysisContext(ISCanDroidOptions options)
			throws IllegalArgumentException, ClassHierarchyException,
			IOException, CancelException, URISyntaxException {
		this(options, new FileProvider().getFile(/**"conf" + File.separator
				+ */"Java60RegressionExclusions.txt"));
	}

	/**
	 * @param exclusions
	 * @param classpath
	 * @param packagename
	 * @throws IOException
	 * @throws IllegalArgumentException
	 * @throws CancelException
	 * @throws ClassHierarchyException
	 * @throws URISyntaxException
	 */
	public AndroidAnalysisContext(ISCanDroidOptions options, File exclusions)
			throws IOException, IllegalArgumentException, CancelException,
			       ClassHierarchyException, URISyntaxException {
		logger.debug(DefaultSCanDroidOptions.dumpString(options));
		this.options = options;
		scope = AndroidAnalysisScope.setUpAndroidAnalysisScope(options.getAndroidLibrary(), options.getClasspath(), exclusions);
		
		
		cha = ClassHierarchy.make(scope);

		
		if (options.classHierarchyWarnings()) {
			// log ClassHierarchy warnings
			for (Iterator<Warning> wi = Warnings.iterator(); wi.hasNext();) {
				Warning w = wi.next();
				logger.warn(w.getMsg());
			}
		}
		Warnings.clear();
	}
	
	

	// ContextSelector, entry points, reflection options, IR Factory, call graph
	// type, include library
	public void buildGraphs(List<Entrypoint> localEntries,
			InputStream summariesStream) throws CancelException {

		

	}

	public static SSAPropagationCallGraphBuilder makeVanillaZeroOneCFABuilder(
			AnalysisOptions options, AnalysisCache cache, IClassHierarchy cha,
			AnalysisScope scope, ContextSelector customSelector,
			SSAContextInterpreter customInterpreter,
			InputStream summariesStream, MethodSummary extraSummary) {

		if (options == null) {
			throw new IllegalArgumentException("options is null");
		}
		Util.addDefaultSelectors(options, cha);
		// addDefaultBypassLogic(options, scope, Util.class.getClassLoader(),
		// cha);
		// addBypassLogic(options, scope,
		// AndroidAppLoader.class.getClassLoader(), methodSpec, cha);
		addBypassLogic(options, scope, summariesStream, cha, extraSummary);

		return ZeroXCFABuilder.make(cha, options, cache, customSelector,
				customInterpreter, ZeroXInstanceKeys.ALLOCATIONS
						| ZeroXInstanceKeys.CONSTANT_SPECIFIC);
	}

	/**
	 * @param options
	 *            options that govern call graph construction
	 * @param cha
	 *            governing class hierarchy
	 * @param scope
	 *            representation of the analysis scope
	 * @param customSelector
	 *            user-defined context selector, or null if none
	 * @param customInterpreter
	 *            user-defined context interpreter, or null if none
	 * @return a 0-CFA Call Graph Builder.
	 * @throws IllegalArgumentException
	 *             if options is null
	 *             
	 * TODO: move
	 */
	public static SSAPropagationCallGraphBuilder makeZeroCFABuilder(
			AnalysisOptions options, AnalysisCache cache, IClassHierarchy cha,
			AnalysisScope scope, ContextSelector customSelector,
			SSAContextInterpreter customInterpreter,
			InputStream summariesStream, MethodSummary extraSummary) {
				return makeZeroCFABuilder(options, cache, cha, scope,
						customSelector, customInterpreter, Lists.newArrayList(summariesStream),
						extraSummary);
			}

	/**
	 * @param options
	 *            options that govern call graph construction
	 * @param cha
	 *            governing class hierarchy
	 * @param scope
	 *            representation of the analysis scope
	 * @param customSelector
	 *            user-defined context selector, or null if none
	 * @param customInterpreter
	 *            user-defined context interpreter, or null if none
	 * @return a 0-CFA Call Graph Builder.
	 * @throws IllegalArgumentException
	 *             if options is null
	 *             
     * TODO: move
	 */
	public static SSAPropagationCallGraphBuilder makeZeroCFABuilder(
			AnalysisOptions options, AnalysisCache cache, IClassHierarchy cha,
			AnalysisScope scope, ContextSelector customSelector,
			SSAContextInterpreter customInterpreter,
			Collection<InputStream> summariesStreams, MethodSummary extraSummary) {

		if (options == null) {
			throw new IllegalArgumentException("options is null");
		}
		Util.addDefaultSelectors(options, cha);
		for (InputStream stream : summariesStreams) {
			addBypassLogic(options, scope, stream, cha, extraSummary);
		}

		return ZeroXCFABuilder.make(cha, options, cache, customSelector,
				customInterpreter, ZeroXInstanceKeys.NONE);
	}

	// public static void addBypassLogic(AnalysisOptions options, AnalysisScope
	// scope, ClassLoader cl, String xmlFile,
	// IClassHierarchy cha) throws IllegalArgumentException {
	public static void addBypassLogic(AnalysisOptions options,
			AnalysisScope scope, InputStream xmlIStream, IClassHierarchy cha,
			MethodSummary extraSummary) throws IllegalArgumentException {

		if (scope == null) {
			throw new IllegalArgumentException("scope is null");
		}
		if (options == null) {
			throw new IllegalArgumentException("options is null");
		}
		// if (cl == null) {
		// throw new IllegalArgumentException("cl is null");
		// }
		if (cha == null) {
			throw new IllegalArgumentException("cha cannot be null");
		}

		InputStream s = null;
		try {
			Set<TypeReference> summaryClasses = Sets.newHashSet();
			Map<MethodReference, MethodSummary> summaries = Maps.newHashMap();

			if (null != xmlIStream) {
				XMLMethodSummaryReader newSummaryXML = loadMethodSummaries(
						scope, xmlIStream);
				summaryClasses.addAll(newSummaryXML.getAllocatableClasses());
				for (MethodSummary summary : newSummaryXML.getSummaries().values()) {
					logger.trace("SSA instructions for summary of {}:\n{}", summary.getMethod().getSignature().toString(), Arrays.toString(summary.getStatements()));					
				}
				summaries.putAll(newSummaryXML.getSummaries());
			}
			logger.debug("loaded " + summaries.size() + " new summaries");
			// for (MethodReference mr : summaries.keySet()) {
			// logger.debug("summary loaded for: "+mr.getSignature());
			// }

			s = new FileProvider().getInputStreamFromClassLoader(pathToSpec
					+ File.separator + methodSpec,
					AndroidAnalysisContext.class.getClassLoader());

			XMLMethodSummaryReader nativeSummaries = loadMethodSummaries(scope,
					s);

			logger.debug("loaded " + nativeSummaries.getSummaries().size()
					+ " native summaries");

			summaries.putAll(nativeSummaries.getSummaries());
			summaryClasses.addAll(nativeSummaries.getAllocatableClasses());
			if (extraSummary != null) {
				summaries.put((MethodReference) extraSummary.getMethod(),
						extraSummary);
			}

			MethodTargetSelector ms = new BypassMethodTargetSelector(
					options.getMethodTargetSelector(), summaries,
					nativeSummaries.getIgnoredPackages(), cha);
			options.setSelector(ms);

			ClassTargetSelector cs = new BypassClassTargetSelector(
					options.getClassTargetSelector(), summaryClasses, cha,
					cha.getLoader(scope.getLoader(Atom
							.findOrCreateUnicodeAtom("Synthetic"))));
			options.setSelector(cs);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} finally {
			if (null != s) {
				try {
					s.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

	}

	private static XMLMethodSummaryReader loadMethodSummaries(
			AnalysisScope scope, InputStream xmlIStream)
			throws FileNotFoundException {
		InputStream s = xmlIStream;
		XMLMethodSummaryReader summary = null;

		try {
			if (null == s) {
				s = AndroidAnalysisContext.class.getClassLoader()
						.getResourceAsStream(
								pathToSpec + File.separator + methodSpec);
			}
			summary = new XMLMethodSummaryReader(s, scope);
		} finally {
			try {
				if (null != s) {
					s.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return summary;
	}
	
	/**
	 * Returns all concrete classes implementing the given interface or any subinterfaces
	 * @param iRoot
	 * @return
	 */
	public Collection<IClass> concreteClassesForInterface(IClass iRoot) {
		Set<IClass> clazzes = Sets.newHashSet();
		Set<IClass> done = Sets.newHashSet();
		Deque<IClass> todo = Queues.newArrayDeque();
		todo.push(iRoot);
		
		while (!todo.isEmpty()) {
			IClass i = todo.pop();
			for (IClass clazz : cha.getImplementors(i.getReference())) {
				if (clazz.isInterface() && !done.contains(clazz)) {
					done.add(i);
					todo.push(clazz);
				} else if (!clazz.isAbstract()) {
					clazzes.add(clazz);
				}
			}
		}
		
		return clazzes;
	}
	
	public ISCanDroidOptions getOptions() {
		return options;
	}
	
	public AnalysisScope getScope() {
		return scope;
	}
	
	public ClassHierarchy getClassHierarchy() {
		return cha;
	}
}
