/*
 * 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.beam.sdk.util;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.beam.sdk.transforms.Combine.CombineFn;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.PTransform;

/** Helpers for extracting the name of objects and classes. */
public class NameUtils {

  /** Classes may implement this interface to change how names are generated for their instances. */
  public interface NameOverride {
    /** Return the name to use for this instance. */
    String getNameOverride();
  }

  private static final String[] STANDARD_NAME_SUFFIXES = new String[] {"DoFn", "CombineFn", "Fn"};

  /**
   * Pattern to match a non-anonymous inner class. Eg, matches "Foo$Bar", or even "Foo$1$Bar", but
   * not "Foo$1" or "Foo$1$2".
   */
  private static final Pattern NAMED_INNER_CLASS = Pattern.compile(".+\\$(?<INNER>[^0-9].*)");

  private static final String ANONYMOUS_CLASS_REGEX = "\\$[0-9]+\\$";

  private static String approximateSimpleName(Class<?> clazz, boolean dropOuterClassNames) {
    checkArgument(!clazz.isAnonymousClass(), "Attempted to get simple name of anonymous class");
    return approximateSimpleName(clazz.getName(), dropOuterClassNames);
  }

  @VisibleForTesting
  static String approximateSimpleName(String fullName, boolean dropOuterClassNames) {
    String shortName = fullName.substring(fullName.lastIndexOf('.') + 1);

    // Drop common suffixes for each named component.
    String[] names = shortName.split("\\$");
    for (int i = 0; i < names.length; i++) {
      names[i] = simplifyNameComponent(names[i]);
    }
    shortName = Joiner.on('$').join(names);

    if (dropOuterClassNames) {
      // Simplify inner class name by dropping outer class prefixes.
      Matcher m = NAMED_INNER_CLASS.matcher(shortName);
      if (m.matches()) {
        shortName = m.group("INNER");
      }
    } else {
      // Dropping anonymous outer classes
      shortName = shortName.replaceAll(ANONYMOUS_CLASS_REGEX, ".");
      shortName = shortName.replaceAll("\\$", ".");
    }
    return shortName;
  }

  private static String simplifyNameComponent(String name) {
    for (String suffix : STANDARD_NAME_SUFFIXES) {
      if (name.endsWith(suffix) && name.length() > suffix.length()) {
        return name.substring(0, name.length() - suffix.length());
      }
    }
    return name;
  }

  /**
   * As {@link #approximateSimpleName(Object, String)} but returning {@code "Anonymous"} when {@code
   * object} is an instance of anonymous class.
   */
  public static String approximateSimpleName(Object object) {
    return approximateSimpleName(object, "Anonymous");
  }

  /**
   * Returns a simple name describing a class that is being used as a function (eg., a {@link DoFn}
   * or {@link CombineFn}, etc.).
   *
   * <p>Note: this is non-invertible - the name may be simplified to an extent that it cannot be
   * mapped back to the original class.
   *
   * <p>This can be used to generate human-readable names. It removes the package and outer classes
   * from the name, and removes common suffixes.
   *
   * <p>If the object is an instanceof {@link NameOverride}, the result of {@link
   * NameOverride#getNameOverride()} is returned. This allows classes that act as wrappers to
   * override the handling of names by delegating to the objects they wrap.
   *
   * <p>If the class is anonymous, the string {@code anonymousValue} is returned.
   *
   * <p>Examples:
   *
   * <ul>
   *   <li>{@code some.package.Word.SummaryDoFn} becomes "Summary"
   *   <li>{@code another.package.PairingFn} becomes "Pairing"
   * </ul>
   */
  public static String approximateSimpleName(Object object, String anonymousValue) {
    if (object instanceof NameOverride) {
      return ((NameOverride) object).getNameOverride();
    }

    Class<?> clazz;
    if (object instanceof Class) {
      clazz = (Class<?>) object;
    } else {
      clazz = object.getClass();
    }
    if (clazz.isAnonymousClass()) {
      return anonymousValue;
    }

    return approximateSimpleName(clazz, /* dropOuterClassNames */ true);
  }

  /**
   * Returns a name for a PTransform class.
   *
   * <p>This can be used to generate human-readable transform names. It removes the package from the
   * name, and removes common suffixes.
   *
   * <p>It is different than approximateSimpleName:
   *
   * <ul>
   *   <li>1. It keeps the outer classes names.
   *   <li>2. It removes the common transform inner class: "Bound".
   *   <li>3. For classes generated by AutoValue, whose names start with AutoValue_, it delegates to
   *       the (parent) class declared in the user's source code.
   * </ul>
   *
   * <p>Examples:
   *
   * <ul>
   *   <li>{@code some.package.Word.Summary} becomes "Word.Summary"
   *   <li>{@code another.package.Pairing.Bound} becomes "Pairing"
   * </ul>
   */
  public static String approximatePTransformName(Class<?> clazz) {
    checkArgument(PTransform.class.isAssignableFrom(clazz));
    if (clazz.getSimpleName().startsWith("AutoValue_")) {
      return approximatePTransformName(clazz.getSuperclass());
    }
    return approximateSimpleName(clazz, /* dropOuterClassNames */ false)
        .replaceFirst("\\.Bound$", "");
  }
}
