// Copyright 2022 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.build.skyframe.state;

import static com.google.common.base.MoreObjects.toStringHelper;

import com.google.devtools.build.skyframe.SkyFunction.LookupEnvironment;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.SkyframeLookupResult;
import com.google.errorprone.annotations.ForOverride;
import java.util.function.Consumer;

/** Captures information about a lookup requested by a state machine. */
abstract class Lookup implements SkyframeLookupResult.QueryDepCallback {
  private final TaskTreeNode parent;
  final SkyKey key;

  private Lookup(TaskTreeNode parent, SkyKey key) {
    this.parent = parent;
    this.key = key;
  }

  final SkyKey key() {
    return key;
  }

  /**
   * Performs a lookup directly against the environment.
   *
   * <p>This is more efficient than {@link LookupEnvironment#getValuesAndExceptions} when there is
   * only one key at a time.
   *
   * @return true if a value was available or an exception was handled. Note: this is false for
   *     unhandled exceptions.
   */
  abstract boolean doLookup(LookupEnvironment env) throws InterruptedException;

  @Override
  public final void acceptValue(SkyKey unusedKey, SkyValue value) {
    acceptValueInternal(value);
    parent.signalChildDoneAndEnqueueIfReady();
  }

  @ForOverride
  protected abstract void acceptValueInternal(SkyValue value);

  @Override
  public final boolean tryHandleException(SkyKey unusedKey, Exception exception) {
    boolean handled = tryHandleExceptionInternal(exception);
    if (handled) {
      parent.signalChildDoneAndEnqueueIfReady();
    }
    return handled;
  }

  @ForOverride
  protected abstract boolean tryHandleExceptionInternal(Exception exception);

  static final class ConsumerLookup extends Lookup {
    private final Consumer<SkyValue> sink;

    ConsumerLookup(TaskTreeNode parent, SkyKey key, Consumer<SkyValue> sink) {
      super(parent, key);
      this.sink = sink;
    }

    @Override
    boolean doLookup(LookupEnvironment env) throws InterruptedException {
      var value = env.getValue(key);
      if (value == null) {
        return false;
      }
      acceptValue(key, value);
      return true;
    }

    @Override
    protected void acceptValueInternal(SkyValue value) {
      sink.accept(value);
    }

    @Override
    protected boolean tryHandleExceptionInternal(Exception unusedException) {
      return false;
    }
  }

  static final class ValueOrExceptionLookup<E extends Exception> extends Lookup {
    private final Class<E> exceptionClass;
    private final StateMachine.ValueOrExceptionSink<E> sink;

    ValueOrExceptionLookup(
        TaskTreeNode parent,
        SkyKey key,
        Class<E> exceptionClass,
        StateMachine.ValueOrExceptionSink<E> sink) {
      super(parent, key);
      this.exceptionClass = exceptionClass;
      this.sink = sink;
    }

    @Override
    boolean doLookup(LookupEnvironment env) throws InterruptedException {
      SkyValue value;
      try {
        if ((value = env.getValueOrThrow(key(), exceptionClass)) == null) {
          return false;
        }
        acceptValue(key, value);
      } catch (Exception e) {
        if (e instanceof InterruptedException interruptedException) {
          throw interruptedException;
        }
        if (!tryHandleException(key, e)) {
          throw new IllegalArgumentException("Unexpected exception for " + key(), e);
        }
      }
      return true;
    }

    @Override
    protected void acceptValueInternal(SkyValue value) {
      sink.acceptValueOrException(value, /* exception= */ null);
    }

    @Override
    protected boolean tryHandleExceptionInternal(Exception exception) {
      if (exceptionClass.isInstance(exception)) {
        sink.acceptValueOrException(/* value= */ null, exceptionClass.cast(exception));
        return true;
      }
      return false;
    }
  }

  static final class ValueOrException2Lookup<E1 extends Exception, E2 extends Exception>
      extends Lookup {
    private final Class<E1> exceptionClass1;
    private final Class<E2> exceptionClass2;
    private final StateMachine.ValueOrException2Sink<E1, E2> sink;

    ValueOrException2Lookup(
        TaskTreeNode parent,
        SkyKey key,
        Class<E1> exceptionClass1,
        Class<E2> exceptionClass2,
        StateMachine.ValueOrException2Sink<E1, E2> sink) {
      super(parent, key);
      this.exceptionClass1 = exceptionClass1;
      this.exceptionClass2 = exceptionClass2;
      this.sink = sink;
    }

    @Override
    boolean doLookup(LookupEnvironment env) throws InterruptedException {
      SkyValue value;
      try {
        if ((value = env.getValueOrThrow(key(), exceptionClass1, exceptionClass2)) == null) {
          return false;
        }
        acceptValue(key, value);
      } catch (Exception e) {
        if (e instanceof InterruptedException interruptedException) {
          throw interruptedException;
        }
        if (!tryHandleException(key, e)) {
          throw new IllegalArgumentException("Unexpected exception for " + key(), e);
        }
      }
      return true;
    }

    @Override
    protected void acceptValueInternal(SkyValue value) {
      sink.acceptValueOrException2(value, /* e1= */ null, /* e2= */ null);
    }

    @Override
    protected boolean tryHandleExceptionInternal(Exception exception) {
      if (exceptionClass1.isInstance(exception)) {
        sink.acceptValueOrException2(
            /* value= */ null, exceptionClass1.cast(exception), /* e2= */ null);
        return true;
      }
      if (exceptionClass2.isInstance(exception)) {
        sink.acceptValueOrException2(
            /* value= */ null, /* e1= */ null, exceptionClass2.cast(exception));
        return true;
      }
      return false;
    }
  }

  static final class ValueOrException3Lookup<
          E1 extends Exception, E2 extends Exception, E3 extends Exception>
      extends Lookup {
    private final Class<E1> exceptionClass1;
    private final Class<E2> exceptionClass2;
    private final Class<E3> exceptionClass3;
    private final StateMachine.ValueOrException3Sink<E1, E2, E3> sink;

    ValueOrException3Lookup(
        TaskTreeNode parent,
        SkyKey key,
        Class<E1> exceptionClass1,
        Class<E2> exceptionClass2,
        Class<E3> exceptionClass3,
        StateMachine.ValueOrException3Sink<E1, E2, E3> sink) {
      super(parent, key);
      this.exceptionClass1 = exceptionClass1;
      this.exceptionClass2 = exceptionClass2;
      this.exceptionClass3 = exceptionClass3;
      this.sink = sink;
    }

    @Override
    boolean doLookup(LookupEnvironment env) throws InterruptedException {
      SkyValue value;
      try {
        if ((value = env.getValueOrThrow(key(), exceptionClass1, exceptionClass2, exceptionClass3))
            == null) {
          return false;
        }
        acceptValue(key, value);
      } catch (Exception e) {
        if (e instanceof InterruptedException interruptedException) {
          throw interruptedException;
        }
        if (!tryHandleException(key, e)) {
          throw new IllegalArgumentException("Unexpected exception for " + key(), e);
        }
      }
      return true;
    }

    @Override
    protected void acceptValueInternal(SkyValue value) {
      sink.acceptValueOrException3(value, /* e1= */ null, /* e2= */ null, /* e3= */ null);
    }

    @Override
    protected boolean tryHandleExceptionInternal(Exception exception) {
      if (exceptionClass1.isInstance(exception)) {
        sink.acceptValueOrException3(
            /* value= */ null, exceptionClass1.cast(exception), /* e2= */ null, /* e3= */ null);
        return true;
      }
      if (exceptionClass2.isInstance(exception)) {
        sink.acceptValueOrException3(
            /* value= */ null, /* e1= */ null, exceptionClass2.cast(exception), /* e3= */ null);
        return true;
      }
      if (exceptionClass3.isInstance(exception)) {
        sink.acceptValueOrException3(
            /* value= */ null, /* e1= */ null, /* e2= */ null, exceptionClass3.cast(exception));
        return true;
      }
      return false;
    }
  }

  @Override
  public String toString() {
    return toStringHelper(this).add("parent", parent).add("key", key).toString();
  }
}
