/*
 *  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 org.apache.isis.core.metamodel.facets;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;

import com.google.common.collect.Lists;

import org.apache.isis.applib.Identifier;
import org.apache.isis.core.commons.lang.StringExtensions;
import org.apache.isis.core.metamodel.facetapi.FacetUtil;
import org.apache.isis.core.metamodel.facetapi.FeatureType;
import org.apache.isis.core.metamodel.facetapi.IdentifiedHolder;
import org.apache.isis.core.metamodel.facets.actcoll.typeof.TypeOfFacet;
import org.apache.isis.core.metamodel.facets.collparam.semantics.CollectionSemanticsFacet;
import org.apache.isis.core.metamodel.facets.collparam.semantics.CollectionSemanticsFacetDefault;
import org.apache.isis.core.metamodel.specloader.*;

/**
 * non-final only so it can be mocked if need be.
 */
public class FacetedMethod extends TypedHolderDefault implements IdentifiedHolder {

    // //////////////////////////////////////////////////
    // Factory methods
    // //////////////////////////////////////////////////

    /**
     * Principally for testing purposes.
     */
    public static FacetedMethod createForProperty(final Class<?> declaringType, final String propertyName) {
        try {
            final Method method = declaringType.getMethod("get" + StringExtensions.asPascal(propertyName));
            return FacetedMethod.createForProperty(declaringType, method);
        } catch (final SecurityException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Principally for testing purposes.
     */
    public static FacetedMethod createForCollection(final Class<?> declaringType, final String collectionName) {
        try {
            final Method method = declaringType.getMethod("get" + StringExtensions.asPascal(collectionName));
            return FacetedMethod.createForCollection(declaringType, method);
        } catch (final SecurityException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Principally for testing purposes.
     */
    public static FacetedMethod createForAction(
            final Class<?> declaringType,
            final String actionName,
            final SpecificationLoader specificationLoader,
            final Class<?>... parameterTypes) {
        try {
            final Method method = declaringType.getMethod(actionName, parameterTypes);
            return FacetedMethod.createForAction(declaringType, method, specificationLoader);
        } catch (final SecurityException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }


    public static FacetedMethod createForProperty(final Class<?> declaringType, final Method method) {
        return new FacetedMethod(FeatureType.PROPERTY, declaringType, method, method.getReturnType(), emptyParameterList());
    }

    public static FacetedMethod createForCollection(final Class<?> declaringType, final Method method) {
        return new FacetedMethod(FeatureType.COLLECTION, declaringType, method, null, emptyParameterList());
    }

    public static FacetedMethod createForAction(
            final Class<?> declaringType,
            final Method method,
            final SpecificationLoader specificationLoader) {
        return new FacetedMethod(FeatureType.ACTION, declaringType, method, method.getReturnType(), getParameters(declaringType, method,
                specificationLoader));
    }

    private static List<FacetedMethodParameter> getParameters(
            final Class<?> declaringType,
            final Method actionMethod,
            final SpecificationLoader specificationLoader) {

        final Class<?>[] parameterTypes = actionMethod.getParameterTypes();
        final Type[] genericParameterTypes = actionMethod.getGenericParameterTypes();
        final List<FacetedMethodParameter> actionParams = Lists.newArrayList();

        for (int paramNum = 0; paramNum < parameterTypes.length; paramNum++) {

            final Class<?> parameterType = parameterTypes[paramNum];
            final Type genericParameterType = genericParameterTypes[paramNum];

            final FeatureType featureType =
                    org.apache.isis.core.metamodel.specloader.CollectionUtils
                            .isParamCollection(parameterType, genericParameterType)
                            ? FeatureType.ACTION_PARAMETER_COLLECTION
                            : FeatureType.ACTION_PARAMETER_SCALAR;

            final FacetedMethodParameter fmp = new FacetedMethodParameter(featureType, declaringType, actionMethod, parameterType);
            actionParams.add(fmp);

            // this is based on similar logic to ActionAnnotationFacetFactory#processTypeOf
            if(featureType == FeatureType.ACTION_PARAMETER_COLLECTION) {

                final CollectionSemanticsFacet semanticsFacet =
                        CollectionSemanticsFacetDefault.forParamType(parameterType, fmp);
                FacetUtil.addFacet(semanticsFacet);

                TypeOfFacet typeOfFacet = TypeOfFacet.Util
                        .inferFromGenericParamType(fmp, parameterType, genericParameterType,
                        specificationLoader);

                if(typeOfFacet == null ) {
                    if (org.apache.isis.core.metamodel.specloader.CollectionUtils.isArrayType(parameterType)) {
                        typeOfFacet = TypeOfFacet.Util.inferFromArrayType(fmp, parameterType, specificationLoader);
                    }
                }

                // copy over (corresponds to similar code for OneToManyAssociation in FacetMethodsBuilder).
                if(typeOfFacet != null ) {
                    FacetUtil.addFacet(typeOfFacet);
                    fmp.setType(typeOfFacet.value());
                }
            }

        }
        return Collections.unmodifiableList(actionParams);
    }

    // //////////////////////////////////////////////////
    // Constructor
    // //////////////////////////////////////////////////

    private final Class<?> owningType;
    private final Method method;
    private final Identifier identifier;
    private final List<FacetedMethodParameter> parameters;

    public List<FacetedMethodParameter> getParameters() {
        return parameters;
    }

    public static List<FacetedMethodParameter> emptyParameterList() {
        final List<FacetedMethodParameter> emptyList = Collections.emptyList();
        return Collections.unmodifiableList(emptyList);
    }

    private FacetedMethod(final FeatureType featureType, final Class<?> declaringType, final Method method, final Class<?> type, final List<FacetedMethodParameter> parameters) {
        super(featureType, type);
        this.owningType = declaringType;
        this.method = method;
        this.identifier = featureType.identifierFor(declaringType, method);
        this.parameters = parameters;
    }

    /**
     * The {@link Class} that owns this {@link Method} (as per
     * {@link Class#getMethods()}, returning the {@link Method}s both of this
     * class and of its superclasses).
     * 
     * <p>
     * Note: we don't call this the 'declaring type' because
     * {@link Class#getDeclaredMethods()} does not return methods from
     * superclasses.
     */
    public Class<?> getOwningType() {
        return owningType;
    }

    /**
     * A {@link Method} obtained from the {@link #getOwningType() owning type}
     * using {@link Class#getMethods()}.
     */
    public Method getMethod() {
        return method;
    }

    @Override
    public Identifier getIdentifier() {
        return identifier;
    }

    // ////////////////////////////////////////////////////////////////////
    // toString
    // ////////////////////////////////////////////////////////////////////

    @Override
    public String toString() {
        return getFeatureType().name() + " Peer [identifier=\"" + getIdentifier() + "\",type=" + getType().getName() + " ]";
    }


}
