/*
 * 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.calcite.rel.metadata;

import org.apache.calcite.rel.RelNode;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * A MetadataHandlerProvider built on a RelMetadataProvider.
 *
 * Uses proxies to call the underlying metadata provider.
 */
public class ProxyingMetadataHandlerProvider implements MetadataHandlerProvider {

  private final RelMetadataProvider provider;

  /**
   * Create a proxying handler provider.
   * @param provider The provider this will operate against.
   */
  public ProxyingMetadataHandlerProvider(RelMetadataProvider provider) {
    this.provider = provider;
  }

  @SuppressWarnings("deprecation")
  @Override public <MH extends MetadataHandler<?>> MH handler(Class<MH> handlerClass) {

    Type[] types = handlerClass.getGenericInterfaces();
    if (types.length != 1 || !(types[0] instanceof ParameterizedType)) {
      throw new UnsupportedOperationException("Unexpected failure. " + handlerClass);
    }

    ParameterizedType pType = (ParameterizedType) types[0];
    if (pType.getRawType() != MetadataHandler.class) {
      throw new UnsupportedOperationException("Unexpected failure. " + handlerClass);
    }
    Class<?> metadataType = (Class<?>) pType.getActualTypeArguments()[0];
    final Field field;
    final MetadataDef<?> def;
    try {
      field = metadataType.getField("DEF");
      def = Objects.requireNonNull((MetadataDef<?>) field.get(null),
          () -> "Unexpected failure. " + handlerClass);
    } catch (NoSuchFieldException | IllegalAccessException e) {
      throw new RuntimeException(e);
    }

    List<Method> methods = def.methods;
    Map<String, Method> methodMap = methods.stream()
        .collect(Collectors.toMap(Method::getName, f -> f));
    InvocationHandler handler = (proxy, method, args) -> {
      Method metadataMethod = Objects.requireNonNull(methodMap.get(method.getName()),
          () -> "Not supported: " + method);
      RelNode rel = Objects.requireNonNull((RelNode) args[0], "rel must be non null");
      RelMetadataQuery mq = Objects.requireNonNull((RelMetadataQuery) args[1],
          "mq must be non null");

      // using deprecated RelMetadataProvider method here as the non-deprecated methods completely
      // sidestep the purpose of RelMetadataProvider reflection-based functionality.
      UnboundMetadata metadata = provider.apply(
          (Class<? extends RelNode>) rel.getClass(),
          (Class<? extends Metadata>) metadataType);

      if (metadata == null) {
        Method handlerMethod = Arrays.stream(handlerClass.getMethods())
            .filter(m -> m.getName().equals(metadataMethod.getName()))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("Unable to find method."));
        throw new IllegalArgumentException(
            String.format(Locale.ROOT, "No handler for "
            + "method [%s] applied to argument of type [%s]; we recommend you create a "
            + "catch-all (RelNode) handler", handlerMethod, rel.getClass()));
      }
      Metadata bound = Objects.requireNonNull(metadata, "expected defined metadata")
          .bind(rel, mq);

      Object[] abbreviatedArgs = new Object[args.length - 2];
      System.arraycopy(args, 2, abbreviatedArgs, 0, abbreviatedArgs.length);
      try {
        return metadataMethod.invoke(bound, abbreviatedArgs);
      } catch (InvocationTargetException ex) {
        if (ex.getCause() instanceof CyclicMetadataException) {
          throw (CyclicMetadataException) ex.getCause();
        }

        throw new RuntimeException(ex.getCause());
      }
    };

    return (MH) Proxy.newProxyInstance(
        handlerClass.getClassLoader(),
        new Class[]{handlerClass},
        handler);
  }

}
