/*
 * 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.activemq.transport.amqp.client.util;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;

import org.apache.qpid.proton.engine.Connection;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Link;
import org.apache.qpid.proton.engine.Receiver;
import org.apache.qpid.proton.engine.Sasl;
import org.apache.qpid.proton.engine.Sender;
import org.apache.qpid.proton.engine.Session;
import org.apache.qpid.proton.engine.Transport;

/**
 * Utility that creates proxy objects for the Proton objects which won't allow any mutating
 * operations to be applied so that the test code does not interact with the proton engine
 * outside the client serialization thread.
 */
public final class UnmodifiableProxy {

   private static ArrayList<String> blacklist = new ArrayList<>();

   // These methods are mutating but don't take an arguments so they
   // aren't automatically filtered out. We will have to keep an eye
   // on proton API in the future and modify this list as it evolves.
   static {
      blacklist.add("close");
      blacklist.add("free");
      blacklist.add("open");
      blacklist.add("sasl");
      blacklist.add("session");
      blacklist.add("close_head");
      blacklist.add("close_tail");
      blacklist.add("outputConsumed");
      blacklist.add("process");
      blacklist.add("processInput");
      blacklist.add("unbind");
      blacklist.add("settle");
      blacklist.add("clear");
      blacklist.add("detach");
      blacklist.add("abort");
   }

   private UnmodifiableProxy() {
   }

   public static Transport transportProxy(final Transport target) {
      Transport wrap = wrap(Transport.class, target);
      return wrap;
   }

   public static Sasl saslProxy(final Sasl target) {
      return wrap(Sasl.class, target);
   }

   public static Connection connectionProxy(final Connection target) {
      return wrap(Connection.class, target);
   }

   public static Session sessionProxy(final Session target) {
      return wrap(Session.class, target);
   }

   public static Delivery deliveryProxy(final Delivery target) {
      return wrap(Delivery.class, target);
   }

   public static Link linkProxy(final Link target) {
      return wrap(Link.class, target);
   }

   public static Receiver receiverProxy(final Receiver target) {
      return wrap(Receiver.class, target);
   }

   public static Sender senderProxy(final Sender target) {
      return wrap(Sender.class, target);
   }

   private static boolean isProtonType(Class<?> clazz) {
      String packageName = clazz.getPackage().getName();

      if (packageName.startsWith("org.apache.qpid.proton.")) {
         return true;
      }

      return false;
   }

   private static <T> T wrap(Class<T> type, final Object target) {
      return type.cast(java.lang.reflect.Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, new InvocationHandler() {
         @Override
         public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            if ("toString".equals(method.getName()) && method.getParameterTypes().length == 0) {
               return "Unmodifiable proxy -> (" + method.invoke(target, objects) + ")";
            }

            // Don't let methods that mutate be invoked.
            if (method.getParameterTypes().length > 0) {
               throw new UnsupportedOperationException("Cannot mutate outside the Client work thread");
            }

            if (blacklist.contains(method.getName())) {
               throw new UnsupportedOperationException("Cannot mutate outside the Client work thread");
            }

            Class<?> returnType = method.getReturnType();

            try {
               Object result = method.invoke(target, objects);
               if (result == null) {
                  return null;
               }

               if (returnType.isPrimitive() || returnType.isArray() || Object.class.equals(returnType)) {
                  // Skip any other checks
               } else if (returnType.isAssignableFrom(ByteBuffer.class)) {
                  // Buffers are modifiable but we can just return null to indicate
                  // there's nothing there to access.
                  result = null;
               } else if (returnType.isAssignableFrom(Map.class)) {
                  // Prevent return of modifiable maps
                  result = Collections.unmodifiableMap((Map<?, ?>) result);
               } else if (isProtonType(returnType) && returnType.isInterface()) {

                  // Can't handle the crazy Source / Target types yet as there's two
                  // different types for Source and Target the result can't be cast to
                  // the one people actually want to use.
                  if (!returnType.getName().equals("org.apache.qpid.proton.amqp.transport.Source")
                     && !returnType.getName().equals("org.apache.qpid.proton.amqp.messaging.Source")
                     && !returnType.getName().equals("org.apache.qpid.proton.amqp.transport.Target")
                     && !returnType.getName().equals("org.apache.qpid.proton.amqp.messaging.Target")) {

                     result = wrap(returnType, result);
                  }
               }

               return result;
            } catch (InvocationTargetException e) {
               throw e.getTargetException();
            }
         }
      }));
   }
}
