/*
 *
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * This program 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.
 *
 * This program 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 at /legal/license.txt).
 *
 * 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 or visit www.sun.com if you need additional
 * information or have any questions.
 *
 */

/**
 * Generates localized string data from source XML file.
 */
package com.sun.midp.l10n.generator;

import java.io.*;
import java.util.*;

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.w3c.dom.*;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;
import org.xml.sax.SAXException;


/**
 *  Driver class with main(). Parses command line arguments and invokes
 *  LocalizedStringsGeneratorImpl instance that does all the transformation work.
 */
public class LocalizedStringsGenerator {
    /** Transformer that does actual transformation */
    private static LocalizedStringsGeneratorImpl transformer = null;

    /** XML file name to apply transformation to */
    public static String xmlFileName = "";

    /** Output file name */
    public static String outFileName = "";

    /** Print debug output while running */
    private static boolean debug = false;

    /** Print usage info and exit */
    private static boolean printHelp = false;


    /**
     * Main method
     *
     * @param args Command line arguments
     */
    public static void main(String[] args) {
        try {
            parseArgs(args);
            if (printHelp) {
                printHelp();
                return;
            }

            transformer = new LocalizedStringsGeneratorImpl(debug);
            transformer.transform(xmlFileName, outFileName);
        } catch (SAXException e) {
            // error was already reported
            e.printStackTrace();
            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    /**
     * Parse command line arguments, adding Transformation objects to
     * <tt>transformations</tt> vector for each transformation specified.
     *
     * @param args command line arguments
     */
    private static void parseArgs(String[] args)
    {
        for (int i = 0; i < args.length; ++i) {
            String arg = args[i];
            if (debug) {
                System.err.println("arg: " + arg);
            }
            if (arg.equals("-xml")) {
                xmlFileName = args[++i];
            } else if (arg.equals("-out")) {
                outFileName = args[++i];
            } else if (arg.equals("-debug")) {
                debug = true;
            } else {
                printHelp = true;
            }
        }
    }

    /**
     * Print usage information
     */
    private static void printHelp() {
        /**
         * Following options are recognized:
         * -xml:        Source XML file.
         * -out:        Output file. If empty, output will be to stdout.
         * -help:       Print usage information
         * -debug:      Be verbose: print some debug info while running.
         */
        System.err.println("Usage: java -jar <l10n_generator_jar_file>"
            + "-xml <localXMLFile> "
            + "-out <localOutputFile> "
            + "[-debug] "
            + "[-help]");
    }
}


/**
 * Perform the transformation
 */
class LocalizedStringsGeneratorImpl {
    /** Factory constructing Transformer objects */
    private TransformerFactory transformerFactory =
        TransformerFactory.newInstance();

    /** Be verbose: print some debug info while running */
    private boolean debug = false;


    /**
     * Constructor
     *
     * @param dbg print some debug output while running
     */
    public LocalizedStringsGeneratorImpl(boolean dbg)
    {
        debug = dbg;
    }

    /**
     * Converts errors.
     */
    class GeneratorErrorHandler
        implements ErrorHandler {

        /**
         * Handles errors.
         * @param e the parsing exception
         */
        public void error(SAXParseException e)
            throws SAXParseException {
            reportError(e);
            // rethrow exception to stop processing on first error
            throw e;
        }

        /**
         * Handles fatal errors.
         * @param e the parsing exception
         */
        public void fatalError(SAXParseException e) {
            reportError(e);
        }

        /**
         * Handles warnings.
         * @param e the parsing exception
         */
        public void warning(SAXParseException e) {
            reportError(e);
        }

        /**
         * Outputs diagnostic messages.
         * @param e the parsing exception
         */
        private void reportError(SAXParseException e) {
            String msg = e.getMessage();
            String location = e.getSystemId();
            int line = e.getLineNumber();

            System.err.print("Error: URI=" + location);
            System.err.println(" Line=" + line + ": " + msg);
        }
    }

    /**
     * Do the actual transformation
     *
     * @param tr transformation to perform
     */
    public void transform(String xmlFileName, String outFileName)
        throws Exception {

        if (debug) {
            System.err.println("xml file: " + xmlFileName);
            System.err.println("out file: " + outFileName);
        }

        // load XML file as DOM tree
        DocumentBuilderFactory domFactory =
            DocumentBuilderFactory.newInstance();

        DocumentBuilder domBuilder = domFactory.newDocumentBuilder();

        domBuilder.setErrorHandler(new GeneratorErrorHandler());
        Document doc = domBuilder.parse(new File(xmlFileName));

        // make source from it
        DOMSource source = new DOMSource(doc);

        generateLocalizedStrings(doc, outFileName);
    }

    /**
     * Creates a directory structure.
     *
     * @param fullFileName Full path to the file to be created. If directory
     * in which file is to be created doesn't exists, it will be created
     * @exception IOException is thrown if directory couldn't be created
     */
    private void makeDirectoryTree(String fullFileName) throws IOException {
        if (debug == true) {
            System.out.println("mkdir: " + fullFileName);
        }
        int index = fullFileName.lastIndexOf(File.separatorChar);
        if (index == -1) {
            // To be compatible with MKS-hosted build on win32, which
            // does not translate / to \.
            index = fullFileName.lastIndexOf('/');
        }
        File outputDirectory = new File(fullFileName.substring(0, index));

        if (!(outputDirectory).exists()) {
            if (!(outputDirectory).mkdirs()) {
                throw new IOException("failed to create output directory");
            }
        }
    }

    /**
     * Generates localized strings.
     * @param doc the current  working file
     * @param outDir the target output directory
     */
    private void generateLocalizedStrings(Document doc, String outDir)
         throws Exception
    {
        processLocalizedStrings(doc.getDocumentElement(), null);
        for (Enumeration e = locales.keys(); e.hasMoreElements();) {
            String key = (String)e.nextElement();
            LocalizedStringSet locale = (LocalizedStringSet)locales.get(key);
            String filename = outDir + File.separatorChar + locale.className;
            makeDirectoryTree(filename);

            // Write LocalizedStrings<locale>.c file
            CSourceWriter cwriter = new CSourceWriter();
            cwriter.writeCSource(locale, filename + ".c", locale.className);

            // Write LocalizedStrings<locale>.java file
            FileOutputStream out = new FileOutputStream(filename + ".java");
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(out));
            writer.println("// This file is auto-generated. Don't edit!");
            writer.println("package com.sun.midp.l10n;");
            writer.println("abstract class " + locale.className + " {");
            writer.println("    native static String getContent(int index);");
            writer.println("}");
            writer.close();
        }
    }

    /**
     * Walk the XML tree to discover all <localized_string> elements,
     * collect them and write the data tables specific for each
     * locale to a pair of Java and C source files.
     * @param n the current node to be processed
     * @param className the name of the class to handle this node
     */
    private void processLocalizedStrings(Node n, String className)
        throws Exception {
        if (n.getNodeName().equals("localized_strings") &&
            (n instanceof Element)) {
            Element e = (Element)n;
            String pkg = e.getAttribute("Package");
            String name = e.getAttribute("Name");
            className = name;
        } else if (n.getNodeName().equals("localized_string") &&
            (n instanceof Element) && className != null) {
            Element e = (Element)n;
            String value = e.getAttribute("Value");
            // we use key value as index
            int valueIndex = Integer.parseInt(e.getAttribute("KeyValue"));

            addLocalizedString(className, valueIndex, value);
        }

        NodeList list = n.getChildNodes();
        for (int i=0; i<list.getLength(); i++) {
            processLocalizedStrings(list.item(i), className);
        }
    }

    /** Supported locales table. */
    Hashtable locales = new Hashtable();

    /**
     * Add one <localized_string> element to the LocalizedStringSet of
     * the enclosing <localized_strings>.
     * @param className the locales handler
     * @param valueIndex key for this entry
     * @param value data for this entry
     */
    private void addLocalizedString(String className,
            int valueIndex, String value)
        throws Exception
    {
        LocalizedStringSet locale = (LocalizedStringSet)locales.get(className);
        if (locale == null) {
            locale = new LocalizedStringSet(className);
            locales.put(className, locale);
        }
        locale.put(valueIndex, value);
    }
}


/**
 * Records all localized strings for the locale represented by
 * a given Java class.
 */
class LocalizedStringSet {
    /** This Java class represents a given locale. */
    String className;

    /** All localized strings for this locale */
    Vector strings;

    /**
     * Default constructor.
     * @param className class name of locale handler
     */
    LocalizedStringSet(String className) {
        this.className = className;
        strings = new Vector(512);
    }

    /**
     * Stores the current entry.
     * @param idx the key
     * @param value the data
     */
    void put(int idx, String value) {
        if (strings.size() < idx + 1) {
            strings.setSize(idx + 1);
        }
        strings.set(idx, value);
    }
}

/**
 * This class records related information about each localized string
 */
class LocalizedString {
    /**
     * Constructor with initial data.
     * @param i the key
     * @param s the value
     */
    LocalizedString(int i, String s) {
        this.index = i;
        this.string = s;
    }
    /** The current search location. */
    int index;
    /** The offset of the current item. */
    int offset;
    /** The length of the table. */
    int length;
    /** The data contents. */
    String string;
}

/**
 * This class deals with generating the LocalizedStringsBase<locale>.c
 * files. We try to compress the footprint of the data, since we
 * don't read from the localized string tables very often
 */
class CSourceWriter {
    /** The locale used. */
    LocalizedStringSet locale;
    /** Output stream writer. */
    PrintWriter writer;
    /** Array of strings to be processed. */
    LocalizedString strings[];

    /**
     * Sort all string data so they can be more easily compressed
     * @return the sorted array
     */
    ArrayList sort() {
        ArrayList list = new ArrayList();
        Vector strTable = locale.strings;
        for (int i = 0; i < strTable.size(); ++i) {
            String value = (String)strTable.get(i);

            if (strings[i] != null) {
                System.out.println("Duplicated content definiton for index " +
                                   i);
                throw new Error();
            }
            strings[i] = new LocalizedString(i, value);
            list.add(strings[i]);
        }

        /**
         * Sorting function.
         */
        Collections.sort(list, new Comparator() {
            /**
             * Comparison method.
             * @param o1 left hand operand
             * @param o2 right hand operand
             * @return positive value if o2 is greater than o1,
             * negative is o2 is less than o1
             */
            public int compare(Object o1, Object o2) {
                LocalizedString r1 = (LocalizedString)o1;
                LocalizedString r2 = (LocalizedString)o2;
                return (r1.string.length() < r2.string.length()) ? 1 : -1;
            }
        });

        return list;
    }

    /**
     * Writes the C source file that stores the local string data in a
     * compact data structure
     * @param locale the target locale for comparison purposes
     * @param filename the source for processing
     * @param classname the locale handler
     */
    void writeCSource(LocalizedStringSet locale,
                      String filename, String classname)
         throws IOException
    {
        this.locale = locale;
        this.strings = new LocalizedString[locale.strings.size()];

        FileOutputStream out = new FileOutputStream(filename);
        writer = new PrintWriter(new OutputStreamWriter(out));
        pl("/* This file is auto-generated. Do not edit! */");
        pl("#include <kni.h>");
        pl("#include <string.h>");
        pl("#include <midpMalloc.h>");

        try {
            int type = writeOptimizedArray();

            pl("KNIEXPORT KNI_RETURNTYPE_OBJECT");
            pl("KNIDECL(com_sun_midp_l10n_" + classname + "_getContent) {");
            if (type != UNICODE) {
                pl("    char stackbuffer[128];");
                pl("    char *utf8; /*0-terminated*/");
            }
            pl("    int index = KNI_GetParameterAsInt(1);");
            pl("    KNI_StartHandles(1);");
            pl("    KNI_DeclareHandle(string);");
            pl("    if (index >= 0 && index <= max_index) {");
            pl("        int offset = (int)offset_data[index * 2 + 0];");
            pl("        int length = (int)offset_data[index * 2 + 1];");
            pl("        if (offset >= 0) {");
            if (type == UNICODE) {
                plx("KNI_NewString(string_data+offset, length, string);");
            } else {
                plx("const char *p = string_data+offset;");
                plx("if (length < 128) {");
                plx("    utf8 = stackbuffer;");
                plx("} else {");
                plx("    utf8 = (char*)midpMalloc(length+1);");
                plx("}");
                plx("if (utf8) {");
                plx("    memcpy(utf8, p, length);");
                plx("    utf8[length] = 0;");
                plx("    KNI_NewStringUTF(utf8, string);");
                plx("}");
                plx("if (utf8 && utf8 != stackbuffer) {");
                plx("    midpFree(utf8);");
                plx("}");
            }
            pl("        }");
            pl("    }");
            pl("    KNI_EndHandlesAndReturnObject(string)");
            pl("}");
        } catch (Throwable t) {
            t.printStackTrace();
        }

        writer.close();
    }

    /**
     * Short-hand for printing a line to the output file
     * @param s the string to output
     */
    void pl(String s) {
        writer.println(s);
    }

    /**
     * Short-hand for printing a line to the output file (with a 12-space
     * prefix)
     * @param s the string to output
     */
    void plx(String s) {
        writer.print("            ");
        writer.println(s);
    }

    /**
     * Short-hand for printing a string into the output file
     * @param s the string to output
     */
    void p(String s) {
        writer.print(s);
    }

    /** Indicate that a table should be stored as an UTF8 array in C code. */
    public final int UTF8    = 1;

    /** Indicate that a table should be stored as an UNICODE array in C code.*/
    public final int UNICODE = 2;

    /**
     * Write the string data using UTF8 or UNICODE, depending on which
     * is smaller.
     * @return the storage type
     */
    int writeOptimizedArray() throws Throwable {
        // (1) Sort all strings and then merge them into a single String.
        // This allows shorter strings that are substrings of longer strings
        // to be omitted.
        ArrayList list = sort();
        StringBuffer sbuf = new StringBuffer();
        for (int i=0; i<list.size(); i++) {
            LocalizedString r = (LocalizedString)list.get(i);
            if (r != null) {
                int n = sbuf.indexOf(r.string);
                if (n >= 0) {
                    r.offset = n;
                } else {
                    r.offset = sbuf.length();
                    sbuf.append(r.string);
                }
                r.length = r.string.length();
            }
        }
        int type = getStorageType(sbuf);
        switch (type) {
        case UTF8:
            writeUTF8(sbuf, list);
            break;
        case UNICODE:
            writeUNICODE(sbuf);
            break;
        }

        writeOffsetTable();

        return type;
    }

    /**
     * Tell which one of UTF8 or UNICODE is smaller
     * @param sbuf the string data to be processed
     * @return the storage type
     */
    int getStorageType(StringBuffer sbuf) throws Throwable {
        boolean hasZero = false;
        for (int i=0; i<sbuf.length(); i++) {
            char c = sbuf.charAt(i);
            if (c == 0x0) {
                hasZero = true;
            }
        }

        if (hasZero) {
            // The VM's handling of 0 is different than specified in
            // the "official" UTF8 encoding, so let's not do it.
        } else {
            String s = sbuf.toString();
            byte bytes[] = s.getBytes("UTF-8");
            if (bytes.length < sbuf.length() * 2) {
                return UTF8;
            }
        }

        return UNICODE;
    }

    /** Hex digits for output. */
    static char digits[] = {
        '0', '1', '2', '3', '4', '5', '6', '7',
        '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
    };

    /**
     * Prints a hex number for the given character.
     * @param c the character to convert
     * @param numdigits the output field width
     */
    void printHex(char c, int numdigits) {
        p("0x");
        for (int i=numdigits-1; i>= 0; i--) {
            int shift = i * 4;
            int n = ((((int)c) >> shift)) & 0x0f;
            writer.print(Character.toString(digits[n]));
        }
    }

    /**
     * Write all the data in a UNICODE array
     * @param sbuf the string data to be processed
     */
    void writeUNICODE(StringBuffer sbuf) {
        int STEP = 10;
        int MAX = sbuf.length();
        pl("static const jchar string_data[] = {");

        for (int i=0; i<MAX; i ++) {
            if ((i % STEP) == 0) {
                pl("");
                p("    ");
            }
            char c = sbuf.charAt(i);
            printHex(c, 4);
            p(",");
        }
        pl("};");
    }

    /**
     * Write all the data in a UTF8 array
     * @param sbuf the string data to be processed
     * @param list the data to be processed
     */
    void writeUTF8(StringBuffer sbuf, ArrayList list) throws Throwable {
        // Recompute the offset and length of each item
        byte array[] = new byte[sbuf.length() * 2]; // MAX possible size
        int end = 0;

        for (int i=0; i<list.size(); i++) {
            LocalizedString r = (LocalizedString)list.get(i);
            if (r != null) {
                byte bytes[] = r.string.getBytes("UTF-8");
                int n = search(bytes, array, end);
                if (n >= 0) {
                    r.offset = n;
                } else {
                    r.offset = end;
                    for (int x=0; x<bytes.length; x++) {
                        array[end++] = bytes[x];
                    }
                }
                r.length = bytes.length;
            }
        }

        // Now write the strings to C source code.
        int STEP = 10;
        int MAX = end;

        p("static const char string_data[] = {");
        for (int i=0; i<MAX; i ++) {
            if ((i % STEP) == 0) {
                pl("");
                p("    ");
            }
            char c = (char)array[i];
            printHex(c, 2);
            p(",");
        }
        pl("};");
    }

    /**
     * Equivalent of String.indexOf(), but works on a UTF8 byte array.
     * @param needle the item to be found
     * @param haystack the data to be searched
     * @param end the limit of the search
     * @return index of the found data or -1 if not found
     */
    int search(byte needle[], byte haystack[], int end) {
        int needleLen = needle.length;
        if (needleLen == 0) {
            return -1;
        }
        end -= needleLen;
        byte b = needle[0];

        for (int i=0; i<end; i++) {
            inner: {
                if (haystack[i] == b) {
                    for (int j=1; j<needleLen; j++) {
                        if (haystack[i+j] != needle[j]) {
                            break inner;
                        }
                    }
                    return i;
                }
            }
        }

        return -1;
    }

    /**
     * Write a table of localized string offsets
     */
    void writeOffsetTable() {
        int maxOffset = 0, maxLen = 0;

        for (int i=0; i<strings.length; i++) {
            LocalizedString r = strings[i];
            if (r == null) {
                System.err.println("Warning: resource index " + i +
                                   " not defined in class " +
                                   locale.className);
            } else {
                if (maxOffset < r.offset) {
                    maxOffset = r.offset;
                }
                if (maxLen < r.length) {
                    maxLen = r.length;
                }
            }
        }
        String type = "int";
        if (maxOffset < 0x7fff && maxLen < 0x7fff) {
            type = "short";
        }
        p("static const " + type + " offset_data[] = {");

        int STEP = 4;
        for (int i=0; i<strings.length; i++) {
            if ((i % STEP) == 0) {
                pl("");
                p("    ");
            }
            LocalizedString r = strings[i];
            if (r == null) {
                p("-1, -1,");
            } else {
                p(r.offset + ", " + r.length + ", ");
            }
        }
        pl("};");

        pl("static int max_index = " + strings.length + ";");
    }
}

