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

/**
 * Represents the API information on a doc element.
 */

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

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Comparator;

class APIInfo {
    // version id for the format of the APIInfo data

    public static final int VERSION = 2;

    // public keys and values for queries on info

    public static final int STA = 0, STA_DRAFT = 0, STA_STABLE = 1, STA_DEPRECATED = 2,
      STA_OBSOLETE = 3, STA_INTERNAL = 4;
    public static final int VIS = 1, VIS_PACKAGE = 0, VIS_PUBLIC= 1, VIS_PROTECTED = 2,
      VIS_PRIVATE = 3;
    public static final int STK = 2, STK_STATIC = 1;
    public static final int FIN = 3, FIN_FINAL = 1;
    public static final int SYN = 4, SYN_SYNCHRONIZED = 1;
    public static final int ABS = 5, ABS_ABSTRACT = 1;
    public static final int CAT = 6, CAT_CLASS = 0, CAT_FIELD = 1, CAT_CONSTRUCTOR = 2,
      CAT_METHOD = 3, CAT_ENUM = 4, CAT_ENUM_CONSTANT = 5;
    public static final int PAK = 7;
    public static final int CLS = 8;
    public static final int NAM = 9;
    public static final int SIG = 10;
    public static final int EXC = 11;
    public static final int NUM_TYPES = 11;

    // the separator between tokens in the data file
    public int[] masks = { 0x7, 0x3, 0x1, 0x1, 0x1, 0x1, 0x7 };
    public int[] shifts = { 0, 3, 5, 6, 7, 8, 9 };

    public static final char SEP = ';';

    // Internal State

    private int    info;       // information about numeric values packed into an int
                               // as variable-length nibbles
    private String pack = "";  // package
    private String cls  = "";  // enclosing class
    private String name = "";  // name
    private String sig  = "";  // signature, class: inheritance, method: signature,
                               // field: type, const: signature
    private String exc  = "";  // throws
    private String stver = ""; // status version

    private boolean includeStatusVer = false;

    @Override
    public int hashCode() {
        return (((pack.hashCode() << 3) ^ cls.hashCode()) << 3) ^ name.hashCode();
    }

    @Override
    public boolean equals(Object rhs) {
        if (rhs == this) return true;
        if (rhs == null) return false;
        try {
            APIInfo that = (APIInfo)rhs;
            return this.info == that.info &&
                this.pack.equals(that.pack) &&
                this.cls.equals(that.cls) &&
                this.name.equals(that.name) &&
                this.sig.equals(that.sig) &&
                this.exc.equals(that.exc) &&
                this.stver.equals(this.stver);
        }
        catch (ClassCastException e) {
            return false;
        }
    }

    public void setDraft() { setType(STA, STA_DRAFT); }
    public void setStable() { setType(STA, STA_STABLE); }
    public void setDeprecated() { setType(STA, STA_DEPRECATED); }
    public void setObsolete() { setType(STA, STA_OBSOLETE); }
    public void setInternal() { setType(STA, STA_INTERNAL); }
    public void setPackage() { setType(VIS, VIS_PACKAGE); }
    public void setPublic() { setType(VIS, VIS_PUBLIC); }
    public void setProtected() { setType(VIS, VIS_PROTECTED); }
    public void setPrivate() { setType(VIS, VIS_PRIVATE); }
    public void setStatic() { setType(STK, STK_STATIC); }
    public void setFinal() { setType(FIN, FIN_FINAL); }
    public void setSynchronized() { setType(SYN, SYN_SYNCHRONIZED); }
    public void setAbstract() { setType(ABS, ABS_ABSTRACT); }
    public void setClass() { setType(CAT, CAT_CLASS); }
    public void setField() { setType(CAT, CAT_FIELD); }
    public void setConstructor() { setType(CAT, CAT_CONSTRUCTOR); }
    public void setMethod() { setType(CAT, CAT_METHOD); }
    public void setEnum() { setType(CAT, CAT_ENUM); }
    public void setEnumConstant() { setType(CAT, CAT_ENUM_CONSTANT); }

    public void setPackage(String val) { setType(PAK, val); }
    public void setClassName(String val) { setType(CLS, val); }
    public void setName(String val) { setType(NAM, val); }
    public void setSignature(String val) { setType(SIG, val); }
    public void setExceptions(String val) { setType(EXC, val); }

    public boolean isDraft() { return getVal(STA) == STA_DRAFT; }
    public boolean isStable() { return getVal(STA) == STA_STABLE; }
    public boolean isDeprecated() { return getVal(STA) == STA_DEPRECATED; }
    public boolean isObsolete() { return getVal(STA) == STA_OBSOLETE; }
    public boolean isInternal() { return getVal(STA) == STA_INTERNAL; }
    public boolean isPackage() { return getVal(VIS) == VIS_PACKAGE; }
    public boolean isPublic() { return getVal(VIS) == VIS_PUBLIC; }
    public boolean isProtected() { return getVal(VIS) == VIS_PROTECTED; }
    public boolean isPrivate() { return getVal(VIS) == VIS_PRIVATE; }
    public boolean isStatic() { return getVal(STK) == STK_STATIC; }
    public boolean isFinal() { return getVal(FIN) == FIN_FINAL; }
    public boolean isSynchronized() { return getVal(SYN) == SYN_SYNCHRONIZED; }
    public boolean isAbstract() { return getVal(ABS) == ABS_ABSTRACT; }
    public boolean isClass() { return getVal(CAT) == CAT_CLASS; }
    public boolean isField() { return getVal(CAT) == CAT_FIELD; }
    public boolean isConstructor() { return getVal(CAT) == CAT_CONSTRUCTOR; }
    public boolean isMethod() { return getVal(CAT) == CAT_METHOD; }
    public boolean isEnum() { return getVal(CAT) == CAT_ENUM; }
    public boolean isEnumConstant() { return getVal(CAT) == CAT_ENUM_CONSTANT; }

    public String getPackageName() { return get(PAK, true); }
    public String getClassName() { return get(CLS, true); }
    public String getName() { return get(NAM, true); }
    public String getSignature() { return get(SIG, true); }
    public String getExceptions() { return get(EXC, true); }

    public void setStatusVersion(String v) { stver = v; }
    public String getStatusVersion() { return stver; }

    /**
     * Return the integer value for the provided type.  The type
     * must be one of the defined type names.  The return value
     * will be one of corresponding values for that type.
     */
    public int getVal(int typ) {
        validateType(typ);
        if (typ >= shifts.length) {
            return 0;
        }
        return (info >>> shifts[typ]) & masks[typ];
    }

    /**
     * Return the string value for the provided type.  The type
     * must be one of the defined type names.  The return value
     * will be one of corresponding values for that type.  Brief
     * should be true for writing data files, false for presenting
     * information to the user.
     */
    public String get(int typ, boolean brief) {
        validateType(typ);
        String[] vals = brief ? shortNames[typ] : names[typ];
        if (vals == null) {
            switch (typ) {
            case PAK: return pack;
            case CLS: return cls;
            case NAM: return name;
            case SIG: return sig;
            case EXC: return exc;
            }
        }
        int val = (info >>> shifts[typ]) & masks[typ];
        return vals[val];
    }

    /**
     * Set the numeric value for the type.  The value should be a
     * value corresponding to the type.  Only the lower two bits
     * of the value are used.
     */
    public void setType(int typ, int val) {
        validateType(typ);
        if (typ < masks.length) {
            info &= ~(masks[typ] << shifts[typ]);
            info |= (val&masks[typ]) << shifts[typ];
        }
    }

    /**
     * Set the string value for the type.  For numeric types,
     * the value should be a string in 'brief' format.  For
     * non-numeric types, the value can be any
     * string.
     */
    private void setType(int typ, String val) {
        validateType(typ);
        String[] vals = shortNames[typ];
        if (vals == null) {
            if (val == null) {
                val = "";
            }
            switch (typ) {
            case PAK: pack = val; break;
            case CLS: cls = val; break;
            case NAM: name = val; break;
            case SIG: sig = val; break;
            case EXC: exc = val; break;
            }
            return;
        }

        // status version
        String version = "";
        if (typ == STA) {
            int idx = val.indexOf('@');
            if (idx != -1) {
                version = val.substring(idx + 1);
                val = val.substring(0, idx);
            }
        }

        for (int i = 0; i < vals.length; ++i) {
            if (val.equalsIgnoreCase(vals[i])) {
                info &= ~(masks[typ] << shifts[typ]);
                info |= i << shifts[typ];
                if (version.length() > 0) {
                    setStatusVersion(version);
                }
                return;
            }
        }

        throw new IllegalArgumentException(
            "unrecognized value '" + val + "' for type '" + typeNames[typ] + "'");
    }

    /**
     * Enable status version included in input/output
     */
    public void includeStatusVersion(boolean include) {
        includeStatusVer = include;
    }

    /**
     * Write the information out as a single line in brief format.
     * If there are IO errors, throws a RuntimeException.
     */
    public void writeln(BufferedWriter w) {
        try {
            for (int i = 0; i < NUM_TYPES; ++i) {
                String s = get(i, true);
                if (s != null) {
                    w.write(s);
                }
                if (includeStatusVer && i == STA) {
                    String ver = getStatusVersion();
                    if (ver.length() > 0) {
                        w.write("@");
                        w.write(getStatusVersion());
                    }
                }
                w.write(SEP);
            }
            w.newLine();
        }
        catch (IOException e) {
            RuntimeException re = new RuntimeException("IO Error");
            re.initCause(e);
            throw re;
        }
    }

    /**
     * Read a record from the input and initialize this APIInfo.
     * Return true if successful, false if EOF, otherwise throw
     * a RuntimeException.
     */
    public boolean read(BufferedReader r) {
        int i = 0;
        try {
            for (; i < NUM_TYPES; ++i) {
                setType(i, readToken(r));
            }
            r.readLine(); // swallow line end sequence
        }
        catch (IOException e) {
            if (i == 0) { // assume if first read returns error, we have reached end of input
                return false;
            }
            RuntimeException re = new RuntimeException("IO Error");
            re.initCause(e);
            throw re;
        }

        return true;
    }

    /**
     * Read one token from input, which should have been written by
     * APIInfo.  Throws IOException if EOF is encountered before the
     * token is complete (i.e. before the separator character is
     * encountered) or if the token exceeds the maximum length of
     * 511 chars.
     */
    public static String readToken(BufferedReader r) throws IOException {
        char[] buf = new char[512];
        int i = 0;
        for (; i < buf.length; ++i) {
            int c = r.read();
            if (c == -1) {
                throw new IOException("unexpected EOF");
            } else if (c == SEP) {
                break;
            }
            buf[i] = (char)c;
        }
        if (i == buf.length) {
            throw new IOException("unterminated token" + new String(buf));
        }

        return new String(buf, 0, i);
    }

    /**
     * The default comparator for APIInfo.  This compares packages, class/name
     * (as the info represents a class or other object), category, name,
     * and signature.
     */
    public static Comparator defaultComparator() {
        final Comparator c = new Comparator() {
                @Override
                public int compare(Object lhs, Object rhs) {
                    APIInfo lhi = (APIInfo)lhs;
                    APIInfo rhi = (APIInfo)rhs;
                    int result = lhi.pack.compareTo(rhi.pack);
                    if (result == 0) {
                        result = (lhi.getVal(CAT) == CAT_CLASS || lhi.getVal(CAT) == CAT_ENUM ? lhi.name : lhi.cls)
                            .compareTo(rhi.getVal(CAT) == CAT_CLASS || rhi.getVal(CAT) == CAT_ENUM ? rhi.name : rhi.cls);
                        if (result == 0) {
                            result = lhi.getVal(CAT)- rhi.getVal(CAT);
                            if (result == 0) {
                                result = lhi.name.compareTo(rhi.name);
                                if (result == 0) {
                                    result = lhi.sig.compareTo(rhi.sig);
                                }
                            }
                        }
                    }
                    return result;
                }
            };
        return c;
    }

    /**
     * This compares two APIInfos by package, class/name, category, name, and then if
     * the APIInfo does not represent a class, by signature.  The difference between
     * this and the default comparator is that APIInfos representing classes are considered
     * equal regardless of their signatures (which represent inheritance for classes).
     */
    public static Comparator changedComparator() {
        final Comparator c = new Comparator() {
                @Override
                public int compare(Object lhs, Object rhs) {
                    APIInfo lhi = (APIInfo)lhs;
                    APIInfo rhi = (APIInfo)rhs;
                    int result = lhi.pack.compareTo(rhi.pack);
                    if (result == 0) {
                        result = (lhi.getVal(CAT) == CAT_CLASS ? lhi.name : lhi.cls)
                            .compareTo(rhi.getVal(CAT) == CAT_CLASS ? rhi.name : rhi.cls);
                        if (result == 0) {
                            result = lhi.getVal(CAT)- rhi.getVal(CAT);
                            if (result == 0) {
                                result = lhi.name.compareTo(rhi.name);
                                if (result == 0 && lhi.getVal(CAT) != CAT_CLASS) {
                                    // signature change on fields ignored
                                    if (lhi.getVal(CAT) != CAT_FIELD) {
                                        result = lhi.sig.compareTo(rhi.sig);
                                    }
                                }
                            }
                        }
                    }
                    return result;
                }
            };
        return c;
    }

    /**
     * This compares two APIInfos by package, then sorts classes before non-classes, then
     * by class/name, category, name, and signature.
     */
    public static Comparator classFirstComparator() {
        final Comparator c = new Comparator() {
                @Override
                public int compare(Object lhs, Object rhs) {
                    APIInfo lhi = (APIInfo)lhs;
                    APIInfo rhi = (APIInfo)rhs;
                    int result = lhi.pack.compareTo(rhi.pack);
                    if (result == 0) {
                        boolean lcls = lhi.getVal(CAT) == CAT_CLASS;
                        boolean rcls = rhi.getVal(CAT) == CAT_CLASS;
                        result = lcls == rcls ? 0 : (lcls ? -1 : 1);
                        if (result == 0) {
                            result = (lcls ? lhi.name : lhi.cls).compareTo(
                                rcls ? rhi.name : rhi.cls);
                            if (result == 0) {
                                result = lhi.getVal(CAT)- rhi.getVal(CAT);
                                if (result == 0) {
                                    result = lhi.name.compareTo(rhi.name);
                                    if (result == 0 && !lcls) {
                                        result = lhi.sig.compareTo(rhi.sig);
                                    }
                                }
                            }
                        }
                    }
                    return result;
                }
            };
        return c;
    }

    /**
     * Write the data in report format.
     */
    public void print(PrintWriter pw, boolean detail, boolean html) {
        print(pw, detail, html, true);
    }

    public void print(PrintWriter pw, boolean detail, boolean html, boolean withStatus) {
        StringBuilder buf = new StringBuilder();
        format(buf, detail, html, withStatus);
        pw.print(buf.toString());
    }

    public void format(StringBuilder buf, boolean detail, boolean html, boolean withStatus) {
        // remove all occurrences of icu packages from the param string
        String xsig = sig;
        if (!detail) {
            final String ICUPACK = "com.ibm.icu.";
            StringBuilder tbuf = new StringBuilder();
            for (int i = 0; i < sig.length();) {
                int n = sig.indexOf(ICUPACK, i);
                if (n == -1) {
                    tbuf.append(sig.substring(i));
                    break;
                }
                tbuf.append(sig.substring(i, n));
                i = n + ICUPACK.length();
                // skip icu public package lang/math/number/text/util
                n = sig.indexOf('.', i);
                if (n >= 0) {
                    i = n + 1;
                }
            }
            xsig = tbuf.toString();
        }

        // construct signature
        for (int i = (withStatus ? STA : VIS) ; i < CAT; ++i) { // include status
            String s = get(i, false);
            if (s != null && s.length() > 0) {
                if (html) {
                    s = s.trim();
                    if (i == STA) {
                        String color = null;
                        if (s.startsWith("(internal)")) {
                            color = "red";
                        } else if (s.startsWith("(draft)")) {
                            color = "orange";
                        } else if (s.startsWith("(stable)")) {
                            color = "green";
                        } else if (s.startsWith("(deprecated)")) {
                            color = "gray";
                        }
                        if (color != null) {
                            s = "<span style='color:" + color + "'>" + prepText(s, html) + "</span>";
                        }
                    }
                }
                buf.append(s);
                buf.append(' ');
            }
        }

        int val = getVal(CAT);
        switch (val) {
        case CAT_CLASS:
            if (sig.indexOf("extends") == -1) {
                buf.append("interface ");
            } else {
                buf.append("class ");
            }
            if (html) {
                buf.append("<i>");
            }
            if (cls.length() > 0) {
                buf.append(prepText(cls, html));
                buf.append('.');
            }
            buf.append(prepText(name, html));
            if (html) {
                buf.append("</i>");
            }
            if (detail) {
                buf.append(' ');
                buf.append(prepText(sig, html));
            }
            break;

        case CAT_ENUM:
            buf.append("enum ");
            if (html) {
                buf.append("<i>");
            }
            if (cls.length() > 0) {
                buf.append(prepText(cls, html));
                buf.append('.');
            }
            buf.append(prepText(name, html));
            if (html) {
                buf.append("</i>");
            }
            if (detail) {
                buf.append(' ');
                buf.append(prepText(sig, html));
            }
            break;

        case CAT_FIELD:
        case CAT_ENUM_CONSTANT:
            buf.append(prepText(xsig, html));
            buf.append(' ');
            buf.append(prepText(name, html));
            break;

        case CAT_METHOD:
        case CAT_CONSTRUCTOR:
            int n = xsig.indexOf('(');
            if (n > 0) {
                buf.append(prepText(xsig.substring(0, n), html));
                buf.append(' ');
            } else {
                n = 0;
            }
            if (html) {
                buf.append("<i>" + prepText(name, html) + "</i>");
            } else {
                buf.append(name);
            }
            buf.append(prepText(xsig.substring(n), html));
            break;
        }
    }

    private static String prepText(String text, boolean html) {
        if (html && (text.indexOf('<') >= 0 || text.indexOf('>') >= 0)) {
            StringBuilder buf = new StringBuilder();
            for (int i = 0; i < text.length(); i++) {
                char c = text.charAt(i);
                if (c == '<') {
                    buf.append("&lt;");
                } else if (c == '>') {
                    buf.append("&gt;");
                } else {
                    buf.append(c);
                }
            }
            text = buf.toString();
        }
        return text;
    }

    public void println(PrintWriter pw, boolean detail, boolean html) {
        print(pw, detail, html);
        pw.println();
    }

    private static final String[] typeNames = {
        "status", "visibility", "static", "final", "synchronized",
        "abstract", "category", "package", "class", "name", "signature"
    };

    public static final String getTypeValName(int typ, int val) {
        try {
            return names[typ][val];
        }
        catch (Exception e) {
            return "";
        }
    }

    private static final String[][] names = {
        { "(draft)     ", "(stable)    ", "(deprecated)", "(obsolete)  ", "*internal*  " },
        { "package", "public", "protected", "private" },
        { "", "static" },
        { "", "final" },
        { "", "synchronized" },
        { "", "abstract" },
        { "class", "field", "constructor", "method", "enum", "enum constant"  },
        null,
        null,
        null,
        null,
        null
    };

    private static final String[][] shortNames = {
        { "DR", "ST", "DP", "OB", "IN" },
        { "PK", "PB", "PT", "PR" },
        { "NS", "ST" },
        { "NF", "FN" },
        { "NS", "SY" },
        { "NA", "AB" },
        { "L", "F", "C", "M", "E", "K" },
        null,
        null,
        null,
        null,
        null
    };

    private static void validateType(int typ) {
        if (typ < 0 || typ > NUM_TYPES) {
            throw new IllegalArgumentException("bad type index: " + typ);
        }
    }

    @Override
    public String toString() {
        return get(NAM, true);
    }
}
