/*   Copyright 2004 The Apache Software Foundation
 *
 *   Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.xmlbeans.impl.tool;

import org.apache.xmlbeans.SystemProperties;
import org.apache.xmlbeans.XmlError;
import org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;

public class Diff {
    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Usage: diff <jarname1> <jarname2> to compare two jars");
            System.out.println("  or   diff <dirname1> <dirname2> to compare two dirs");
            return;
        }
        File file1 = new File(args[0]);
        if (!file1.exists()) {
            System.out.println("File \"" + args[0] + "\" not found.");
            return;
        }
        File file2 = new File(args[1]);
        if (!file2.exists()) {
            System.out.println("File \"" + args[1] + "\" not found.");
            return;
        }
        List<XmlError> result = new ArrayList<>();
        if (file1.isDirectory()) {
            if (!file2.isDirectory()) {
                System.out.println("Both parameters have to be directories if the first parameter is a directory.");
                return;
            }
            dirsAsTypeSystems(file1, file2, result);
        } else {
            if (file2.isDirectory()) {
                System.out.println("Both parameters have to be jar files if the first parameter is a jar file.");
                return;
            }
            try {
                JarFile jar1 = new JarFile(file1);
                JarFile jar2 = new JarFile(file2);
                jarsAsTypeSystems(jar1, jar2, result);
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
        if (result.size() < 1) {
            System.out.println("No differences encountered.");
        } else {
            System.out.println("Differences:");
            for (XmlError s : result) {
                System.out.println(s);
            }
        }
    }

    /**
     * Diffs the contents of two jars, looking only at the schema typesystems
     * saved inside those jars
     */
    public static void jarsAsTypeSystems(JarFile jar1, JarFile jar2, List<XmlError> diffs) {
        Enumeration<JarEntry> entries1 = jar1.entries();
        Enumeration<JarEntry> entries2 = jar2.entries();
        List<ZipEntry> list1 = new ArrayList<>();
        List<ZipEntry> list2 = new ArrayList<>();
        while (entries1.hasMoreElements()) {
            ZipEntry ze = entries1.nextElement();
            String name = ze.getName();
            if (name.startsWith(SchemaTypeSystemImpl.METADATA_PACKAGE_GEN + "/system/s") && name.endsWith(".xsb")) {
                list1.add(ze);
            }
        }
        while (entries2.hasMoreElements()) {
            ZipEntry ze = entries2.nextElement();
            String name = ze.getName();
            if (name.startsWith(SchemaTypeSystemImpl.METADATA_PACKAGE_GEN + "/system/s") && name.endsWith(".xsb")) {
                list2.add(ze);
            }
        }
        ZipEntry[] files1 = list1.toArray(new ZipEntry[0]);
        ZipEntry[] files2 = list2.toArray(new ZipEntry[0]);
        Comparator<ZipEntry> comparator = Comparator.comparing(ZipEntry::getName);
        Arrays.sort(files1, comparator);
        Arrays.sort(files2, comparator);
        int i1 = 0;
        int i2 = 0;
        while (i1 < files1.length && i2 < files2.length) {
            String name1 = files1[i1].getName();
            String name2 = files2[i2].getName();
            int dif = name1.compareTo(name2);
            if (dif == 0) {
                // Compare the files
                zipEntriesAsXsb(files1[i1], jar1, files2[i2], jar2, diffs);
                i1++;
                i2++; // Move to next pair
            } else if (dif < 0) {
                // dir1 contains a file that dir2 doesn't
                diffs.add(XmlError.forMessage("Jar \"" + jar1.getName() + "\" contains an extra file: \"" + name1 + "\""));
                i1++;
            } else {
                // dir2 contains a file that dir1 doesn't
                diffs.add(XmlError.forMessage("Jar \"" + jar2.getName() + "\" contains an extra file: \"" + name2 + "\""));
                i2++;
            }
        }
        while (i1 < files1.length) {
            diffs.add(XmlError.forMessage("Jar \"" + jar1.getName() + "\" contains an extra file: \"" + files1[i1].getName() + "\""));
            i1++;
        }
        while (i2 < files2.length) {
            diffs.add(XmlError.forMessage("Jar \"" + jar2.getName() + "\" contains an extra file: \"" + files2[i2].getName() + "\""));
            i2++;
        }
    }

    /**
     * Diffs the contents of two dirs looking only at the xsb files
     * contained in these two dirs
     * Updated diffs with a list of differences (for the time being, strings
     * describing the difference)
     */
    public static void dirsAsTypeSystems(File dir1, File dir2, List<XmlError> diffs) {
        assert dir1.isDirectory() : "Parameters must be directories";
        assert dir2.isDirectory() : "Parameters must be directories";

        /*
          Navigate three directories deep to get to the type system.
          Assume the schema[METADATA_PACKAGE_LOAD]/system/* structure
         */
        File temp1 = new File(dir1, SchemaTypeSystemImpl.METADATA_PACKAGE_GEN + "/system");
        File temp2 = new File(dir2, SchemaTypeSystemImpl.METADATA_PACKAGE_GEN + "/system");
        if (temp1.exists() && temp2.exists()) {
            File[] files1 = temp1.listFiles();
            File[] files2 = temp2.listFiles();

            assert (files1 != null && files2 != null);

            if (files1.length == 1 && files2.length == 1) {
                temp1 = files1[0];
                temp2 = files2[0];
            } else {
                if (files1.length == 0) {
                    temp1 = null;
                }
                if (files2.length == 0) {
                    temp2 = null;
                }
                if (files1.length > 1) {
                    diffs.add(XmlError.forMessage("More than one typesystem found in dir \"" + dir1.getName() + "\""));
                    return;
                }
                if (files2.length > 1) {
                    diffs.add(XmlError.forMessage("More than one typesystem found in dir \"" + dir2.getName() + "\""));
                    return;
                }
            }
        } else {
            if (!temp1.exists()) {
                temp1 = null;
            }
            if (!temp2.exists()) {
                temp2 = null;
            }
        }
        if (temp1 == null && temp2 == null) {
            return;
        } else if (temp1 == null || temp2 == null) {
            if (temp1 == null) {
                diffs.add(XmlError.forMessage("No typesystems found in dir \"" + dir1 + "\""));
            }
            if (temp2 == null) {
                diffs.add(XmlError.forMessage("No typesystems found in dir \"" + dir2 + "\""));
            }
            return;
        } else {
            dir1 = temp1;
            dir2 = temp2;
        }

        boolean diffIndex = isDiffIndex();
        FilenameFilter xsbName = (dir, name) -> name.endsWith(".xsb");
        File[] files1 = dir1.listFiles(xsbName);
        File[] files2 = dir2.listFiles(xsbName);

        assert (files1 != null && files2 != null);

        Comparator<File> comparator = Comparator.comparing(File::getName);
        Arrays.sort(files1, comparator);
        Arrays.sort(files2, comparator);
        int i1 = 0;
        int i2 = 0;
        while (i1 < files1.length && i2 < files2.length) {
            String name1 = files1[i1].getName();
            String name2 = files2[i2].getName();
            int dif = name1.compareTo(name2);
            if (dif == 0) {
                if (diffIndex || !files1[i1].getName().equals("index.xsb")) {
                    filesAsXsb(files1[i1], files2[i2], diffs); // Compare the files
                }
                i1++;
                i2++; // Move to next pair
            } else if (dif < 0) {
                // dir1 contains a file that dir2 doesn't
                diffs.add(XmlError.forMessage("Dir \"" + dir1.getName() + "\" contains an extra file: \"" + name1 + "\""));
                i1++;
            } else {
                // dir2 contains a file that dir1 doesn't
                diffs.add(XmlError.forMessage("Dir \"" + dir2.getName() + "\" contains an extra file: \"" + name2 + "\""));
                i2++;
            }
        }
        while (i1 < files1.length) {
            diffs.add(XmlError.forMessage("Dir \"" + dir1.getName() + "\" contains an extra file: \"" + files1[i1].getName() + "\""));
            i1++;
        }
        while (i2 < files2.length) {
            diffs.add(XmlError.forMessage("Dir \"" + dir2.getName() + "\" contains an extra file: \"" + files2[i2].getName() + "\""));
            i2++;
        }
    }

    private static boolean isDiffIndex() {
        String prop = SystemProperties.getProperty("xmlbeans.diff.diffIndex");
        return prop == null || !"0".equals(prop) && !"false".equalsIgnoreCase(prop);
    }

    /**
     * Diffs the two given files assuming they are in xsb format
     * Updates diffs with differences in string format
     */
    public static void filesAsXsb(File file1, File file2, List<XmlError> diffs) {
        assert file1.exists() : "File \"" + file1.getAbsolutePath() + "\" does not exist.";
        assert file2.exists() : "File \"" + file2.getAbsolutePath() + "\" does not exist.";
        try (InputStream stream1 = new FileInputStream(file1);
             InputStream stream2 = new FileInputStream(file2)) {
            streamsAsXsb(stream1, file1.getName(), stream2, file2.getName(), diffs);
        } catch (IOException ignored) {
        }
    }

    public static void zipEntriesAsXsb(ZipEntry file1, JarFile jar1,
                                       ZipEntry file2, JarFile jar2, List<XmlError> diffs) {
        try (InputStream stream1 = jar1.getInputStream(file1);
             InputStream stream2 = jar2.getInputStream(file2)) {
            streamsAsXsb(stream1, file1.getName(), stream2, file2.getName(), diffs);
        } catch (IOException ignored) {
        }
    }

    public static void streamsAsXsb(InputStream stream1, String name1,
                                    InputStream stream2, String name2, List<XmlError> diffs)
        throws IOException {
        String charset = StandardCharsets.UTF_8.name();
        ByteArrayOutputStream buf1 = new ByteArrayOutputStream();
        ByteArrayOutputStream buf2 = new ByteArrayOutputStream();
        XsbDumper.dump(stream1, "", new PrintStream(buf1, true, charset));
        XsbDumper.dump(stream2, "", new PrintStream(buf2, true, charset));
        readersAsText(new StringReader(buf1.toString(charset)), name1,
            new StringReader(buf2.toString(charset)), name2, diffs.stream().map(XmlError::getMessage).collect(Collectors.toList()));
    }

    public static void readersAsText(Reader r1, String name1, Reader r2, String name2,
                                     List<String> diffs)
        throws IOException {
        org.apache.xmlbeans.impl.util.Diff.readersAsText(r1, name1, r2, name2, diffs);
    }
}
