// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
 *******************************************************************************
 * Copyright (C) 2005-2012, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 *
 */

package com.ibm.icu.dev.tool.docs;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * Compare ICU4J and JDK APIS.
 *
 * TODO: compare protected APIs.  Reflection on Class allows you
 * to either get all inherited methods with public access, or get methods
 * on the particular class with any access, but no way to get all
 * inherited methods with any access.  Go figure.
 */
public class ICUJDKCompare {
    static final boolean DEBUG = false;

    // set up defaults
    private static final String kSrcPrefix = "java.";
    private static final String kTrgPrefix = "com.ibm.icu.";
    private static final String[] kPairInfo = {
        "lang.Character/UCharacter",
        "lang.Character$UnicodeBlock/UCharacter$UnicodeBlock",
        "text.BreakIterator",
        "text.Collator",
        "text.DateFormat",
        "text.DateFormatSymbols",
        "text.DecimalFormat",
        "text.DecimalFormatSymbols",
        "text.Format/UFormat",
        "text.MessageFormat",
        "text.NumberFormat",
        "text.SimpleDateFormat",
        "util.Calendar",
        "util.Currency",
        "util.GregorianCalendar",
        "util.SimpleTimeZone",
        "util.TimeZone",
        "util.Locale/ULocale",
        "util.ResourceBundle/UResourceBundle",
    };

    private static final String[] kIgnore = new String[] {
        "lang.Character <init> charValue compareTo MAX_VALUE MIN_VALUE TYPE",
        "lang.Character$UnicodeBlock SURROGATES_AREA",
        "util.Calendar FIELD_COUNT",
        "util.GregorianCalendar FIELD_COUNT",
        "util.SimpleTimeZone STANDARD_TIME UTC_TIME WALL_TIME",
    };

    private PrintWriter pw;
    private String srcPrefix;
    private String trgPrefix;
    private Class[] classPairs;
    private String[] namePairs;
    private String[] ignore;
    private boolean swap;
    //private boolean signature;

    // call System.exit with non-zero if there were some missing APIs
    public static void main(String[] args) {
        System.exit(doMain(args));
    }

    // return non-zero if there were some missing APIs
    public static int doMain(String[] args) {
        ICUJDKCompare p = new ICUJDKCompare();
        p.setOutputWriter(new PrintWriter(System.out));
        p.setup(args);
        return p.process();
    }

    // setters
    public ICUJDKCompare setOutputWriter(PrintWriter pw) {
        this.pw = pw;
        return this;
    }

    public ICUJDKCompare setSrcPrefix(String srcPrefix) {
        this.srcPrefix = srcPrefix;
        return this;
    }

    public ICUJDKCompare setTrgPrefix(String trgPrefix) {
        this.trgPrefix = trgPrefix;
        return this;
    }

    public ICUJDKCompare setClassPairs(Class[] classPairs) {
        this.classPairs = classPairs;
        return this;
    }

    public ICUJDKCompare setNamePairs(String[] namePairs) {
        this.namePairs = namePairs;
        return this;
    }

    public ICUJDKCompare setIgnore(String[] ignore) {
        this.ignore = ignore;
        return this;
    }

    public ICUJDKCompare setSwap(boolean swap) {
        this.swap = swap;
        return this;
    }

    public ICUJDKCompare setup(String[] args) {
        String namelist = null;
        String ignorelist = null;
        for (int i = 0; i < args.length; ++i) {
            String arg = args[i];
            if (arg.equals("-swap")) {
                swap = true;
            } else if (arg.equals("-srcPrefix:")) {
                srcPrefix = args[++i];
                if (!srcPrefix.endsWith(".")) {
                    srcPrefix += '.';
                }
            } else if (arg.equals("-trgPrefix:")) {
                trgPrefix = args[++i];
                if (!trgPrefix.endsWith(".")) {
                    trgPrefix += '.';
                }
            } else if (arg.equals("-names:")) {
                namelist = args[++i];
            } else if (arg.equals("-ignore:")) {
                ignorelist = args[++i];
            } else {
                System.err.println("unrecognized argument: " + arg);
                throw new IllegalStateException();
            }
        }

        if (ignorelist != null) {
            if (ignorelist.charAt(0) == '@') { // a file containing ignoreinfo
                BufferedReader br = null;
                try {
                    ArrayList nl = new ArrayList();
                    File f = new File(namelist.substring(1));
                    FileInputStream fis = new FileInputStream(f);
                    InputStreamReader isr = new InputStreamReader(fis);
                    br = new BufferedReader(isr);
                    String line = null;
                    while (null != (line = br.readLine())) {
                        nl.add(line);
                    }
                    ignore = (String[])nl.toArray(new String[nl.size()]);
                }
                catch (Exception e) {
                    System.err.println(e);
                    throw new IllegalStateException();
                }
                finally {
                    if (br != null) {
                        try {
                            br.close();
                        } catch (Exception e) {
                            // ignore
                        }
                    }
                }
            } else { // a list of ignoreinfo separated by semicolons
                ignore = ignorelist.split("\\s*;\\s*");
            }
        }

        if (namelist != null) {
            String[] names = null;
            if (namelist.charAt(0) == '@') { // a file
                BufferedReader br = null;
                try {
                    ArrayList nl = new ArrayList();
                    File f = new File(namelist.substring(1));
                    FileInputStream fis = new FileInputStream(f);
                    InputStreamReader isr = new InputStreamReader(fis);
                    br = new BufferedReader(isr);
                    String line = null;
                    while (null != (line = br.readLine())) {
                        nl.add(line);
                    }
                    names = (String[])nl.toArray(new String[nl.size()]);
                }
                catch (Exception e) {
                    System.err.println(e);
                    throw new IllegalStateException();
                } finally {
                    if (br != null) {
                        try {
                            br.close();
                        } catch (Exception e) {
                            // ignore
                        }
                    }
                }

            } else { // a list of names separated by semicolons
                names = namelist.split("\\s*;\\s*");
            }
            processPairInfo(names);
        }

        pw.flush();

        return this;
    }

    private void processPairInfo(String[] names) {
        ArrayList cl = new ArrayList();
        ArrayList nl = new ArrayList();
        for (int i = 0; i < names.length; ++i) {
            String name = names[i];
            String srcName = srcPrefix;
            String trgName = trgPrefix;

            int n = name.indexOf('/');
            if (n == -1) {
                srcName += name;
                trgName += name;
            } else {
                String srcSuffix = name.substring(0, n).trim();
                String trgSuffix = name.substring(n+1).trim();
                int jx = srcSuffix.length()+1;
                int ix = trgSuffix.length()+1;
                while (ix != -1) {
                    jx = srcSuffix.lastIndexOf('.', jx-1);
                    ix = trgSuffix.lastIndexOf('.', ix-1);
                }
                srcName += srcSuffix;
                trgName += srcSuffix.substring(0, jx+1) + trgSuffix;
            }

            try {
                Class jc = Class.forName(srcName);
                Class ic = Class.forName(trgName);
                cl.add(ic);
                cl.add(jc);
                nl.add(ic.getName());
                nl.add(jc.getName());
            }
            catch (Exception e) {
                if (DEBUG) System.err.println("can't load class: " + e.getMessage());
            }
        }
        classPairs = (Class[])cl.toArray(new Class[cl.size()]);
        namePairs = (String[])nl.toArray(new String[nl.size()]);
    }

    private void println(String s) {
        if (pw != null) pw.println(s);
    }

    private void flush() {
        if (pw != null) pw.flush();
    }

    public int process() {
        // set defaults
        if (srcPrefix == null) {
            srcPrefix = kSrcPrefix;
        }

        if (trgPrefix == null) {
            trgPrefix = kTrgPrefix;
        }

        if (classPairs == null) {
            processPairInfo(kPairInfo);
        }

        if (ignore == null) {
            ignore = kIgnore;
        }

        println("ICU and Java API Comparison");
        String ICU_VERSION = "unknown";
        try {
            Class cls = Class.forName("com.ibm.icu.util.VersionInfo");
            Field fld = cls.getField("ICU_VERSION");
            ICU_VERSION = fld.get(null).toString();
        }
        catch (Exception e) {
            if (DEBUG) System.err.println("can't get VersionInfo: " + e.getMessage());
        }
        println("ICU Version " + ICU_VERSION);
        println("JDK Version " + System.getProperty("java.version"));

        int errorCount = 0;
        for (int i = 0; i < classPairs.length; i += 2) {
            try {
                if (swap) {
                    errorCount += compare(classPairs[i+1], classPairs[i]);
                } else {
                    errorCount += compare(classPairs[i], classPairs[i+1]);
                }
            }
            catch (Exception e) {
                System.err.println("exception: " + e);
                System.err.println("between " + namePairs[i] + " and " + namePairs[i+1]);
                e.printStackTrace();
                errorCount += 1;
            }
        }
        return errorCount;
    }

    static class MorC {
        private Method mref;
        private Constructor cref;

        MorC(Method m) {
            mref = m;
        }

        MorC(Constructor c) {
            cref = c;
        }

        int getModifiers() {
            return mref == null ? cref.getModifiers() : mref.getModifiers();
        }

        Class getReturnType() {
            return mref == null ? void.class : mref.getReturnType();
        }

        Class[] getParameterTypes() {
            return mref == null ? cref.getParameterTypes() : mref.getParameterTypes();
        }

        String getName() {
            return mref == null ? "<init>" : mref.getName();
        }

        String getSignature() {
            return mref == null ? cref.toString() : mref.toString();
        }
    }

    private int compare(Class class1, Class class2) throws Exception {
        String n1 = class1.getName();
        String n2 = class2.getName();

        println("\ncompare " + n1 + " <> " + n2);

        MorC[] conss1 = getMorCArray(class1.getConstructors());
        MorC[] conss2 = getMorCArray(class2.getConstructors());

        Map cmap1 = getMethodMap(conss1);
        Map cmap2 = getMethodMap(conss2);

        MorC[] meths1 = getMorCArray(class1.getMethods());
        MorC[] meths2 = getMorCArray(class2.getMethods());

        Map map1 = getMethodMap(meths1);
        Map map2 = getMethodMap(meths2);

        Field[] fields1 = class1.getFields();
        Field[] fields2 = class2.getFields();

        Set set1 = getFieldSet(fields1);
        Set set2 = getFieldSet(fields2);

        if (n1.indexOf("DecimalFormatSymbols") != -1) {
          pw.format("fields in %s: %s%n", n1, set1);
          pw.format("fields in %s: %s%n", n2, set2);
        }

        Map diffConss = diffMethodMaps(cmap2, cmap1);
        Map diffMeths = diffMethodMaps(map2, map1);
        Set diffFields = diffFieldSets(set2, set1);

        diffConss = removeIgnored(n2, diffConss);
        diffMeths = removeIgnored(n2, diffMeths);
        diffFields = removeIgnored(n2, diffFields);

        int result = diffConss.size() + diffMeths.size() + diffFields.size();
        if (result > 0 && pw != null) {
            pw.println("Public API in " + n2 + " but not in " + n1);
            if (diffConss.size() > 0) {
                pw.println("CONSTRUCTORS");
                dumpMethodMap(diffConss, pw);
            }
            if (diffMeths.size() > 0) {
                pw.println("METHODS");
                dumpMethodMap(diffMeths, pw);
            }
            if (diffFields.size() > 0) {
                pw.println("FIELDS");
                dumpFieldSet(diffFields, pw);
            }
        }

        flush();

        return result;
    }

    final class MethodRecord {
        MorC[] overrides;

        MethodRecord(MorC m) {
            overrides = new MorC[] { m };
        }

        MethodRecord(MorC[] ms) {
            overrides = ms;
        }

        MethodRecord copy() {
            return new MethodRecord((MorC[])overrides.clone());
        }

        int count() {
            for (int i = 0; i < overrides.length; ++i) {
                if (overrides[i] == null) {
                    return i;
                }
            }
            return overrides.length;
        }

        void add(MorC m) {
            MorC[] temp = new MorC[overrides.length + 1];
            for (int i = 0; i < overrides.length; ++i) {
                temp[i] = overrides[i];
            }
            temp[overrides.length] = m;
            overrides = temp;
        }

        void remove(int index) {
            int i = index;
            while (overrides[i] != null && i < overrides.length-1) {
                overrides[i] = overrides[i+1];
                ++i;
            }
            overrides[i] = null;
        }

        // if a call to a method can be handled by a call to t, remove the
        // method from our list, and return true
        boolean removeOverridden(MorC t) {
            boolean result = false;
            int i = 0;
            while (i < overrides.length) {
                MorC m = overrides[i];
                if (m == null) {
                    break;
                }
                if (handles(t, m)) {
                    remove(i);
                    result = true;
                } else {
                    ++i;
                }
            }
            return result;
        }

        // remove all methods handled by any method of mr
        boolean removeOverridden(MethodRecord mr) {
            boolean result = false;
            for (int i = 0; i < mr.overrides.length; ++i) {
                MorC t = mr.overrides[i];
                if (t == null) {
                    // this shouldn't happen, as the target record should not have been modified
                    throw new IllegalStateException();
                }
                if (removeOverridden(t)) {
                    result = true;
                }
            }
            return result;
        }

        void debugmsg(MorC t, MorC m, String msg) {
            StringBuffer buf = new StringBuffer();
            buf.append(t.getName());
            buf.append(" ");
            buf.append(msg);
            buf.append("\n   ");
            toString(t, buf);
            buf.append("\n   ");
            toString(m, buf);
            System.out.println(buf.toString());
        }

        boolean handles(MorC t, MorC m) {
            // relevant modifiers must match
            if ((t.getModifiers() & MOD_MASK) != (m.getModifiers() & MOD_MASK)) {
                if (DEBUG) debugmsg(t, m, "modifier mismatch");
                return false;
            }

            Class tr = pairClassEquivalent(t.getReturnType());
            Class mr = pairClassEquivalent(m.getReturnType());
            if (!assignableFrom(mr, tr)) { // t return type must be same or narrower than m
                if (DEBUG) debugmsg(t, m, "return value mismatch");
                return false;
            }
            Class[] tts = t.getParameterTypes();
            Class[] mts = m.getParameterTypes();
            if (tts.length != mts.length) {
                if (DEBUG) debugmsg(t, m, "param count mismatch");
                return false;
            }

            for (int i = 0; i < tts.length; ++i) {
                Class tc = pairClassEquivalent(tts[i]);
                Class mc = pairClassEquivalent(mts[i]);
                if (!assignableFrom(tc, mc)) { // m param must be same or narrower than t
                    if (DEBUG) debugmsg(t, m, "parameter " + i + " mismatch, " +
                                   tts[i].getName() + " not assignable from " + mts[i].getName());
                    return false;
                }
            }
            return true;
        }

        public void toString(MorC m, StringBuffer buf) {
            int mod = m.getModifiers();
            if (mod != 0) {
                buf.append(Modifier.toString(mod) + " ");
            }
            buf.append(nameOf(m.getReturnType()));
            buf.append(" ");
            buf.append(m.getName());
            buf.append("(");
            Class[] ptypes = m.getParameterTypes();
            for (int j = 0; j < ptypes.length; ++j) {
                if (j > 0) {
                    buf.append(", ");
                }
                buf.append(nameOf(ptypes[j]));
            }
            buf.append(')');
        }

        public String toString() {
            StringBuffer buf = new StringBuffer();
            buf.append(overrides[0].getName());
            for (int i = 0; i < overrides.length; ++i) {
                MorC m = overrides[i];
                if (m == null) {
                    break;
                }
                buf.append("\n   ");
                toString(m, buf);
            }
            return buf.toString();
        }
    }

    public static String nameOf(Class c) {
        if (c.isArray()) {
            return nameOf(c.getComponentType()) + "[]";
        }
        String name = c.getName();
        return name.substring(name.lastIndexOf('.') + 1);
    }

    static MorC[] getMorCArray(Constructor[] cons) {
        MorC[] result = new MorC[cons.length];
        for (int i = 0 ; i < cons.length; ++i) {
            result[i] = new MorC(cons[i]);
        }
        return result;
    }

    static MorC[] getMorCArray(Method[] meths) {
        MorC[] result = new MorC[meths.length];
        for (int i = 0 ; i < meths.length; ++i) {
            result[i] = new MorC(meths[i]);
        }
        return result;
    }

    private Map getMethodMap(MorC[] meths) {
        Map result = new TreeMap();
        for (int i = 0; i < meths.length; ++i) {
            MorC m = meths[i];
            String key = m.getName();
            MethodRecord mr = (MethodRecord)result.get(key);
            if (mr == null) {
                mr = new MethodRecord(m);
                result.put(key, mr);
            } else {
                mr.add(m);
            }
        }
        return result;
    }

    private void dumpMethodMap(Map m, PrintWriter pw) {
        Iterator iter = m.entrySet().iterator();
        while (iter.hasNext()) {
            dumpMethodRecord((MethodRecord)((Map.Entry)iter.next()).getValue());
        }
        pw.flush();
    }

    private void dumpMethodRecord(MethodRecord mr) {
        pw.println(mr.toString());
    }

    static Map diffMethodMaps(Map m1, Map m2) {
        // get all the methods in m1 that aren't mentioned in m2 at all
        Map result = (Map)((TreeMap)m1).clone();
        result.keySet().removeAll(m2.keySet());
        return result;
    }

    private Map removeIgnored(String name, Map m1) {
        if (ignore == null) {
            return m1;
        }
        if (name.startsWith(srcPrefix)) {
            name = name.substring(srcPrefix.length());
        }
        name += " "; // to avoid accidental prefix of nested class name

        // prune ignore list to relevant items
        ArrayList il = null;
        for (int i = 0; i < ignore.length; ++i) {
            String s = ignore[i];
            if (s.startsWith(name)) {
                if (il == null) {
                    il = new ArrayList();
                }
                il.add(s);
            }
        }
        if (il == null) {
            return m1;
        }

        Map result = new TreeMap(((TreeMap)m1).comparator());
        result.putAll(m1);
        Iterator iter = result.entrySet().iterator();
        loop: while (iter.hasNext()) {
            Map.Entry e = (Map.Entry)iter.next();
            String key = (String)e.getKey();
            for (int i = 0; i < il.size(); ++i) {
                String ig = (String)il.get(i);
                if (ig.indexOf(" " + key) != 0) {
                    iter.remove();
                    continue loop;
                }
            }
        }
        return result;
    }

    private Set removeIgnored(String name, Set s1) {
        if (ignore == null) {
            return s1;
        }
        if (name.startsWith(srcPrefix)) {
            name = name.substring(srcPrefix.length());
        }
        name += " "; // to avoid accidental prefix of nested class name

        // prune ignore list to relevant items
        ArrayList il = null;
        for (int i = 0; i < ignore.length; ++i) {
            String s = ignore[i];
            if (s.startsWith(name)) {
                if (il == null) {
                    il = new ArrayList();
                }
                il.add(s);
            }
        }
        if (il == null) {
            return s1;
        }

        Set result = (Set)((TreeSet)s1).clone();
        Iterator iter = result.iterator();
        loop: while (iter.hasNext()) {
            String key = (String)iter.next();
            String fieldname = key.substring(0, key.indexOf(' '));
            for (int i = 0; i < il.size(); ++i) {
                String ig = (String)il.get(i);
                if (ig.indexOf(" " + fieldname) != 0) {
                    iter.remove();
                    continue loop;
                }
            }
        }
        return result;
    }

    static final boolean[][] assignmentMap = {
        // bool   char   byte  short    int   long  float double   void
        {  true, false, false, false, false, false, false, false, false }, // boolean
        { false,  true,  true,  true, false, false, false, false, false }, // char
        { false, false,  true, false, false, false, false, false, false }, // byte
        { false, false,  true,  true, false, false, false, false, false }, // short
        { false,  true,  true,  true,  true, false, false, false, false }, // int
        { false,  true,  true,  true,  true,  true, false, false, false }, // long
        { false,  true,  true,  true,  true, false,  true, false, false }, // float
        { false,  true,  true,  true,  true, false,  true,  true, false }, // double
        { false, false, false, false, false, false, false, false,  true }, // void
    };

    static final Class[] prims = {
        boolean.class, char.class, byte.class, short.class,
        int.class, long.class, float.class, double.class, void.class
    };

    static int primIndex(Class cls) {
        for (int i = 0; i < prims.length; ++i) {
            if (cls == prims[i]) {
                return i;
            }
        }
        throw new IllegalStateException("could not find primitive class: " + cls);
    }

    static boolean assignableFrom(Class lhs, Class rhs) {
        if (lhs == rhs) {
            return true;
        }
        if (lhs.isPrimitive()) {
            if (!rhs.isPrimitive()) {
                return false;
            }
            int lhsx = primIndex(lhs);
            int rhsx = primIndex(rhs);
            return assignmentMap[lhsx][rhsx];
        }
        return lhs.isAssignableFrom(rhs);
    }

    private String toString(Field f) {
        StringBuffer buf = new StringBuffer(f.getName());
        int mod = f.getModifiers() & MOD_MASK;
        if (mod != 0) {
            buf.append(" " + Modifier.toString(mod));
        }
        buf.append(" ");
        String n = pairEquivalent(f.getType().getName());
        n = n.substring(n.lastIndexOf('.') + 1);
        buf.append(n);
        return buf.toString();
    }

    private Set getFieldSet(Field[] fs) {
        Set set = new TreeSet();
        for (int i = 0; i < fs.length; ++i) {
            set.add(toString(fs[i]));
        }
        return set;
    }

    static Set diffFieldSets(Set s1, Set s2) {
        Set result = (Set)((TreeSet)s1).clone();
        result.removeAll(s2);
        return result;
    }

    private void dumpFieldSet(Set s, PrintWriter pw) {
        Iterator iter = s.iterator();
        while (iter.hasNext()) {
            pw.println(iter.next());
        }
        pw.flush();
    }

    // given a target string, if it matches the first of one of our pairs, return the second
    // or vice-versa if swap is true
    private String pairEquivalent(String target) {
        for (int i = 0; i < namePairs.length; i += 2) {
            if (swap) {
                if (target.equals(namePairs[i+1])) {
                    return namePairs[i];
                }
            } else {
                if (target.equals(namePairs[i])) {
                    return namePairs[i+1];
                }
            }
        }
        return target;
    }

    private Class pairClassEquivalent(Class target) {
        for (int i = 0; i < classPairs.length; i += 2) {
            if (target.equals(classPairs[i])) {
                return classPairs[i+1];
            }
        }
        return target;
    }

    static final int MOD_MASK = ~(Modifier.FINAL|Modifier.SYNCHRONIZED|
                                  Modifier.VOLATILE|Modifier.TRANSIENT|Modifier.NATIVE);
}
