package com.limegroup.gnutella.i18n;

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.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * Rebuilds the language files, based on the English one.
 */
class LanguageUpdater {

    private static final String MARKER = "# TRANSLATIONS START BELOW.";

    private final File lib;

    private final Map langs;

    private final List englishList;

    private boolean verbose = true;

    /**
     * Constructs a new LanguageUpdater.
     * 
     * @param dir
     * @param langs
     * @param englishLines
     */
    LanguageUpdater(File dir, Map langs, List englishLines) {
        this.lib = dir;
        this.langs = langs;
        this.englishList = englishLines;
        removeInitialComments(englishList);
    }

    /**
     * Determines if stuff should be printed.
     * 
     * @param silent
     */
    void setSilent(boolean silent) {
        verbose = !silent;
    }

    /**
     * Prints a message out if we're being verbose.
     * 
     * @param msg
     */
    void print(String msg) {
        if (verbose)
            System.out.print(msg);
    }

    /**
     * @param msg
     */
    void println(String msg) {
        if (verbose)
            System.out.println(msg);
    }

    /**
     * 
     */
    void println() {
        if (verbose)
            System.out.println();
    }

    /**
     * Updates all languages.
     */
    void updateAllLanguages() {
        for (Iterator i = langs.values().iterator(); i.hasNext();) {
            LanguageInfo next = (LanguageInfo) i.next();
            // TODO: work with variants.
            if (next.isVariant())
                continue;
            updateLanguage(next);
        }
    }

    /**
     * Updates a single language.
     * 
     * @param info
     */
    void updateLanguage(LanguageInfo info) {
        if (info == null) {
            println("Unknown language.");
            return;
        }

        print("Updating language: " + info.getName() + " (" + info.getCode()
                + ")... ");
        String filename = info.getFileName();
        File f = new File(lib, filename);
        if (!f.isFile())
            throw new IllegalArgumentException("Invalid info: " + info);

        File temp;
        BufferedReader reader;
        PrintWriter printer;
        try {
            temp = File.createTempFile("TEMP", info.getCode(), lib);
            reader = new BufferedReader(new InputStreamReader(
                    new FileInputStream(f), info.isUTF8() ? "UTF-8"
                            : "ISO-8859-1"));
            printer = new PrintWriter(temp, info.isUTF8() ? "UTF-8"
                    : "ISO-8859-1");
            if (info.isUTF8()) {
                reader.mark(1);
                if (reader.read() != '\uFEFF')
                    reader.reset(); /* was not a leading BOM */
                printer.print('\uFEFF'); /* force a leading BOM */
            }
            printInitialComments(printer, reader, info);
            printBody(printer, info);
            reader.close();
            printer.close();
            if (isDifferent(f, temp)) {
                println("...changes.");
                f.delete();
                temp.renameTo(f);
            } else {
                println("...no changes!");
                temp.delete();
            }
            if (info.isUTF8())
                native2ascii(info);
        } catch (IOException ioe) {
            println("...error! (" + ioe.getMessage() + ")");
        }
    }

    /**
     * Home-made native2ascii.
     */
    private void native2ascii(LanguageInfo info) {
        if (!info.isUTF8())
            throw new IllegalArgumentException("requires utf8 language.");

        InputStream in = null;
        OutputStream out = null;

        print("\tConverting to ASCII... ");

        try {
            in = new BufferedInputStream(
                    new FileInputStream(info.getFileName()));
            in.mark(3);
            if (in.read() != 0xEF || in.read() != 0xBB || in.read() != 0xBF)
                in.reset();
            BufferedReader reader = new BufferedReader(new InputStreamReader(
                    in, "UTF8"));

            out = new BufferedOutputStream(new FileOutputStream(info
                    .getAlternateFileName()));
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
                    out, "ISO-8859-1"));

            String read;
            while ((read = reader.readLine()) != null) {
                writer.write(ascii(read));
                writer.newLine();
            }
            writer.flush();
            out.flush();
            println("... done!");
        } catch (IOException ignored) {
            println("... error! (" + ignored.getMessage() + ")");
        } finally {
            if (in != null)
                try {
                    in.close();
                } catch (IOException ignored) {
                }
            if (out != null)
                try {
                    out.close();
                } catch (IOException ignored) {
                }
        }
    }

    /**
     * Determines if there is any difference between file a & file b.
     */
    private boolean isDifferent(File a, File b) {
        InputStream ia = null, ib = null;
        try {
            ia = new BufferedInputStream(new FileInputStream(a));
            ib = new BufferedInputStream(new FileInputStream(b));
            int c;
            while ((c = ia.read()) == ib.read()) {
                // if we got here, both got to EOF at same time
                if (c == -1)
                    return false;
            }
        } catch (IOException ignored) {
        } finally {
            if (ia != null)
                try {
                    ia.close();
                } catch (IOException ignored) {
                }
            if (ib != null)
                try {
                    ib.close();
                } catch (IOException ignored) {
                }
        }
        // if we didn't exit in the loop, a character was different
        // or one stream ended before another.
        return true;
    }

    /**
     * Writes the body of the bundle.
     */
    private void printBody(PrintWriter printer, LanguageInfo info) {
        Properties props = info.getProperties();
        boolean reachedTranslations = false;
        for (Iterator i = englishList.iterator(); i.hasNext();) {
            Line line = (Line) i.next();
            if (MARKER.equals(line.getLine()))
                reachedTranslations = true;

            if (line.isComment()) {
                printer.println(line.getLine());
            } else {
                String key = line.getKey();
                String value = props.getProperty(key);
                // always write the English version, so translators
                // have a reference point for possibly needing to update
                // an older translation.
                if (reachedTranslations) {
                    printer.print("#### ");
                    printer.print(key);
                    printer.print("=");
                    printer.print(escape(line.getValue()));
                    printer.println();
                }

                if (value != null
                        && line.getBraceCount() == Line.parseBraceCount(value)) {
                    printer.print(key);
                    printer.print("=");
                    printer.println(escape(value));
                } else {
                    printer.print("#? ");
                    printer.print(key);
                    printer.print("=");
                    // only write the non-translated value if we didn't
                    // above.
                    if (!reachedTranslations)
                        printer.print(escape(line.getValue()));
                    printer.println();
                }
            }
        }
    }

    /**
     * Writes the initial comments from a given file to fos.
     */
    private void printInitialComments(PrintWriter printer,
            BufferedReader reader, LanguageInfo info) throws IOException {
        // TODO: look into initial comments, to see if more information should
        // be generated from 'info'.
        try {
            String read;
            // Read through and write the initial lines until we reach a
            // non-comment
            while ((read = reader.readLine()) != null) {
                Line line = new Line(read);
                if (!line.isComment())
                    break;
                printer.println(read);
            }
        } finally {
        }
    }

    /**
     * Removes the initial comments from the English properties file.
     */
    private void removeInitialComments(List l) {
        for (Iterator i = l.iterator(); i.hasNext();) {
            Line line = (Line) i.next();
            if (line.isComment())
                i.remove();
            else
                break;
        }
    }

    /**
     * Returns a string suitable for insertion into a UTF-8 encoded Properties
     * file. Some characters will always be escaped.
     */
    private String escape(String s) {
        final int n = s.length();
        StringBuffer sb = new StringBuffer(n);
        for (int i = 0; i < n; i++) {
            int cp;
            if (Character.isSupplementaryCodePoint(cp = s.codePointAt(i)))
                i++;
            // TODO: use switch(getType(cp)) for more generic handling.
            // Whitespace's include all Spacechar's but not non-breaking spaces;
            // Spacechar's include all Whitespace's but not C0 controls.
            if (Character.isWhitespace(cp)
                    || Character.isSpaceChar(cp)
                    || Character.isISOControl(cp)
                    ||
                    // treat isolated surrogates like controls
                    !Character.isSupplementaryCodePoint(cp)
                    && (Character.isLowSurrogate((char) cp) || Character
                            .isHighSurrogate((char) cp))) {
                switch (cp) {
                // only ASCII regular SPACE can be left unchanged;
                case ' ':
                    sb.append(' ');
                    break;
                // all other whitespaces and controls must be escaped.
                case '\n':
                    sb.append("\\n");
                    break;
                case '\t':
                    sb.append("\\t");
                    break;
                case '\f':
                    sb.append("\\f");
                    break;
                case '\r':
                    sb.append("\\r");
                    break;
                default:
                    sb.append(hexUnicode(cp));
                }
            } else {
                // valid non-controls non-whitespaces can be left unchanged.
                sb.appendCodePoint(cp);
            }
        }
        return sb.toString();
    }

    /**
     * Converts the input string to ascii, using \\u escapes.
     */
    private String ascii(final String s) {
        final int n = s.length();
        final StringBuffer sb = new StringBuffer(n * 5);
        for (int i = 0; i < n; i++) {
            int p = s.codePointAt(i);
            if (p > 0x00ff || // not Latin-1
                    p <= 0x001f || // C0 controls
                    p >= 0x007f && p <= 0x00a0 || // DEL, C1 controls, NBSP
                    p == 0x00ad) // SHY (Soft Hyphen)
                sb.append(hexUnicode(p));
            else
                sb.appendCodePoint(p);
        }
        return sb.toString();
    }

    /**
     * Returns the escaped unicode hex representation of the codepoint.
     * 
     * @param cp
     *            the codepoint to represent; must be in the 17 first planes,
     *            and not a surrogate.
     */
    private String hexUnicode(final int cp) {
        if (cp <= 0xffff) {
            final String hex = Integer.toHexString(cp);
            final StringBuffer sb = new StringBuffer(6);
            sb.append("\\u");
            for (int j = hex.length(); j < 4; j++)
                sb.append('0');
            sb.append(hex);
            return sb.toString();
        }
        return new StringBuffer(12).append("\\u").append(
                Integer.toHexString(((cp - 0x10000) >> 10) + 0xD800)).append(
                "\\u").append(Integer.toHexString((cp & 0x3ff) + 0xDC00))
                .toString();
    }
}
