package jdiff;

import java.io.*;
import java.util.*;
import javax.xml.parsers.ParserConfigurationException;

/* For SAX XML parsing */
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.*;

/**
 * Creates a Comments from an XML file. The Comments object is the internal 
 * representation of the comments for the changes.
 * All methods in this class for populating a Comments object are static.
 * 
 * See the file LICENSE.txt for copyright details.
 * @author Matthew Doar, mdoar@pobox.com
 */
public class Comments {

    /** 
     * All the possible comments known about, accessible by the commentID.
     */
    public static Hashtable allPossibleComments = new Hashtable();

    /** The old Comments object which is populated from the file read in. */ 
    private static Comments oldComments_ = null;

    /** Default constructor. */
    public Comments() {
        commentsList_ = new ArrayList(); // SingleComment[]
    }   
  
    // The list of comments elements associated with this objects
    public List commentsList_ = null; // SingleComment[]

    /** 
     * Read the file where the XML for comments about the changes between
     * the old API and new API is stored and create a Comments object for 
     * it. The Comments object may be null if no file exists.
     */
    public static Comments readFile(String filename) {
        // If validation is desired, write out the appropriate comments.xsd 
        // file in the same directory as the comments XML file.
        if (XMLToAPI.validateXML) {
            writeXSD(filename);
        }

        // If the file does not exist, return null
        File f = new File(filename);
        if (!f.exists())
            return null;

        // The instance of the Comments object which is populated from the file. 
        oldComments_ = new Comments();
        try {
            DefaultHandler handler = new CommentsHandler(oldComments_);
            XMLReader parser = null;
            try {
                parser = javax.xml.parsers.SAXParserFactory.newInstance().newSAXParser().getXMLReader();
            } catch (SAXException saxe) {
                System.out.println("SAXException: " + saxe);
                saxe.printStackTrace();
                System.exit(1);
            } catch (ParserConfigurationException pce) {
                System.out.println("ParserConfigurationException: " + pce);
                pce.printStackTrace();
                System.exit(1);
            }

            if (XMLToAPI.validateXML) {
                parser.setFeature("https://xml.org/sax/features/namespaces", true);
                parser.setFeature("https://xml.org/sax/features/validation", true);
                parser.setFeature("https://apache.org/xml/features/validation/schema", true);
            }
            parser.setContentHandler(handler);
            parser.setErrorHandler(handler);
            parser.parse(new InputSource(new FileInputStream(new File(filename))));
        } catch(org.xml.sax.SAXNotRecognizedException snre) {
            System.out.println("SAX Parser does not recognize feature: " + snre);
            snre.printStackTrace();
            System.exit(1);
        } catch(org.xml.sax.SAXNotSupportedException snse) {
            System.out.println("SAX Parser feature is not supported: " + snse);
            snse.printStackTrace();
            System.exit(1);
        } catch(org.xml.sax.SAXException saxe) {
            System.out.println("SAX Exception parsing file '" + filename + "' : " + saxe);
            saxe.printStackTrace();
            System.exit(1);
        } catch(java.io.IOException ioe) {
            System.out.println("IOException parsing file '" + filename + "' : " + ioe);
            ioe.printStackTrace();
            System.exit(1);
        }

        Collections.sort(oldComments_.commentsList_);
        return oldComments_;
    } //readFile()

    /**
     * Write the XML Schema file used for validation.
     */
    public static void writeXSD(String filename) {
        String xsdFileName = filename;
        int idx = xsdFileName.lastIndexOf('\\');
        int idx2 = xsdFileName.lastIndexOf('/');
        if (idx == -1 && idx2 == -1) {
            xsdFileName = "";
        } else if (idx == -1 && idx2 != -1) {
            xsdFileName = xsdFileName.substring(0, idx2+1);
        } else if (idx != -1  && idx2 == -1) {
            xsdFileName = xsdFileName.substring(0, idx+1);
        } else if (idx != -1  && idx2 != -1) {
            int max = idx2 > idx ? idx2 : idx;
            xsdFileName = xsdFileName.substring(0, max+1);
        }
        xsdFileName += "comments.xsd";
        try {
            FileOutputStream fos = new FileOutputStream(xsdFileName);
            PrintWriter xsdFile = new PrintWriter(fos);
            // The contents of the comments.xsd file
            xsdFile.println("<?xml version=\"1.0\" encoding=\"iso-8859-1\" standalone=\"no\"?>");
            xsdFile.println("<xsd:schema xmlns:xsd=\"https://www.w3.org/2001/XMLSchema\">");
            xsdFile.println();
            xsdFile.println("<xsd:annotation>");
            xsdFile.println("  <xsd:documentation>");
            xsdFile.println("  Schema for JDiff comments.");
            xsdFile.println("  </xsd:documentation>");
            xsdFile.println("</xsd:annotation>");
            xsdFile.println();
            xsdFile.println("<xsd:element name=\"comments\" type=\"commentsType\"/>");
            xsdFile.println();
            xsdFile.println("<xsd:complexType name=\"commentsType\">");
            xsdFile.println("  <xsd:sequence>");
            xsdFile.println("    <xsd:element name=\"comment\" type=\"commentType\" minOccurs='0' maxOccurs='unbounded'/>");
            xsdFile.println("  </xsd:sequence>");
            xsdFile.println("  <xsd:attribute name=\"name\" type=\"xsd:string\"/>");
            xsdFile.println("  <xsd:attribute name=\"jdversion\" type=\"xsd:string\"/>");
            xsdFile.println("</xsd:complexType>");
            xsdFile.println();
            xsdFile.println("<xsd:complexType name=\"commentType\">");
            xsdFile.println("  <xsd:sequence>");
            xsdFile.println("    <xsd:element name=\"identifier\" type=\"identifierType\" minOccurs='1' maxOccurs='unbounded'/>");
            xsdFile.println("    <xsd:element name=\"text\" type=\"xsd:string\" minOccurs='1' maxOccurs='1'/>");
            xsdFile.println("  </xsd:sequence>");
            xsdFile.println("</xsd:complexType>");
            xsdFile.println();
            xsdFile.println("<xsd:complexType name=\"identifierType\">");
            xsdFile.println("  <xsd:attribute name=\"id\" type=\"xsd:string\"/>");
            xsdFile.println("</xsd:complexType>");
            xsdFile.println();
            xsdFile.println("</xsd:schema>");
            xsdFile.close();
        } catch(IOException e) {
            System.out.println("IO Error while attempting to create " + xsdFileName);
            System.out.println("Error: " +  e.getMessage());
            System.exit(1);
        }
    }

//
// Methods to add data to a Comments object. Called by the XML parser and the 
// report generator.
//

    /**
     * Add the SingleComment object to the list of comments kept by this 
     * object. 
     */
    public void addComment(SingleComment comment) {
        commentsList_.add(comment); 
    }

//
// Methods to get data from a Comments object. Called by the report generator
//

    /** 
     * The text placed into XML comments file where there is no comment yet.
     * It never appears in reports.
     */
    public static final String placeHolderText = "InsertCommentsHere";
    
    /** 
     * Return the comment associated with the given id in the Comment object.
     * If there is no such comment, return the placeHolderText.
     */
    public static String getComment(Comments comments, String id) {
        if (comments == null)
            return placeHolderText;
        SingleComment key = new SingleComment(id, null);
        int idx = Collections.binarySearch(comments.commentsList_, key);
        if (idx < 0) {
            return placeHolderText;
        } else {
            int startIdx = comments.commentsList_.indexOf(key);
            int endIdx = comments.commentsList_.indexOf(key);
            int numIdx = endIdx - startIdx + 1;
            if (numIdx != 1) {
                System.out.println("Warning: " + numIdx + " identical ids in the existing comments file. Using the first instance.");
            }
            SingleComment singleComment = (SingleComment)(comments.commentsList_.get(idx));
            // Convert @link tags to links
            return singleComment.text_;
        }
    }

    /** 
     * Convert @link tags to HTML links. 
     */
    public static String convertAtLinks(String text, String currentElement, 
                                        PackageAPI pkg, ClassAPI cls) {
        if (text == null)
            return null;
	
        StringBuffer result = new StringBuffer();
        
        int state = -1;
        
        final int NORMAL_TEXT = -1;
        final int IN_LINK = 1;
        final int IN_LINK_IDENTIFIER = 2;
        final int IN_LINK_IDENTIFIER_REFERENCE = 3;
        final int IN_LINK_IDENTIFIER_REFERENCE_PARAMS = 6;
        final int IN_LINK_LINKTEXT = 4;
        final int END_OF_LINK = 5;

        StringBuffer identifier = null;
        StringBuffer identifierReference = null;
        StringBuffer linkText = null;
        
        // Figure out relative reference if required.
        String ref = "";
        if (currentElement.compareTo("class") == 0 ||
            currentElement.compareTo("interface") == 0) {
	    ref = pkg.name_ + "." + cls.name_ + ".";
        } else if (currentElement.compareTo("package") == 0) {
	    ref = pkg.name_ + ".";
        }
        ref = ref.replace('.', '/');        
        
        for (int i=0; i < text.length(); i++) {
	    char c = text.charAt( i);
	    char nextChar = i < text.length()-1 ? text.charAt( i+1) : (char)-1;
	    int remainingChars = text.length() - i;
          
	    switch (state) {
	    case NORMAL_TEXT:
		if (c == '{' && remainingChars >= 5) {
		    if ("{@link".equals(text.substring(i, i + 6))) {
			state = IN_LINK;
			identifier = null;
			identifierReference = null;
			linkText = null;
			i += 5;
			continue;
		    }
		}
		result.append( c);
		break;
	    case IN_LINK:
		if (Character.isWhitespace(nextChar)) continue;
		if (nextChar == '}') {
		    // End of the link
		    state = END_OF_LINK;
		} else if (!Character.isWhitespace(nextChar)) {
		    state = IN_LINK_IDENTIFIER;
		}
		break;
            case IN_LINK_IDENTIFIER:
		if (identifier == null) {
		    identifier = new StringBuffer();
		}
            
		if (c == '#') {
		    // We have a reference.
		    state = IN_LINK_IDENTIFIER_REFERENCE;
		    // Don't append #
		    continue;
		} else if (Character.isWhitespace(c)) {
		    // We hit some whitespace: the next character is the beginning
		    // of the link text.
		    state = IN_LINK_LINKTEXT;
		    continue;
		}
		identifier.append(c);              
		// Check for a } that ends the link.
		if (nextChar == '}') {
		    state = END_OF_LINK;
		}
		break;
            case IN_LINK_IDENTIFIER_REFERENCE:
		if (identifierReference == null) {
		    identifierReference = new StringBuffer();
		}
		if (Character.isWhitespace(c)) {
		    state = IN_LINK_LINKTEXT;
		    continue;
		}
		identifierReference.append(c);
              
		if (c == '(') {
		    state = IN_LINK_IDENTIFIER_REFERENCE_PARAMS;
		}
              
		if (nextChar == '}') {
		    state = END_OF_LINK;
		}
		break;
            case IN_LINK_IDENTIFIER_REFERENCE_PARAMS:
		// We're inside the parameters of a reference. Spaces are allowed.
		if (c == ')') {
		    state = IN_LINK_IDENTIFIER_REFERENCE;
		}
		identifierReference.append(c);
		if (nextChar == '}') {
		    state = END_OF_LINK;
		}
		break;
            case IN_LINK_LINKTEXT:
		if (linkText == null) linkText = new StringBuffer();
              
		linkText.append(c);
              
		if (nextChar == '}') {
		    state = END_OF_LINK;
		}
		break;
            case END_OF_LINK:
		if (identifier != null) {
		    result.append("<A HREF=\"");
		    result.append(HTMLReportGenerator.newDocPrefix);
		    result.append(ref);
		    result.append(identifier.toString().replace('.', '/'));
		    result.append(".html");
		    if (identifierReference != null) {
			result.append("#");
			result.append(identifierReference);
		    }
		    result.append("\">");   // target=_top?
                
		    result.append("<TT>");
		    if (linkText != null) {
			result.append(linkText);
		    } else {
			result.append(identifier);
			if (identifierReference != null) {
			    result.append(".");
			    result.append(identifierReference);
			}
		    }
		    result.append("</TT>");
		    result.append("</A>");
		}
		state = NORMAL_TEXT;
		break;
	    }
        }
        return result.toString();
    }

//
// Methods to write a Comments object out to a file.
//

    /**
     * Write the XML representation of comments to a file.
     *
     * @param outputFileName The name of the comments file.
     * @param oldComments The old comments on the changed APIs.
     * @param newComments The new comments on the changed APIs.
     * @return true if no problems encountered
     */
    public static boolean writeFile(String outputFileName, 
                                    Comments newComments) {
        try {
            FileOutputStream fos = new FileOutputStream(outputFileName);
            outputFile = new PrintWriter(fos);
            newComments.emitXMLHeader(outputFileName);
            newComments.emitComments();
            newComments.emitXMLFooter();
            outputFile.close();
        } catch(IOException e) {
            System.out.println("IO Error while attempting to create " + outputFileName);
            System.out.println("Error: "+ e.getMessage());
            System.exit(1);
        }
        return true;
    }
    
    /**
     * Write the Comments object out in XML.
     */
    public void emitComments() {
        Iterator iter = commentsList_.iterator();
        while (iter.hasNext()) {
            SingleComment currComment = (SingleComment)(iter.next());
            if (!currComment.isUsed_)
                outputFile.println("<!-- This comment is no longer used ");
            outputFile.println("<comment>");
            outputFile.println("  <identifier id=\"" + currComment.id_ + "\"/>");
            outputFile.println("  <text>");
            outputFile.println("    " + currComment.text_);
            outputFile.println("  </text>");
            outputFile.println("</comment>");
            if (!currComment.isUsed_)
                outputFile.println("-->");
        }        
    }

    /** 
     * Dump the contents of a Comments object out for inspection.
     */
    public void dump() {
        Iterator iter = commentsList_.iterator();
        int i = 0;
        while (iter.hasNext()) {
            i++;
            SingleComment currComment = (SingleComment)(iter.next());
            System.out.println("Comment " + i);
            System.out.println("id = " + currComment.id_);
            System.out.println("text = \"" + currComment.text_ + "\"");
            System.out.println("isUsed = " + currComment.isUsed_);
        }        
    }

    /**
     * Emit messages about which comments are now unused and which are new.
     */
    public static void noteDifferences(Comments oldComments, Comments newComments) {
        if (oldComments == null) {
            System.out.println("Note: all the comments have been newly generated");
            return;
        }
        
        // See which comment ids are no longer used and add those entries to 
        // the new comments, marking them as unused.
        Iterator iter = oldComments.commentsList_.iterator();
        while (iter.hasNext()) {
            SingleComment oldComment = (SingleComment)(iter.next());
            int idx = Collections.binarySearch(newComments.commentsList_, oldComment);
            if (idx < 0) {
                System.out.println("Warning: comment \"" + oldComment.id_ + "\" is no longer used.");
                oldComment.isUsed_ = false;
                newComments.commentsList_.add(oldComment);
            }
        }        
        
    }

    /**
     * Emit the XML header.
     */
    public void emitXMLHeader(String filename) {
        outputFile.println("<?xml version=\"1.0\" encoding=\"iso-8859-1\" standalone=\"no\"?>");
        outputFile.println("<comments");
        outputFile.println("  xmlns:xsi='" + RootDocToXML.baseURI + "/2001/XMLSchema-instance'");
        outputFile.println("  xsi:noNamespaceSchemaLocation='comments.xsd'");
        // Extract the identifier from the filename by removing the suffix
        int idx = filename.lastIndexOf('.');
        String apiIdentifier = filename.substring(0, idx);
        // Also remove the output directory and directory separator if present
        if (HTMLReportGenerator.commentsDir != null)
	    apiIdentifier = apiIdentifier.substring(HTMLReportGenerator.commentsDir.length()+1);
        else if (HTMLReportGenerator.outputDir != null)
            apiIdentifier = apiIdentifier.substring(HTMLReportGenerator.outputDir.length()+1);
        // Also remove "user_comments_for_"
        apiIdentifier = apiIdentifier.substring(18);
        outputFile.println("  name=\"" + apiIdentifier + "\"");
        outputFile.println("  jdversion=\"" + JDiff.version + "\">");
        outputFile.println();
        outputFile.println("<!-- Use this file to enter an API change description. For example, when you remove a class, ");
        outputFile.println("     you can enter a comment for that class that points developers to the replacement class. ");
        outputFile.println("     You can also provide a change summary for modified API, to give an overview of the changes ");
        outputFile.println("     why they were made, workarounds, etc.  -->");
        outputFile.println();
        outputFile.println("<!-- When the API diffs report is generated, the comments in this file get added to the tables of ");
        outputFile.println("     removed, added, and modified packages, classes, methods, and fields. This file does not ship ");
        outputFile.println("     with the final report. -->");
        outputFile.println();
        outputFile.println("<!-- The id attribute in an identifier element identifies the change as noted in the report. ");
        outputFile.println("     An id has the form package[.class[.[ctor|method|field].signature]], where [] indicates optional ");
        outputFile.println("     text. A comment element can have multiple identifier elements, which will will cause the same ");
        outputFile.println("     text to appear at each place in the report, but will be converted to separate comments when the ");
        outputFile.println("     comments file is used. -->");
        outputFile.println();
        outputFile.println("<!-- HTML tags in the text field will appear in the report. You also need to close p HTML elements, ");
        outputFile.println("     used for paragraphs - see the top-level documentation. -->");
        outputFile.println();
        outputFile.println("<!-- You can include standard javadoc links in your change descriptions. You can use the @first command  ");
        outputFile.println("     to cause jdiff to include the first line of the API documentation. You also need to close p HTML ");
        outputFile.println("     elements, used for paragraphs - see the top-level documentation. -->");
        outputFile.println();
    }

    /**
     * Emit the XML footer.
     */
    public void emitXMLFooter() {
        outputFile.println();
        outputFile.println("</comments>");
    }

    private static List oldAPIList = null;
    private static List newAPIList = null;

    /** 
     * Return true if the given HTML tag has no separate </tag> end element. 
     *
     * If you want to be able to use sloppy HTML in your comments, then you can
     * add the element, e.g. li back into the condition here. However, if you 
     * then become more careful and do provide the closing tag, the output is 
     * generally just the closing tag, which is incorrect.
     *
     * tag.equalsIgnoreCase("tr") || // Is sometimes minimized
     * tag.equalsIgnoreCase("th") || // Is sometimes minimized
     * tag.equalsIgnoreCase("td") || // Is sometimes minimized
     * tag.equalsIgnoreCase("dt") || // Is sometimes minimized
     * tag.equalsIgnoreCase("dd") || // Is sometimes minimized
     * tag.equalsIgnoreCase("img") || // Is sometimes minimized
     * tag.equalsIgnoreCase("code") || // Is sometimes minimized (error)
     * tag.equalsIgnoreCase("font") || // Is sometimes minimized (error)
     * tag.equalsIgnoreCase("ul") || // Is sometimes minimized
     * tag.equalsIgnoreCase("ol") || // Is sometimes minimized
     * tag.equalsIgnoreCase("li") // Is sometimes minimized
     */
    public static boolean isMinimizedTag(String tag) {
        if (tag.equalsIgnoreCase("p") ||
            tag.equalsIgnoreCase("br") ||
            tag.equalsIgnoreCase("hr")
            ) {
            return true;
	}
        return false;
    }

    /** 
     * The file where the XML representing the new Comments object is stored. 
     */
    private static PrintWriter outputFile = null;
    
}


