/*
 * Copyright (C) 2004-2011, Unicode, Inc., Google, Inc., and others.
 * For terms of use, see http://www.unicode.org/terms_of_use.html
 */
package org.unicode.cldr.tool.resolver;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.unicode.cldr.tool.FilterFactory;
import org.unicode.cldr.tool.Option;
import org.unicode.cldr.tool.Option.Options;
import org.unicode.cldr.util.CLDRFile;
import org.unicode.cldr.util.CLDRFile.DraftStatus;
import org.unicode.cldr.util.CLDRPaths;
import org.unicode.cldr.util.CldrUtility;
import org.unicode.cldr.util.Factory;
import org.unicode.cldr.util.LocaleIDParser;
import org.unicode.cldr.util.LruMap;
import org.unicode.cldr.util.SimpleXMLSource;

/**
 * Class designed for the resolution of CLDR XML Files (e.g., removing aliases
 * but leaving the inheritance structure intact).
 * 
 * Instances of this class are not thread-safe. Any attempts to use an object of
 * this class in multiple threads must be externally synchronized.
 * 
 * @author ryanmentley@google.com (Ryan Mentley), jchye@google.com (Jennifer Chye)
 * 
 */
public class CldrResolver {
    /**
     * The name of the code-fallback locale
     */
    public static final String CODE_FALLBACK = "code-fallback";

    /**
     * The name of the root locale
     */
    public static final String ROOT = "root";

    /* The command-line options. */
    private static final Options options = new Options(
        "This program is used to convert CLDR XML files into their resolved versions.\n" +
            "Please refer to the following options. Options are not case sensitive.\n" +
            "\texample: org.unicode.cldr.tool.resolver.CldrResolver -s xxx -d yyy -l en")
        .add("locale", 'l', ".*", ".*", "The locales to generate resolved files for")
        .add("sourcedir", ".*", "Source directory for CLDR files")
        .add("destdir", ".*", "Destination directory for output files")
        .add("resolutiontype", 'r', "\\w+", "simple", "The resolution type to be used")
        .add("mindraftstatus", 'm', ".*", "unconfirmed", "The minimum draft status")
        .add("verbosity", 'v', "\\d", "2", "The verbosity level for comments during generation")
        .add("usealtvalues", 'a', null, null, "Use alternate values in FilterFactory for the locale data to be resolved.")
        .add("organization", 'o', ".*", null, "Filter by this organization's coverage level");

    /* Private instance variables */
    private Factory cldrFactory;
    private ResolutionType resolutionType;
    // Cache for resolved CLDRFiles.
    // This is most useful for simple resolution, where the resolved locales are
    // required to resolve their children.
    private Map<String, CLDRFile> resolvedCache = new LruMap<String, CLDRFile>(5);

    public static void main(String[] args) {
        options.parse(args, true);

        // Parse the options
        ResolutionType resolutionType = ResolutionType.SIMPLE;
        Option option = options.get("resolutiontype");
        if (option.doesOccur()) {
            try {
                resolutionType = ResolutionType.forString(option.getValue());
            } catch (IllegalArgumentException e) {
                ResolverUtils.debugPrintln("Warning: " + e.getMessage(), 1);
                ResolverUtils.debugPrintln("Using default resolution type " + resolutionType.toString(), 1);
            }
        }

        String srcDir = null;
        option = options.get("sourcedir");
        if (option.doesOccur()) {
            srcDir = option.getValue();
        } else {
            srcDir = CLDRPaths.MAIN_DIRECTORY;
        }

        option = options.get("destdir");
        File dest;
        if (option.doesOccur()) {
            dest = new File(option.getValue());
        } else {
            dest = new File(CLDRPaths.GEN_DIRECTORY, "resolver");
        }
        if (!dest.exists()) {
            dest.mkdir();
        }
        String destDir = dest.getAbsolutePath();

        int verbosityParsed = Integer.parseInt(options.get("verbosity").getValue());
        if (verbosityParsed < 0 || verbosityParsed > 5) {
            ResolverUtils.debugPrintln(
                "Warning: Verbosity must be between 0 and 5, inclusive.  Using default value "
                    + ResolverUtils.verbosity, 1);
        } else {
            ResolverUtils.verbosity = verbosityParsed;
        }

        option = options.get("mindraftstatus");
        DraftStatus minDraftStatus = option.doesOccur() ? DraftStatus.forString(option.getValue()) : DraftStatus.unconfirmed;
        Factory factory = Factory.make(srcDir, ".*", minDraftStatus);
        boolean useAltValues = options.get("usealtvalues").doesOccur();
        String org = options.get("organization").getValue();
        if (useAltValues || org != null) {
            factory = FilterFactory.load(factory, org, useAltValues);
        }
        CldrResolver resolver = new CldrResolver(factory, resolutionType);

        // Perform the resolution
        String localeRegex = options.get("locale").getValue();
        resolver.resolve(localeRegex, destDir);
        ResolverUtils.debugPrintln("Execution complete.", 3);
    }

    /**
     * Constructs a CLDR partial resolver given the path to a directory of XML
     * files.
     * 
     * @param factory the factory containing the files to be resolved
     * @param resolutionType the resolution type of the resolver.
     */
    public CldrResolver(Factory factory, ResolutionType resolutionType) {
        /*
         * We don't do the regex filter here so that we can still resolve parent
         * files that don't match the regex
         */
        cldrFactory = factory;
        this.resolutionType = resolutionType;
    }

    /**
     * Resolves all locales that match the given regular expression and outputs
     * their XML files to the given directory.
     * 
     * @param localeRegex a regular expression that will be matched against the
     *        names of locales
     * @param outputDir the directory to which to output the partially-resolved
     *        XML files
     * @param resolutionType the type of resolution to perform
     * @throws IllegalArgumentException if outputDir is not a directory
     */
    public void resolve(String localeRegex, File outputDir) {
        if (!outputDir.isDirectory()) {
            throw new IllegalArgumentException(outputDir.getPath() + " is not a directory");
        }

        // Iterate through all the locales
        for (String locale : getLocaleNames(localeRegex)) {
            // Resolve the file
            ResolverUtils.debugPrintln("Processing locale " + locale + "...", 2);
            CLDRFile resolved = resolveLocale(locale);

            // Output the file to disk
            printToFile(resolved, outputDir);
        }
    }

    /**
     * Returns the locale names from the resolver that match a given regular
     * expression.
     * 
     * @param localeRegex a regular expression to match against
     * @return all of the locales that will be resolved by a call to resolve()
     *         with the same localeRegex
     */
    public Set<String> getLocaleNames(String localeRegex) {
        ResolverUtils.debugPrint("Getting list of locales...", 3);
        Set<String> allLocales = cldrFactory.getAvailable();
        Set<String> locales = new TreeSet<String>();
        // Iterate through all the locales
        for (String locale : allLocales) {
            // Check if the locale name matches the regex
            if (locale.matches(localeRegex)) {
                locales.add(locale);
            } else {
                ResolverUtils.debugPrintln("Locale " + locale
                    + "does not match the pattern.  Skipping...\n", 4);
            }

        }
        ResolverUtils.debugPrintln("done.\n", 3);
        return locales;
    }

    /**
     * Resolves a locale to a {@link CLDRFile} object
     * 
     * @param locale the name of the locale to resolve
     * @param resolutionType the type of resolution to perform
     * @return a {@link CLDRFile} containing the resolved data
     */
    public CLDRFile resolveLocale(String locale) {
        // Create CLDRFile for current (base) locale
        CLDRFile base = cldrFactory.make(locale, true);
        CLDRFile resolved = resolvedCache.get(locale);
        if (resolved != null) return resolved;

        ResolverUtils.debugPrintln("Processing " + locale + "...", 2);
        resolved = resolveLocaleInternal(base, resolutionType);
        resolvedCache.put(locale, resolved);
        return resolved;
    }

    private CLDRFile resolveLocaleInternal(CLDRFile file, ResolutionType resolutionType) {
        String locale = file.getLocaleID();
        // Make parent files for simple resolution.
        List<CLDRFile> ancestors = new ArrayList<CLDRFile>();
        if (resolutionType == ResolutionType.SIMPLE && !locale.equals(ROOT)) {
            String parentLocale = locale;
            do {
                parentLocale = LocaleIDParser.getSimpleParent(parentLocale);
                ancestors.add(resolveLocale(parentLocale));
            } while (!parentLocale.equals(ROOT));
        }

        // Create empty file to hold (partially or fully) resolved data.
        CLDRFile resolved = new CLDRFile(new SimpleXMLSource(locale));

        // Go through the XPaths, filter out appropriate values based on the
        // inheritance model,
        // then copy to the new CLDRFile.
        Set<String> basePaths = ResolverUtils.getAllPaths(file);
        for (String distinguishedPath : basePaths) {
            ResolverUtils.debugPrintln("Distinguished path: " + distinguishedPath, 5);

            if (distinguishedPath.endsWith("/alias")) {
                // Ignore any aliases.
                ResolverUtils.debugPrintln("This path is an alias.  Dropping...", 5);
                continue;
            }

            /*
             * If we're fully resolving the locale (and, if code-fallback suppression
             * is enabled, if the value is not from code-fallback) or the values
             * aren't equal, add it to the resolved file.
             */
            if (resolutionType == ResolutionType.NO_CODE_FALLBACK && file.getSourceLocaleID(
                distinguishedPath, null).equals(CODE_FALLBACK)) {
                continue;
            }

            // For simple resolution, don't add paths to child locales if the parent
            // locale contains the same path with the same value.
            String baseValue = file.getStringValue(distinguishedPath);
            if (resolutionType == ResolutionType.SIMPLE) {
                String parentValue = null;
                for (CLDRFile ancestor : ancestors) {
                    parentValue = ancestor.getStringValue(distinguishedPath);
                    if (parentValue != null) break;
                }
                ResolverUtils.debugPrintln(
                    "    Parent value : " + ResolverUtils.strRep(parentValue), 5);
                if (areEqual(parentValue, baseValue)) continue;
            }

            ResolverUtils.debugPrintln("  Adding to resolved file.", 5);
            // Suppress non-distinguishing attributes in simple inheritance
            String path = resolutionType == ResolutionType.SIMPLE ?
                distinguishedPath : file.getFullXPath(distinguishedPath);
            ResolverUtils.debugPrintln("Path to be saved: " + path, 5);
            resolved.add(path, baseValue);
        }

        // Sanity check in simple resolution to make sure that all paths in the parent are also in the child.
        if (ancestors.size() > 0) {
            CLDRFile ancestor = ancestors.get(0);
            ResolverUtils.debugPrintln(
                "Adding UNDEFINED values based on ancestor: " + ancestor.getLocaleID(), 3);
            for (String distinguishedPath : ResolverUtils.getAllPaths(ancestor)) {
                // Do the comparison with distinguished paths to prevent errors
                // resulting from duplicate full paths but the same distinguished path
                if (!basePaths.contains(distinguishedPath) &&
                    !ancestor.getStringValue(distinguishedPath).equals(CldrUtility.NO_INHERITANCE_MARKER)) {
                    ResolverUtils.debugPrintln(
                        "Added UNDEFINED value for path: " + distinguishedPath, 4);
                    resolved.add(distinguishedPath, CldrUtility.NO_INHERITANCE_MARKER);
                }
            }
        }
        return resolved;
    }

    /**
     * Resolves all locales that match the given regular expression and outputs
     * their XML files to the given directory.
     * 
     * @param localeRegex a regular expression that will be matched against the
     *        names of locales
     * @param outputDir the directory to which to output the partially-resolved
     *        XML files
     * @param resolutionType the type of resolution to perform
     * @throws IllegalArgumentException if outputDir is not a directory
     */
    public void resolve(String localeRegex, String outputDir) {
        resolve(localeRegex, new File(outputDir));
    }

    /**
     * Writes out the given CLDRFile in XML form to the given directory
     * 
     * @param cldrFile the CLDRFile to print to XML
     * @param directory the directory to which to add the file
     */
    private static void printToFile(CLDRFile cldrFile, File directory) {
        ResolverUtils.debugPrint("Printing file...", 2);
        try {
            PrintWriter pw =
                new PrintWriter(new File(directory, cldrFile.getLocaleID() + ".xml"), "UTF-8");
            cldrFile.write(pw);
            pw.close();
            ResolverUtils.debugPrintln("done.\n", 2);
        } catch (FileNotFoundException e) {
            ResolverUtils.debugPrintln("\nFile not found: " + e.getMessage(), 1);
            System.exit(1);
            return;
        } catch (UnsupportedEncodingException e) {
            // This should never ever happen.
            ResolverUtils.debugPrintln("Your system does not support UTF-8 encoding: " + e.getMessage(),
                1);
            System.exit(1);
            return;
        }
    }

    /**
     * Convenience method to compare objects that works with nulls
     * 
     * @param o1 the first object
     * @param o2 the second object
     * @return true if objects o1 == o2 or o1.equals(o2); false otherwise
     */
    private static boolean areEqual(Object o1, Object o2) {
        if (o1 == o2) {
            return true;
        } else if (o1 == null) {
            return false;
        } else {
            return o1.equals(o2);
        }
    }
}
