/*
 * 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.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.WillClose;

import edu.umd.cs.findbugs.SystemProperties;
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));
    }
}
