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

import org.checkerframework.checker.initialization.qual.UnderInitialization;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.PolyNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
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.text.DateFormat;
import java.text.Format;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;

import static java.lang.reflect.Modifier.isStatic;

import static org.apache.calcite.linq4j.Nullness.castNonNull;

import static java.util.Objects.requireNonNull;
import static org.apache.calcite.util.ReflectUtil.isStatic;

/**
 * Defining wrapper classes around resources that allow the compiler to check
 * whether the resources exist, and that uses of resources have the appropriate
 * number and types of arguments to match the message.
 */
public class Resources {
  private static final ThreadLocal<@Nullable Locale> MAP_THREAD_TO_LOCALE =
      new ThreadLocal<>();

  private Resources() {}

  /** Returns the preferred locale of the current thread, or
   * the default locale if the current thread has not called
   * {@link #setThreadLocale}.
   *
   * @return Locale */
  protected static Locale getThreadOrDefaultLocale() {
    Locale locale = getThreadLocale();
    if (locale == null) {
      return Locale.getDefault();
    } else {
      return locale;
    }
  }

  /** Sets the locale for the current thread.
   *
   * @param locale Locale */
  public static void setThreadLocale(Locale locale) {
    MAP_THREAD_TO_LOCALE.set(locale);
  }

  /** Returns the preferred locale of the current thread, or null if the
   * thread has not called {@link #setThreadLocale}.
   *
   * @return Locale */
  public static @Nullable Locale getThreadLocale() {
    return MAP_THREAD_TO_LOCALE.get();
  }

  /** Creates an instance of the resource object, using the class's name as
   * the name of the resource file.
   *
   * @see #create(String, Class)
   *
   * @param <T> Resource type
   * @param clazz Interface that contains a method for each resource
   * @return Instance of the interface that can be used to instantiate
   * resources
   */
  public static <T> T create(Class<T> clazz) {
    return create(clazz.getCanonicalName(), clazz);
  }

  /** Creates an instance of the resource object.
   *
   * <p>The resource interface has methods that return {@link Inst} and
   * {@link ExInst} values. Each of those methods is basically a factory method.
   *
   * <p>This method creates an instance of that interface backed by a resource
   * bundle, using a dynamic proxy ({@link Proxy}).
   *
   * <p>Suppose that base = "com.example.MyResource" and the current locale is
   * "en_US". A method
   *
   * <blockquote><pre><code>
   *     &#64;BaseMessage("Illegal binary string {0}")
   *     ExInst&lt;IllegalArgumentException&gt; illegalBinaryString(String a0);
   * </code></pre></blockquote>
   *
   * <p>will look up a resource "IllegalBinaryString" from the resource file
   * "com/example/MyResource_en_US.properties", and substitute in the parameter
   * value {@code a0}.
   *
   * <p>The resource in the properties file may or may not be equal to the
   * base message "Illegal binary string {0}". But in the base locale, it
   * probably should be.
   *
   * @param <T> Resource type
   * @param base Base name of the resource.properties file
   * @param clazz Interface that contains a method for each resource
   * @return Instance of the interface that can be used to instantiate
   * resources
   */
  public static <T> T create(@Nullable String base, Class<T> clazz) {
    return create(base, EmptyPropertyAccessor.INSTANCE, clazz);
  }

  /** Creates an instance of the resource object that can access properties
   * but not resources. */
  public static <T> T create(PropertyAccessor accessor, Class<T> clazz) {
    return create(null, accessor, clazz);
  }

  /** Creates an instance of the resource object that can access properties
   * but not resources. */
  public static <T> T create(final Properties properties, Class<T> clazz) {
    return create(null, new PropertiesAccessor(properties), clazz);
  }

  private static <T> T create(final @Nullable String base,
      final PropertyAccessor accessor, Class<T> clazz) {
    //noinspection unchecked
    return (T) Proxy.newProxyInstance(clazz.getClassLoader(),
        new Class[] {clazz},
        new InvocationHandler() {
          final Map<String, Object> cache = new ConcurrentHashMap<>();

          @Override public Object invoke(Object proxy, Method method, @Nullable Object @Nullable [] args)
              throws Throwable {
            if (args == null || args.length == 0) {
              Object o = cache.get(method.getName());
              if (o == null) {
                o = create(method, args);
                cache.put(method.getName(), o);
              }
              return o;
            }
            return create(method, args);
          }

          private Object create(Method method, @Nullable Object @Nullable [] args)
              throws NoSuchMethodException, InstantiationException,
              IllegalAccessException, InvocationTargetException {
            if (method.equals(BuiltinMethod.OBJECT_TO_STRING.method)) {
              return toString();
            }
            final Class<?> returnType = method.getReturnType();
            try {
              if (Inst.class.isAssignableFrom(returnType)) {
                final Constructor<?> constructor =
                    returnType.getConstructor(String.class, Locale.class,
                        Method.class, Object[].class);
                final Locale locale = Resources.getThreadOrDefaultLocale();
                return constructor.newInstance(base, locale, method,
                    args != null ? args : new Object[0]);
              } else {
                final Constructor<?> constructor =
                    returnType.getConstructor(PropertyAccessor.class,
                        Method.class);
                return constructor.newInstance(accessor, method);
              }
            } catch (InvocationTargetException e) {
              final Throwable e2 = e.getTargetException();
              if (e2 instanceof RuntimeException) {
                throw (RuntimeException) e2;
              }
              if (e2 instanceof Error) {
                throw (Error) e2;
              }
              throw e;
            }
          }
        });
  }

  /** Applies all validations to all resource methods in the given
   * resource object.
   *
   * @param o Resource object to validate
   */
  public static void validate(Object o) {
    validate(o, EnumSet.allOf(Validation.class));
  }

  /** Applies the given validations to all resource methods in the given
   * resource object.
   *
   * @param o Resource object to validate
   * @param validations Validations to perform
   */
  public static void validate(Object o, EnumSet<Validation> validations) {
    int count = 0;
    for (Method method : o.getClass().getMethods()) {
      if (!isStatic(method)
          && Inst.class.isAssignableFrom(method.getReturnType())) {
        ++count;
        final Class<?>[] parameterTypes = method.getParameterTypes();
        @Nullable Object[] args = new Object[parameterTypes.length];
        for (int i = 0; i < parameterTypes.length; i++) {
          args[i] = zero(parameterTypes[i]);
        }
        try {
          Inst inst = (Inst) method.invoke(o, args);
          assert inst != null : "got null from " + method;
          inst.validate(validations);
        } catch (IllegalAccessException e) {
          throw new RuntimeException("in " + method, e);
        } catch (InvocationTargetException e) {
          throw new RuntimeException("in " + method, e.getCause());
        }
      }
    }
    if (count == 0 && validations.contains(Validation.AT_LEAST_ONE)) {
      throw new AssertionError("resource object " + o
          + " contains no resources");
    }
  }

  private static @Nullable Object zero(Class<?> clazz) {
    return clazz == String.class ? ""
        : clazz == byte.class ? (byte) 0
        : clazz == char.class ? (char) 0
        : clazz == short.class ? (short) 0
        : clazz == int.class ? 0
        : clazz == long.class ? 0L
        : clazz == float.class ? 0F
        : clazz == double.class ? 0D
        : clazz == boolean.class ? false
        : null;
  }

  /** Returns whether two objects are equal or are both null. */
  private static boolean equal(@Nullable Object o0, @Nullable Object o1) {
    return o0 == o1 || o0 != null && o0.equals(o1);
  }

  /** Element in a resource (either a resource or a property). */
  public static class Element {
    protected final Method method;
    protected final String key;

    @SuppressWarnings("method.invocation.invalid")
    public Element(Method method) {
      this.method = method;
      this.key = deriveKey();
    }

    protected String deriveKey() {
      final Resource resource = method.getAnnotation(Resource.class);
      if (resource != null) {
        return resource.value();
      } else {
        final String name = method.getName();
        return Character.toUpperCase(name.charAt(0)) + name.substring(1);
      }
    }
  }

  /** Resource instance. It contains the resource method (which
   * serves to identify the resource), the locale with which we
   * expect to render the resource, and any arguments. */
  public static class Inst extends Element {
    private final Locale locale;
    protected final String base;
    protected final @Nullable Object[] args;

    public Inst(String base, Locale locale, Method method, @Nullable Object... args) {
      super(method);
      this.base = base;
      this.locale = locale;
      this.args = args;
    }

    @Override public boolean equals(@Nullable Object obj) {
      return this == obj
          || obj != null
          && obj.getClass() == this.getClass()
          && locale == ((Inst) obj).locale
          && method == ((Inst) obj).method
          && Arrays.equals(args, ((Inst) obj).args);
    }

    @Override public int hashCode() {
      return Arrays.asList(locale, method, Arrays.asList(args)).hashCode();
    }

    public ResourceBundle bundle() {
      return ResourceBundle.getBundle(base, locale);
    }

    public Inst localize(Locale locale) {
      return new Inst(base, locale, method, args);
    }

    public void validate(EnumSet<Validation> validations) {
      final ResourceBundle bundle = bundle();
      for (Validation validation : validations) {
        switch (validation) {
        case BUNDLE_HAS_RESOURCE:
          if (!bundle.containsKey(key)) {
            String suggested = null;
            final BaseMessage annotation =
                method.getAnnotation(BaseMessage.class);
            if (annotation != null) {
              final String message = annotation.value();
              suggested = "; add the following line to "
                  + bundle.getBaseBundleName() + ".properties:\n"
                  + key + '=' + message + "\n";
            }
            throw new AssertionError("key '" + key
                + "' not found for resource '" + method.getName()
                + "' in bundle '" + bundle + "'"
                + (suggested == null ? "" : suggested));
          }
          break;
        case MESSAGE_SPECIFIED:
          final BaseMessage annotation1 =
              method.getAnnotation(BaseMessage.class);
          if (annotation1 == null) {
            throw new AssertionError("resource '" + method.getName()
                + "' must specify BaseMessage");
          }
          break;
        case EVEN_QUOTES:
          String message = requireNonNull(
              method.getAnnotation(BaseMessage.class),
              () -> "@BaseMessage is missing for resource '" + method.getName() + "'").value();
          if (countQuotesIn(message) % 2 == 1) {
            throw new AssertionError("resource '" + method.getName()
                + "' should have even number of quotes");
          }
          break;
        case MESSAGE_MATCH:
          final BaseMessage annotation2 =
              method.getAnnotation(BaseMessage.class);
          if (annotation2 != null) {
            final String value = annotation2.value();
            final String value2 = bundle.containsKey(key)
                ? bundle.getString(key)
                : null;
            if (!equal(value, value2)) {
              throw new AssertionError("message for resource '"
                  + method.getName()
                  + "' is different between class and resource file");
            }
          }
          break;
        case ARGUMENT_MATCH:
          String raw = raw();
          MessageFormat format = new MessageFormat(raw);
          final @Nullable Format[] formats = format.getFormatsByArgumentIndex();
          final List<Class> types = new ArrayList<>();
          final Class<?>[] parameterTypes = method.getParameterTypes();
          for (int i = 0; i < formats.length; i++) {
            Format format1 = formats[i];
            Class parameterType = parameterTypes[i];
            final Class<?> e;
            if (format1 instanceof NumberFormat) {
              e = parameterType == short.class
                  || parameterType == int.class
                  || parameterType == long.class
                  || parameterType == float.class
                  || parameterType == double.class
                  || Number.class.isAssignableFrom(parameterType)
                  ? parameterType
                  : Number.class;
            } else if (format1 instanceof DateFormat) {
              e = Date.class;
            } else {
              e = String.class;
            }
            types.add(e);
          }
          final List<Class<?>> parameterTypeList =
              Arrays.asList(parameterTypes);
          if (!types.equals(parameterTypeList)) {
            throw new AssertionError("type mismatch in method '"
                + method.getName() + "' between message format elements "
                + types + " and method parameters " + parameterTypeList);
          }
          break;
        default:
          break;
        }
      }
    }

    private static int countQuotesIn(String message) {
      int count = 0;
      for (int i = 0, n = message.length(); i < n; i++) {
        if (message.charAt(i) == '\'') {
          ++count;
        }
      }
      return count;
    }

    public String str() {
      String message = raw();
      MessageFormat format = new MessageFormat(message);
      format.setLocale(locale);
      return format.format(args);
    }

    public String raw() {
      try {
        return bundle().getString(key);
      } catch (MissingResourceException e) {
        // Resource is not in the bundle. (It is probably missing from the
        // .properties file.) Fall back to the base message.
        return requireNonNull(
            method.getAnnotation(BaseMessage.class),
            () -> "@BaseMessage is missing for resource '" + method.getName() + "'").value();
      }
    }

    public Map<String, String> getProperties() {
      // At present, annotations allow at most one property per resource. We
      // could design new annotations if any resource needed more.
      final Property property = method.getAnnotation(Property.class);
      if (property == null) {
        return Collections.emptyMap();
      } else {
        return Collections.singletonMap(property.name(), property.value());
      }
    }
  }

  /** Sub-class of {@link Inst} that can throw an exception. Requires caused
   * by exception.*/
  public static class ExInstWithCause<T extends Exception> extends Inst {
    public ExInstWithCause(String base, Locale locale, Method method,
        @Nullable Object... args) {
      super(base, locale, method, args);
    }

    @Override public Inst localize(Locale locale) {
      return new ExInstWithCause<T>(base, locale, method, args);
    }

    public T ex(@Nullable Throwable cause) {
      try {
        //noinspection unchecked
        final Class<T> exceptionClass =
            getExceptionClass(method.getGenericReturnType());
        Constructor<T> constructor;
        final String str = str();
        boolean causeInConstructor = false;
        try {
          constructor = exceptionClass.getConstructor(String.class,
              Throwable.class);
          causeInConstructor = true;
        } catch (NoSuchMethodException nsmStringThrowable) {
          try {
            constructor = exceptionClass.getConstructor(String.class);
          } catch (NoSuchMethodException nsmString) {
            // Ignore nsmString to encourage users to have (String,
            // Throwable) constructors.
            throw nsmStringThrowable;
          }
        }
        if (causeInConstructor) {
          return constructor.newInstance(str, cause);
        }
        T ex = constructor.newInstance(str);
        if (cause != null) {
          try {
            ex.initCause(cause);
          } catch (IllegalStateException iae) {
            // Sorry, unable to add cause via constructor and via initCause
          }
        }
        return ex;
      } catch (InstantiationException | IllegalAccessException
          | NoSuchMethodException e) {
        throw new RuntimeException(e);
      } catch (InvocationTargetException e) {
        if (e.getCause() instanceof Error) {
          throw (Error) e.getCause();
        } else if (e.getCause() instanceof RuntimeException) {
          throw (RuntimeException) e.getCause();
        } else {
          throw new RuntimeException(e);
        }
      }
    }

    public static Class getExceptionClass(Type type) {
      // Get exception type from ExInstWithCause<MyException> type parameter.
      // ExInstWithCause might be one of super classes.
      // And, this class may be a parameter-less sub-class of a generic base.
      //
      // NOTE: We used to use
      // com.fasterxml.jackson.databind.type.TypeFactory.findTypeParameters.
      // More powerful, but we can't afford an extra dependency.

      final Type type0 = type;
      for (;;) {
        if (type instanceof ParameterizedType) {
          final Type[] types =
              ((ParameterizedType) type).getActualTypeArguments();
          if (types.length >= 1
              && types[0] instanceof Class
              && Throwable.class.isAssignableFrom((Class) types[0])) {
            return (Class) types[0];
          }
          throw new IllegalStateException(
              "Unable to find superclass ExInstWithCause for " + type);
        }
        if (type instanceof Class) {
          Type superclass = ((Class) type).getGenericSuperclass();
          if (superclass == null) {
            throw new IllegalStateException(
                "Unable to find superclass ExInstWithCause for " + type0);
          }
          type = superclass;
        }
      }
    }

    protected void validateException(Callable<? extends @Nullable Exception> exSupplier) {
      Throwable cause = null;
      try {
        //noinspection ThrowableResultOfMethodCallIgnored
        final Exception ex = exSupplier.call();
        if (ex == null) {
          cause = new NullPointerException();
        }
      } catch (AssertionError | Exception e) {
        // catch(Exception) is required since Callable#call throws Exception.
        // But in practice, e will always be AssertionError or RuntimeException,
        // since exSupplier should be just a ex() call.
        cause = e;
      }
      if (cause != null) {
        throw new AssertionError("error instantiating exception for resource '"
            + method.getName() + "'", cause);
      }
    }

    @Override public void validate(EnumSet<Validation> validations) {
      super.validate(validations);
      if (validations.contains(Validation.CREATE_EXCEPTION)) {
        validateException(() -> ex(new NullPointerException("test")));
      }
    }
  }

  /** Sub-class of {@link Inst} that can throw an exception without caused
   * by. */
  public static class ExInst<T extends Exception> extends ExInstWithCause<T> {
    public ExInst(String base, Locale locale, Method method, Object... args) {
      super(base, locale, method, args);
    }

    public T ex() {
      return ex(null);
    }

    @Override public void validate(EnumSet<Validation> validations) {
      super.validate(validations);
      if (validations.contains(Validation.CREATE_EXCEPTION)) {
        validateException(this::ex);
      }
    }
  }

  /** Property instance. */
  public abstract static class Prop extends Element {
    protected final PropertyAccessor accessor;
    protected final boolean hasDefault;

    protected Prop(PropertyAccessor accessor, Method method) {
      super(method);
      this.accessor = accessor;
      final Default resource = method.getAnnotation(Default.class);
      this.hasDefault = resource != null;
    }

    @RequiresNonNull("method")
    protected final @Nullable Default getDefault(
        @UnderInitialization Prop this
    ) {
      if (hasDefault) {
        return castNonNull(method.getAnnotation(Default.class));
      } else {
        return null;
      }
    }

    public boolean isSet() {
      return accessor.isSet(this);
    }

    public boolean hasDefault() {
      return hasDefault;
    }

    void checkDefault() {
      if (!hasDefault) {
        throw new NoDefaultValueException("Property " + key
            + " has no default value");
      }
    }

    void checkDefault2() {
      if (!hasDefault) {
        throw new NoDefaultValueException("Property " + key
            + " is not set and has no default value");
      }
    }
  }

  /** Integer property instance. */
  public static class IntProp extends Prop {
    private final int defaultValue;

    public IntProp(PropertyAccessor accessor, Method method) {
      super(accessor, method);
      final Default resource = getDefault();
      if (resource != null) {
        defaultValue = Integer.parseInt(resource.value(), 10);
      } else {
        defaultValue = 0;
      }
    }

    /** Returns the value of this integer property. */
    public int get() {
      return accessor.intValue(this);
    }

    /** Returns the value of this integer property, returning the given default
     * value if the property is not set. */
    public int get(int defaultValue) {
      return accessor.intValue(this, defaultValue);
    }

    public int defaultValue() {
      checkDefault();
      return defaultValue;
    }
  }

  /** Boolean property instance. */
  public static class BooleanProp extends Prop {
    private final boolean defaultValue;

    public BooleanProp(PropertyAccessor accessor, Method method) {
      super(accessor, method);
      final Default resource = getDefault();
      if (resource != null) {
        defaultValue = Boolean.parseBoolean(resource.value());
      } else {
        defaultValue = false;
      }
    }

    /** Returns the value of this boolean property. */
    public boolean get() {
      return accessor.booleanValue(this);
    }

    /** Returns the value of this boolean property, returning the given default
     * value if the property is not set. */
    public boolean get(boolean defaultValue) {
      return accessor.booleanValue(this, defaultValue);
    }

    public boolean defaultValue() {
      checkDefault();
      return defaultValue;
    }
  }

  /** Double property instance. */
  public static class DoubleProp extends Prop {
    private final double defaultValue;

    public DoubleProp(PropertyAccessor accessor, Method method) {
      super(accessor, method);
      final Default resource = getDefault();
      if (resource != null) {
        defaultValue = Double.parseDouble(resource.value());
      } else {
        defaultValue = 0d;
      }
    }

    /** Returns the value of this double property. */
    public double get() {
      return accessor.doubleValue(this);
    }

    /** Returns the value of this double property, returning the given default
     * value if the property is not set. */
    public double get(double defaultValue) {
      return accessor.doubleValue(this, defaultValue);
    }

    public double defaultValue() {
      checkDefault();
      return defaultValue;
    }
  }

  /** String property instance. */
  public static class StringProp extends Prop {
    private final @Nullable String defaultValue;

    public StringProp(PropertyAccessor accessor, Method method) {
      super(accessor, method);
      final Default resource = getDefault();
      if (resource != null) {
        defaultValue = resource.value();
      } else {
        defaultValue = null;
      }
    }

    /** Returns the value of this String property. */
    public @Nullable String get() {
      return accessor.stringValue(this);
    }

    /** Returns the value of this String property, returning the given default
     * value if the property is not set.
     *
     * <p>If {@code defaultValue} is not null, never returns null. */
    public @PolyNull String get(@PolyNull String defaultValue) {
      return accessor.stringValue(this, defaultValue);
    }

    public @Nullable String defaultValue() {
      checkDefault();
      return defaultValue;
    }
  }

  /** Thrown when a default value is needed but a property does not have
   * one. */
  public static class NoDefaultValueException extends RuntimeException {
    NoDefaultValueException(String message) {
      super(message);
    }
  }

  /** Means by which a resource can get values of properties, given their
   * name. */
  public interface PropertyAccessor {
    boolean isSet(Prop p);
    int intValue(IntProp p);
    int intValue(IntProp p, int defaultValue);
    @Nullable String stringValue(StringProp p);
    @PolyNull String stringValue(StringProp p, @PolyNull String defaultValue);
    boolean booleanValue(BooleanProp p);
    boolean booleanValue(BooleanProp p, boolean defaultValue);
    double doubleValue(DoubleProp p);
    double doubleValue(DoubleProp p, double defaultValue);
  }

  enum EmptyPropertyAccessor implements PropertyAccessor {
    INSTANCE;

    @Override
    public boolean isSet(Prop p) {
      return false;
    }

    @Override
    public int intValue(IntProp p) {
      return p.defaultValue();
    }

    @Override
    public int intValue(IntProp p, int defaultValue) {
      return defaultValue;
    }

    @Override public @Nullable String stringValue(StringProp p) {
      return p.defaultValue();
    }

    @Override public @PolyNull String stringValue(StringProp p,
        @PolyNull String defaultValue) {
      return defaultValue;
    }

    @Override
    public boolean booleanValue(BooleanProp p) {
      return p.defaultValue();
    }

    @Override
    public boolean booleanValue(BooleanProp p, boolean defaultValue) {
      return defaultValue;
    }

    @Override
    public double doubleValue(DoubleProp p) {
      return p.defaultValue();
    }

    @Override
    public double doubleValue(DoubleProp p, double defaultValue) {
      return defaultValue;
    }
  }

  /** Types of validation that can be performed on a resource. */
  public enum Validation {
    /** Checks that each method's resource key corresponds to a resource in the
     * bundle. */
    BUNDLE_HAS_RESOURCE,

    /** Checks that there is at least one resource in the bundle. */
    AT_LEAST_ONE,

    /** Checks that the base message annotation is on every resource. */
    MESSAGE_SPECIFIED,

    /** Checks that every message contains even number of quotes. */
    EVEN_QUOTES,

    /** Checks that the base message matches the message in the bundle. */
    MESSAGE_MATCH,

    /** Checks that it is possible to create an exception. */
    CREATE_EXCEPTION,

    /** Checks that the parameters of the method are consistent with the
     * format elements in the base message. */
    ARGUMENT_MATCH,
  }

  /** The message in the default locale. */
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.METHOD)
  public @interface BaseMessage {
    String value();
  }

  /** The name of the property in the resource file. */
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.METHOD)
  public @interface Resource {
    String value();
  }

  /** Property of a resource. */
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.METHOD)
  public @interface Property {
    String name();
    String value();
  }

  /** Default value of a property. */
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.METHOD)
  public @interface Default {
    String value();
  }

  /**
   * <code>ShadowResourceBundle</code> is an abstract base class for
   * {@link ResourceBundle} classes which are backed by a properties file. When
   * the class is created, it loads a properties file with the same name as the
   * class.
   *
   * <p>In the standard scheme (see {@link ResourceBundle}), if
   * you call <code>{@link ResourceBundle#getBundle}("foo.MyResource")</code>,
   * it first looks for a class called <code>foo.MyResource</code>, then
   * looks for a file called <code>foo/MyResource.properties</code>. If it finds
   * the file, it creates a {@link PropertyResourceBundle} and loads the class.
   * The problem is if you want to load the <code>.properties</code> file
   * into a dedicated class; <code>ShadowResourceBundle</code> helps with this
   * case.
   *
   * <p>You should create a class as follows:
   *
   * <blockquote><pre><code>
   * package foo;
   *
   * class MyResource extends ShadowResourceBundle {
   *   public MyResource() throws java.io.IOException {
   *   }
   * }
   * </code></pre></blockquote>
   *
   * <p>Then when you call
   * {@link ResourceBundle#getBundle ResourceBundle.getBundle("foo.MyResource")},
   * it will find the class before the properties file, but still automatically
   * load the properties file based upon the name of the class.
   */
  public abstract static class ShadowResourceBundle extends ResourceBundle {
    private final PropertyResourceBundle bundle;

    /**
     * Creates a <code>ShadowResourceBundle</code>, and reads resources from
     * a <code>.properties</code> file with the same name as the current class.
     * For example, if the class is called <code>foo.MyResource_en_US</code>,
     * reads from <code>foo/MyResource_en_US.properties</code>, then
     * <code>foo/MyResource_en.properties</code>, then
     * <code>foo/MyResource.properties</code>.
     *
     * @throws IOException on error
     */
    protected ShadowResourceBundle() throws IOException {
      super();
      Class clazz = getClass();
      InputStream stream = openPropertiesFile(clazz);
      if (stream == null) {
        throw new IOException("could not open properties file for "
            + getClass());
      }
      MyPropertyResourceBundle previousBundle =
          new MyPropertyResourceBundle(stream);
      bundle = previousBundle;
      stream.close();
      // Now load properties files for parent locales, which we deduce from
      // the names of our super-class, and its super-class.
      while (true) {
        clazz = clazz.getSuperclass();
        if (clazz == null
            || clazz == ShadowResourceBundle.class
            || !ResourceBundle.class.isAssignableFrom(clazz)) {
          break;
        }
        stream = openPropertiesFile(clazz);
        if (stream == null) {
          continue;
        }
        MyPropertyResourceBundle newBundle =
            new MyPropertyResourceBundle(stream);
        stream.close();
        previousBundle.setParentTrojan(newBundle);
        previousBundle = newBundle;
      }
    }

    /**
     * Opens the properties file corresponding to a given class. The code is
     * copied from {@link ResourceBundle}.
     */
    @SuppressWarnings("removal")
    private static @Nullable InputStream openPropertiesFile(Class clazz) {
      final ClassLoader loader = clazz.getClassLoader();
      final String resName = clazz.getName().replace('.', '/') + ".properties";
      return java.security.AccessController.doPrivileged(
          (java.security.PrivilegedAction<@Nullable InputStream>) () -> {
            if (loader != null) {
              return loader.getResourceAsStream(resName);
            } else {
              return ClassLoader.getSystemResourceAsStream(resName);
            }
          });
    }

    @Override
    public Enumeration<String> getKeys() {
      return bundle.getKeys();
    }

    @Override
    protected Object handleGetObject(String key) {
      return bundle.getObject(key);
    }

    /**
     * Returns the instance of the <code>baseName</code> resource bundle
     * for the given locale.
     *
     * <p> This method should be called from a derived class, with the proper
     * casting:
     *
     * <blockquote><pre><code>
     * class MyResource extends ShadowResourceBundle {
     *    ...
     *
     *    /&#42;&#42;
     *      &#42; Retrieves the instance of {&#64;link MyResource} appropriate
     *      &#42; to the given locale.
     *      &#42;&#42;/
     *    public static MyResource instance(Locale locale) {
     *       return (MyResource) instance(
     *           MyResource.class.getName(), locale,
     *           ResourceBundle.getBundle(MyResource.class.getName(), locale));
     *    }
     *    ...
     * }
     * </code></pre></blockquote>
     *
     * @param baseName Base name
     * @param locale Locale
     * @param bundle Resource bundle
     * @return Resource bundle
     */
    protected static ShadowResourceBundle instance(
        String baseName, Locale locale, ResourceBundle bundle) {
      if (bundle instanceof PropertyResourceBundle) {
        throw new ClassCastException(
            "ShadowResourceBundle.instance('" + baseName + "','"
            + locale + "') found "
            + baseName + "_" + locale + ".properties but not "
            + baseName + "_" + locale + ".class");
      }
      return (ShadowResourceBundle) bundle;
    }
  }

  /** Resource bundle based on properties. */
  static class MyPropertyResourceBundle extends PropertyResourceBundle {
    public MyPropertyResourceBundle(InputStream stream) throws IOException {
      super(stream);
    }

    void setParentTrojan(ResourceBundle parent) {
      super.setParent(parent);
    }
  }

  enum BuiltinMethod {
    OBJECT_TO_STRING(Object.class, "toString");

    @SuppressWarnings("ImmutableEnumChecker")
    public final Method method;

    BuiltinMethod(Class clazz, String methodName, Class... argumentTypes) {
      this.method = lookupMethod(clazz, methodName, argumentTypes);
    }

    /**
     * Finds a method of a given name that accepts a given set of arguments.
     * Includes in its search inherited methods and methods with wider argument
     * types.
     *
     * @param clazz Class against which method is invoked
     * @param methodName Name of method
     * @param argumentTypes Types of arguments
     *
     * @return A method with the given name that matches the arguments given
     * @throws RuntimeException if method not found
     */
    public static Method lookupMethod(Class clazz, String methodName,
        Class... argumentTypes) {
      try {
        //noinspection unchecked
        return clazz.getMethod(methodName, argumentTypes);
      } catch (NoSuchMethodException e) {
        throw new RuntimeException(
            "while resolving method '" + methodName
                + Arrays.toString(argumentTypes)
                + "' in class " + clazz, e);
      }
    }
  }

  /** Implementation of {@link PropertyAccessor} that reads from a
   * {@link Properties}. */
  private static class PropertiesAccessor implements PropertyAccessor {
    private final Properties properties;

    PropertiesAccessor(Properties properties) {
      this.properties = properties;
    }

    @Override
    public boolean isSet(Prop p) {
      return properties.containsKey(p.key);
    }

    @Override
    public int intValue(IntProp p) {
      final String s = properties.getProperty(p.key);
      if (s != null) {
        return Integer.parseInt(s, 10);
      }
      p.checkDefault2();
      return p.defaultValue;
    }

    @Override
    public int intValue(IntProp p, int defaultValue) {
      final String s = properties.getProperty(p.key);
      return s == null ? defaultValue : Integer.parseInt(s, 10);
    }

    @Override public @Nullable String stringValue(StringProp p) {
      final String s = properties.getProperty(p.key);
      if (s != null) {
        return s;
      }
      p.checkDefault2();
      return p.defaultValue;
    }

    @Override public @PolyNull String stringValue(StringProp p,
        @PolyNull String defaultValue) {
      final String s = properties.getProperty(p.key);
      return s == null ? defaultValue : s;
    }

    @Override
    public boolean booleanValue(BooleanProp p) {
      final String s = properties.getProperty(p.key);
      if (s != null) {
        return Boolean.parseBoolean(s);
      }
      p.checkDefault2();
      return p.defaultValue;
    }

    @Override
    public boolean booleanValue(BooleanProp p, boolean defaultValue) {
      final String s = properties.getProperty(p.key);
      return s == null ? defaultValue : Boolean.parseBoolean(s);
    }

    @Override
    public double doubleValue(DoubleProp p) {
      final String s = properties.getProperty(p.key);
      if (s != null) {
        return Double.parseDouble(s);
      }
      p.checkDefault2();
      return p.defaultValue;
    }

    @Override
    public double doubleValue(DoubleProp p, double defaultValue) {
      final String s = properties.getProperty(p.key);
      return s == null ? defaultValue : Double.parseDouble(s);
    }
  }
}
