/*
 *  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.wrapper.proxy;

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

import com.google.common.collect.MapMaker;

import org.apache.isis.applib.services.wrapper.WrapperObject;
import org.apache.isis.applib.services.wrapper.WrappingObject;
import org.apache.isis.core.commons.lang.ArrayExtensions;
import org.apache.isis.core.metamodel.specloader.classsubstitutor.JavassistEnhanced;
import org.apache.isis.core.wrapper.handlers.DelegatingInvocationHandler;
import org.apache.isis.core.wrapper.internal.util.Util;

import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.Proxy;
import javassist.util.proxy.ProxyFactory;

public class ProxyCreator {

    /**
     * Lazily constructed cache.
     */
    private final Map<Class, ProxyFactory> proxyFactoryByClass;

    public ProxyCreator() {
        this(new MapMaker().weakKeys().concurrencyLevel(10).<Class, ProxyFactory>makeMap());
    }

    public ProxyCreator(Map<Class, ProxyFactory> proxyFactoryByClass) {
        this.proxyFactoryByClass = proxyFactoryByClass;
    }

    @SuppressWarnings("unchecked")
    public <T> T instantiateProxy(final DelegatingInvocationHandler<T> handler) {

        final T toProxy = handler.getDelegate();

        final Class<T> clazz = (Class<T>) toProxy.getClass();

        if (clazz.isInterface()) {
            return Util.createInstance(clazz, handler, WrapperObject.class);
        } else {
            final ProxyFactory proxyFactory = proxyFactoryFor(clazz);

            final Class<T> enhancedClass = proxyFactory.createClass();
            final Proxy proxy = (Proxy) Util.createInstance(enhancedClass);

            proxy.setHandler(new MethodHandler() {
                @Override
                public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
                    return handler.invoke(self, thisMethod, args);
                }
            });

            return (T) proxy;
        }
    }

    private <T> ProxyFactory proxyFactoryFor(final Class<T> toProxyClass) {
        ProxyFactory proxyFactory = proxyFactoryByClass.get(toProxyClass);
        if(proxyFactory == null) {
            proxyFactory = createProxyFactoryFor(toProxyClass);
            proxyFactoryByClass.put(toProxyClass, proxyFactory);
        }
        return proxyFactory;
    }

    private <T> ProxyFactory createProxyFactoryFor(final Class<T> toProxyClass) {

        final ProxyFactory proxyFactory = new ProxyFactory();

        proxyFactory.setSuperclass(toProxyClass);
        final Class[] types = ArrayExtensions.combine(
                toProxyClass.getInterfaces(),
                new Class<?>[] { JavassistEnhanced.class, WrappingObject.class });
        proxyFactory.setInterfaces(types);

        proxyFactory.setFilter(new MethodFilter() {
            @Override
            public boolean isHandled(final Method m) {
                // ignore finalize()
                return !m.getName().equals("finalize") || m.isBridge();
            }
        });

        // this is the default, I believe
        // calling it here only because I know that it will throw an exception if the code were
        // in the future changed such that caching is invalid
        // (ie fail fast if future change could introduce a bug)
        proxyFactory.setUseCache(true);

        return proxyFactory;
    }

}
