package org.unicode.cldr.util;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.unicode.cldr.test.CheckCLDR;
import org.unicode.cldr.test.CheckCLDR.CheckStatus;
import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype;
import org.unicode.cldr.test.CheckCoverage;
import org.unicode.cldr.test.CheckNew;
import org.unicode.cldr.test.CoverageLevel2;
import org.unicode.cldr.test.OutdatedPaths;
import org.unicode.cldr.tool.Option;
import org.unicode.cldr.tool.Option.Options;
import org.unicode.cldr.util.CLDRFile.Status;
import org.unicode.cldr.util.CLDRFile.WinningChoice;
import org.unicode.cldr.util.PathHeader.PageId;
import org.unicode.cldr.util.PathHeader.SectionId;
import org.unicode.cldr.util.StandardCodes.LocaleCoverageType;
import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo;
import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;

import com.ibm.icu.dev.util.BagFormatter;
import com.ibm.icu.dev.util.CollectionUtilities;
import com.ibm.icu.dev.util.TransliteratorUtilities;
import com.ibm.icu.impl.Relation;
import com.ibm.icu.impl.Row;
import com.ibm.icu.impl.Row.R2;
import com.ibm.icu.text.Collator;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.Transform;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.ULocale;

/**
 * Provides a HTML tables showing the important issues for vetters to review for
 * a given locale. See the main for an example. Most elements have CSS styles,
 * allowing for customization of the display.
 *
 * @author markdavis
 */
public class VettingViewer<T> {

    private static boolean SHOW_SUBTYPES = true; // CldrUtility.getProperty("SHOW_SUBTYPES", "false").equals("true");

    private static final String CONNECT_PREFIX = "₍_";
    private static final String CONNECT_SUFFIX = "₎";

    private static final String TH_AND_STYLES = "<th class='tv-th' style='text-align:left'>";

    private static final String SPLIT_CHAR = "\uFFFE";

    private static final boolean SUPPRESS = true;

    private static final String TEST_PATH = "//ldml/localeDisplayNames/territories/territory[@type=\"SX\"]";
    private static final double NANOSECS = 1000000000.0;
    private static final boolean TESTING = CldrUtility.getProperty("TEST", false);
    private static final boolean SHOW_ALL = CldrUtility.getProperty("SHOW", true);

    public static final Pattern ALT_PROPOSED = PatternCache.get("\\[@alt=\"[^\"]*proposed");

    public static Set<CheckCLDR.CheckStatus.Subtype> OK_IF_VOTED = EnumSet.of(Subtype.sameAsEnglishOrCode,
        Subtype.sameAsEnglishOrCode);

    public enum Choice {
        /**
         * There is a console-check error
         */
        error('E', "Error", "The Survey Tool detected an error in the winning value.", 1),
        /**
         * My choice is not the winning item
         */
        weLost(
            'L',
            "Losing",
            "The value that your organization chose (overall) is either not the winning value, or doesn’t have enough votes to be approved. "
                + "This might be due to a dispute between members of your organization.", 2),
                /**
                 * There is a dispute.
                 */
                notApproved('P', "Provisional", "There are not enough votes for this item to be approved (and used).", 3),
                /**
                 * There is a dispute.
                 */
                hasDispute('D', "Disputed", "Different organizations are choosing different values. "
                    + "Please review to approve or reach consensus.", 4),
                    /**
                     * There is a console-check warning
                     */
                    warning('W', "Warning", "The Survey Tool detected a warning about the winning value.", 5),
                    /**
                     * The English value for the path changed AFTER the current value for
                     * the locale.
                     */
                    englishChanged('U', "English Changed",
                        "The English value has changed in CLDR, but the corresponding value for your language has not. Check if any changes are needed in your language.",
                        6),
                        /**
                         * The value changed from the last version of CLDR
                         */
                        changedOldValue('N', "New", "The winning value was altered from the last-released CLDR value. (Informational)", 7),
                        /**
                         * Given the users' coverage, some items are missing.
                         */
                        missingCoverage(
                            'M',
                            "Missing",
                            "Your current coverage level requires the item to be present. (During the vetting phase, this is informational: you can’t add new values.)", 8),
                            // /**
                            // * There is a console-check error
                            // */
                            // other('O', "Other", "Everything else."),
                            ;

        public final char abbreviation;
        public final String buttonLabel;
        public final String description;
        public final int order;

        Choice(char abbreviation, String buttonLabel, String description, int order) {
            this.abbreviation = abbreviation;
            this.buttonLabel = TransliteratorUtilities.toHTML.transform(buttonLabel);
            this.description = TransliteratorUtilities.toHTML.transform(description);
            this.order = order;

        }

        public static <T extends Appendable> T appendDisplay(Set<Choice> choices, String htmlMessage, T target) {
            try {
                boolean first = true;
                for (Choice item : choices) {
                    if (first) {
                        first = false;
                    } else {
                        target.append(", ");
                    }
                    item.appendDisplay(htmlMessage, target);
                }
                return target;
            } catch (IOException e) {
                throw new IllegalArgumentException(e); // damn'd checked
                // exceptions
            }
        }

        private <T extends Appendable> void appendDisplay(String htmlMessage, T target) throws IOException {
            target.append("<span title='")
            .append(description);
            if (!htmlMessage.isEmpty()) {
                target.append(": ")
                .append(htmlMessage);
            }
            target.append("'>")
            .append(buttonLabel)
            .append("*</span>");
        }

        public static Choice fromString(String i) {
            try {
                return valueOf(i);
            } catch (NullPointerException e) {
                throw e;
            } catch (RuntimeException e) {
                if (i.isEmpty()) {
                    throw e;
                }
                int cp = i.codePointAt(0);
                for (Choice choice : Choice.values()) {
                    if (cp == choice.abbreviation) {
                        return choice;
                    }
                }
                throw e;
            }
        }

        public static Appendable appendRowStyles(Set<Choice> choices, Appendable target) {
            try {
                if (choices.contains(Choice.changedOldValue)) {
                    int x = 0; // debugging
                }
                target.append("hide");
                for (Choice item : choices) {
                    target.append(' ').append("vv").append(Character.toLowerCase(item.abbreviation));
                }
                return target;
            } catch (IOException e) {
                throw new IllegalArgumentException(e); // damn'd checked
                // exceptions
            }
        }
    }

    public static OutdatedPaths getOutdatedPaths() {
        return outdatedPaths;
    }

    static private PathHeader.Factory pathTransform;
    static final Pattern breaks = PatternCache.get("\\|");
    static final OutdatedPaths outdatedPaths = new OutdatedPaths();

//    private static final UnicodeSet NEEDS_PERCENT_ESCAPED = new UnicodeSet("[[\\u0000-\\u009F]-[a-zA-z0-9]]");
//    private static final Transform<String, String> percentEscape = new Transform<String, String>() {
//        @Override
//        public String transform(String source) {
//            StringBuilder buffer = new StringBuilder();
//            buffer.setLength(0);
//            for (int cp : CharSequences.codePoints(source)) {
//                if (NEEDS_PERCENT_ESCAPED.contains(cp)) {
//                    buffer.append('%').append(Utility.hex(cp, 2));
//                } else {
//                    buffer.appendCodePoint(cp);
//                }
//            }
//            return buffer.toString();
//        }
//    };

    /**
     * See VoteResolver getStatusForOrganization to see how this is computed.
     */
    public enum VoteStatus {
        /**
         * The value for the path is either contributed or approved, and
         * the user's organization didn't vote. (see class def for null user)
         */
        ok_novotes,

        /**
         * The value for the path is either contributed or approved, and
         * the user's organization chose the winning value. (see class def for null user)
         */
        ok,

        /**
         * The user's organization chose the winning value for the path, but
         * that value is neither contributed nor approved. (see class def for null user)
         */
        provisionalOrWorse,

        /**
         * The user's organization's choice is not winning. There may be
         * insufficient votes to overcome a previously approved value, or other
         * organizations may be voting against it. (see class def for null user)
         */
        losing,

        /**
         * There is a dispute, meaning more than one item with votes, or the item with votes didn't win.
         */
        disputed
    }

    /**
     * @author markdavis
     *
     * @param <T>
     */
    public static interface UsersChoice<T> {
        /**
         * Return the value that the user's organization (as a whole) voted for,
         * or null if none of the users in the organization voted for the path. <br>
         * NOTE: Would be easier if this were a method on CLDRFile.
         * NOTE: if user = null, then it must return the absolute winning value.
         *
         * @param locale
         */
        public String getWinningValueForUsersOrganization(CLDRFile cldrFile, String path, T user);

        /**
         *
         * Return the vote status
         * NOTE: if user = null, then it must disregard the user and never return losing. See VoteStatus.
         *
         * @param locale
         */
        public VoteStatus getStatusForUsersOrganization(CLDRFile cldrFile, String path, T user);
    }

    public static interface ErrorChecker {
        enum Status {
            ok, error, warning
        }

        /**
         * Initialize an error checker with a cldrFile. MUST be called before
         * any getErrorStatus.
         */
        public Status initErrorStatus(CLDRFile cldrFile);

        /**
         * Return the detailed CheckStatus information.
         */
        public List<CheckStatus> getErrorCheckStatus(String path, String value);

        /**
         * Return the status, and append the error message to the status
         * message. If there are any errors, then the warnings are not included.
         */
        public Status getErrorStatus(String path, String value, StringBuilder statusMessage);

        /**
         * Return the status, and append the error message to the status
         * message, and get the subtypes. If there are any errors, then the warnings are not included.
         */
        public Status getErrorStatus(String path, String value, StringBuilder statusMessage,
            EnumSet<Subtype> outputSubtypes);
    }

    public static class NoErrorStatus implements ErrorChecker {
        @Override
        public Status initErrorStatus(CLDRFile cldrFile) {
            return Status.ok;
        }

        @Override
        public List<CheckStatus> getErrorCheckStatus(String path, String value) {
            return Collections.emptyList();
        }

        @Override
        public Status getErrorStatus(String path, String value, StringBuilder statusMessage) {
            return Status.ok;
        }

        @Override
        public Status getErrorStatus(String path, String value, StringBuilder statusMessage,
            EnumSet<Subtype> outputSubtypes) {
            return Status.ok;
        }

    }

    public static class DefaultErrorStatus implements ErrorChecker {

        private CheckCLDR checkCldr;
        private HashMap<String, String> options = new HashMap<String, String>();
        private ArrayList<CheckStatus> result = new ArrayList<CheckStatus>();
        private CLDRFile cldrFile;
        private Factory factory;

        public DefaultErrorStatus(Factory cldrFactory) {
            this.factory = cldrFactory;
        }

        @Override
        public Status initErrorStatus(CLDRFile cldrFile) {
            this.cldrFile = cldrFile;
            options = new HashMap<String, String>();
            result = new ArrayList<CheckStatus>();
            checkCldr = CheckCLDR.getCheckAll(factory, ".*");
            checkCldr.setCldrFileToCheck(cldrFile, options, result);
            return Status.ok;
        }

        @Override
        public List<CheckStatus> getErrorCheckStatus(String path, String value) {
            String fullPath = cldrFile.getFullXPath(path);
            ArrayList<CheckStatus> result2 = new ArrayList<CheckStatus>();
            checkCldr.check(path, fullPath, value, options, result2);
            return result2;
        }

        @Override
        public Status getErrorStatus(String path, String value, StringBuilder statusMessage) {
            return getErrorStatus(path, value, statusMessage, null);
        }

        @Override
        public Status getErrorStatus(String path, String value, StringBuilder statusMessage,
            EnumSet<Subtype> outputSubtypes) {
            Status result0 = Status.ok;
            StringBuilder errorMessage = new StringBuilder();
            String fullPath = cldrFile.getFullXPath(path);
            checkCldr.check(path, fullPath, value, options, result);
            for (CheckStatus checkStatus : result) {
                final CheckCLDR cause = checkStatus.getCause();
                if (cause instanceof CheckCoverage || cause instanceof CheckNew) {
                    continue;
                }
                CheckStatus.Type statusType = checkStatus.getType();
                if (statusType.equals(CheckStatus.errorType)) {
                    // throw away any accumulated warning messages
                    if (result0 == Status.warning) {
                        errorMessage.setLength(0);
                        if (outputSubtypes != null) {
                            outputSubtypes.clear();
                        }
                    }
                    result0 = Status.error;
                    if (outputSubtypes != null) {
                        outputSubtypes.add(checkStatus.getSubtype());
                    }
                    appendToMessage(checkStatus.getMessage(), checkStatus.getSubtype(), errorMessage);
                } else if (result0 != Status.error && statusType.equals(CheckStatus.warningType)) {
                    result0 = Status.warning;
                    // accumulate all the warning messages
                    if (outputSubtypes != null) {
                        outputSubtypes.add(checkStatus.getSubtype());
                    }
                    appendToMessage(checkStatus.getMessage(), checkStatus.getSubtype(), errorMessage);
                }
            }
            if (result0 != Status.ok) {
                appendToMessage(errorMessage, statusMessage);
            }
            return result0;
        }
    }

    private final Factory cldrFactory;
    private final Factory cldrFactoryOld;
    private final CLDRFile englishFile;
    //private final CLDRFile oldEnglishFile;
    private final UsersChoice<T> userVoteStatus;
    private final SupplementalDataInfo supplementalDataInfo;
    private final String lastVersionTitle;
    private final String currentWinningTitle;
    //private final PathDescription pathDescription;
    private ErrorChecker errorChecker; // new

    private final Set<String> defaultContentLocales;

    // NoErrorStatus();
    // //
    // for
    // testing

    /**
     * Create the Vetting Viewer.
     *
     * @param supplementalDataInfo
     * @param cldrFactory
     * @param cldrFactoryOld
     * @param lastVersionTitle
     *            The title of the last released version of CLDR.
     * @param currentWinningTitle
     *            The title of the next version of CLDR to be released.
     */
    public VettingViewer(SupplementalDataInfo supplementalDataInfo, Factory cldrFactory, Factory cldrFactoryOld,
        UsersChoice<T> userVoteStatus,
        String lastVersionTitle, String currentWinningTitle) {
        super();
        this.cldrFactory = cldrFactory;
        this.cldrFactoryOld = cldrFactoryOld;
        englishFile = cldrFactory.make("en", true);
        if (pathTransform == null) {
            pathTransform = PathHeader.getFactory(englishFile);
        }
        //oldEnglishFile = cldrFactoryOld.make("en", true);
        this.userVoteStatus = userVoteStatus;
        this.supplementalDataInfo = supplementalDataInfo;
        this.defaultContentLocales = supplementalDataInfo.getDefaultContentLocales();

        this.lastVersionTitle = lastVersionTitle;
        this.currentWinningTitle = currentWinningTitle;
        //Map<String, List<Set<String>>> starredPaths = new HashMap<String, List<Set<String>>>();
        //Map<String, String> extras = new HashMap<String, String>();
        reasonsToPaths = Relation.of(new HashMap<String, Set<String>>(), HashSet.class);
        //this.pathDescription = new PathDescription(supplementalDataInfo, englishFile, extras, starredPaths,
        //    PathDescription.ErrorHandling.CONTINUE);
        errorChecker = new DefaultErrorStatus(cldrFactory);
    }

    public class WritingInfo implements Comparable<WritingInfo> {
        public final PathHeader codeOutput;
        public final Set<Choice> problems;
        public final String htmlMessage;

        public WritingInfo(PathHeader pretty, EnumSet<Choice> problems, CharSequence htmlMessage) {
            super();
            this.codeOutput = pretty;
            this.problems = Collections.unmodifiableSet(problems.clone());
            this.htmlMessage = htmlMessage.toString();
        }

        @Override
        public int compareTo(WritingInfo other) {
            return codeOutput.compareTo(other.codeOutput);
        }

        public String getUrl(CLDRLocale locale) {
            return urls.forPathHeader(locale, codeOutput);
        }
    }

    // public void generateHtmlErrorTablesOld(Appendable output, EnumSet<Choice> choices, String localeID, T user, Level
    // usersLevel) {
    // generateHtmlErrorTablesOld(output, choices, localeID, user, usersLevel, false);
    // }

    // private void generateHtmlErrorTablesOld(Appendable output, EnumSet<Choice> choices, String localeID, T user,
    // Level usersLevel, boolean showAll) {
    //
    // // first gather the relevant paths
    // // each one will be marked with the choice that it triggered.
    //
    // CLDRFile sourceFile = cldrFactory.make(localeID, true);
    // Matcher altProposed = PatternCache.get("\\[@alt=\"[^\"]*proposed").matcher("");
    // EnumSet<Choice> problems = EnumSet.noneOf(Choice.class);
    //
    // // Initialize
    // CoverageLevel2 coverage = CoverageLevel2.getInstance(supplementalDataInfo, localeID);
    // CLDRFile lastSourceFile = null;
    // try {
    // lastSourceFile = cldrFactoryOld.make(localeID, true);
    // } catch (Exception e) {
    // }
    //
    // // set the following only where needed.
    // Status status = null;
    //
    // Map<String, String> options = null;
    // List<CheckStatus> result = null;
    //
    // for (Choice choice : choices) {
    // switch (choice) {
    // case changedOldValue:
    // break;
    // case missingCoverage:
    // status = new Status();
    // break;
    // case englishChanged:
    // break;
    // case error:
    // case warning:
    // errorChecker.initErrorStatus(sourceFile);
    // break;
    // case weLost:
    // case hasDispute:
    // //case other:
    // break;
    // default:
    // System.out.println(choice + " not implemented yet");
    // }
    // }
    //
    // // now look through the paths
    //
    // Relation<R2<SectionId, PageId>, WritingInfo> sorted = Relation.of(new TreeMap<R2<SectionId, PageId>,
    // Set<WritingInfo>>(), TreeSet.class);
    //
    // Counter<Choice> problemCounter = new Counter<Choice>();
    // StringBuilder htmlMessage = new StringBuilder();
    // StringBuilder statusMessage = new StringBuilder();
    //
    // for (String path : sourceFile) {
    // progressCallback.nudge(); // Let the user know we're moving along.
    //
    // // note that the value might be missing!
    //
    // // make sure we only look at the real values
    // if (altProposed.reset(path).find()) {
    // continue;
    // }
    //
    // if (path.contains("/exemplarCharacters") || path.contains("/references")) {
    // continue;
    // }
    //
    // Level level = coverage.getLevel(path);
    //
    // // skip anything above the requested level
    // if (level.compareTo(usersLevel) > 0) {
    // continue;
    // }
    //
    // String value = sourceFile.getWinningValue(path);
    //
    // problems.clear();
    // htmlMessage.setLength(0);
    // boolean haveError = false;
    // VoteStatus voteStatus = null;
    //
    // for (Choice choice : choices) {
    // switch (choice) {
    // case changedOldValue:
    // String oldValue = lastSourceFile == null ? null : lastSourceFile.getWinningValue(path);
    // if (oldValue != null && !oldValue.equals(value)) {
    // problems.add(choice);
    // problemCounter.increment(choice);
    // }
    // break;
    // case missingCoverage:
    // if (showAll && !localeID.equals("root")) {
    // if (isMissing(sourceFile, path, status)) {
    // problems.add(choice);
    // problemCounter.increment(choice);
    // }
    // }
    // break;
    // case englishChanged:
    // if (outdatedPaths.isOutdated(localeID, path)
    // // ||
    // // !CharSequences.equals(englishFile.getWinningValue(path),
    // // oldEnglishFile.getWinningValue(path))
    // ) {
    // // the outdated paths compares the base value, before
    // // data submission,
    // // so see if the value changed.
    // String lastValue = lastSourceFile == null ? null : lastSourceFile.getWinningValue(path);
    // if (CharSequences.equals(value, lastValue)) {
    // problems.add(choice);
    // problemCounter.increment(choice);
    // }
    // }
    // break;
    // case error:
    // case warning:
    // if (haveError) {
    // break;
    // }
    // statusMessage.setLength(0);
    // ErrorChecker.Status errorStatus = errorChecker.getErrorStatus(path, value, statusMessage);
    // if ((choice == Choice.error && errorStatus == ErrorChecker.Status.error)
    // || (choice == Choice.warning && errorStatus == ErrorChecker.Status.warning)) {
    // if (choice == Choice.warning) {
    // // for now, suppress cases where the English changed
    // if (outdatedPaths.isOutdated(localeID, path)) {
    // break;
    // }
    // }
    // problems.add(choice);
    // appendToMessage(statusMessage, htmlMessage);
    // problemCounter.increment(choice);
    // haveError = true;
    // break;
    // }
    // break;
    // case weLost:
    // if (voteStatus == null) {
    // voteStatus = userVoteStatus.getStatusForUsersOrganization(sourceFile, path, user);
    // }
    // switch (voteStatus) {
    // case provisionalOrWorse:
    // case losing:
    // if (choice == Choice.weLost) {
    // problems.add(choice);
    // problemCounter.increment(choice);
    // String usersValue = userVoteStatus.getWinningValueForUsersOrganization(sourceFile, path, user);
    // // appendToMessage(usersValue, testMessage);
    // }
    // break;
    // }
    // break;
    // case hasDispute:
    // if (voteStatus == null) {
    // voteStatus = userVoteStatus.getStatusForUsersOrganization(sourceFile, path, user);
    // }
    // if (voteStatus == VoteStatus.disputed) {
    // problems.add(choice);
    // problemCounter.increment(choice);
    // String usersValue = userVoteStatus.getWinningValueForUsersOrganization(sourceFile, path, user);
    // if (usersValue != null) {
    // // appendToMessage(usersValue, testMessage);
    // }
    // }
    // break;
    // }
    // }
    // if (!problems.isEmpty()) { // showAll ||
    // // if (showAll && problems.isEmpty()) {
    // // problems.add(Choice.other);
    // // problemCounter.increment(Choice.other);
    // // }
    // reasonsToPaths.clear();
    // // appendToMessage("level:" + level.toString(), testMessage);
    // // final String description =
    // // pathDescription.getDescription(path, value, level, null);
    // // if (!reasonsToPaths.isEmpty()) {
    // // appendToMessage(level + " " +
    // // TransliteratorUtilities.toHTML.transform(reasonsToPaths.toString()),
    // // testMessage);
    // // }
    // // if (description != null && !description.equals("SKIP")) {
    // // appendToMessage(TransliteratorUtilities.toHTML.transform(description),
    // // testMessage);
    // // }
    // //final String prettyPath = pathTransform.getPrettyPath(path);
    // // String[] pathParts = breaks.split(prettyPath);
    // // String section = pathParts.length == 3 ? pathParts[0] :
    // // "Unknown";
    // // String subsection = pathParts.length == 3 ? pathParts[1] :
    // // "Unknown";
    // // String code = pathParts.length == 3 ? pathParts[2] : pretty;
    //
    // PathHeader pretty = pathTransform.fromPath(path);
    // //String[] pathParts = breaks.split(pretty);
    // // String sectionOutput = pathParts.length == 3 ? pathParts[0] : "Unknown";
    // // String subsectionOutput = pathParts.length == 3 ? pathParts[1] : "Unknown";
    // // String codeOutput = pathParts.length == 3 ? pathParts[2] : pretty;
    //
    // R2<SectionId, PageId> group = Row.of(pretty.getSectionId(), pretty.getPageId());
    //
    // sorted.put(group, new WritingInfo(pretty, problems, htmlMessage));
    // }
    // }
    //
    // // now write the results out
    // writeTables(output, sourceFile, lastSourceFile, sorted, problemCounter, choices, localeID, showAll);
    // }

    /**
     * Show a table of values, filtering according to the choices here and in
     * the constructor.
     *
     * @param output
     * @param choices
     *            See the class description for more information.
     * @param localeId
     * @param user
     * @param usersLevel
     * @param nonVettingPhase
     */
    public void generateHtmlErrorTables(Appendable output, EnumSet<Choice> choices, String localeID, T user,
        Level usersLevel, boolean nonVettingPhase, boolean quick) {

        // Gather the relevant paths
        // each one will be marked with the choice that it triggered.
        Relation<R2<SectionId, PageId>, WritingInfo> sorted = Relation.of(
            new TreeMap<R2<SectionId, PageId>, Set<WritingInfo>>(), TreeSet.class);

        CLDRFile sourceFile = cldrFactory.make(localeID, true);

        // Initialize
        CLDRFile lastSourceFile = null;
        if (!quick) {
            try {
                lastSourceFile = cldrFactoryOld.make(localeID, true);
            } catch (Exception e) {
            }
        }

        FileInfo fileInfo = new FileInfo().
            getFileInfo(sourceFile, lastSourceFile, sorted, choices, localeID, nonVettingPhase, user,
                usersLevel, quick);

        // now write the results out
        writeTables(output, sourceFile, lastSourceFile, sorted, choices, localeID, nonVettingPhase, fileInfo, quick);
    }

    /**
     * Give the list of errors
     *
     * @param output
     * @param choices
     *            See the class description for more information.
     * @param localeId
     * @param user
     * @param usersLevel
     * @param nonVettingPhase
     */
    public Relation<R2<SectionId, PageId>, WritingInfo> generateFileInfoReview(Appendable output, EnumSet<Choice> choices, String localeID, T user,
        Level usersLevel, boolean nonVettingPhase, boolean quick) {

        // Gather the relevant paths
        // each one will be marked with the choice that it triggered.
        Relation<R2<SectionId, PageId>, WritingInfo> sorted = Relation.of(
            new TreeMap<R2<SectionId, PageId>, Set<WritingInfo>>(), TreeSet.class);

        CLDRFile sourceFile = cldrFactory.make(localeID, true);

        // Initialize
        CLDRFile lastSourceFile = null;
        if (!quick) {
            try {
                lastSourceFile = cldrFactoryOld.make(localeID, true);
            } catch (Exception e) {
            }
        }

        FileInfo fileInfo = new FileInfo().
            getFileInfo(sourceFile, lastSourceFile, sorted, choices, localeID, nonVettingPhase, user,
                usersLevel, quick);

        // now write the results out

        return sorted;
    }

    class FileInfo {
        Counter<Choice> problemCounter = new Counter<Choice>();
        Counter<Subtype> errorSubtypeCounter = new Counter<Subtype>();
        Counter<Subtype> warningSubtypeCounter = new Counter<Subtype>();
        EnumSet<Choice> problems = EnumSet.noneOf(Choice.class);

        public void addAll(FileInfo other) {
            problemCounter.addAll(other.problemCounter);
            errorSubtypeCounter.addAll(other.errorSubtypeCounter);
            warningSubtypeCounter.addAll(other.warningSubtypeCounter);
        }

        private FileInfo getFileInfo(CLDRFile sourceFile, CLDRFile lastSourceFile,
            Relation<R2<SectionId, PageId>, WritingInfo> sorted,
            EnumSet<Choice> choices, String localeID, boolean nonVettingPhase,
            T user, Level usersLevel, boolean quick) {
            return this.getFileInfo(sourceFile, lastSourceFile, sorted,
                choices, localeID, nonVettingPhase,
                user, usersLevel, quick, null);
        }

        private FileInfo getFileInfo(CLDRFile sourceFile, CLDRFile lastSourceFile,
            Relation<R2<SectionId, PageId>, WritingInfo> sorted,
            EnumSet<Choice> choices, String localeID, boolean nonVettingPhase,
            T user, Level usersLevel, boolean quick, String xpath) {

            Status status = new Status();
            errorChecker.initErrorStatus(sourceFile);
            Matcher altProposed = ALT_PROPOSED.matcher("");
            problems = EnumSet.noneOf(Choice.class);

            // now look through the paths

            StringBuilder htmlMessage = new StringBuilder();
            StringBuilder statusMessage = new StringBuilder();
            EnumSet<Subtype> subtypes = EnumSet.noneOf(Subtype.class);
            Set<String> seenSoFar = new HashSet<String>();
            boolean latin = VettingViewer.isLatinScriptLocale(sourceFile);
            for (String path : sourceFile.fullIterable()) {
                if (xpath != null && !xpath.equals(path))
                    continue;
                String value = sourceFile.getWinningValue(path);
                statusMessage.setLength(0);
                subtypes.clear();
                ErrorChecker.Status errorStatus = errorChecker.getErrorStatus(path, value, statusMessage, subtypes);

                if (quick && errorStatus != ErrorChecker.Status.error && errorStatus != ErrorChecker.Status.warning) { //skip all values but errors and warnings if in "quick" mode
                    continue;
                }

                if (seenSoFar.contains(path)) {
                    continue;
                }
                seenSoFar.add(path);
                progressCallback.nudge(); // Let the user know we're moving along.

                PathHeader pretty = pathTransform.fromPath(path);
                if (pretty.getSurveyToolStatus() == PathHeader.SurveyToolStatus.HIDE) {
                    continue;
                }

                // note that the value might be missing!

                // make sure we only look at the real values
                if (altProposed.reset(path).find()) {
                    continue;
                }

                if (path.contains("/references")) {
                    continue;
                }

                Level level = supplementalDataInfo.getCoverageLevel(path, sourceFile.getLocaleID());

                // skip anything above the requested level
                if (level.compareTo(usersLevel) > 0) {
                    continue;
                }

                problems.clear();
                htmlMessage.setLength(0);
                String oldValue = lastSourceFile == null ? null : lastSourceFile.getWinningValue(path);

                if (choices.contains(Choice.changedOldValue)) {
                    if (oldValue != null && !oldValue.equals(value)) {
                        problems.add(Choice.changedOldValue);
                        problemCounter.increment(Choice.changedOldValue);
                    }
                }
                VoteStatus voteStatus = userVoteStatus.getStatusForUsersOrganization(sourceFile, path, user);

                MissingStatus missingStatus = getMissingStatus(sourceFile, path, status, latin);
                if (choices.contains(Choice.missingCoverage) && missingStatus == MissingStatus.ABSENT) {
                    problems.add(Choice.missingCoverage);
                    problemCounter.increment(Choice.missingCoverage);
                }
                boolean itemsOkIfVoted = SUPPRESS
                    && voteStatus == VoteStatus.ok;

                if (!itemsOkIfVoted
                    && outdatedPaths.isOutdated(localeID, path)) {
                    // the outdated paths compares the base value, before
                    // data submission,
                    // so see if the value changed.
                    // String lastValue = lastSourceFile == null ? null : lastSourceFile.getWinningValue(path);
                    if (Objects.equals(value, oldValue) && choices.contains(Choice.englishChanged)) {
                        // check to see if we voted
                        problems.add(Choice.englishChanged);
                        problemCounter.increment(Choice.englishChanged);
                    }
                }

                Choice choice = errorStatus == ErrorChecker.Status.error ? Choice.error
                    : errorStatus == ErrorChecker.Status.warning ? Choice.warning
                        : null;
                if (choice == Choice.error && choices.contains(Choice.error)
                    && (!itemsOkIfVoted
                        || !OK_IF_VOTED.containsAll(subtypes))) {
                    problems.add(choice);
                    appendToMessage(statusMessage, htmlMessage);
                    problemCounter.increment(choice);
                    for (Subtype subtype : subtypes) {
                        errorSubtypeCounter.increment(subtype);
                    }
                } else if (choice == Choice.warning && choices.contains(Choice.warning)
                    && (!itemsOkIfVoted
                        || !OK_IF_VOTED.containsAll(subtypes))) {
                    problems.add(choice);
                    appendToMessage(statusMessage, htmlMessage);
                    problemCounter.increment(choice);
                    for (Subtype subtype : subtypes) {
                        warningSubtypeCounter.increment(subtype);
                    }
                }

                switch (voteStatus) {
                case losing:
                    if (choices.contains(Choice.weLost)) {
                        problems.add(Choice.weLost);
                        problemCounter.increment(Choice.weLost);
                    }
                    String usersValue = userVoteStatus.getWinningValueForUsersOrganization(sourceFile, path, user);
                    if (usersValue != null) {
                        usersValue = "Losing value: <" + TransliteratorUtilities.toHTML.transform(usersValue) + ">";
                        appendToMessage(usersValue, htmlMessage);
                    }
                    break;
                case disputed:
                    if (choices.contains(Choice.hasDispute)) {
                        problems.add(Choice.hasDispute);
                        problemCounter.increment(Choice.hasDispute);
                    }
                    break;
                case provisionalOrWorse:
                    if (missingStatus == MissingStatus.PRESENT && choices.contains(Choice.notApproved)) {
                        problems.add(Choice.notApproved);
                        problemCounter.increment(Choice.notApproved);
                    }
                    break;
                }

                if (xpath != null)
                    return this;

                if (!problems.isEmpty()) {
                    // showAll ||
                    // if (showAll && problems.isEmpty()) {
                    // problems.add(Choice.other);
                    // problemCounter.increment(Choice.other);
                    // }
                    if (sorted != null) {
                        reasonsToPaths.clear();
                        // final String prettyPath = pathTransform.getPrettyPath(path);

                        // String[] pathParts = breaks.split(pretty);
                        // String sectionOutput = pathParts.length == 3 ? pathParts[0] : "Unknown";
                        // String subsectionOutput = pathParts.length == 3 ? pathParts[1] : "Unknown";
                        // String codeOutput = pathParts.length == 3 ? pathParts[2] : pretty;

                        R2<SectionId, PageId> group = Row.of(pretty.getSectionId(), pretty.getPageId());

                        sorted.put(group, new WritingInfo(pretty, problems, htmlMessage));
                    }
                }

            }
            return this;
        }
    }

    public static final class LocalesWithExplicitLevel implements Predicate<String> {
        private final Organization org;
        private final Level desiredLevel;

        public LocalesWithExplicitLevel(Organization org, Level level) {
            this.org = org;
            this.desiredLevel = level;
        }

        @Override
        public boolean is(String localeId) {
            Output<LocaleCoverageType> output = new Output<LocaleCoverageType>();
            // For admin - return true if SOME organization has explicit coverage for the locale
            // TODO: Make admin pick up any locale that has a vote
            if (org.equals(Organization.surveytool)) {
                for (Organization checkorg : Organization.values()) {
                    StandardCodes.make().getLocaleCoverageLevel(checkorg, localeId, output);
                    if (output.value == StandardCodes.LocaleCoverageType.explicit) {
                        return true;
                    }
                }
                return false;
            } else {
                Level level = StandardCodes.make().getLocaleCoverageLevel(org, localeId, output);
                return desiredLevel == level && output.value == StandardCodes.LocaleCoverageType.explicit;
            }
        }
    };

    public void generateSummaryHtmlErrorTables(Appendable output, EnumSet<Choice> choices,
        Predicate<String> includeLocale, T organization) {
        try {

            output
            .append("<p>The following summarizes the Priority Items across locales, " +
                "using the default coverage levels for your organization for each locale. " +
                "Before using, please read the instructions at " +
                "<a target='CLDR_ST_DOCS' href='http://cldr.unicode.org/translation/vetting-summary'>Priority " +
                "Items Summary</a>.</p>\n");

            StringBuilder headerRow = new StringBuilder();
            headerRow
            .append("<tr class='tvs-tr'>")
            .append(TH_AND_STYLES)
            .append("Locale</th>")
            .append(TH_AND_STYLES)
            .append("Codes</th>");
            for (Choice choice : choices) {
                headerRow.append("<th class='tv-th'>");
                choice.appendDisplay("", headerRow);
                headerRow.append("</th>");
            }
            headerRow.append("</tr>\n");
            String header = headerRow.toString();

            if (organization.equals(Organization.surveytool)) {
                writeSummaryTable(output, header, Level.COMPREHENSIVE, choices, organization);
            } else {
                for (Level level : Level.values()) {
                    writeSummaryTable(output, header, level, choices, organization);
                }
            }
        } catch (IOException e) {
            throw new IllegalArgumentException(e); // dang'ed checked exceptions
        }

    }

    private void writeSummaryTable(Appendable output, String header, Level desiredLevel,
        EnumSet<Choice> choices, T organization) throws IOException {

        Map<String, String> sortedNames = new TreeMap<String, String>(Collator.getInstance());

        // Gather the relevant paths
        // Each one will be marked with the choice that it triggered.

        // TODO Fix HACK
        // We are going to ignore the predicate for now, just using the locales that have explicit coverage.
        // in that locale, or allow all locales for admin@
        LocalesWithExplicitLevel includeLocale = new LocalesWithExplicitLevel((Organization) organization, desiredLevel);

        for (String localeID : cldrFactory.getAvailable()) {
            if (defaultContentLocales.contains(localeID)
                || localeID.equals("en")
                || !includeLocale.is(localeID)) {
                continue;
            }

            sortedNames.put(getName(localeID), localeID);
        }
        if (sortedNames.isEmpty()) {
            return;
        }

        EnumSet<Choice> thingsThatRequireOldFile = EnumSet.of(Choice.englishChanged, Choice.missingCoverage, Choice.changedOldValue);
        EnumSet<Choice> ourChoicesThatRequireOldFile = choices.clone();
        ourChoicesThatRequireOldFile.retainAll(thingsThatRequireOldFile);
        output.append("<h2>Level: ").append(desiredLevel.toString()).append("</h2>");
        output.append("<table class='tvs-table'>\n");
        char lastChar = ' ';
        Map<String, FileInfo> localeNameToFileInfo = new TreeMap();
        FileInfo totals = new FileInfo();

        for (Entry<String, String> entry : sortedNames.entrySet()) {
            String name = entry.getKey();
            String localeID = entry.getValue();
            // Initialize

            CLDRFile sourceFile = cldrFactory.make(localeID, true);

            CLDRFile lastSourceFile = null;
            if (!ourChoicesThatRequireOldFile.isEmpty()) {
                try {
                    lastSourceFile = cldrFactoryOld.make(localeID, true);
                } catch (Exception e) {
                }
            }
            Level level = Level.MODERN;
            if (organization != null) {
                level = StandardCodes.make().getLocaleCoverageLevel(organization.toString(), localeID);
            }
            FileInfo fileInfo = new FileInfo().
                getFileInfo(sourceFile, lastSourceFile, null, choices, localeID, true, organization, level, false);
            localeNameToFileInfo.put(name, fileInfo);
            totals.addAll(fileInfo);

            char nextChar = name.charAt(0);
            if (lastChar != nextChar) {
                output.append(header);
                lastChar = nextChar;
            }

            writeSummaryRow(output, choices, fileInfo.problemCounter, name, localeID);

            if (output instanceof Writer) {
                ((Writer) output).flush();
            }
        }
        output.append(header);
        writeSummaryRow(output, choices, totals.problemCounter, "Total", null);
        output.append("</table>");
        if (SHOW_SUBTYPES) {
            showSubtypes(output, sortedNames, localeNameToFileInfo, totals, true);
            showSubtypes(output, sortedNames, localeNameToFileInfo, totals, false);
        }
    }

    private void showSubtypes(Appendable output, Map<String, String> sortedNames,
        Map<String, FileInfo> localeNameToFileInfo,
        FileInfo totals,
        boolean errors) throws IOException {
        output.append("<h3>Details: ").append(errors ? "Error Types" : "Warning Types").append("</h3>");
        output.append("<table class='tvs-table'>");
        Counter<Subtype> subtypeCounterTotals = errors ? totals.errorSubtypeCounter : totals.warningSubtypeCounter;
        Set<Subtype> sortedBySize = subtypeCounterTotals.getKeysetSortedByCount(false);

        // header
        writeDetailHeader(subtypeCounterTotals, sortedBySize, output);

        // items
        for (Entry<String, FileInfo> entry : localeNameToFileInfo.entrySet()) {
            Counter<Subtype> counter = errors ? entry.getValue().errorSubtypeCounter : entry.getValue().warningSubtypeCounter;
            if (counter.getTotal() == 0) {
                continue;
            }
            String name = entry.getKey();
            //String[] names = name.split(SPLIT_CHAR);
            String localeID = sortedNames.get(name);
            output.append("<tr>").append(TH_AND_STYLES);
            appendNameAndCode(name, localeID, output);
            output.append("</th>");
            for (Subtype subtype : sortedBySize) {
                long count = counter.get(subtype);
                output.append("<td class='tvs-count'>");
                if (count != 0) {
                    output.append(nf.format(count));
                }
                output.append("</td>");
            }
        }

        // subtotals
        writeDetailHeader(subtypeCounterTotals, sortedBySize, output);
        output.append("<tr>").append(TH_AND_STYLES).append("<i>Total</i>").append("</th>").append(TH_AND_STYLES).append("</th>");
        for (Subtype subtype : sortedBySize) {
            long count = subtypeCounterTotals.get(subtype);
            output.append("<td class='tvs-count'>");
            if (count != 0) {
                output.append("<b>").append(nf.format(count)).append("</b>");
            }
            output.append("</td>");
        }
        output.append("</table>");
    }

    private void writeDetailHeader(Counter<Subtype> subtypeCounterTotals, Set<Subtype> sortedBySize, Appendable output) throws IOException {
        output.append("<tr>")
        .append(TH_AND_STYLES).append("Name").append("</th>")
        .append(TH_AND_STYLES).append("ID").append("</th>");
        for (Subtype subtype : sortedBySize) {
            output.append(TH_AND_STYLES).append(subtype.toString()).append("</th>");
        }
    }

    private void writeSummaryRow(Appendable output, EnumSet<Choice> choices, Counter<Choice> problemCounter,
        String name, String localeID) throws IOException {
        output
        .append("<tr>")
        .append(TH_AND_STYLES);
        if (localeID == null) {
            output
            .append("<i>")
            .append(name)
            .append("</i>")
            .append("</th>")
            .append(TH_AND_STYLES);
        } else {
            appendNameAndCode(name, localeID, output);
        }
        output.append("</th>\n");
        for (Choice choice : choices) {
            long count = problemCounter.get(choice);
            output.append("<td class='tvs-count'>");
            if (localeID == null) {
                output.append("<b>");
            }
            output.append(nf.format(count));
            if (localeID == null) {
                output.append("</b>");
            }
            output.append("</td>\n");
        }
        output.append("</tr>\n");
    }

    private void appendNameAndCode(String name, String localeID, Appendable output) throws IOException {
        String[] names = name.split(SPLIT_CHAR);
        output
        .append("<a href='" + urls.forSpecial(CLDRURLS.Special.Vetting, CLDRLocale.getInstance(localeID)))
        .append("'>")
        .append(TransliteratorUtilities.toHTML.transform(names[0]))
        .append("</a>")
        .append("</th>")
        .append(TH_AND_STYLES)
        .append("<code>")
        .append(names[1])
        .append("</code>");
    }

    LanguageTagParser ltp = new LanguageTagParser();

    private String getName(String localeID) {
        Set<String> contents = supplementalDataInfo.getEquivalentsForLocale(localeID);
        // put in special character that can be split on later
        String name = englishFile.getName(localeID, true, CLDRFile.SHORT_ALTS) + SPLIT_CHAR + gatherCodes(contents);
        return name;
    }

    /**
     * Collapse the names
     {en_Cyrl, en_Cyrl_US} => en_Cyrl(_US)
     {en_GB, en_Latn_GB} => en(_Latn)_GB
     {en, en_US, en_Latn, en_Latn_US} => en(_Latn)(_US)
     {az_IR, az_Arab, az_Arab_IR} => az_IR, az_Arab(_IR)
     */
    public static String gatherCodes(Set<String> contents) {
        Set<Set<String>> source = new LinkedHashSet<Set<String>>();
        for (String s : contents) {
            source.add(new LinkedHashSet<String>(Arrays.asList(s.split("_"))));
        }
        Set<Set<String>> oldSource = new LinkedHashSet<Set<String>>();

        do {
            // exchange source/target
            oldSource.clear();
            oldSource.addAll(source);
            source.clear();
            Set<String> last = null;
            for (Set<String> ss : oldSource) {
                if (last == null) {
                    last = ss;
                } else {
                    if (ss.containsAll(last)) {
                        last = combine(last, ss);
                    } else {
                        source.add(last);
                        last = ss;
                    }
                }
            }
            source.add(last);
        } while (oldSource.size() != source.size());

        StringBuilder b = new StringBuilder();
        for (Set<String> stringSet : source) {
            if (b.length() != 0) {
                b.append(", ");
            }
            String sep = "";
            for (String string : stringSet) {
                if (string.startsWith(CONNECT_PREFIX)) {
                    b.append(string + CONNECT_SUFFIX);
                } else {
                    b.append(sep + string);
                }
                sep = "_";
            }
        }
        return b.toString();
    }

    private static Set<String> combine(Set<String> last, Set<String> ss) {
        LinkedHashSet<String> result = new LinkedHashSet<String>();
        for (String s : ss) {
            if (last.contains(s)) {
                result.add(s);
            } else {
                result.add(CONNECT_PREFIX + s);
            }
        }
        return result;
    }

    public enum MissingOK {
        ok, latin, alias, compact
    }

    public static Transform<String, MissingOK> MISSING_STATUS_TRANSFORM = new Transform<String, MissingOK>() {
        public MissingOK transform(String source) {
            return MissingOK.valueOf(source);
        }
    };

    static final RegexLookup<MissingOK> missingOk = new RegexLookup<MissingOK>()
        .setPatternTransform(RegexLookup.RegexFinderTransformPath)
        .setValueTransform(MISSING_STATUS_TRANSFORM)
        .loadFromFile(
            VettingViewer.class,
            "data/paths/missingOk.txt");

    private static boolean isMissingOk(CLDRFile sourceFile, String path, boolean latin, boolean aliased) {
        Output<String[]> arguments = new Output<String[]>();
        MissingOK value = missingOk.get(path, null, arguments);
        if (value == null) {
            return false;
        }
        switch (value) {
        case ok:
            return true;
        case latin:
            return latin;
        case alias:
            return aliased;
        case compact:
            // special processing for compact numbers
            if (path.contains("[@count=\"other\"]")) {
                return false; // the 'other' class always counts as missing
            }
            String otherPath = "//ldml/numbers/decimalFormats[@numberSystem=\"" + arguments.value[1]
                + "\"]/decimalFormatLength[@type=\"" + arguments.value[2]
                    + "\"]/decimalFormat[@type=\"standard\"]/pattern[@type=\"" + arguments.value[3]
                        + "\"][@count=\"other\"]";
            String otherValue = sourceFile.getWinningValue(otherPath);
            if (otherValue == null) {
                return false; // something's wrong, bail
            }
            int digits = countZeros(otherValue);
            if (digits > 4) { // we can only handle to 4 digits
                return false;
            }
            // if there are no possible Count values for this many digits, then it is ok to be missing.
            Count c = Count.valueOf(arguments.value[4]);
            SupplementalDataInfo supplementalDataInfo2 = CLDRConfig.getInstance().getSupplementalDataInfo();
            // SupplementalDataInfo.getInstance(sourceFile.getSupplementalDirectory());
            PluralInfo plurals = supplementalDataInfo2.getPlurals(sourceFile.getLocaleID());
            return plurals == null || !plurals.hasSamples(c, digits); // ok if no samples
            // TODO: handle fractions
        default:
            throw new IllegalArgumentException();
        }
    }

    private static int countZeros(String otherValue) {
        int result = 0;
        for (int i = 0; i < otherValue.length(); ++i) {
            if (otherValue.charAt(i) == '0') {
                ++result;
            }
        }
        return result;
    }

    public enum MissingStatus {
        PRESENT, ALIASED, MISSING_OK, ROOT_OK, ABSENT
    }

    public static MissingStatus getMissingStatus(CLDRFile sourceFile, String path, Status status, boolean latin) {
        if (sourceFile == null) {
            return MissingStatus.ABSENT;
        }
        if ("root".equals(sourceFile.getLocaleID()) || path.startsWith("//ldml/layout/orientation/")) {
            return MissingStatus.MISSING_OK;
        }
        if (path.equals(TEST_PATH)) {
            int debug = 1;
        }
        MissingStatus result;

        String value = sourceFile.getStringValue(path);
        boolean isAliased = path.equals(status.pathWhereFound);

        if (value == null) {
            result = isMissingOk(sourceFile, path, latin, isAliased) ? MissingStatus.MISSING_OK : MissingStatus.ABSENT;
        } else {
            String localeFound = sourceFile.getSourceLocaleID(path, status);

            // only count it as missing IF the (localeFound is root or codeFallback)
            // AND the aliasing didn't change the path
            if (localeFound.equals("root")
                || localeFound.equals(XMLSource.CODE_FALLBACK_ID)
                // || voteStatus == VoteStatus.provisionalOrWorse
                ) {
                result = isMissingOk(sourceFile, path, latin, isAliased)
                    || sourceFile.getLocaleID().equals("en") ? MissingStatus.ROOT_OK : MissingStatus.ABSENT;
            } else if (isAliased) {
                result = MissingStatus.PRESENT;
                // } else if (path.contains("decimalFormatLength[@type=\"long\"]") &&
                // path.contains("pattern[@type=\"1")) { // aliased
                // // special case compact numbers
                // //
                // ldml/numbers/decimalFormats[@numberSystem="latn"]/decimalFormatLength[@type="long"]/decimalFormat[@type="standard"]/pattern[@type="10000000"]
                // result = MissingStatus.ABSENT;
            } else {
                result = MissingStatus.ALIASED;
            }
        }
        return result;
    }

    public static final UnicodeSet LATIN = new UnicodeSet("[:sc=Latn:]").freeze();

    public static boolean isLatinScriptLocale(CLDRFile sourceFile) {
        UnicodeSet main = sourceFile.getExemplarSet("", WinningChoice.WINNING);
        return LATIN.containsSome(main);
    }

    private static StringBuilder appendToMessage(CharSequence usersValue, EnumSet<Subtype> subtypes, StringBuilder testMessage) {
        if (subtypes != null) {
            usersValue = "&lt;" + CollectionUtilities.join(subtypes, ", ") + "&gt; " + usersValue;
        }
        return appendToMessage(usersValue, testMessage);
    }

    private static StringBuilder appendToMessage(CharSequence usersValue, Subtype subtype, StringBuilder testMessage) {
        if (subtype != null) {
            usersValue = "&lt;" + subtype + "&gt; " + usersValue;
        }
        return appendToMessage(usersValue, testMessage);
    }

    private static StringBuilder appendToMessage(CharSequence usersValue, StringBuilder testMessage) {
        if (usersValue.length() == 0) {
            return testMessage;
        }
        if (testMessage.length() != 0) {
            testMessage.append("<br>");
        }
        return testMessage.append(usersValue);
    }

    static final NumberFormat nf = NumberFormat.getIntegerInstance(ULocale.ENGLISH);
    private Relation<String, String> reasonsToPaths;
    private CLDRURLS urls = CLDRConfig.getInstance().urls();

    static {
        nf.setGroupingUsed(true);
    }

    /**
     * Class that allows the relaying of progress information
     *
     * @author srl
     *
     */
    public static class ProgressCallback {
        /**
         * Note any progress. This will be called before any output is printed.
         * It will be called approximately once per xpath.
         */
        public void nudge() {
        }

        /**
         * Called when all operations are complete.
         */
        public void done() {
        }
    }

    private ProgressCallback progressCallback = new ProgressCallback(); // null

    // instance
    // by
    // default

    /**
     * Select a new callback. Must be set before running.
     *
     * @return
     *
     */
    public VettingViewer<T> setProgressCallback(ProgressCallback newCallback) {
        progressCallback = newCallback;
        return this;
    }

    public ErrorChecker getErrorChecker() {
        return errorChecker;
    }

    /**
     * Select a new error checker. Must be set before running.
     *
     * @return
     *
     */
    public VettingViewer<T> setErrorChecker(ErrorChecker errorChecker) {
        this.errorChecker = errorChecker;
        return this;
    }

    /**
     * Provide the styles for inclusion into the ST &lt;head&gt; element.
     *
     * @return
     */
    public static String getHeaderStyles() {
        return "<style type='text/css'>\n"
            + ".hide {display:none}\n"
            + ".vve {}\n"
            + ".vvn {}\n"
            + ".vvp {}\n"
            + ".vvl {}\n"
            + ".vvm {}\n"
            + ".vvu {}\n"
            + ".vvw {}\n"
            + ".vvd {}\n"
            + ".vvo {}\n"
            + "</style>";
    }

    private void writeTables(Appendable output, CLDRFile sourceFile, CLDRFile lastSourceFile,
        Relation<R2<SectionId, PageId>, WritingInfo> sorted,
        EnumSet<Choice> choices,
        String localeID,
        boolean nonVettingPhase,
        FileInfo outputFileInfo,
        boolean quick
        ) {
        try {

            boolean latin = VettingViewer.isLatinScriptLocale(sourceFile);

            Status status = new Status();

            output.append("<h2>Summary</h2>\n")
            .append("<p><i>It is important that you read " +
                "<a target='CLDR-ST-DOCS' href='http://cldr.unicode.org/translation/vetting-view'>" +
                "Priority Items</a> before starting!</i></p>")
                .append("<form name='checkboxes' action='#'>\n")
                .append("<table class='tvs-table'>\n")
                .append("<tr class='tvs-tr'>" +
                    "<th class='tv-th'>Count</th>" +
                    "<th class='tv-th'>Issue</th>" +
                    "<th class='tv-th'>Description</th>" +
                    "</tr>\n");

            // find the choice to check
            // OLD if !vetting and missing != 0, use missing. Otherwise pick first.
            Choice checkedItem = null;
            // if (nonVettingPhase && problemCounter.get(Choice.missingCoverage) != 0) {
            // checkedItem = Choice.missingCoverage;
            // }

            for (Choice choice : choices) {
                if (quick && choice != Choice.error && choice != Choice.warning) { //if "quick" mode, only show errors and warnings
                    continue;
                }
                long count = outputFileInfo.problemCounter.get(choice);
                output.append("<tr><td class='tvs-count'>")
                .append(nf.format(count))
                .append("</td>\n\t<td nowrap class='tvs-abb'>")
                .append("<input type='checkbox' name='")
                .append(Character.toLowerCase(choice.abbreviation))
                .append("' onclick='setStyles()'");
                if (checkedItem == choice || checkedItem == null && count != 0) {
                    output.append(" checked");
                    checkedItem = choice;
                }
                output.append(">");
                choice.appendDisplay("", output);
                output.append("</td>\n\t<td class='tvs-desc'>")
                .append(choice.description)
                .append("</td></tr>\n");
            }
            output.append("</table>\n</form>\n"
                + "<script type='text/javascript'>\n" +
                "<!-- \n" +
                "setStyles()\n" +
                "-->\n"
                + "</script>");

            // gather information on choices on each page

            Relation<Row.R3<SectionId, PageId, String>, Choice> choicesForHeader = Relation.of(
                new HashMap<Row.R3<SectionId, PageId, String>, Set<Choice>>(), HashSet.class);

            Relation<Row.R2<SectionId, PageId>, Choice> choicesForSection = Relation.of(
                new HashMap<R2<SectionId, PageId>, Set<Choice>>(), HashSet.class);

            for (Entry<R2<SectionId, PageId>, Set<WritingInfo>> entry0 : sorted.keyValuesSet()) {
                SectionId section = entry0.getKey().get0();
                PageId subsection = entry0.getKey().get1();
                final Set<WritingInfo> rows = entry0.getValue();
                for (WritingInfo pathInfo : rows) {
                    String header = pathInfo.codeOutput.getHeader();
                    Set<Choice> choicesForPath = pathInfo.problems;
                    choicesForSection.putAll(Row.of(section, subsection), choicesForPath);
                    choicesForHeader.putAll(Row.of(section, subsection, header), choicesForPath);
                }
            }

            final String localeId = sourceFile.getLocaleID();
            final CLDRLocale locale = CLDRLocale.getInstance(localeId);
            int count = 0;
            for (Entry<R2<SectionId, PageId>, Set<WritingInfo>> entry0 : sorted.keyValuesSet()) {
                SectionId section = entry0.getKey().get0();
                PageId subsection = entry0.getKey().get1();
                final Set<WritingInfo> rows = entry0.getValue();

                rows.iterator().next(); // getUrl(localeId); (no side effect?)
                // http://kwanyin.unicode.org/cldr-apps/survey?_=ur&x=scripts
                // http://unicode.org/cldr-apps/survey?_=ur&x=scripts

                output.append("\n<h2 class='tv-s'>Section: ")
                .append(section.toString())
                .append(" — <i><a " + /*target='CLDR_ST-SECTION' */"href='")
                .append(urls.forPage(locale, subsection))
                .append("'>Page: ")
                .append(subsection.toString())
                .append("</a></i> (" + rows.size() + ")</h2>\n");
                startTable(choicesForSection.get(Row.of(section, subsection)), output);

                String oldHeader = "";
                for (WritingInfo pathInfo : rows) {
                    String header = pathInfo.codeOutput.getHeader();
                    String code = pathInfo.codeOutput.getCode();
                    String path = pathInfo.codeOutput.getOriginalPath();
                    Set<Choice> choicesForPath = pathInfo.problems;

                    if (!header.equals(oldHeader)) {
                        Set<Choice> headerChoices = choicesForHeader.get(Row.of(section, subsection, header));
                        output.append("<tr class='");
                        Choice.appendRowStyles(headerChoices, output);
                        output.append("'>\n");
                        output.append(" <th class='partsection' colSpan='6'>");
                        output.append(header);
                        output.append("</th>\n</tr>\n");
                        oldHeader = header;
                    }

                    output.append("<tr class='");
                    Choice.appendRowStyles(choicesForPath, output);
                    output.append("'>\n");
                    addCell(output, nf.format(++count), null, "tv-num", HTMLType.plain);
                    // path
                    addCell(output, code, null, "tv-code", HTMLType.plain);
                    // English value
                    if (choicesForPath.contains(Choice.englishChanged)) {
                        String winning = englishFile.getWinningValue(path);
                        String cellValue = winning == null ? "<i>missing</i>" : TransliteratorUtilities.toHTML
                            .transform(winning);
                        String previous = outdatedPaths.getPreviousEnglish(path);
                        if (previous != null) {
                            cellValue += "<br><span style='color:#900'><b>OLD: </b>"
                                + TransliteratorUtilities.toHTML.transform(previous) + "</span>";
                        } else {
                            cellValue += "<br><b><i>missing</i></b>";
                        }
                        addCell(output, cellValue, null, "tv-eng", HTMLType.markup);
                    } else {
                        addCell(output, englishFile.getWinningValue(path), null, "tv-eng", HTMLType.plain);
                    }
                    // value for last version
                    final String oldStringValue = lastSourceFile == null ? null : lastSourceFile.getWinningValue(path);
                    MissingStatus oldValueMissing = getMissingStatus(lastSourceFile, path, status, latin);

                    addCell(output, oldStringValue, null, oldValueMissing != MissingStatus.PRESENT ? "tv-miss"
                        : "tv-last", HTMLType.plain);
                    // value for last version
                    String newWinningValue = sourceFile.getWinningValue(path);
                    if (Objects.equals(newWinningValue, oldStringValue)) {
                        newWinningValue = "=";
                    }
                    addCell(output, newWinningValue, null, choicesForPath.contains(Choice.missingCoverage) ? "tv-miss"
                        : "tv-win", HTMLType.plain);
                    // Fix?
                    // http://unicode.org/cldr/apps/survey?_=az&xpath=%2F%2Fldml%2FlocaleDisplayNames%2Flanguages%2Flanguage%5B%40type%3D%22az%22%5D
                    output.append(" <td class='tv-fix'><a target='_blank' href='")
                    .append(pathInfo.getUrl(locale)) // .append(c)baseUrl + "?_=")
                    // .append(localeID)
                    // .append("&amp;xpath=")
                    // .append(percentEscape.transform(path))
                    .append("'>");
                    Choice.appendDisplay(choicesForPath, "", output);
                    // String otherUrl = pathInfo.getUrl(sourceFile.getLocaleID());
                    output.append("</a></td>");
                    // if (!otherUrl.equals(url)) {
                    // output.append("<td class='tv-test'><a "+/*target='CLDR_ST-SECTION' */"href='")
                    // .append(otherUrl)
                    // .append("'><i>Section*</i></a></td>");
                    // }
                    if (!pathInfo.htmlMessage.isEmpty()) {
                        addCell(output, pathInfo.htmlMessage, null, "tv-test", HTMLType.markup);
                    }
                    output.append("</tr>\n");
                }
                output.append("</table>\n");
            }
        } catch (IOException e) {
            throw new IllegalArgumentException(e); // damn'ed checked exceptions
        }
    }

    /**
     *
     * @param output
     * @param choices
     *            See the class description for more information.
     * @param localeId
     * @param user
     * @param usersLevel
     * @param nonVettingPhase
     */
    public ArrayList<String> getErrorOnPath(EnumSet<Choice> choices, String localeID, T user,
        Level usersLevel, boolean nonVettingPhase, String path) {

        // Gather the relevant paths
        // each one will be marked with the choice that it triggered.
        Relation<R2<SectionId, PageId>, WritingInfo> sorted = Relation.of(
            new TreeMap<R2<SectionId, PageId>, Set<WritingInfo>>(), TreeSet.class);

        CLDRFile sourceFile = cldrFactory.make(localeID, true);

        // Initialize
        CLDRFile lastSourceFile = null;
        try {
            lastSourceFile = cldrFactoryOld.make(localeID, true);
        } catch (Exception e) {
        }

        EnumSet<Choice> errors = new FileInfo().
            getFileInfo(sourceFile, lastSourceFile, sorted, choices, localeID, nonVettingPhase, user, usersLevel,
                false, path).problems;

        ArrayList<String> out = new ArrayList<String>();
        for (Object error : errors.toArray()) {
            out.add(((Choice) error).buttonLabel);
        }

        return out;
    }

    /*private void getJSONReview(Appendable output, CLDRFile sourceFile, CLDRFile lastSourceFile,
        Relation<R2<SectionId, PageId>, WritingInfo> sorted,
        EnumSet<Choice> choices,
        String localeID,
        boolean nonVettingPhase,
        FileInfo outputFileInfo,
        boolean quick
        ) {

        try {
            boolean latin = VettingViewer.isLatinScriptLocale(sourceFile);
            JSONObject reviewInfo = new JSONObject();
            JSONArray notificationsCount = new JSONArray();
            List<String> notifications = new ArrayList<String>();
            Status status = new Status();



            for (Choice choice : choices) {
                    notificationsCount.put(new JSONObject().put("name",choice.buttonLabel.replace(' ', '_')).put("description", choice.description).put("count", outputFileInfo.problemCounter.get(choice)));
                    notifications.add(choice.buttonLabel);
            }

            reviewInfo.put("notification", notificationsCount);
            // gather information on choices on each page
            //output.append(reviewInfo.toString());


            Relation<Row.R3<SectionId, PageId, String>, Choice> choicesForHeader = Relation.of(
                new HashMap<Row.R3<SectionId, PageId, String>, Set<Choice>>(), HashSet.class);

            Relation<Row.R2<SectionId, PageId>, Choice> choicesForSection = Relation.of(
                new HashMap<R2<SectionId, PageId>, Set<Choice>>(), HashSet.class);

            Comparator<? super R4<Choice, SectionId, PageId, String>> comparator = new Comparator<Row.R4<Choice,SectionId, PageId, String>>() {


                @Override
                public int compare(R4<Choice, SectionId, PageId, String> o1, R4<Choice, SectionId, PageId, String> o2) {
                    int compChoice = o2.get0().order - o1.get0().order;
                    if(compChoice == 0) {
                        int compSection = o1.get1().compareTo(o2.get1());
                        if(compSection == 0) {
                            int compPage = o1.get2().compareTo(o2.get2());
                            if(compPage == 0)
                                return o1.get3().compareTo(o2.get3());
                            else
                                return 0;
                        }
                        else
                            return compSection;
                    }
                    else
                        return compChoice;
                }
            };

            Relation<Row.R4<Choice,SectionId, PageId, String>, WritingInfo> notificationsList = Relation.of(
                new TreeMap<Row.R4<Choice,SectionId, PageId, String>, Set<WritingInfo>>(comparator), TreeSet.class);


            //TODO we can prob do it in only one loop, but with that we can sort
            for (Entry<R2<SectionId, PageId>, Set<WritingInfo>> entry0 : sorted.keyValuesSet()) {
                final Set<WritingInfo> rows = entry0.getValue();
                for (WritingInfo pathInfo : rows) {
                    Set<Choice> choicesForPath = pathInfo.problems;
                    SectionId section = entry0.getKey().get0();
                    PageId subsection = entry0.getKey().get1();
                    for(Choice choice : choicesForPath) {
                        //reviewInfo
                        notificationsList.put(Row.of(choice, section, subsection, pathInfo.codeOutput.getHeader()), pathInfo);
                    }
                }

            }

            JSONArray allNotifications = new JSONArray();
            for(Entry<R4<Choice, SectionId, PageId, String>, Set<WritingInfo>> entry : notificationsList.keyValuesSet()) {

                        String notificationName = entry.getKey().get0().buttonLabel.replace(' ', '_');
                        int notificationNumber = entry.getKey().get0().order;

                        String sectionName = entry.getKey().get1().name();
                        String pageName = entry.getKey().get2().name();
                        String headerName = entry.getKey().get3();

                        if(allNotifications.optJSONObject(notificationNumber) == null) {
                            allNotifications.put(notificationNumber,new JSONObject().put(notificationName, new JSONObject()));
                        }

                        JSONObject sections = allNotifications.getJSONObject(notificationNumber).getJSONObject(notificationName);

                        if(sections.optJSONObject(sectionName) == null) {
                            sections.accumulate(sectionName, new JSONObject());
                        }
                        JSONObject pages = sections.getJSONObject(sectionName);

                        if(pages.optJSONObject(pageName) == null) {
                            pages.accumulate(pageName, new JSONObject());
                        }
                        JSONObject header = pages.getJSONObject(pageName);

                        JSONArray allContent = new JSONArray();
                        //real info
                        for(WritingInfo info : entry.getValue()) {
                            JSONObject content = new JSONObject();
                            String code = info.codeOutput.getCode();
                            String path = info.codeOutput.getOriginalPath();
                            Set<Choice> choicesForPath = info.problems;

                            //code
                            content.put("code",code);
                            content.put("path", ctx.sm.xpt.getByXpath(path));

                            //english
                            if (choicesForPath.contains(Choice.englishChanged)) {
                                String winning = englishFile.getWinningValue(path);
                                String cellValue = winning == null ? "<i>missing</i>" : TransliteratorUtilities.toHTML
                                    .transform(winning);
                                String previous = outdatedPaths.getPreviousEnglish(path);
                                if (previous != null) {
                                    cellValue += "<br><span style='color:#900'><b>OLD: </b>"
                                        + TransliteratorUtilities.toHTML.transform(previous) + "</span>";
                                } else {
                                    cellValue += "<br><b><i>missing</i></b>";
                                }
                                content.put("english", cellValue);
                            } else {
                                content.put("english",englishFile.getWinningValue(path));
                            }

                            //old release
                            final String oldStringValue = lastSourceFile == null ? null : lastSourceFile.getWinningValue(path);
                            content.put("old", oldStringValue);

                            //

                            //winning value
                            String newWinningValue = sourceFile.getWinningValue(path);
                            if (CharSequences.equals(newWinningValue, oldStringValue)) {
                                newWinningValue = "=";
                            }
                            content.put("winning",newWinningValue);

                            //comment
                            String comment = "";
                            if (!info.htmlMessage.isEmpty()) {
                                comment = info.htmlMessage;
                            }
                            content.put("comment", comment.replace("\"", "&quot;"));

                            content.put("id", StringId.getHexId(info.codeOutput.getOriginalPath()));
                           allContent.put(content);
                        }
                        header.put(headerName, allContent);

            }
            reviewInfo.put("allNotifications", allNotifications);

            //hidden info
            ReviewHide review = new ReviewHide();
            reviewInfo.put("hidden", review.getHiddenField(ctx.userId(), ctx.getLocale().toString()));
            reviewInfo.put("direction", ctx.getDirectionForLocale());
            output.append(reviewInfo.toString());
        }
        catch (JSONException | IOException e) {
                e.printStackTrace();
        }
    }
     */
    private void startTable(Set<Choice> choices, Appendable output) throws IOException {
        output.append("<table class='tv-table'>\n");
        output.append("<tr class='");
        Choice.appendRowStyles(choices, output);
        output.append("'>" +
            "<th class='tv-th'>No.</th>" +
            "<th class='tv-th'>Code</th>" +
            "<th class='tv-th'>English</th>" +
            "<th class='tv-th'>" + lastVersionTitle + "</th>" +
            "<th class='tv-th'>" + currentWinningTitle + "</th>" +
            "<th class='tv-th'>Fix?</th>" +
            "<th class='tv-th'>Comment</th>" +
            "</tr>\n");
    }

    enum HTMLType {
        plain, markup
    }

    private void addCell(Appendable output, String value, String title, String classValue, HTMLType htmlType)
        throws IOException {
        output.append(" <td class='")
        .append(classValue);
        if (value == null) {
            output.append(" tv-null'><i>missing</i></td>");
        } else {
            if (title != null && !title.equals(value)) {
                output.append("title='").append(TransliteratorUtilities.toHTML.transform(title)).append('\'');
            }
            output
            .append("'>")
            .append(htmlType == HTMLType.markup ? value : TransliteratorUtilities.toHTML.transform(value))
            .append("</td>\n");
        }
    }

    /**
     * Find the status of the items in the file.
     * @param file the source. Must be a resolved file, made with minimalDraftStatus = unconfirmed
     * @param pathHeaderFactory PathHeaderFactory.
     * @param foundCounter output counter of the number of paths with values having contributed or approved status
     * @param unconfirmedCounter output counter of the number of paths with values, but neither contributed nor approved status
     * @param missingCounter output counter of the number of paths without values
     * @param missingPaths output if not null, the specific paths that are missing.
     * @param unconfirmedPaths TODO
     */
    public static void getStatus(CLDRFile file, PathHeader.Factory pathHeaderFactory,
        Counter<Level> foundCounter, Counter<Level> unconfirmedCounter,
        Counter<Level> missingCounter,
        Relation<MissingStatus, String> missingPaths,
        Set<String> unconfirmedPaths) {
        getStatus(file.fullIterable(), file, pathHeaderFactory, foundCounter, unconfirmedCounter, missingCounter, missingPaths, unconfirmedPaths);
    }

    /**
     * Find the status of the items in the file.
     * @param allPaths manual list of paths
     * @param file the source. Must be a resolved file, made with minimalDraftStatus = unconfirmed
     * @param pathHeaderFactory PathHeaderFactory.
     * @param foundCounter output counter of the number of paths with values having contributed or approved status
     * @param unconfirmedCounter output counter of the number of paths with values, but neither contributed nor approved status
     * @param missingCounter output counter of the number of paths without values
     * @param missingPaths output if not null, the specific paths that are missing.
     * @param unconfirmedPaths TODO
     */
    public static void getStatus(Iterable<String> allPaths, CLDRFile file,
        PathHeader.Factory pathHeaderFactory, Counter<Level> foundCounter,
        Counter<Level> unconfirmedCounter,
        Counter<Level> missingCounter,
        Relation<MissingStatus, String> missingPaths, Set<String> unconfirmedPaths) {

        if (!file.isResolved()) {
            throw new IllegalArgumentException("File must be resolved, no minimal draft status");
        }
        foundCounter.clear();
        unconfirmedCounter.clear();
        missingCounter.clear();

        Status status = new Status();
        boolean latin = VettingViewer.isLatinScriptLocale(file);
        CoverageLevel2 coverageLevel2 = CoverageLevel2.getInstance(file.getLocaleID());

        for (String path : allPaths) {

            PathHeader ph = pathHeaderFactory.fromPath(path);
            if (ph.getSectionId() == SectionId.Special) {
                continue;
            }

            Level level = coverageLevel2.getLevel(path);
            // String localeFound = file.getSourceLocaleID(path, status);
            // String value = file.getSourceLocaleID(path, status);
            MissingStatus missingStatus = VettingViewer.getMissingStatus(file, path, status, latin);

            switch (missingStatus) {
            case ABSENT:
                missingCounter.add(level, 1);
                if (missingPaths != null && level.compareTo(Level.MODERN) <= 0) {
                    missingPaths.put(missingStatus, path);
                }
                break;
            case ALIASED:
            case PRESENT:
                String fullPath = file.getFullXPath(path);
                if (fullPath.contains("unconfirmed")
                    || fullPath.contains("provisional")) {
                    unconfirmedCounter.add(level, 1);
                    if (unconfirmedPaths != null && level.compareTo(Level.MODERN) <= 0) {
                        unconfirmedPaths.add(path);
                    }
                } else {
                    foundCounter.add(level, 1);
                }
                break;
            case MISSING_OK:
            case ROOT_OK:
                break;
            default:
                throw new IllegalArgumentException();
            }
        }
    }

    /**
     * Simple example of usage
     *
     * @param args
     * @throws IOException
     */
    final static Options myOptions = new Options();

    enum MyOptions {
        repeat(null, null, "Repeat indefinitely"),
        filter(".*", ".*", "Filter files"),
        locale(".*", "af", "Single locale for testing"),
        source(".*", CLDRPaths.MAIN_DIRECTORY, // CldrUtility.TMP2_DIRECTORY + "/vxml/common/main"
            "if summary, creates filtered version (eg -d main): does a find in the name, which is of the form dir/file"),
            verbose(null, null, "verbose debugging messages"),
            output(".*", CLDRPaths.GEN_DIRECTORY + "vetting/", "filter the raw files (non-summary, mostly for debugging)"), ;
        // boilerplate
        final Option option;

        MyOptions(String argumentPattern, String defaultArgument, String helpText) {
            option = myOptions.add(this, argumentPattern, defaultArgument, helpText);
        }
    }

    public static void main(String[] args) throws IOException {
        SHOW_SUBTYPES = true;
        myOptions.parse(MyOptions.source, args, true);
        boolean repeat = MyOptions.repeat.option.doesOccur();
        String fileFilter = MyOptions.filter.option.getValue();
        String myOutputDir = repeat ? null : MyOptions.output.option.getValue();
        String LOCALE = MyOptions.locale.option.getValue();
        String CURRENT_MAIN = MyOptions.source.option.getValue();
        final String version = "24.0";
        //final String lastMain = CLDRPaths.ARCHIVE_DIRECTORY + "/cldr-" + version + "/common/main";
        final String lastMain = CLDRPaths.ARCHIVE_DIRECTORY + "/common/main";
        do {
            Timer timer = new Timer();
            timer.start();

            Factory cldrFactory = Factory.make(CURRENT_MAIN, fileFilter);
            cldrFactory.setSupplementalDirectory(new File(CLDRPaths.SUPPLEMENTAL_DIRECTORY));
            Factory cldrFactoryOld = Factory.make(lastMain, fileFilter);
            SupplementalDataInfo supplementalDataInfo = SupplementalDataInfo
                .getInstance(CLDRPaths.SUPPLEMENTAL_DIRECTORY);
            CheckCLDR.setDisplayInformation(cldrFactory.make("en", true));

            // FAKE this, because we don't have access to ST data

            UsersChoice<Organization> usersChoice = new UsersChoice<Organization>() {
                // Fake values for now
                public String getWinningValueForUsersOrganization(CLDRFile cldrFile, String path, Organization user) {
                    if (path.contains("USD")) {
                        return "&dummy ‘losing’ value";
                    }
                    return null; // assume we didn't vote on anything else.
                }

                // Fake values for now
                public VoteStatus getStatusForUsersOrganization(CLDRFile cldrFile, String path, Organization user) {
                    String usersValue = getWinningValueForUsersOrganization(cldrFile, path, user);
                    String winningValue = cldrFile.getWinningValue(path);
                    if (usersValue != null && !Objects.equals(usersValue, winningValue)) {
                        return VoteStatus.losing;
                    }
                    String fullPath = cldrFile.getFullXPath(path);
                    if (fullPath.contains("AMD") || fullPath.contains("unconfirmed") || fullPath.contains("provisional")) {
                        return VoteStatus.provisionalOrWorse;
                    } else if (fullPath.contains("AED")) {
                        return VoteStatus.disputed;
                    } else if (fullPath.contains("AED")) {
                        return VoteStatus.ok_novotes;
                    }
                    return VoteStatus.ok;
                }
            };

            // create the tableView and set the options desired.
            // The Options should come from a GUI; from each you can get a long
            // description and a button label.
            // Assuming user can be identified by an int
            VettingViewer<Organization> tableView = new VettingViewer<Organization>(supplementalDataInfo, cldrFactory,
                cldrFactoryOld, usersChoice, "CLDR " + version,
                "Winning Proposed");

            // here are per-view parameters

            final EnumSet<Choice> choiceSet = EnumSet.allOf(Choice.class);
            String localeStringID = LOCALE;
            int userNumericID = 666;
            Level usersLevel = Level.MODERN;
            // http: // unicode.org/cldr-apps/survey?_=ur

            if (!repeat) {
                FileCopier.copy(VettingViewer.class, "vettingView.css", myOutputDir);
                FileCopier.copy(VettingViewer.class, "vettingView.js", myOutputDir);
            }
            System.out.println("Creation: " + timer.getDuration() / NANOSECS + " secs");

            // timer.start();
            // writeFile(tableView, choiceSet, "", localeStringID, userNumericID, usersLevel, CodeChoice.oldCode);
            // System.out.println(timer.getDuration() / NANOSECS + " secs");

            timer.start();
            writeFile(myOutputDir, tableView, choiceSet, "", localeStringID, userNumericID, usersLevel, CodeChoice.newCode, null);
            System.out.println("Code: " + timer.getDuration() / NANOSECS + " secs");

            timer.start();
            writeFile(myOutputDir, tableView, choiceSet, "", localeStringID, userNumericID, usersLevel, CodeChoice.summary,
                Organization.google);
            System.out.println("Summary: " + timer.getDuration() / NANOSECS + " secs");

            //        timer.start();
            //        writeFile(tableView, choiceSet, "", localeStringID, userNumericID, usersLevel, CodeChoice.summary,
            //                Organization.ibm);
            //        System.out.println(timer.getDuration() / NANOSECS + " secs");

            // // check that the choices work.
            // for (Choice choice : choiceSet) {
            // timer.start();
            // writeFile(tableView, EnumSet.of(choice), "-" + choice.abbreviation, localeStringID, userNumericID,
            // usersLevel);
            // System.out.println(timer.getDuration() / NANOSECS + " secs");
            // }
        } while (repeat);
    }

    public enum CodeChoice {
        /** For the normal (locale) view of data **/
        newCode,
        // /** @deprecated **/
        // oldCode,
        /** For a summary view of data **/
        summary
    }

    public static void writeFile(String myOutputDir, VettingViewer<Organization> tableView, final EnumSet<Choice> choiceSet,
        String name, String localeStringID, int userNumericID,
        Level usersLevel,
        CodeChoice newCode, Organization organization)
            throws IOException {
        // open up a file, and output some of the styles to control the table
        // appearance
        PrintWriter out = myOutputDir == null ? new PrintWriter(new StringWriter())
        : BagFormatter.openUTF8Writer(myOutputDir, "vettingView"
            + name
            + (newCode == CodeChoice.newCode ? "" : newCode == CodeChoice.summary ? "-summary" : "")
            + (organization == null ? "" : "-" + organization.toString())
            + ".html");
//        FileUtilities.appendFile(VettingViewer.class, "vettingViewerHead.txt", out);
        FileCopier.copy(VettingViewer.class, "vettingViewerHead.txt", out);
        out.append(getHeaderStyles());
        out.append("</head><body>\n");

        out.println("<p>Note: this is just a sample run. The user, locale, user's coverage level, and choices of tests will change the output. In a real ST page using these, the first three would "
            + "come from context, and the choices of tests would be set with radio buttons. Demo settings are: </p>\n<ol>"
            + "<li>choices: "
            + choiceSet
            + "</li><li>localeStringID: "
            + localeStringID
            + "</li><li>userNumericID: "
            + userNumericID
            + "</li><li>usersLevel: "
            + usersLevel
            + "</ol>"
            + "<p>Notes: This is a static version, using old values and faked values (L) just for testing."
            + (TESTING ? "Also, the white cell after the Fix column is just for testing." : "")
            + "</p><hr>\n");

        // now generate the table with the desired options
        // The options should come from a GUI; from each you can get a long
        // description and a button label.
        // Assuming user can be identified by an int

        switch (newCode) {
        case newCode:
            tableView.generateHtmlErrorTables(out, choiceSet, localeStringID, organization, usersLevel, SHOW_ALL, false);
            break;
            // case oldCode:
            // tableView.generateHtmlErrorTablesOld(out, choiceSet, localeStringID, userNumericID, usersLevel, SHOW_ALL);
            // break;
        case summary:
            //System.out.println(tableView.getName("zh_Hant_HK"));
            tableView.generateSummaryHtmlErrorTables(out, choiceSet, null, organization);
            break;
        }
        out.println("</body>\n</html>\n");
        out.close();
    }
}
