/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 groovy.inspect;

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import groovy.lang.MetaMethod;
import groovy.lang.PropertyValue;
import groovy.lang.Tuple2;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.FormatHelper;
import org.codehaus.groovy.runtime.InvokerHelper;

import java.io.PrintStream;
import java.io.Serial;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

/**
 * The Inspector provides a unified access to an object's
 * information that can be determined by introspection.
 */
public class Inspector {
    /**
     * Object currently being inspected.
     */
    protected Object objectUnderInspection;

    // Indexes to retrieve Class Property information
    /**
     * Index for package information.
     */
    public static final int CLASS_PACKAGE_IDX = 0;
    /**
     * Index for class information.
     */
    public static final int CLASS_CLASS_IDX = 1;
    /**
     * Index for implemented interface information.
     */
    public static final int CLASS_INTERFACE_IDX = 2;
    /**
     * Index for superclass information.
     */
    public static final int CLASS_SUPERCLASS_IDX = 3;
    /**
     * Index for other class information.
     */
    public static final int CLASS_OTHER_IDX = 4;

    // Indexes to retrieve field and method information
    /**
     * Index for member origin information.
     */
    public static final int MEMBER_ORIGIN_IDX = 0;
    /**
     * Index for member modifier information.
     */
    public static final int MEMBER_MODIFIER_IDX = 1;
    /**
     * Index for member declarer information.
     */
    public static final int MEMBER_DECLARER_IDX = 2;
    /**
     * Index for member type information.
     */
    public static final int MEMBER_TYPE_IDX = 3;
    /**
     * Index for member name information.
     */
    public static final int MEMBER_NAME_IDX = 4;
    /**
     * Index for member parameter information.
     */
    public static final int MEMBER_PARAMS_IDX = 5;
    /**
     * Index for member value information.
     */
    public static final int MEMBER_VALUE_IDX = 5;
    /**
     * Index for member exception information.
     */
    public static final int MEMBER_EXCEPTIONS_IDX = 6;

    /**
     * Placeholder for unavailable information.
     */
    public static final String NOT_APPLICABLE = "n/a";
    /**
     * Origin label for Groovy members.
     */
    public static final String GROOVY = "GROOVY";
    /**
     * Origin label for Java members.
     */
    public static final String JAVA = "JAVA";

    /**
     * @param objectUnderInspection must not be null
     */
    public Inspector(Object objectUnderInspection) {
        if (null == objectUnderInspection) {
            throw new IllegalArgumentException("argument must not be null");
        }
        this.objectUnderInspection = objectUnderInspection;
    }

    /**
     * Get the Class Properties of the object under inspection.
     *
     * @return String array to be indexed by the CLASS_xxx_IDX constants
     */
    public String[] getClassProps() {
        String[] result = new String[CLASS_OTHER_IDX + 1];
        Package pack = getClassUnderInspection().getPackage();
        result[CLASS_PACKAGE_IDX] = "package " + ((pack == null) ? NOT_APPLICABLE : pack.getName());
        String modifiers = Modifier.toString(getClassUnderInspection().getModifiers());
        result[CLASS_CLASS_IDX] = modifiers + " class " + shortName(getClassUnderInspection());
        result[CLASS_INTERFACE_IDX] = "implements ";
        Class[] interfaces = getClassUnderInspection().getInterfaces();
        for (Class anInterface : interfaces) {
            result[CLASS_INTERFACE_IDX] += shortName(anInterface) + " ";
        }
        result[CLASS_SUPERCLASS_IDX] = "extends " + shortName(getClassUnderInspection().getSuperclass());
        result[CLASS_OTHER_IDX] = "is Primitive: " + getClassUnderInspection().isPrimitive()
                + ", is Array: " + getClassUnderInspection().isArray()
                + ", is Groovy: " + isGroovy();
        return result;
    }

    /**
     * Indicates whether the inspected type implements {@link GroovyObject}.
     *
     * @return {@code true} if the inspected type is Groovy-aware
     */
    public boolean isGroovy() {
        return GroovyObject.class.isAssignableFrom(getClassUnderInspection());
    }

    /**
     * Gets the object being inspected.
     *
     * @return the object
     */
    public Object getObject() {
        return objectUnderInspection;
    }

    /**
     * Get info about usual Java instance and class Methods as well as Constructors.
     *
     * @return Array of StringArrays that can be indexed with the MEMBER_xxx_IDX constants
     */
    public Object[] getMethods() {
        Method[] methods = getClassUnderInspection().getMethods();
        Constructor[] ctors = getClassUnderInspection().getConstructors();
        Object[] result = new Object[methods.length + ctors.length];
        int resultIndex = 0;
        for (; resultIndex < methods.length; resultIndex++) {
            Method method = methods[resultIndex];
            result[resultIndex] = methodInfo(method);
        }
        for (int i = 0; i < ctors.length; i++, resultIndex++) {
            Constructor ctor = ctors[i];
            result[resultIndex] = methodInfo(ctor);
        }
        return result;
    }

    /**
     * Get info about usual Java instance and class Methods as well as Constructors.
     */
    public Tuple2[] getMethodsWithInfo() {
        Method[] methods = getClassUnderInspection().getMethods();
        Constructor[] ctors = getClassUnderInspection().getConstructors();
        Tuple2[] result = new Tuple2[methods.length + ctors.length];
        int resultIndex = 0;
        for (; resultIndex < methods.length; resultIndex++) {
            Method method = methods[resultIndex];
            result[resultIndex] = Tuple2.tuple(method, methodInfo(method));
        }
        for (int i = 0; i < ctors.length; i++, resultIndex++) {
            Constructor ctor = ctors[i];
            result[resultIndex] = Tuple2.tuple(ctor, methodInfo(ctor));
        }
        return result;
    }

    /**
     * Get info about instance and class Methods that are dynamically added through Groovy.
     *
     * @return Array of StringArrays that can be indexed with the MEMBER_xxx_IDX constants
     */
    public Object[] getMetaMethods() {
        MetaClass metaClass = InvokerHelper.getMetaClass(objectUnderInspection);
        List metaMethods = metaClass.getMetaMethods();
        Object[] result = new Object[metaMethods.size()];
        int i = 0;
        for (Iterator iter = metaMethods.iterator(); iter.hasNext(); i++) {
            MetaMethod metaMethod = (MetaMethod) iter.next();
            result[i] = methodInfo(metaMethod);
        }
        return result;
    }

    /**
     * Get info about instance and class Methods that are dynamically added through Groovy.
     */
    public Tuple2[] getMetaMethodsWithInfo() {
        MetaClass metaClass = InvokerHelper.getMetaClass(objectUnderInspection);
        List<MetaMethod> metaMethods = metaClass.getMetaMethods();
        Tuple2[] result = new Tuple2[metaMethods.size()];
        int i = 0;
        for (Iterator<MetaMethod> iter = metaMethods.iterator(); iter.hasNext(); i++) {
            MetaMethod metaMethod = iter.next();
            result[i] = Tuple2.tuple(metaMethod, methodInfo(metaMethod));
        }
        return result;
    }

    /**
     * Get info about usual Java public fields incl. constants.
     *
     * @return Array of StringArrays that can be indexed with the MEMBER_xxx_IDX constants
     */
    public Object[] getPublicFields() {
        Field[] fields = getClassUnderInspection().getFields();
        Object[] result = new Object[fields.length];
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            result[i] = fieldInfo(field);
        }
        return result;
    }

    /**
     * Get info about Properties (Java and Groovy alike).
     *
     * @return Array of StringArrays that can be indexed with the MEMBER_xxx_IDX constants
     */
    public Object[] getPropertyInfo() {
        List props = DefaultGroovyMethods.getMetaPropertyValues(objectUnderInspection);
        Object[] result = new Object[props.size()];
        int i = 0;
        for (Iterator iter = props.iterator(); iter.hasNext(); i++) {
            PropertyValue pv = (PropertyValue) iter.next();
            result[i] = fieldInfo(pv);
        }
        return result;
    }

    /**
     * Builds descriptive information for a Java field.
     *
     * @param field the field to describe
     * @return the field information array
     */
    protected String[] fieldInfo(Field field) {
        String[] result = new String[MEMBER_VALUE_IDX + 1];
        result[MEMBER_ORIGIN_IDX] = JAVA;
        result[MEMBER_MODIFIER_IDX] = Modifier.toString(field.getModifiers());
        result[MEMBER_DECLARER_IDX] = shortName(field.getDeclaringClass());
        result[MEMBER_TYPE_IDX] = shortName(field.getType());
        result[MEMBER_NAME_IDX] = field.getName();
        try {
            result[MEMBER_VALUE_IDX] = FormatHelper.inspect(field.get(objectUnderInspection));
        } catch (IllegalAccessException e) {
            result[MEMBER_VALUE_IDX] = NOT_APPLICABLE;
        }
        return withoutNulls(result);
    }

    /**
     * Builds descriptive information for a Groovy property value.
     *
     * @param pv the property value to describe
     * @return the property information array
     */
    protected String[] fieldInfo(PropertyValue pv) {
        return fieldWithInfo(pv).getV2();
    }

    /**
     * Gets properties together with their values and formatted metadata.
     *
     * @return the property/value tuples
     */
    public Object[] getPropertiesWithInfo() {
        List<PropertyValue> props = DefaultGroovyMethods.getMetaPropertyValues(objectUnderInspection);
        Object[] result = new Object[props.size()];
        int i = 0;
        for (Iterator<PropertyValue> iter = props.iterator(); iter.hasNext(); i++) {
            result[i] = fieldWithInfo(iter.next());
        }
        return result;
    }

    /**
     * Builds the value and metadata tuple for a Groovy property.
     *
     * @param pv the property value to describe
     * @return the property tuple
     */
    protected Tuple2<Object, String[]> fieldWithInfo(PropertyValue pv) {
        String[] info = new String[MEMBER_VALUE_IDX + 1];
        info[MEMBER_ORIGIN_IDX] = GROOVY;
        info[MEMBER_MODIFIER_IDX] = "public";
        info[MEMBER_DECLARER_IDX] = NOT_APPLICABLE;
        info[MEMBER_TYPE_IDX] = shortName(pv.getType());
        info[MEMBER_NAME_IDX] = pv.getName();
        Object field = null;
        try {
            field = pv.getValue();
            info[MEMBER_VALUE_IDX] = FormatHelper.inspect(field);
        } catch (Exception e) {
            info[MEMBER_VALUE_IDX] = NOT_APPLICABLE;
        }
        info = withoutNulls(info);
        return new Tuple2<>(field, info);
    }

    /**
     * Gets the class of the inspected object.
     *
     * @return the inspected class
     */
    protected Class getClassUnderInspection() {
        return objectUnderInspection.getClass();
    }

    /**
     * Returns the class name without its package prefix.
     *
     * @param clazz the class to shorten
     * @return the shortened class name, or {@link #NOT_APPLICABLE}
     */
    public static String shortName(Class clazz) {
        if (null == clazz) return NOT_APPLICABLE;
        String className = clazz.getName();
        if (null == clazz.getPackage()) return className;
        String packageName = clazz.getPackage().getName();
        int offset = packageName.length();
        if (offset > 0) offset++;
        className = className.substring(offset);
        return className;
    }

    private static String makeTypesInfo(Class[] types) {
        StringBuilder sb = new StringBuilder(32);
        for (int k = 0; k < types.length; k++) {
            sb.append(shortName(types[k]));
            if (k < (types.length - 1)) sb.append(", ");
        }

        return sb.toString();
    }

    private static String makeParamsInfo(Class[] params) {
        return makeTypesInfo(params);
    }

    private static String makeExceptionInfo(Class[] exceptions) {
        return makeTypesInfo(exceptions);
    }

    /**
     * Builds descriptive information for a Java method.
     *
     * @param method the method to describe
     * @return the method information array
     */
    protected String[] methodInfo(Method method) {
        String[] result = new String[MEMBER_EXCEPTIONS_IDX + 1];
        result[MEMBER_ORIGIN_IDX] = JAVA;
        result[MEMBER_DECLARER_IDX] = shortName(method.getDeclaringClass());
        result[MEMBER_MODIFIER_IDX] = Modifier.toString(method.getModifiers());
        result[MEMBER_NAME_IDX] = method.getName();
        result[MEMBER_TYPE_IDX] = shortName(method.getReturnType());
        result[MEMBER_PARAMS_IDX] = makeParamsInfo(method.getParameterTypes());
        result[MEMBER_EXCEPTIONS_IDX] = makeExceptionInfo(method.getExceptionTypes());

        return withoutNulls(result);
    }

    /**
     * Builds descriptive information for a constructor.
     *
     * @param ctor the constructor to describe
     * @return the constructor information array
     */
    protected String[] methodInfo(Constructor ctor) {
        String[] result = new String[MEMBER_EXCEPTIONS_IDX + 1];
        result[MEMBER_ORIGIN_IDX] = JAVA;
        result[MEMBER_MODIFIER_IDX] = Modifier.toString(ctor.getModifiers());
        result[MEMBER_DECLARER_IDX] = shortName(ctor.getDeclaringClass());
        result[MEMBER_TYPE_IDX] = shortName(ctor.getDeclaringClass());
        result[MEMBER_NAME_IDX] = ctor.getName();
        result[MEMBER_PARAMS_IDX] = makeParamsInfo(ctor.getParameterTypes());
        result[MEMBER_EXCEPTIONS_IDX] = makeExceptionInfo(ctor.getExceptionTypes());

        return withoutNulls(result);
    }

    /**
     * Builds descriptive information for a Groovy meta-method.
     *
     * @param method the meta-method to describe
     * @return the method information array
     */
    protected String[] methodInfo(MetaMethod method) {
        String[] result = new String[MEMBER_EXCEPTIONS_IDX + 1];
        result[MEMBER_ORIGIN_IDX] = GROOVY;
        result[MEMBER_MODIFIER_IDX] = Modifier.toString(method.getModifiers());
        result[MEMBER_DECLARER_IDX] = shortName(method.getDeclaringClass().getTheClass());
        result[MEMBER_TYPE_IDX] = shortName(method.getReturnType());
        result[MEMBER_NAME_IDX] = method.getName();
        result[MEMBER_PARAMS_IDX] = makeParamsInfo(method.getNativeParameterTypes());
        result[MEMBER_EXCEPTIONS_IDX] = NOT_APPLICABLE; // no exception info for Groovy MetaMethods
        return withoutNulls(result);
    }

    /**
     * Replaces {@code null} entries with {@link #NOT_APPLICABLE}.
     *
     * @param toNormalize the array to normalize
     * @return the normalized array
     */
    protected String[] withoutNulls(String[] toNormalize) {
        for (int i = 0; i < toNormalize.length; i++) {
            String s = toNormalize[i];
            if (null == s) toNormalize[i] = NOT_APPLICABLE;
        }
        return toNormalize;
    }

    /**
     * Prints member information to {@link System#out}.
     *
     * @param memberInfo the member information to print
     */
    public static void print(Object[] memberInfo) {
        print(System.out, memberInfo);
    }

    /**
     * Prints member information to the supplied stream.
     *
     * @param out the destination stream
     * @param memberInfo the member information to print
     */
    static void print(final PrintStream out, Object[] memberInfo) {
        for (int i = 0; i < memberInfo.length; i++) {
            String[] metaMethod = (String[]) memberInfo[i];
            out.print(i + ":\t");
            for (String s : metaMethod) {
                out.print(s + " ");
            }
            out.println();
        }
    }

    /**
     * Sorts member information using the default comparator.
     *
     * @param memberInfo the member information to sort
     * @return the sorted collection
     */
    public static Collection sort(List<Object> memberInfo) {
        return sort(memberInfo, new MemberComparator());
    }

    /**
     * Sorts member information using the supplied comparator.
     *
     * @param memberInfo the member information to sort
     * @param comparator the comparator to apply
     * @return the sorted collection
     */
    public static Collection sort(List<Object> memberInfo, Comparator<Object> comparator) {
        memberInfo.sort(comparator);
        return memberInfo;
    }

    /**
     * Comparator for member information arrays.
     */
    public static class MemberComparator implements Comparator<Object>, Serializable {
        @Serial
        private static final long serialVersionUID = -7691851726606749541L;

        /** {@inheritDoc} */
        @Override
        public int compare(Object a, Object b) {
            String[] aStr = (String[]) a;
            String[] bStr = (String[]) b;
            int result = aStr[Inspector.MEMBER_NAME_IDX].compareTo(bStr[Inspector.MEMBER_NAME_IDX]);
            if (0 != result) return result;
            result = aStr[Inspector.MEMBER_TYPE_IDX].compareTo(bStr[Inspector.MEMBER_TYPE_IDX]);
            if (0 != result) return result;
            result = aStr[Inspector.MEMBER_PARAMS_IDX].compareTo(bStr[Inspector.MEMBER_PARAMS_IDX]);
            if (0 != result) return result;
            result = aStr[Inspector.MEMBER_DECLARER_IDX].compareTo(bStr[Inspector.MEMBER_DECLARER_IDX]);
            if (0 != result) return result;
            result = aStr[Inspector.MEMBER_MODIFIER_IDX].compareTo(bStr[Inspector.MEMBER_MODIFIER_IDX]);
            if (0 != result) return result;
            result = aStr[Inspector.MEMBER_ORIGIN_IDX].compareTo(bStr[Inspector.MEMBER_ORIGIN_IDX]);
            return result;
        }
    }

    /**
     * Comparator for property tuples that include values.
     */
    public static class MemberComparatorWithValue implements Comparator<Object>, Serializable {
        @Serial
        private static final long serialVersionUID = 294298614093394525L;
        private static final MemberComparator delegate = new MemberComparator();

        /**
         * Compares two property tuples using the underlying property metadata.
         *
         * @param a the first tuple to compare
         * @param b the second tuple to compare
         * @return a negative value, zero, or a positive value as the first tuple is less than,
         *         equal to, or greater than the second
         */
        @Override
        public int compare(Object a, Object b) {
            Tuple2<Object, String[]> aTuple = (Tuple2<Object, String[]>) a;
            Tuple2<Object, String[]> bTuple = (Tuple2<Object, String[]>) b;
            return delegate.compare(aTuple.getV2(), bTuple.getV2());
        }
    }
}
