// 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.tapestry5.ioc.internal.services;

import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.plastic.PlasticUtils;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * A representation of a {@link java.lang.reflect.Method}, identifying the name, return type, parameter types and
 * exception types. Actual Method objects are tied to a particular class, and don't compare well with other otherwise
 * identical Methods from other classes or interface; MethodSignatures are distinct from classes and compare well.
 *
 * Because the intended purpose is to compare methods from interfaces (which are always public and abstract) we don't
 * bother to actually track the modifiers. In addition, at this time, MethodSignature <em>does not distinguish between
 * instance and static methods</em>.
 */
@SuppressWarnings("all")
public class MethodSignature
{
    private int hashCode = -1;

    private final Class returnType;

    private final String name;

    private final Class[] parameterTypes;

    private final Class[] exceptionTypes;

    private final Method method;

    public MethodSignature(Class returnType, String name, Class[] parameterTypes, Class[] exceptionTypes)
    {
        this(null, returnType, name, parameterTypes, exceptionTypes);
    }

    private MethodSignature(Method method, Class returnType, String name, Class[] parameterTypes, Class[] exceptionTypes)
    {
        this.method = method;
        assert returnType != null;
        this.returnType = returnType;
        assert InternalUtils.isNonBlank(name);
        this.name = name;

        // Can be null!
        this.parameterTypes = parameterTypes;
        this.exceptionTypes = exceptionTypes;
    }

    public MethodSignature(Method m)
    {
        this(m, m.getReturnType(), m.getName(), m.getParameterTypes(), m.getExceptionTypes());
    }

    /**
     * Returns the exceptions for this method. Caution: do not modify the returned array. May return null.
     */
    public Class[] getExceptionTypes()
    {
        return exceptionTypes;
    }

    public String getName()
    {
        return name;
    }

    /**
     * If this signature was created from a method, return that method.
     *
     * @since 5.3
     */
    public Method getMethod()
    {
        return method;
    }

    /**
     * Returns the parameter types for this method. May return null. Caution: do not modify the returned array.
     */
    public Class[] getParameterTypes()
    {
        return parameterTypes;
    }

    public Class getReturnType()
    {
        return returnType;
    }

    @Override
    public int hashCode()
    {
        if (hashCode == -1)
        {

            hashCode = returnType.hashCode();

            hashCode = 31 * hashCode + name.hashCode();

            int count = InternalUtils.size(parameterTypes);

            for (int i = 0; i < count; i++)
                hashCode = 31 * hashCode + parameterTypes[i].hashCode();

            count = InternalUtils.size(exceptionTypes);

            for (int i = 0; i < count; i++)
                hashCode = 31 * hashCode + exceptionTypes[i].hashCode();
        }

        return hashCode;
    }

    /**
     * Returns true if the other object is an instance of MethodSignature with <em>identical</em> values for return
     * type, name, parameter types and exception types.
     *
     * @see #isOverridingSignatureOf(MethodSignature)
     */
    @Override
    public boolean equals(Object o)
    {
        if (o == null || !(o instanceof MethodSignature))
            return false;

        MethodSignature ms = (MethodSignature) o;

        if (returnType != ms.returnType)
            return false;

        if (!name.equals(ms.name))
            return false;

        if (mismatch(parameterTypes, ms.parameterTypes))
            return false;

        return !mismatch(exceptionTypes, ms.exceptionTypes);
    }

    private boolean mismatch(Class[] a1, Class[] a2)
    {
        int a1Count = InternalUtils.size(a1);
        int a2Count = InternalUtils.size(a2);

        if (a1Count != a2Count)
            return true;

        // Hm. What if order is important (for exceptions)? We're really saying here that they
        // were derived from the name Method.

        for (int i = 0; i < a1Count; i++)
        {
            if (a1[i] != a2[i])
                return true;
        }

        return false;
    }

    @Override
    public String toString()
    {
        StringBuilder buffer = new StringBuilder();

        buffer.append(PlasticUtils.toTypeName(returnType));
        buffer.append(' ');
        buffer.append(name);
        buffer.append('(');

        for (int i = 0; i < InternalUtils.size(parameterTypes); i++)
        {
            if (i > 0)
                buffer.append(", ");

            buffer.append(PlasticUtils.toTypeName(parameterTypes[i]));
        }

        buffer.append(')');

        int _exceptionCount = InternalUtils.size(exceptionTypes);
        String _exceptionNames[] = new String[_exceptionCount];
        for (int i = 0; i < _exceptionCount; i++)
        {
            _exceptionNames[i] = exceptionTypes[i].getName();
        }

        Arrays.sort(_exceptionNames);

        for (int i = 0; i < _exceptionCount; i++)
        {
            if (i == 0)
                buffer.append(" throws ");
            else
                buffer.append(", ");

            buffer.append(_exceptionNames[i]);
        }

        return buffer.toString();
    }

    /**
     * Returns a string consisting of the name of the method and its parameter types. This is similar to
     * {@link #toString()}, but omits the return type and information about thrown exceptions. A unique id is used by
     * {@link org.apache.tapestry5.ioc.internal.services.MethodIterator} to identify overlapping methods (methods with the same name and parameter types but with
     * different thrown exceptions).
     *
     * @see #isOverridingSignatureOf(MethodSignature)
     */
    public String getUniqueId()
    {
        StringBuilder buffer = new StringBuilder(name);
        buffer.append('(');

        for (int i = 0; i < InternalUtils.size(parameterTypes); i++)
        {
            if (i > 0)
                buffer.append(',');

            buffer.append(PlasticUtils.toTypeName(parameterTypes[i]));
        }

        buffer.append(')');

        return buffer.toString();
    }

    /**
     * Returns true if this signature has the same return type, name and parameters types as the method signature passed
     * in, and this signature's exceptions "trump" (are the same as, or super-implementations of, all exceptions thrown
     * by the other method signature).
     */

    public boolean isOverridingSignatureOf(MethodSignature ms)
    {
        if (returnType != ms.returnType)
            return false;

        if (!name.equals(ms.name))
            return false;

        if (mismatch(parameterTypes, ms.parameterTypes))
            return false;

        return exceptionsEncompass(ms.exceptionTypes);
    }

    /**
     * The nuts and bolts of checking that another method signature's exceptions are a subset of this signature's.
     */

    @SuppressWarnings("unchecked")
    private boolean exceptionsEncompass(Class[] otherExceptions)
    {
        int ourCount = InternalUtils.size(exceptionTypes);
        int otherCount = InternalUtils.size(otherExceptions);

        // If we have no exceptions, then ours encompass theirs only if they
        // have no exceptions, either.

        if (ourCount == 0)
            return otherCount == 0;

        boolean[] matched = new boolean[otherCount];
        int unmatched = otherCount;

        for (int i = 0; i < ourCount && unmatched > 0; i++)
        {
            for (int j = 0; j < otherCount; j++)
            {
                // Ignore exceptions that have already been matched

                if (matched[j])
                    continue;

                // When one of our exceptions is a super-class of one of their exceptions,
                // then their exceptions is matched.

                if (exceptionTypes[i].isAssignableFrom(otherExceptions[j]))
                {
                    matched[j] = true;
                    unmatched--;
                }
            }
        }

        return unmatched == 0;
    }
}
