/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999-2005 Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package sample.preproc;

import java.io.IOException;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.util.Vector;
import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.ClassPool;

/**
 * This is a preprocessor for Java source programs using annotated
 * import declarations.
 *
 * <ul><pre>
 * import <i>class-name</i> by <i>assistant-name</i> [(<i>arg1, arg2, ...</i>)]
 * </pre></ul>
 *
 * <p>To process this annotation, run this class as follows:
 *
 * <ul><pre>
 * java sample.preproc.Compiler sample.j
 * </pre></ul>
 *
 * <p>This command produces <code>sample.java</code>, which only includes
 * regular import declarations.  Also, the Javassist program
 * specified by <i>assistant-name</i> is executed so that it produces
 * class files under the <code>./tmpjvst</code> directory.  The class
 * specified by <i>assistant-name</i> must implement
 * <code>sample.preproc.Assistant</code>.
 *
 * @see sample.preproc.Assistant
 */

public class Compiler {
    protected BufferedReader input;
    protected BufferedWriter output;
    protected ClassPool classPool;

    /**
     * Constructs a <code>Compiler</code> with a source file.
     *
     * @param inputname         the name of the source file.
     */
    public Compiler(String inputname) throws CannotCompileException {
        try {
            input = new BufferedReader(new FileReader(inputname));
        }
        catch (IOException e) {
            throw new CannotCompileException("cannot open: " + inputname);
        }

        String outputname = getOutputFilename(inputname);
        if (outputname.equals(inputname))
            throw new CannotCompileException("invalid source name: "
                                             + inputname);

        try {
            output = new BufferedWriter(new FileWriter(outputname));
        }
        catch (IOException e) {
            throw new CannotCompileException("cannot open: " + outputname);
        }

        classPool = ClassPool.getDefault();
    }

    /**
     * Starts preprocessing.
     */
    public void process() throws IOException, CannotCompileException {
        int c;
        CommentSkipper reader = new CommentSkipper(input, output);
        while ((c = reader.read()) != -1) {
            output.write(c);
            if (c == 'p') {
                if (skipPackage(reader))
                    break;
            }
            else if (c == 'i')
                readImport(reader);
            else if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
                break;
        }

        while ((c = input.read()) != -1)
            output.write(c);

        input.close();
        output.close();
    }

    private boolean skipPackage(CommentSkipper reader) throws IOException {
        int c;
        c = reader.read();
        output.write(c);
        if (c != 'a')
            return true;

        while ((c = reader.read()) != -1) {
            output.write(c);
            if (c == ';')
                break;
        }

        return false;
    }

    private void readImport(CommentSkipper reader)
                                throws IOException, CannotCompileException
    {
        int word[] = new int[5];
        int c;
        for (int i = 0; i < 5; ++i) {
            word[i] = reader.read();
            output.write(word[i]);
        }

        if (word[0] != 'm' || word[1] != 'p' || word[2] != 'o'
            || word[3] != 'r' || word[4] != 't')
            return;     // syntax error?

        c = skipSpaces(reader, ' ');
        StringBuffer classbuf = new StringBuffer();
        while (c != ' ' && c != '\t' && c != '\n' && c != '\r'
               && c != ';' && c != -1) {
            classbuf.append((char)c);
            c = reader.read();
        }

        String importclass = classbuf.toString();
        c = skipSpaces(reader, c);
        if (c == ';') {
            output.write(importclass);
            output.write(';');
            return;
        }
        if (c != 'b')
            syntaxError(importclass);

        reader.read();  // skip 'y'

        StringBuffer assistant = new StringBuffer();
        Vector args = new Vector();
        c = readAssistant(reader, importclass, assistant, args);
        c = skipSpaces(reader, c);
        if (c != ';')
            syntaxError(importclass);

        runAssistant(importclass, assistant.toString(), args);
    }

    void syntaxError(String importclass) throws CannotCompileException {
        throw new CannotCompileException("Syntax error.  Cannot import "
                                         + importclass);
    }

    int readAssistant(CommentSkipper reader, String importclass,
                      StringBuffer assistant, Vector args)
        throws IOException, CannotCompileException
    {
        int c = readArgument(reader, assistant);
        c = skipSpaces(reader, c);
        if (c == '(') {
            do {
                StringBuffer arg = new StringBuffer();
                c = readArgument(reader, arg);
                args.addElement(arg.toString());
                c = skipSpaces(reader, c);
            } while (c == ',');

            if (c != ')')
                syntaxError(importclass);

            return reader.read();
        }

        return c;
    }

    int readArgument(CommentSkipper reader, StringBuffer buf)
        throws IOException
    {
        int c = skipSpaces(reader, ' ');
        while ('A' <= c && c <= 'Z' || 'a' <= c && c <= 'z'
               || '0' <= c && c <= '9' || c == '.' || c == '_') {
            buf.append((char)c);
            c = reader.read();
        }

        return c;
    }

    int skipSpaces(CommentSkipper reader, int c) throws IOException {
        while (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
            if (c == '\n' || c == '\r')
                output.write(c);

            c = reader.read();
        }

        return c;
    }

    /**
     * Is invoked if this compiler encoutenrs:
     *
     * <ul><pre>
     * import <i>class name</i> by <i>assistant</i> (<i>args1</i>, <i>args2</i>, ...);
     * </pre></ul>
     *
     * @param   classname       class name
     * @param   assistantname   assistant
     * @param   argv            args1, args2, ...
     */
    private void runAssistant(String importname, String assistantname,
                              Vector argv)
        throws IOException, CannotCompileException
    {
        Class assistant;
        Assistant a;
        int s = argv.size();
        String[] args = new String[s];
        for (int i = 0; i < s; ++i)
            args[i] = (String)argv.elementAt(i);

        try {
            assistant = Class.forName(assistantname);
        }
        catch (ClassNotFoundException e) {
            throw new CannotCompileException("Cannot find " + assistantname);
        }

        try {
            a = (Assistant)assistant.newInstance();
        }
        catch (Exception e) {
            throw new CannotCompileException(e);
        }

        CtClass[] imports = a.assist(classPool, importname, args);
        s = imports.length;
        if (s < 1)
            output.write(" java.lang.Object;");
        else {
            output.write(' ');
            output.write(imports[0].getName());
            output.write(';');
            for (int i = 1; i < s; ++i) {
                output.write(" import ");
                output.write(imports[1].getName());
                output.write(';');
            }
        }
    }

    private String getOutputFilename(String input) {
        int i = input.lastIndexOf('.');
        if (i < 0)
            i = input.length();

        return input.substring(0, i) + ".java";
    }

    public static void main(String[] args) {
        if (args.length > 0)
            try {
                Compiler c = new Compiler(args[0]);
                c.process();
            }
            catch (IOException e) {
                System.err.println(e);
            }
            catch (CannotCompileException e) {
                System.err.println(e);
            }
        else {
            System.err.println("Javassist version " + CtClass.version);
            System.err.println("No source file is specified.");
        }
    }
}

class CommentSkipper {
    private BufferedReader input;
    private BufferedWriter output;

    public CommentSkipper(BufferedReader reader, BufferedWriter writer) {
        input = reader;
        output = writer;
    }

    public int read() throws IOException {
        int c;
        while ((c = input.read()) != -1)
            if (c != '/')
                return c;
            else {
                c = input.read();
                if (c == '/')
                    skipCxxComments();
                else if (c == '*')
                    skipCComments();
                else 
                    output.write('/');
            }

        return c;
    }

    private void skipCxxComments() throws IOException {
        int c;
        output.write("//");
        while ((c = input.read()) != -1) {
            output.write(c);
            if (c == '\n' || c == '\r')
                break;
        }
    }

    private void skipCComments() throws IOException {
        int c;
        boolean star = false;
        output.write("/*");
        while ((c = input.read()) != -1) {
            output.write(c);
            if (c == '*')
                star = true;
            else if(star && c == '/')
                break;
            else
                star = false;
        }
    }
}