/*
 * Copyright 2018 The Error Prone Authors.
 *
 * 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.errorprone.bugpatterns.time;

import static com.google.errorprone.BugPattern.ProvidesFix.REQUIRES_HUMAN_ATTENTION;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.matchers.Matchers.instanceMethod;

import com.google.common.base.Enums;
import com.google.common.base.Optional;
import com.google.common.primitives.Longs;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import java.util.concurrent.TimeUnit;

/** Check for problematic or suspicious TimeUnit conversion calls. */
@BugPattern(
    name = "TimeUnitConversionChecker",
    summary =
        "This TimeUnit conversion looks buggy: converting from a smaller unit to a larger unit "
            + "(and passing a constant), converting to/from the same TimeUnit, or converting "
            + "TimeUnits where the result is statically known to be 0 or 1 are all buggy patterns.",
    explanation =
        "This checker flags potential problems with TimeUnit conversions: "
            + "1) conversions that are statically known to be equal to 0 or 1; "
            + "2) conversions that are converting from a given unit back to the same unit; "
            + "3) conversions that are converting from a smaller unit to a larger unit and passing "
            + "a constant value",
    severity = WARNING,
    providesFix = REQUIRES_HUMAN_ATTENTION)
public final class TimeUnitConversionChecker extends BugChecker
    implements MethodInvocationTreeMatcher {

  // TODO(kak): We should probably also extend this to recognize TimeUnit.convertTo() invocations

  private static final Matcher<ExpressionTree> MATCHER =
      instanceMethod()
          .onExactClass("java.util.concurrent.TimeUnit")
          .namedAnyOf(
              "toDays", "toHours", "toMinutes", "toSeconds", "toMillis", "toMicros", "toNanos");

  @Override
  public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
    if (!MATCHER.matches(tree, state)) {
      return Description.NO_MATCH;
    }

    String timeUnitName =
        ASTHelpers.getSymbol(ASTHelpers.getReceiver(tree)).getSimpleName().toString();
    Optional<TimeUnit> receiver = Enums.getIfPresent(TimeUnit.class, timeUnitName);
    if (!receiver.isPresent()) {
      return Description.NO_MATCH;
    }

    String methodName = ASTHelpers.getSymbol(tree).getSimpleName().toString();
    TimeUnit convertTo = methodNameToTimeUnit(methodName);
    ExpressionTree arg0 = tree.getArguments().get(0);

    // if we have a constant and can Long-parse it...
    Long constant = Longs.tryParse(String.valueOf(arg0));
    if (constant != null) {
      long converted = invokeConversion(receiver.get(), methodName, constant);

      // ... and the conversion results in 0 or 1, just inline it!
      if (converted == 0 || converted == 1 || constant == converted) {
        SuggestedFix fix = replaceTreeWith(tree, convertTo, converted + "L");
        return describeMatch(tree, fix);
      }

      // otherwise we have a suspect case: SMALLER_UNIT.toLargerUnit(constantValue)
      // because: "people usually don't like to have constants like 60_000_000 and use"
      //          "libraries to turn them into smaller numbers"
      if (receiver.get().compareTo(convertTo) < 0) {
        // We can't suggest a replacement here, so we just have to error out.
        return describeMatch(tree);
      }
    }

    // if we're trying to convert the unit to itself, just return the arg
    if (receiver.get().equals(convertTo)) {
      SuggestedFix fix = replaceTreeWith(tree, convertTo, arg0.toString());
      return describeMatch(tree, fix);
    }
    return Description.NO_MATCH;
  }

  private static SuggestedFix replaceTreeWith(
      MethodInvocationTree tree, TimeUnit units, String replacement) {
    return SuggestedFix.builder()
        .postfixWith(tree, " /* " + units.toString().toLowerCase() + " */")
        .replace(tree, replacement)
        .build();
  }

  private static long invokeConversion(TimeUnit timeUnit, String methodName, long duration) {
    switch (methodName) {
      case "toDays":
        return timeUnit.toDays(duration);
      case "toHours":
        return timeUnit.toHours(duration);
      case "toMinutes":
        return timeUnit.toMinutes(duration);
      case "toSeconds":
        return timeUnit.toSeconds(duration);
      case "toMillis":
        return timeUnit.toMillis(duration);
      case "toMicros":
        return timeUnit.toMicros(duration);
      case "toNanos":
        return timeUnit.toNanos(duration);
      default:
        throw new IllegalArgumentException();
    }
  }

  private static TimeUnit methodNameToTimeUnit(String methodName) {
    switch (methodName) {
      case "toDays":
        return TimeUnit.DAYS;
      case "toHours":
        return TimeUnit.HOURS;
      case "toMinutes":
        return TimeUnit.MINUTES;
      case "toSeconds":
        return TimeUnit.SECONDS;
      case "toMillis":
        return TimeUnit.MILLISECONDS;
      case "toMicros":
        return TimeUnit.MICROSECONDS;
      case "toNanos":
        return TimeUnit.NANOSECONDS;
      default:
        throw new IllegalArgumentException();
    }
  }
}

@@ -1495,24 +1495,57 @@
 cationTree;%0A
+import com.sun.source.tree.Tree;%0A
 import java.
@@ -3134,87 +3134,466 @@
     
-String timeUnitName =%0A        ASTHelpers.getSymbol(ASTHelpers.getReceiver(tree)
+Tree receiverOfConversion = ASTHelpers.getReceiver(tree);%0A    if (receiverOfConversion == null) %7B%0A      // Usage inside TimeUnit itself, no changes we can make here.%0A      return Description.NO_MATCH;%0A    %7D%0A    // This trips up on code like:%0A    // TimeUnit SECONDS = TimeUnit.MINUTES;%0A    // long about2500 = SECONDS.toSeconds(42);%0A    // but... I think that's bad enough to ignore here :)%0A    String timeUnitName = ASTHelpers.getSymbol(receiverOfConversion
 ).ge

Fix crash in TimeUnitConversionChecker when run directly on java.util.concurrent.TimeUnit