/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2006, David Hovemeyer <daveho@users.sourceforge.net>
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.ba;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.regex.Pattern;

import javax.annotation.WillClose;

import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.util.Util;

/**
 * Global information about the source code for an application.
 * Currently, this object contains a map of source line information
 * for fields and classes (items we don't get line number information
 * for directly in classfiles), and also source line information
 * for methods that don't appear directly in classfiles,
 * such as abstract and native methods.
 * 
 * @author David Hovemeyer
 */
public class SourceInfoMap {
	static class FieldDescriptor implements Comparable<FieldDescriptor> {
		String className;
		String fieldName;

		public FieldDescriptor(String className, String fieldName) {
			this.className = className;
			this.fieldName = fieldName;
		}

		@Override
		public String toString() {
			return className +"." + fieldName;
		}
		/* (non-Javadoc)
		 * @see java.lang.Comparable#compareTo(T)
		 */
		public int compareTo(FieldDescriptor o) {
			int cmp = className.compareTo(o.className);
			if (cmp != 0)
				return cmp;
			return fieldName.compareTo(o.fieldName);
		}

		/* (non-Javadoc)
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			return 1277 * className.hashCode() +fieldName.hashCode(); 
		}

		/* (non-Javadoc)
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj) {
			if (obj == null || obj.getClass() != this.getClass())
				return false;
			FieldDescriptor other = (FieldDescriptor) obj;
			return className.equals(other.className) && fieldName.equals(other.fieldName);
		}
	}

	static class MethodDescriptor implements Comparable<MethodDescriptor> {
		private String className;
		private String methodName;
		private String methodSignature;

		public MethodDescriptor(String className, String methodName, String methodSignature) {
			this.className = className;
			this.methodName = methodName;
			this.methodSignature = methodSignature;
		}
		@Override
		public String toString() {
			return className +"." + methodName + ":" + methodSignature;
		}
		/* (non-Javadoc)
		 * @see java.lang.Comparable#compareTo(T)
		 */
		public int compareTo(MethodDescriptor o) {
			int cmp;
			if ((cmp = className.compareTo(o.className)) != 0)
				return cmp;
			if ((cmp = methodName.compareTo(o.methodName)) != 0)
				return cmp;
			return methodSignature.compareTo(o.methodSignature);
		}

		/* (non-Javadoc)
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			return 1277 * className.hashCode()
				+ 37 * methodName.hashCode()
				+ methodSignature.hashCode();
		}

		/* (non-Javadoc)
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj) {
			if (obj == null || obj.getClass() != this.getClass())
				return false;
			MethodDescriptor other = (MethodDescriptor) obj;
			return className.equals(other.className)
				&& methodName.equals(other.methodName)
				&& methodSignature.equals(other.methodSignature);
		}
	}

	/**
	 * A range of source lines.
	 */
	public static class SourceLineRange {
		private final Integer start, end;

		/**
		 * Constructor for a single line.
		 */
		public SourceLineRange(@NonNull Integer line) {
			this.start = this.end = line;
		}

		/**
		 * Constructor for a range of lines.
		 * 
		 * @param start start line in range
		 * @param end   end line in range
		 */
		public SourceLineRange(@NonNull Integer start, @NonNull Integer end) {
			this.start = start;
			this.end = end;
		}

		/**
		 * @return Returns the start.
		 */
		public @NonNull Integer getStart() {
			return start;
		}

		/**
		 * @return Returns the end.
		 */
		public @NonNull Integer getEnd() {
			return end;
		}

		/* (non-Javadoc)
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString() {
			return start + (start.equals(end) ? "" : "-" + end);
		}
	}

	private static final boolean DEBUG = SystemProperties.getBoolean("sourceinfo.debug");

	private Map<FieldDescriptor, SourceLineRange> fieldLineMap;
	private Map<MethodDescriptor, SourceLineRange> methodLineMap;
	private Map<String, SourceLineRange> classLineMap;

	public boolean fallBackToClassfile() {
		return isEmpty();
	}
	public boolean isEmpty() {
		return fieldLineMap.isEmpty() && methodLineMap.isEmpty() && classLineMap.isEmpty();
	}
	/**
	 * Constructor.
	 * Creates an empty object.
	 */
	public SourceInfoMap() {
		this.fieldLineMap = new HashMap<FieldDescriptor, SourceLineRange>();
		this.methodLineMap = new HashMap<MethodDescriptor, SourceLineRange>();
		this.classLineMap = new HashMap<String, SourceLineRange>();
	}

	/**
	 * Add a line number entry for a field.
	 * 
	 * @param className name of class containing the field
	 * @param fieldName name of field
	 * @param range      the line number(s) of the field
	 */
	public void addFieldLine(String className, String fieldName, SourceLineRange range) {
		fieldLineMap.put(new FieldDescriptor(className, fieldName), range);
	}

	/**
	 * Add a line number entry for a method.
	 * 
	 * @param className       name of class containing the method
	 * @param methodName      name of method
	 * @param methodSignature signature of method
	 * @param range           the line number of the method
	 */
	public void addMethodLine(String className, String methodName, String methodSignature,
			SourceLineRange range) {
		methodLineMap.put(new MethodDescriptor(className, methodName, methodSignature), range);
	}

	/**
	 * Add line number entry for a class.
	 * 
	 * @param className name of class
	 * @param range      the line numbers of the class
	 */
	public void addClassLine(String className, SourceLineRange range) {
		classLineMap.put(className, range);
	}

	/**
	 * Look up the line number range for a field.
	 * 
	 * @param className name of class containing the field
	 * @param fieldName name of field
	 * @return the line number range, or null if no line number is known for the field
	 */
	public @CheckForNull SourceLineRange getFieldLine(String className, String fieldName) {
		return fieldLineMap.get(new FieldDescriptor(className, fieldName));
	}

	/**
	 * Look up the line number range for a method.
	 * 
	 * @param className       name of class containing the method
	 * @param methodName      name of method
	 * @param methodSignature signature of method
	 * @return the line number range, or null if no line number is known for the method
	 */
	public @CheckForNull SourceLineRange getMethodLine(String className, String methodName, String methodSignature) {
		return methodLineMap.get(new MethodDescriptor(className, methodName, methodSignature));
	}

	/**
	 * Look up the line number range for a class.
	 * 
	 * @param className name of the class
	 * @return the line number range, or null if no line number is known for the class
	 */
	public @CheckForNull SourceLineRange getClassLine(String className) {
		return classLineMap.get(className);
	}

	private static final Pattern DIGITS = Pattern.compile("^[0-9]+$");

	/**
	 * Read source info from given InputStream.
	 * The stream is guaranteed to be closed.
	 * 
	 * @param inputStream  the InputStream
	 * @throws IOException if an I/O error occurs, or if the format is invalid
	 */
	public void read(@WillClose InputStream inputStream) throws IOException {
		BufferedReader reader = new BufferedReader(Util.getReader(inputStream));

		int lineNumber = 0;
		try {
			String line;
			int lparen;
			String version;

			while ((line = reader.readLine()) != null) {
				++lineNumber;

				if (lineNumber == 1) {
					if (DEBUG) System.out.println("First line: " + line);
					// Try to parse the version number string from the first line.
					// null means that the line does not appear to be a version number.
					version = parseVersionNumber(line);
					if (version != null) {
						// Check to see if version is supported.
						// Only 1.0 supported for now.
						if (!version.equals("1.0"))
							throw new IOException("Unsupported sourceInfo version " + version);

						// Version looks good.  Skip to next line of file.
						continue;
					}
				}

				StringTokenizer tokenizer = new StringTokenizer(line, ",");

				String className = tokenizer.nextToken();
				String next = tokenizer.nextToken();
				if (DIGITS.matcher(next).matches()) {
					// Line number for class
					SourceLineRange range = createRange(next, tokenizer.nextToken());
					classLineMap.put(className, range);
					if (DEBUG) System.out.println("class:" + className + "," + range);
				} else if ((lparen = next.indexOf('(')) >= 0) {
					// Line number for method
					String methodName = next.substring(0, lparen);
					String methodSignature = next.substring(lparen);

					if (methodName.equals("init^"))
						methodName = "<init>";
					else if (methodName.equals("clinit^"))
						methodName = "<clinit>";

					SourceLineRange range = createRange(tokenizer.nextToken(), tokenizer.nextToken());
					methodLineMap.put(new MethodDescriptor(className, methodName, methodSignature), range);
					if (DEBUG) System.out.println("method:" + methodName+methodSignature + "," + range);
				} else {
					// Line number for field
					String fieldName = next;
					SourceLineRange range = createRange(tokenizer.nextToken(), tokenizer.nextToken());
					fieldLineMap.put(
							new FieldDescriptor(className, fieldName),
							range);
					if (DEBUG) System.out.println("field:" + className + "," +
							fieldName + "," + range);
				}

				// Note: we could complain if there are more tokens,
				// but instead we'll just ignore them.
			}
		} catch (NoSuchElementException e) {
			IOException ioe =
				new IOException("Invalid syntax in source info file at line " + lineNumber);
			ioe.initCause(e);
			throw ioe;
		} finally {
			try {
				reader.close();
			} catch (IOException e) {
				// ignore
			}
		}
	}

	/**
	 * Parse the sourceInfo version string.
	 * 
	 * @param line the first line of the sourceInfo file
	 * @return the version number constant, or null if
	 *         the line does not appear to be a version string
	 */
	private static String parseVersionNumber(String line) {
		StringTokenizer tokenizer = new StringTokenizer(line, " \t");

		if (!expect(tokenizer, "sourceInfo")
				|| !expect(tokenizer, "version")
				|| !tokenizer.hasMoreTokens())
			return null;

		return tokenizer.nextToken();
	}

	/**
	 * Expect a particular token string to be returned by the given
	 * StringTokenizer.
	 * 
	 * @param tokenizer the StringTokenizer
	 * @param token     the expectedToken
	 * @return true if the expected token was returned, false if not
	 */
	private static boolean expect(StringTokenizer tokenizer, String token) {
		if (!tokenizer.hasMoreTokens())
			return false;
		String s = tokenizer.nextToken();
		if (DEBUG) System.out.println("token="+s);
		return s.equals(token);
	}

	private static SourceLineRange createRange(String start, String end) {
		return new SourceLineRange(Integer.valueOf(start), Integer.valueOf(end));
	}
}
