/*******************************************************************************
 * Based heavily on Jacoco's Analyzer.java, original author and license below.
 * Modified by Jonathan Bell
 *
 *
 * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
 * 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:
 *    Marc R. Hoffmann - initial API and implementation
 *
 *******************************************************************************/
package edu.gmu.swe.kp.report.jacoco;

import org.jacoco.core.analysis.ICoverageVisitor;
import org.jacoco.core.data.ExecutionDataStore;
import org.jacoco.core.internal.ContentTypeDetector;
import org.jacoco.core.internal.Java9Support;
import org.jacoco.core.internal.Pack200Streams;
import org.jacoco.core.internal.analysis.StringPool;
import org.jacoco.core.internal.data.CRC64;
import org.jacoco.core.internal.flow.ClassProbesAdapter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * An {@link Analyzer} instance processes a set of Java class files and
 * calculates coverage data for them. For each class file the result is reported
 * to a given {@link ICoverageVisitor} instance. In addition the
 * {@link Analyzer} requires a {@link ExecutionDataStore} instance that holds
 * the execution data for the classes to analyze. The {@link Analyzer} offers
 * several methods to analyze classes from a variety of sources.
 */
public class Analyzer {

	private final HashMap<String, HashMap<String, boolean[]>> executionData;
	private final SessionCoverageVisitor visitor;

	private final StringPool stringPool;
	private final JSONFormatter formatter;

	/**
	 * Creates a new analyzer reporting to the given output.
	 *
	 * @param executionData execution data
	 * @param visitor       the output instance that will coverage data for every analyzed
	 *                      class
	 */
	public Analyzer(final HashMap<String, HashMap<String, boolean[]>> executionData, final JSONFormatter formatter,
	                final SessionCoverageVisitor visitor, StringPool stringPool) {
		this.executionData = executionData;
		this.visitor = visitor;
		this.formatter = formatter;
		this.stringPool = stringPool;
	}

	/**
	 * Creates an ASM class visitor for analysis.
	 *
	 * @param classid   id of the class calculated with {@link CRC64}
	 * @param className VM name of the class
	 * @return ASM visitor to write class definition to
	 */
	private ClassVisitor createAnalyzingVisitor(final long classid,
	                                            final String className) {
//		final ClassCoverageImpl coverage = new ClassCoverageImpl(className,
//				classid, false);
		final ClassAnalyzer analyzer = new ClassAnalyzer(visitor, executionData,
				stringPool, classid, className) {
			@Override
			public void visitEnd() {
				super.visitEnd();
//				coverageVisitor.visitCoverage(coverage);
			}
		};
		return new ClassProbesAdapter(analyzer, false);
	}

	/**
	 * Analyzes the class given as a ASM reader.
	 *
	 * @param reader reader with class definitions
	 */
	public void analyzeClass(final ClassReader reader) {
		try {
			final ClassVisitor visitor = createAnalyzingVisitor(
					CRC64.checksum(reader.b), reader.getClassName());
			reader.accept(visitor, 0);
		} catch (Throwable t) {
			t.printStackTrace();
		}
	}

	/**
	 * Analyzes the class definition from a given in-memory buffer.
	 *
	 * @param buffer   class definitions
	 * @param location a location description used for exception messages
	 * @throws IOException if the class can't be analyzed
	 */
	public void analyzeClass(final byte[] buffer, final String location)
			throws IOException {
		try {
			analyzeClass(
					new ClassReader(Java9Support.downgradeIfRequired(buffer)));
		} catch (final Throwable cause) {
			System.err.println("Error while analyzing " + location);
			cause.printStackTrace();
//			throw analyzerError(location, cause);
		}
	}

	/**
	 * Analyzes the class definition from a given input stream.
	 *
	 * @param input    stream to read class definition from
	 * @param location a location description used for exception messages
	 * @throws IOException if the stream can't be read or the class can't be analyzed
	 */
	public void analyzeClass(final InputStream input, final String location)
			throws IOException {
		final byte[] buffer;
		try {
			buffer = Java9Support.readFully(input);
		} catch (final IOException e) {
			throw analyzerError(location, e);
		}
		analyzeClass(buffer, location);
	}

	private IOException analyzerError(final String location,
	                                  final Exception cause) {
		final IOException ex = new IOException(String.format(
				"Error while analyzing %s.", location));
		ex.initCause(cause);
		return ex;
	}

	/**
	 * Analyzes all classes found in the given input stream. The input stream
	 * may either represent a single class file, a ZIP archive, a Pack200
	 * archive or a gzip stream that is searched recursively for class files.
	 * All other content types are ignored.
	 *
	 * @param input    input data
	 * @param location a location description used for exception messages
	 * @return number of class files found
	 * @throws IOException if the stream can't be read or a class can't be analyzed
	 */
	public int analyzeAll(final InputStream input, final String location)
			throws IOException {
		final ContentTypeDetector detector;
		try {
			detector = new ContentTypeDetector(input);
		} catch (IOException e) {
			throw analyzerError(location, e);
		}
		switch (detector.getType()) {
			case ContentTypeDetector.CLASSFILE:
				analyzeClass(detector.getInputStream(), location);
				return 1;
			case ContentTypeDetector.ZIPFILE:
				return analyzeZip(detector.getInputStream(), location);
			case ContentTypeDetector.GZFILE:
				return analyzeGzip(detector.getInputStream(), location);
			case ContentTypeDetector.PACK200FILE:
				return analyzePack200(detector.getInputStream(), location);
			default:
				return 0;
		}
	}

	/**
	 * Analyzes all class files contained in the given file or folder. Class
	 * files as well as ZIP files are considered. Folders are searched
	 * recursively.
	 *
	 * @param file file or folder to look for class files
	 * @return number of class files found
	 * @throws IOException if the file can't be read or a class can't be analyzed
	 */
	public int analyzeAll(final File file) throws IOException {
		int count = 0;
		if (file.isDirectory()) {
			for (final File f : file.listFiles()) {
				count += analyzeAll(f);
			}
		} else {
			final InputStream in = new FileInputStream(file);
			try {
				count += analyzeAll(in, file.getPath());
			} finally {
				in.close();
			}
		}
		return count;
	}

	/**
	 * Analyzes all classes from the given class path. Directories containing
	 * class files as well as archive files are considered.
	 *
	 * @param path    path definition
	 * @param basedir optional base directory, if <code>null</code> the current
	 *                working directory is used as the base for relative path
	 *                entries
	 * @return number of class files found
	 * @throws IOException if a file can't be read or a class can't be analyzed
	 */
	public int analyzeAll(final String path, final File basedir)
			throws IOException {
		int count = 0;
		final StringTokenizer st = new StringTokenizer(path,
				File.pathSeparator);
		while (st.hasMoreTokens()) {
			count += analyzeAll(new File(basedir, st.nextToken()));
		}
		return count;
	}

	private int analyzeZip(final InputStream input, final String location)
			throws IOException {
		final ZipInputStream zip = new ZipInputStream(input);
		ZipEntry entry;
		int count = 0;
		while ((entry = nextEntry(zip, location)) != null) {
			count += analyzeAll(zip, location + "@" + entry.getName());
		}
		return count;
	}

	private ZipEntry nextEntry(ZipInputStream input, String location)
			throws IOException {
		try {
			return input.getNextEntry();
		} catch (IOException e) {
			throw analyzerError(location, e);
		}
	}

	private int analyzeGzip(final InputStream input, final String location)
			throws IOException {
		GZIPInputStream gzipInputStream;
		try {
			gzipInputStream = new GZIPInputStream(input);
		} catch (IOException e) {
			throw analyzerError(location, e);
		}
		return analyzeAll(gzipInputStream, location);
	}

	private int analyzePack200(final InputStream input, final String location)
			throws IOException {
		InputStream unpackedInput;
		try {
			unpackedInput = Pack200Streams.unpack(input);
		} catch (IOException e) {
			throw analyzerError(location, e);
		}
		return analyzeAll(unpackedInput, location);
	}

}
