/*
 *  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.causeway.core.runtimeservices.wrapper.handlers;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;

import org.apache.causeway.applib.services.wrapper.events.CollectionMethodEvent;
import org.apache.causeway.commons.internal.assertions._Assert;
import org.apache.causeway.commons.semantics.CollectionSemantics;
import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
import org.apache.causeway.core.runtime.wrap.WrapperInvocationHandler;

/**
 * InvocationHandler in support of non-scalar types to be proxied up.
 *
 * @param <T> Domain Object type
 * @param <P> non-scalar type (eg. {@link Collection} or {@link Map}) to be proxied
 */
record PluralInvocationHandler<T, P>(
        P collectionOrMapToBeProxied,
        WrapperInvocationHandler.ClassMetaData classMetaData,
        OneToManyAssociation oneToManyAssociation,
        CollectionSemantics collectionSemantics
        ) implements InvocationHandler {

    // -- FACTORIES
    
   static <T, C extends Collection<?>> PluralInvocationHandler<T, C> forCollection(
           final C collectionToBeProxied,
           final OneToManyAssociation otma) {
       
       _Assert.assertTrue(Collection.class.isAssignableFrom(collectionToBeProxied.getClass()),
               ()->String.format("Cannot use %s for type %s, these are not compatible.",
                       PluralInvocationHandler.class.getName() + ".forCollection(..)",
                       collectionToBeProxied.getClass()));
       
       return new PluralInvocationHandler<>(collectionToBeProxied, otma,
                CollectionSemantics
                    .valueOfElseFail(collectionToBeProxied.getClass()));
    }
    
   static <T, M extends Map<?,?>> PluralInvocationHandler<T, M> forMap(
           final M mapToBeProxied,
           final OneToManyAssociation otma) {

       _Assert.assertTrue(Map.class.isAssignableFrom(mapToBeProxied.getClass()),
               ()->String.format("Cannot use %s for type %s, these are not compatible.",
                       PluralInvocationHandler.class.getName() + ".forMap(..)",
                       mapToBeProxied.getClass()));
       
       return new PluralInvocationHandler<>(mapToBeProxied, otma,
               CollectionSemantics.MAP);
   }
   
    // -- NON CANONICAL CONSTRUCTOR
    
    private PluralInvocationHandler(
            final P collectionOrMapToBeProxied,
            final OneToManyAssociation otma,
            final CollectionSemantics collectionSemantics) {
        
        this(collectionOrMapToBeProxied, 
                WrapperInvocationHandler.ClassMetaData.of(collectionOrMapToBeProxied.getClass()), 
                otma, collectionSemantics);
    }

    @Override
    public Object invoke(final Object collectionObject, final Method method, final Object[] args) throws Throwable {

        var policy = collectionSemantics.getInvocationHandlingPolicy();
        if (policy.intercepts(method)) {

            final Object returnValueObj = method.invoke(collectionOrMapToBeProxied, args);
            
            var event =
                    new CollectionMethodEvent(
                            collectionOrMapToBeProxied,
                            oneToManyAssociation().getFeatureIdentifier(),
                            collectionOrMapToBeProxied,
                            method.getName(),
                            args,
                            returnValueObj);
            oneToManyAssociation().getWrapperFactory().notifyListeners(event);
            return returnValueObj;
        }

        if (policy.vetoes(method)) {
            throw new UnsupportedOperationException(
                    String.format("Method '%s' may not be called directly.", method.getName()));
        }

        return method.invoke(collectionOrMapToBeProxied, args);
    }

}
