import com.saxonica.config.EnterpriseTransformerFactory;
import com.saxonica.deploy.CompiledUserQuery;
import com.saxonica.config.EnterpriseConfiguration;
import net.sf.saxon.Configuration;
import net.sf.saxon.lib.FeatureKeys;
import net.sf.saxon.TransformerFactoryImpl;
import net.sf.saxon.Version;
import net.sf.saxon.event.Sink;
import net.sf.saxon.om.DocumentInfo;
import net.sf.saxon.lib.Validation;
import net.sf.saxon.query.DynamicQueryContext;
import net.sf.saxon.query.StaticQueryContext;
import net.sf.saxon.query.XQueryExpression;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.value.DateTimeValue;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.DecimalFormat;
import java.util.Properties;


/**
 * Runs the XMark XQuery benchmark: see
 * <a href="http://monetdb.cwi.nl/xml/index.html">http://monetdb.cwi.nl/xml/index.html</a
 *
 */
public class XMarkBenchmark {

    /**
     * Class is not instantiated, so give it a private constructor
     */
    private XMarkBenchmark() {
    }

    static boolean pull = false;
    static boolean lazy = false;
    static boolean saproduct = false;
    static boolean satests = false;
    static boolean xslt = false;
    static boolean runCompiled = false;
    static boolean checkResults = false;
    static boolean val = false;
    static int test = -1;

    /**
     * Method main
     * @param argv command line arguments. The following arguments are recognised:
     * <dl>
     * <dt>-dir:directory</dt><dd>The directory containing the tests</dt>
     * <dt>-pull</dt><dd>Run in pull mode</dd>
     * <dt>-lazy</dt><dd>Enable lazy construction mode</dd>
     * <dt>-sa</dt><dd>Run standard tests in schema-aware mode</dd>
     * <dt>-satests</dt><dd>Run schema-aware versions of the tests in schema-aware mode</dd>
     * <dt>-xslt</dt><dd>Run XSLT versions of the tests</dd>
     * <dt>-runcomp</dt><dd>Run XQuery tests compiled into Java code</dd>
     * <dt>-qN</dt><dd>Run query N only</dd>
     * </dl>
     */
    public static void main(String argv[]) throws Exception {

        String dir = null;
        for (int i=0; i<argv.length; i++) {
            if (argv[i].startsWith("-dir:")) dir=argv[i].substring(5);
            if (argv[i].equals("-pull")) pull = true;
            if (argv[i].equals("-lazy")) lazy = true;
            if (argv[i].equals("-sa")) saproduct = true;
                // means use Saxon-EE optimizer (not the schema-aware version of the tests)
            if (argv[i].equals("-satests")) satests = true;
            if (argv[i].equals("-xslt")) xslt = true;
            if (argv[i].equals("-runcomp")) runCompiled = true;
            if (argv[i].equals("-check")) checkResults = true;
            if (argv[i].equals("-val")) val = true;
            if (argv[i].startsWith("-q")) {
                test = Integer.parseInt(argv[i].substring(2));
            }
            if (argv[i].equals("-?")) {
                System.err.println("Options: -pull -lazy -sa -xslt -compile -runcomp");
            }
        }

        if (dir==null) {
            dir = (satests ? "e:\\javalib\\xmark\\sa\\" : "e:\\javalib\\xmark\\");
        }

        if (xslt) {
            transform(lazy, saproduct, dir);
        } else {
            query(dir);
        }
    }

    /**
     * Run all the XQuery tests
     * @param dir the directory containing the tests
     * @throws Exception
     */

    public static void query(String dir) throws Exception {

        String[] tests = {"xmark1.xml", "xmark4.xml", "xmark10.xml"};
        String options = "";
        String when = DateTimeValue.getCurrentDateTime(null).getStringValue();
        if (saproduct) options += "-sa ";
        if (satests) options += "-satests ";
        if (pull) options += "-pull ";
        if (lazy) options += "-lazy ";
        if (val) options += "-val ";
        if (runCompiled) options += "-runcomp";
        System.out.println("<xmark-results product='Saxon' lang='query' version='" + Version.getProductVersion() +
                "' options='" + options + "' when='" + when + "'>");

        for (int f=0; f<tests.length; f++) {
            File file = new File(dir + tests[f]);

            Configuration config = (saproduct ? new EnterpriseConfiguration() : new Configuration());
            if (lazy) {
                config.setLazyConstructionMode(true);
            }
            if (satests) {
                config.setSchemaValidationMode(Validation.STRICT);
                config.addSchemaSource(new StreamSource(new File(dir + "schema.xsd")),
                        config.getErrorListener());
                config.setConfigurationProperty(FeatureKeys.USE_TYPED_VALUE_CACHE, "false");
            }
            if (val) {
                config.loadSchema(new File(dir + "sa\\schema.xsd").toURI().toString());
                config.setSchemaValidationMode(Validation.STRICT);
            }

            Source streamSource = new StreamSource(file);
            long pstart = System.currentTimeMillis();
            DocumentInfo doc = config.buildDocument(streamSource);
            long pend = System.currentTimeMillis();

            System.out.println("  <file name='" + tests[f] +
                    "' size='" + file.length() +
                    "' build-time='" + (pend-pstart) + "'>");
            Properties props = new Properties();
            props.setProperty("method", "xml");
            props.setProperty("indent", "yes");

            if (runCompiled) {
                for (int q=1; q<=20; q++) {
                    if (test != -1 && test != q) {
                        continue;
                    }
                    try {

                        ClassLoader usercodeLoader = new URLClassLoader(
                                new URL[]{new URL("file:///e:/javalib/xMark/java/")});
                        Class userClass = usercodeLoader.loadClass("Q" + q);
                        CompiledUserQuery cuq = (CompiledUserQuery) userClass.newInstance();
                        cuq.setConfiguration(config);
                        final DynamicQueryContext dqc = new DynamicQueryContext(config);
                        cuq.setDynamicContext(dqc);

                        int runs = 0;
                        long totalTime = 0;
                        long min = Integer.MAX_VALUE;
                        long max = 0;
                        if (checkResults && f==0) {
                            dqc.setContextItem(doc);
                            File out = new File("e:/javalib/xmark/output/q" + q + ".out");
                            //out.mkdirs();
                            OutputStream os = new FileOutputStream(out);
                            Result result = new StreamResult(os);
                            cuq.run(result, props);
                            os.close();
                            compare(new File("e:/javalib/xmark/output/q" + q + ".out"),
                                    new File("e:/javalib/xmark/gold1m/q" + q + ".out"));
                        }
                        for (int t=0; t<1000; t++) {

                            dqc.setContextItem(doc);
                            Result result = new Sink();
                            long start, end;
                            start = System.currentTimeMillis();
                            cuq.run(result, props);
                            end = System.currentTimeMillis();

                            runs++;
                            long time = (end - start);
                            if (time < min) min = time;
                            if (time > max) max = time;
                            totalTime += time;
                            if (totalTime > 30000 && t>=2) break;
                        }
                        double avg = ((double)totalTime) / runs;
                        String avgs = format.format(avg);
                        System.out.println("    <query q='" + q +
                                    "' avg='" + avgs +
                                    "' runs='" + runs +
                                    "' min='" + min +
                                    "' max='" + max + "'/>");
                    } catch (XPathException err) {
                        System.err.println("Error in test " + q);
                        System.err.println(err.getMessageAndLocation());
                    }
                }
            } else {

                for (int q=1; q<=20; q++) {
                    if (test != -1 && test != q) {
                        continue;
                    }
                    try {
                        File query = new File(dir + 'q' + q + ".xq");
                        StaticQueryContext qenv = config.newStaticQueryContext();
                        qenv.setBaseURI(query.toURI().toString());
                        if (val) {
                            qenv.setSchemaAware(true);
                        }
                        XQueryExpression exp = qenv.compileQuery(new FileReader(query));
                        int runs = 0;
                        long totalTime = 0;
                        long min = Integer.MAX_VALUE;
                        long max = 0;
                        for (int t=0; t<1000; t++) {
                            final DynamicQueryContext context = new DynamicQueryContext(config);
                            context.setContextItem(doc);
                            Result result = new Sink();
                            long start, end;
                            if (pull) {
                                start = System.currentTimeMillis();
                                exp.pull(context, result, props);
                                end = System.currentTimeMillis();
                            } else {
                                start = System.currentTimeMillis();
                                exp.run(context, result, props);
                                end = System.currentTimeMillis();
                            }
                            runs++;
                            long time = (end - start);
                            if (time < min) min = time;
                            if (time > max) max = time;
                            totalTime += time;
                            if (totalTime > 30000 && t>=2) break;
                        }
                        double avg = ((double)totalTime) / runs;
                        String avgs = format.format(avg);
                        System.out.println("    <query q='" + q +
                                    "' avg='" + avgs +
                                    "' runs='" + runs +
                                    "' min='" + min +
                                    "' max='" + max + "'/>");
                    } catch (XPathException err) {
                        System.err.println("Error in test " + q);
                        System.err.println(err.getMessageAndLocation());
                    }
                }
            }
            System.out.println("  </file>");
        }
        System.out.println("</xmark-results>");
    }

    /**
     * Run an XSLT transformation
     * @param lazy true if lazy construction mode is to be set
     * @param sa true if schema-aware
     * @param dir the directory containing the tests
     * @throws Exception
     */

    public static void transform(boolean lazy, boolean sa, String dir) throws Exception {

        String[] tests = {"xmark1.xml", "xmark4.xml", "xmark10.xml"};

        System.out.println("<xmark-results product='Saxon' lang='xslt' version='" + Version.getProductVersion() + "'>");

        for (int f=0; f<tests.length; f++) {
            File file = new File(dir + tests[f]);

//            String factoryName = (sa ? "com.saxonica.config.EnterpriseTransformerFactory" : "net.sf.saxon.TransformerFactoryImpl");
//            System.setProperty("java.xml.transform.TransformerFactory", factoryName);
            TransformerFactory factory;
            if (sa) {
                factory = new EnterpriseTransformerFactory();
            } else {
                factory = new TransformerFactoryImpl();
            }
            Configuration config = ((TransformerFactoryImpl)factory).getConfiguration();
            if (lazy) {
                config.setLazyConstructionMode(true);
            }

            Source streamSource = new StreamSource(file);
            long pstart = System.currentTimeMillis();
            DocumentInfo doc = config.buildDocument(streamSource);
            long pend = System.currentTimeMillis();

            System.out.println("  <file name='" + tests[f] +
                    "' size='" + file.length() +
                    "' schema-aware='" + config.isLicensedFeature(Configuration.LicenseFeature.ENTERPRISE_XSLT) +
                    "' build-time='" + (pend-pstart) + "'>");

            for (int q=1; q<=20; q++) {
                //if (q==9) continue;
                File sheet = new File(dir + 'q' + q + ".xsl");
                if (!sheet.exists()) {
                    continue;
                }
                Templates templates = factory.newTemplates(new StreamSource(sheet));

                int runs = 0;
                long totalTime = 0;
                long min = Integer.MAX_VALUE;
                long max = 0;
                for (int t=0; t<10; t++) {
                    Result result = new Sink();
                    long start, end;

                    start = System.currentTimeMillis();
                    templates.newTransformer().transform(doc, result);
                    end = System.currentTimeMillis();

                    runs++;
                    long time = (end - start);
                    if (time < min) min = time;
                    if (time > max) max = time;
                    totalTime += time;
                    if (totalTime > 1000 && t>=2) break;
                }
                System.out.println("    <query q='" + q +
                            "' avg='" + (totalTime / runs) +
                            "' runs='" + runs +
                            "' min='" + min +
                            "' max='" + max + "'/>");
            }
            System.out.println("  </file>");
        }
        System.out.println("</xmark-results>");
    }

    /**
     * Compare two files containing test results
     * @param actual the actual results
     * @param gold the reference results
     */

    private static void compare(File actual, File gold) {
        try {
            // This is decoding bytes assuming the platform default encoding
            FileReader reader1 = new FileReader(actual);
            FileReader reader2 = new FileReader(gold);
            char[] contents1 = new char[65536];
            char[] contents2 = new char[65536];
            int size1 = reader1.read(contents1, 0, 65536);
            int size2 = reader2.read(contents2, 0, 65536);
            reader1.close();
            reader2.close();
            int offset1 = 0;
            int offset2 = 0;
            if (contents1[0]=='\u00ef' && contents1[1]=='\u00bb' && contents1[2]=='\u00bf') {
                offset1 += 3;
            }
            if (contents2[0]=='\u00ef' && contents2[1]=='\u00bb' && contents2[2]=='\u00bf') {
                offset2 += 3;
            }
            String actResult = (size1==-1 ? "" : new String(contents1, offset1, size1-offset1));
            String refResult = (size2==-1 ? "" : new String(contents2, offset2, size2-offset2));

//            actResult = actResult.replace("\r\n", "\n");
//            refResult = refResult.replace("\r\n", "\n");
            if (actResult.equals(refResult)) {
                System.err.println("Results compared OK");
            } else {
                System.err.println("*** Results do not match");
            }
        } catch (Exception err) {
            System.err.println("Failed to compare with gold results: " + err.getMessage());
        }
    }

    private final static DecimalFormat format = new DecimalFormat("######0.00");


}

//
// The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License");
// you may not use this file except in compliance with the License. You may obtain a copy of the
// License at http://www.mozilla.org/MPL/
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied.
// See the License for the specific language governing rights and limitations under the License.
//
// The Original Code is: all this file.
//
// The Initial Developer of the Original Code is Michael H. Kay.
//
// Portions created by (your name) are Copyright (C) (your legal entity). All Rights Reserved.
//
// Contributor(s): none.
//
