// Copyright 2026 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.common.options;

import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.annotation.Nullable;

/**
 * Knowledge about how the option parser should get type information about options and set their
 * values that are defined by methods.
 */
public class MethodOptionDefinition extends OptionDefinition {

  /** Returns an {@code MethodOptionDefinition} for the given method. */
  @Nullable
  static MethodOptionDefinition from(Method method) {
    Option annotation = method.getAnnotation(Option.class);
    if (annotation == null) {
      return null;
    }
    return new MethodOptionDefinition(method, annotation);
  }

  /** Returns the generated implementation class for the given options class. */
  public static Class<? extends OptionsBase> getImplClass(
      Class<? extends OptionsBase> optionsClass) {
    Verify.verify(optionsClass.isAnnotationPresent(OptionsClass.class));
    String packageName = optionsClass.getPackage().getName();
    String className = optionsClass.getName().substring(packageName.length() + 1);
    String implClassName = packageName + "." + className.replace('$', '_') + "Impl";
    try {
      return Class.forName(implClassName, true, optionsClass.getClassLoader())
          .asSubclass(OptionsBase.class);
    } catch (ClassNotFoundException e) {
      throw new IllegalStateException(e); // The annotation processor should have been run
    }
  }

  /**
   * Returns an {@code MethodOptionDefinition} for the given method name in the given class.
   *
   * <p>This is intended to be used by the generated implementation classes.
   */
  public static MethodOptionDefinition get(
      Class<? extends OptionsBase> optionsClass, String methodName) {
    try {
      if (!optionsClass.isAnnotationPresent(OptionsClass.class)) {
        throw new IllegalStateException(optionsClass + " is not an @OptionsClass");
      }
      Method method = optionsClass.getMethod(methodName);
      Option result = method.getAnnotation(Option.class);
      if (result == null) {
        throw new IllegalStateException(methodName + " is not an @Option");
      }
      return new MethodOptionDefinition(method, result);
    } catch (NoSuchMethodException e) {
      throw new IllegalStateException(e);
    }
  }

  private final Method method;
  private final String fieldName;

  // This is needed because options classes sometimes inherit from each other. In this case, the
  // field storing the value can be on multiple classes (e.g. if FooOptions inherits from
  // BarOptions, the implementation of both will have fields corresponding to the options in
  // BarOptions)
  private final ConcurrentMap<Class<? extends OptionsBase>, Field> fieldCache =
      new ConcurrentHashMap<>();

  private MethodOptionDefinition(Method method, Option optionAnnotation) {
    super(optionAnnotation);
    this.method = method;
    String methodName = method.getName();
    Verify.verify(methodName.startsWith("get")); // Enforced by the annotation processor
    this.fieldName = methodName.substring(3, 4).toLowerCase(Locale.ROOT) + methodName.substring(4);
  }

  @Override
  public <C extends OptionsBase> Class<? extends C> getDeclaringClass(Class<C> baseClass) {
    // The implementation class is not technically the "declaring" class, but it's the one that is
    // referenced everywhere, so this is what needs to be returned. In particular, that's the one
    // that needs to be passed to getOptions().
    Class<?> methodClass = method.getDeclaringClass();
    Preconditions.checkArgument(baseClass.isAssignableFrom(methodClass));
    @SuppressWarnings("unchecked") // This should be safe based on the previous check.
    Class<? extends C> castClass = (Class<? extends C>) methodClass;
    return castClass;
  }

  @Override
  public Object getRawValue(OptionsBase optionsBase) {
    try {
      return method.invoke(optionsBase);
    } catch (ReflectiveOperationException e) {
      throw new IllegalStateException(e);
    }
  }

  private Field getField(Class<? extends OptionsBase> optionsClass) {
    try {
      Class<? extends OptionsBase> implClass =
          getImplClass(optionsClass.asSubclass(OptionsBase.class));
      Field f = implClass.getDeclaredField(fieldName);
      f.setAccessible(true);
      return f;
    } catch (ReflectiveOperationException e) {
      throw new IllegalStateException(
          "Could not find field " + fieldName + " in implementation of " + optionsClass, e);
    }
  }

  @Override
  public void setValue(OptionsBase optionsBase, Object value) {
    Field field =
        fieldCache.computeIfAbsent(
            optionsBase.getOptionsClass().asSubclass(OptionsBase.class), this::getField);
    try {
      field.set(optionsBase, value);
    } catch (ReflectiveOperationException e) {
      throw new IllegalStateException(e);
    }
  }

  @Override
  public boolean isDeprecated() {
    return method.isAnnotationPresent(Deprecated.class);
  }

  @Override
  public Class<?> getType() {
    return method.getReturnType();
  }

  @Override
  protected Type getSingularType() {
    return method.getGenericReturnType();
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof MethodOptionDefinition that)) {
      return false;
    }
    return this.method.equals(that.method);
  }

  @Override
  public int hashCode() {
    return method.hashCode();
  }

  @Override
  public String getMemberName() {
    return method.getName();
  }

  @Override
  public String toString() {
    return String.format("option '--%s'", getOptionName());
  }
}
