/**
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This 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 software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.exoplatform.web.application.javascript;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.PushbackReader;
import java.io.Reader;
import java.io.Writer;


public class JSMin {
    private static final int EOF = -1;

    private PushbackReader in;

    private Writer out;

    private int theA;

    private int theB;

    public JSMin(Reader in, Writer out) {
        this.in = new PushbackReader(in);
        this.out = out;
    }

    /**
     * isAlphanum -- return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character.
     */
    static boolean isAlphanum(int c) {
        return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$' || c == '\\' || c > 126);
    }

    /**
     * get -- return the next character from stdin. Watch out for lookahead. If the character is a control character, translate
     * it to a space or linefeed.
     */
    int get() throws IOException {
        int c = in.read();

        if (c >= ' ' || c == '\n' || c == EOF) {
            return c;
        }

        if (c == '\r') {
            return '\n';
        }

        return ' ';
    }

    /**
     * Get the next character without getting it.
     */
    int peek() throws IOException {
        int lookaheadChar = in.read();
        in.unread(lookaheadChar);
        return lookaheadChar;
    }

    /**
     * next -- get the next character, excluding comments. peek() is used to see if a '/' is followed by a '/' or '*'.
     */
    int next() throws IOException, UnterminatedCommentException {
        int c = get();
        if (c == '/') {
            switch (peek()) {
                case '/':
                    for (;;) {
                        c = get();
                        if (c <= '\n') {
                            return c;
                        }
                    }

                case '*':
                    get();
                    for (;;) {
                        switch (get()) {
                            case '*':
                                if (peek() == '/') {
                                    get();
                                    return ' ';
                                }
                                break;
                            case EOF:
                                throw new UnterminatedCommentException();
                        }
                    }

                default:
                    return c;
            }

        }
        return c;
    }

    /**
     * action -- do something! What you do is determined by the argument: <br>
     * 1. Output A. Copy B to A. Get the next B. <br>
     * 2. Copy B to A. Get the next B. (Delete A). <br>
     * 3. Get the next B. (Delete B). action treats a string as a single character. <br>
     * Wow! action recognizes a regular expression if it is preceded by ( or , or =.
     */

    void action(int d) throws IOException, UnterminatedRegExpLiteralException, UnterminatedCommentException,
            UnterminatedStringLiteralException {
        switch (d) {
            case 1:
                out.write(theA);
            case 2:
                theA = theB;

                if (theA == '\'' || theA == '"') {
                    for (;;) {
                        out.write(theA);
                        theA = get();
                        if (theA == theB) {
                            break;
                        }
                        if (theA <= '\n') {
                            throw new UnterminatedStringLiteralException();
                        }
                        if (theA == '\\') {
                            out.write(theA);
                            theA = get();
                        }
                    }
                }

            case 3:
                theB = next();
                if (theB == '/'
                        && (theA == '(' || theA == ',' || theA == '=' || theA == ':' || theA == '[' || theA == '!'
                                || theA == '&' || theA == '|' || theA == '?' || theA == '{' || theA == '}' || theA == ';' || theA == '\n')) {
                    out.write(theA);
                    out.write(theB);
                    for (;;) {
                        theA = get();
                        if (theA == '/') {
                            break;
                        } else if (theA == '\\') {
                            out.write(theA);
                            theA = get();
                        } else if (theA <= '\n') {
                            throw new UnterminatedRegExpLiteralException();
                        }
                        out.write(theA);
                    }
                    theB = next();
                }
        }
    }

    /**
     * jsmin -- Copy the input to the output, deleting the characters which are insignificant to JavaScript. Comments will be
     * removed. Tabs will be replaced with spaces. Carriage returns will be replaced with linefeeds. Most spaces and linefeeds
     * will be removed.
     */
    public void jsmin() throws IOException, UnterminatedRegExpLiteralException, UnterminatedCommentException,
            UnterminatedStringLiteralException {
        theA = '\n';
        action(3);
        while (theA != EOF) {
            switch (theA) {
                case ' ':
                    if (isAlphanum(theB)) {
                        action(1);
                    } else {
                        action(2);
                    }
                    break;
                case '\n':
                    switch (theB) {
                        case '{':
                        case '[':
                        case '(':
                        case '+':
                        case '-':
                            action(1);
                            break;
                        case ' ':
                            action(3);
                            break;
                        default:
                            if (isAlphanum(theB)) {
                                action(1);
                            } else {
                                action(2);
                            }
                    }
                    break;
                default:
                    switch (theB) {
                        case ' ':
                            if (isAlphanum(theA)) {
                                action(1);
                                break;
                            }
                            action(3);
                            break;
                        case '\n':
                            switch (theA) {
                                case '}':
                                case ']':
                                case ')':
                                case '+':
                                case '-':
                                case '"':
                                case '\'':
                                    action(1);
                                    break;
                                default:
                                    if (isAlphanum(theA)) {
                                        action(1);
                                    } else {
                                        action(3);
                                    }
                            }
                            break;
                        default:
                            action(1);
                            break;
                    }
            }
        }
        out.flush();
    }

    class UnterminatedCommentException extends Exception {
    }

    class UnterminatedStringLiteralException extends Exception {
    }

    class UnterminatedRegExpLiteralException extends Exception {
    }

    public static void main(String[] arg) throws IOException, UnterminatedRegExpLiteralException, UnterminatedCommentException, UnterminatedStringLiteralException {
        JSMin jsmin = new JSMin(new FileReader(arg[0]), new PrintWriter(System.out));
        jsmin.jsmin();
    }

}
