/*
 * Copyright 2003-2016 JetBrains s.r.o.
 *
 * Licensed 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 jetbrains.mps.util;

import org.jetbrains.annotations.NotNull;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

/**
 * Support for generated classes with query methods, like QueriesGenerated in generator and actions.
 * Refactored out from {@link QueryMethodGenerated} to facilitate reuse and better lifecycle control.
 * {@implNote} Implementation of the class relies on unique method names.
 * XXX perhaps, shall evolve into facility that would use annotations to find out proper methods
 *
 * @author Artem Tikhomirov
 * @since 3.4
 */
public class QueryMethods {
  private final Class<?> myQueriesClass;
  private final Map<String, Method> myMethods;

  public QueryMethods(@NotNull Class<?> queriesClass) {
    myQueriesClass = queriesClass;
    // to avoid extra synchronization, populate at construction time
    HashMap<String, Method> methods = new HashMap<String, Method>();
    for (Method declaredMethod : myQueriesClass.getDeclaredMethods()) {
      if (!Modifier.isPublic(declaredMethod.getModifiers())) {
        continue;
      }
      String name = declaredMethod.getName();
      declaredMethod.setAccessible(true);
      methods.put(name, declaredMethod);
    }
    myMethods = methods;
  }

  public Method getMethodPrim(String methodName) throws NoSuchMethodException {
    final Method method = myMethods.get(methodName);
    if (method == null) {
      throw new NoSuchMethodException(String.format("couldn't find method '%s' in '%s'", methodName, myQueriesClass.getName()));
    }
    return method;
  }

  public boolean hasMethod(String methodName) {
    return myMethods.containsKey(methodName);
  }

  public <T> QueryMethod<T> getMethod(final String methodName) {
    return new QueryMethod<T>() {
      @Override
      @SuppressWarnings("unchecked")
      public T invoke(Object contextObject) throws IllegalQueryMethodException, InvocationTargetException {
        try {
          final Method method = getMethodPrim(methodName);
          return (T) method.invoke(null, contextObject);
        } catch (IllegalArgumentException | IllegalAccessException | NoSuchMethodException e ) {
          String s = String.format("method invocation error: '%s' in '%s'", methodName, myQueriesClass.getName());
          throw new IllegalQueryMethodException(s, e);
        }
      }
    };
  }

  /**
   * Handy wrapper around {@link Method} to have typed return value and streamlined error message for errors.
   * @param <T>
   */
  public interface QueryMethod<T> {
    /**
     * @param contextObject whatever query method takes as an argument
     * @return whatever query method returns
     * @throws IllegalQueryMethodException if there were troubles accessing the method (missing, wrong arguments, non-accessible, etc).
     * @throws InvocationTargetException if there were exceptions in the user-supplied method implementation code.
     */
    T invoke(Object contextObject) throws IllegalQueryMethodException, InvocationTargetException;
  }

  public static class IllegalQueryMethodException extends Exception {
    public IllegalQueryMethodException(String message, Throwable throwable) {
      super(message, throwable);
    }
  }
}
