/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.HashSet;
import java.util.Set;

import com.sun.javatest.util.I18NResourceBundle;

/**
 * A utility to rewrite links within a set of HTML files.
 */
public class EditLinks
{
    /**
     * An exception to report bad command line arguments.
     */
    public static class BadArgs extends Exception {
	BadArgs(I18NResourceBundle i18n, String key) {
	    super(i18n.getString(key));
	}
	BadArgs(I18NResourceBundle i18n, String key, Object arg) {
	    super(i18n.getString(key, arg));
	}
    }

    /**
     * Command line entry point.<br>
     * Usage:
     * <pre>
     *    java com.sun.javatest.EditLinks options files...
     * </pre>
     * Arguments:
     * <dl>
     * <dt>-e oldPrefix newPrefix
     * <dd>Links beginning with oldPrefix are rewritten to begin with newPrefix
     * <dt>-ignore file
     * <dd>Ignore files and directories named 'file' when scanning directories.
     *     E.g. -ignore SCCS
     * <dt>-o file
     * <dd>Output file or directory. It should only be a file if the input is a
     *     single file; otherwise it should be a directory.
     * <dt>files...
     * <dd>Input files or directories to be copied, with the links edited.
     * </dl>
     * @param args Command line arguments, per the usage as described.
     */
    public static void main(String[] args) {
	try {
	    if (args.length == 0)
		usage(System.err);
	    else {
		EditLinks el = new EditLinks(args);
		if (el.edits.length == 0)
		    System.err.println(i18n.getString("editLinks.noEdits"));
		el.run();
	    }
	}
	catch (BadArgs e) {
	    System.err.println(e.getMessage());
	    usage(System.err);
	    System.exit(1);
	}
	catch (IOException e) {
	    System.err.println(e);
	}
	catch (Throwable t) {
	    t.printStackTrace();
	    System.exit(2);
	}
    }

    /**
     * Write out short command line help.
     * @param out A stream to which to write the help.
     */
    private static void usage(PrintStream out) {
	String program = System.getProperty("program", "java " + EditLinks.class.getName());
	String msg = i18n.getString("editLinks.usage", program);
	int start = 0;
	int i;
	while ((i = msg.indexOf("\n", start)) != -1) {
	    System.err.println(msg.substring(start, i));
	    start = i + 1;
	}
	if (start < msg.length())
	    System.err.println(msg.substring(start));
    }

    /**
     * Create an empty editor object.
     */
    public EditLinks() {
    }

    /**
     * Create an editor object based on command line args.
     * It is an error if no edits, no input files, or no output file is given.
     * @param args Command line args.
     * @see #main
     * @throws EditLinks.BadArgs if problems are found in the given arguments.
     */
    public EditLinks(String[] args) throws BadArgs {
	for (int i = 0; i < args.length; i++) {
	    if (args[i].equals("-e") && i + 2 < args.length) {
		String oldPrefix = args[++i];
		String newPrefix = args[++i];
		addEdit(oldPrefix, newPrefix);
	    }
	    else if (args[i].equals("-ignore") && i + 2 < args.length) {
		ignore(args[++i]);
	    }		    
	    else if (args[i].equals("-o") && i + 1 < args.length) {
		outFile = new File(args[++i]);
	    }
	    else if (args[i].startsWith("-")) {
		throw new BadArgs(i18n, "editLinks.badOpt", args[i]);
	    }
	    else {
		inFiles = new File[args.length - i];
		for (int j = 0; j < inFiles.length; j++)
		    inFiles[j] = new File(args[i++]);
	    }
	}
	
	if (inFiles == null || inFiles.length == 0) 
	    throw new BadArgs(i18n, "editLinks.noInput");
	
	if (outFile == null) {
	    if (inFiles.length == 1)
		outFile = inFiles[0];
	    else
		throw new BadArgs(i18n, "editLinks.noOutput");
	}
    }

    /**
     * Add another edit to be applied when the files are edited.
     * @param oldPrefix The prefix of HTML references to be updated.
     * @param newPrefix The replacement value for occurrences of oldPrefix.
     */
    public void addEdit(String oldPrefix, String newPrefix) {
	String[][] newEdits = new String[edits.length + 1][];
	System.arraycopy(edits, 0, newEdits, 0, edits.length);
	newEdits[edits.length] = new String[] { oldPrefix, newPrefix };
	edits = newEdits;
    }

    /**
     * Add another file to be ignored when the files are edited.
     * For example, specify "SCCS" to ignore SCCS directories.
     * @param file The name of a file to be ignored when editing.
     */
    public void ignore(String file) {
	ignores.add(file);
    }
    
    /**
     * Edit the files set up by the {@link #EditLinks(String[])} constructor.
     * @throws IOException if any errors occur while editing the specified files.
     */
    public void run() throws IOException {
	edit(inFiles, outFile);
    }

    /**
     * Edit the given files, using the current set of edits and ignores.
     * The source files may be files or directories; the destination can 
     * be a directory, or a file if the source is a single file.
     * @param src An array of files or directories of files to be edited.
     * @param dest A destination file for the edit.
     * @throws IOException if any problems occur while editing the specified
     * files.
     * @throws IllegalArgumentException if the destination is a single file 
     * but the source file is not.
     * @see #edit(File, File)
     */
    public void edit(File[] src, File dest) throws IOException {
	for (int i = 0; i < src.length; i++)
	    edit(src[i], dest);
    }

    /**
     * Edit the given file, using the current set of edits and ignores.
     * The source files may be file or directory; the destination can 
     * be a directory, or a file if the source is a single file.
     * @param src A file or directory of files to be edited.
     * @param dest A destination file for the edit.
     * @throws IOException if any problems occur while editing the specified
     * files.
     * @throws IllegalArgumentException if the destination is a single file 
     * but the source file is not.
     * @see #edit(File, File)
     */
    public void edit(File src, File dest) throws IOException {
	if (!src.exists())
	    throw new FileNotFoundException(src.getPath());

	if (src.isDirectory()) {
	    if (!dest.exists()) {
		if (!dest.mkdir())
		    throw new FileNotFoundException(dest.getPath());
	    }
	    else if (!dest.isDirectory())
		throw new IllegalArgumentException(i18n.getString("editLinks.dirExpected", dest));

	    File canonicalDest = dest.getCanonicalFile();
	    ignores.add(canonicalDest);
	    String[] files = src.list();
	    if (files != null) {
		for (int i = 0; i < files.length; i++) {
		    String file = files[i];
		    File srcFile = new File(src, file);
		    File destFile = new File(dest, file);
		    if (ignores.contains(file) || ignores.contains(srcFile.getCanonicalFile())) {
			continue;
		    }
		    edit(srcFile, destFile);
		}
	    }
	    ignores.remove(canonicalDest);
	}	
	else {
	    if (dest.exists() && dest.isDirectory()) {
		// source is a file, but the target is a directory, so assume a
		// file of the same name in the target directory
		dest = new File(dest, src.getName());
	    }

	    if (src.getName().endsWith(".html")) {
		in = new BufferedReader(new FileReader(src));
		if (dest.equals(src)) {
		    int size = (int)(src.length());
		    char data[] = new char[size];
		    for (int total = 0; total < data.length; ) {
			total += in.read(data, total, data.length - total);
		    }
		    in.close();
		    String s = new String(data, 0, data.length);
		    in = new StringReader(s);
		}
		out = new BufferedWriter(new FileWriter(dest));
		currFile = src;
		line = 1;
		edit(in, out);
		in.close();
		out.close();
	    }
	    else
		copyFile(src, dest);
	}
    }

    /**
     * Copy a file.
     */
    private void copyFile(File from, File to) throws IOException {
	if (from.equals(to))
	    return;

	int size = (int)(from.length());
	byte data[] = new byte[size];

	InputStream in = new BufferedInputStream(new FileInputStream(from));
	try {
	    for (int total = 0; total < data.length; ) {
		total += in.read(data, total, data.length - total);
	    }
	} 
	finally {
	    in.close();
	}

	OutputStream out = new BufferedOutputStream(new FileOutputStream(to));
	try {
	    out.write(data, 0, data.length);
	}
	finally {
	    out.close();
	}
    }

    /**
     * Copy the input stream to the output, looking for <a href="....">.
     * If a link is found that begins with one of the given edit strings,
     * it is rewritten to begin with the replacement string.
     */
    private void edit(Reader in, Writer out) throws IOException {
	copying = false;
	nextCh();
	copying = true;
	while (c >= 0) {
	    if (c == '<') {
		nextCh();
		skipSpace();
		switch (c) {
		case '!':
		    nextCh();
		    if (c == '-') {
			nextCh();
			if (c == '-') {
			    nextCh();
			    skipComment();
			}
		    }
		    break;
		    
		case '/':
		    nextCh();
		    scanIdentifier();
		    skipTag();
		    break;

		default:
		    String tag = scanIdentifier();
		    if (tag.equals("a"))
			scanLink();
		    else
			skipTag();
		}
	    }
	    else {
		nextCh();
	    }
	}
	
    }

    /**
     * Process the contents of <a href=...>
     */
    private void scanLink() throws IOException {
	skipSpace();
	while (c != '>') {
	    String att = scanIdentifier();
	    if (att.equalsIgnoreCase("href")) {
		// the current character should be a whitespace or =
		// either way, we just write out =
		out.write('=');
		copying = false;
		String target = scanValue();
		out.write('"');
		out.write(edit(target));
		out.write('"');
		copying = true;
	    }
	    else 
		scanValue();
	    skipSpace();
	}
	nextCh();
    }

    /**
     * Read an identifier, and lowercase it
     */
    private String scanIdentifier() throws IOException {
	StringBuffer buf = new StringBuffer();
	while (true) {
	    if ((c >= 'a') && (c <= 'z')) {
		buf.append((char)c);
		nextCh();
	    } else if ((c >= 'A') && (c <= 'Z')) {
	        buf.append((char)('a' + (c - 'A')));
	        nextCh();
	    } else if ((c >= '0') && (c <= '9')) {
	        buf.append((char)c);
	        nextCh();
	    } else if (c == '-') {  // needed for <META HTTP-EQUIV ....>
	        buf.append((char)c);
	        nextCh();
	    } else
	        if (buf.length() == 0)
		    throw new IOException(i18n.getString("editLinks.idExpected",
							 new Object[] { currFile, new Integer(line) }));
		else
	            return buf.toString();
	}
    }

    /**
     * Read the value of an HTML attribute, which may be quoted.
     */
    private String scanValue() throws IOException {
	skipSpace();
	if (c != '=')
	    return "";

	int quote = -1;
	nextCh();
	skipSpace();
	if ((c == '\'') || (c == '\"')) {
	    quote = c;
	    nextCh();
	    skipSpace();
	}
	StringBuffer buf = new StringBuffer();
	while (((quote < 0) && (c != ' ') && (c != '\t') &&
		(c != '\n') && (c != '\r') && (c != '>')) ||
	       ((quote >= 0) && (c != quote))) {
	    if (c == -1 || c == '\n' || c == '\r') {
		throw new IOException(i18n.getString("editLinks.mismatchQuotes",
							 new Object[] { currFile, new Integer(line) }));
	    }
	    buf.append((char)c);
	    nextCh();
	}
	if (c == quote)
	    nextCh();
	skipSpace();
	return buf.toString();
    }

    /**
     * Skip an HTML comment  <!-- ... -->
     */
    private void skipComment() throws IOException {
	// a comment sequence is "<!--" ... "-->"
	// at the time this is called, "<!--" has been read;
	int numHyphens = 0;
	//System.out.print("SKIPCOMMENT: <!--" + (char)c);
	while (c != -1 && (numHyphens < 2 || c != '>')) {
	    if (c == '-')
		numHyphens++;
	    else
		numHyphens = 0;
	    nextCh();
	    //System.out.print((char)c);
	}
	//System.out.println("END SKIPCOMMENT");
	nextCh();
    }

    /**
     * Skip whitespace.
     */
    private void skipSpace() throws IOException {
	while ((c == ' ') || (c == '\t') || (c == '\n') || (c == '\r')) {
	    nextCh();
	}
    }

    /**
     * Skip the contents of an HTML tag i.e. <...>
     */
    private void skipTag() throws IOException {
	skipSpace();
	while (c != '>') {
	    String att = scanIdentifier();
	    if (att == "")
		throw new IOException(i18n.getString("editLinks.badHTML",
							 new Object[] { currFile, new Integer(line) }));
	    String value = scanValue();
	    skipSpace();
	}
	nextCh();
    }

    /**
     * Read the next character, after writing out the previous one, if <code>copying</code>
     * is set to <code>true</code>.
     */
    private void nextCh() throws IOException {
	if (copying)
	    out.write((char) c);
	c = in.read();
	if (c == '\n')
	    line++;
    }

    /**
     * Edit the contents of a link. If it begins with one of the current set of
     * edits, the prefix is replaced with the corresponding new value.
     */
    private String edit(String ref) {
	for (int i = 0; i < edits.length; i++) {
	    String[] entry = edits[i];
	    if (ref.startsWith(entry[0])) {
		String oldHead = entry[0];
		String newHead = entry[1];
		char oldSep = guessSep(oldHead);
		char newSep = guessSep(newHead);
		String oldTail = ref.substring(oldHead.length());
		String newTail;
		if (oldSep != 0 && newSep != 0 && oldSep != newSep)
		    newTail = oldTail.replace(oldSep, newSep);
		else
		    newTail = oldTail;
		return (newHead + newTail);
	    }
	}
	return ref;
    }

    /**
     * Guess the file separator in a string by looking for whichever
     * is later of / and \. If neither are found, the result is 0.
     */
    private static char guessSep(String path) {
	int fwd = path.lastIndexOf('/');
	int back = path.lastIndexOf('\\');
	if (fwd > back)
	    return '/';
	else if (back > fwd)
	    return '\\';
	else
	    return (char)0;
    }

    private File[] inFiles = new File[0];
    private File outFile;
    private String[][] edits = new String[0][];
    private Set ignores = new HashSet();

    private int c;
    private int line;
    private File currFile;
    private Reader in;
    private Writer out;
    private boolean copying;

    private static final I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(EditLinks.class);
}
