// Copyright 2018 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.lib.worker;

import com.google.common.annotations.VisibleForTesting;
import com.google.devtools.build.lib.actions.UserExecException;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.Worker.Code;
import com.google.devtools.build.lib.vfs.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;

/**
 * A manager to instantiate and destroy multiplexers. There should only be one {@code
 * WorkerMultiplexer} corresponding to workers with the same {@code WorkerKey}. If the {@code
 * WorkerMultiplexer} has been constructed, other workers should point to the same one.
 */
public class WorkerMultiplexerManager {
  /**
   * A map from the hash of {@code WorkerKey} objects to the corresponding information about the
   * multiplexer instance.
   */
  private static final Map<WorkerKey, InstanceInfo> multiplexerInstance = new HashMap<>();

  private WorkerMultiplexerManager() {}

  /**
   * A counter used to provide unique IDs across sandboxed multiplexer instances. It is used in
   * determining the workdir for the multiplexer process. This is analogous to the {@code
   * pidCounter} in {@code WorkerFactory}. It is ok to use an {@code AtomicInteger} here for the
   * same reasons as it is there: the counter is only incremented when spawning a new multiplexer,
   * so even in the worst case of workers quitting after each action it shouldn't overflow.
   */
  private static final AtomicInteger multiplexerIdCounter = new AtomicInteger(1);

  /**
   * Returns a {@code WorkerMultiplexer} instance to {@code WorkerProxy}. {@code WorkerProxy}
   * objects with the same {@code WorkerKey} talk to the same {@code WorkerMultiplexer}. Also,
   * record how many {@code WorkerProxy} objects are talking to this {@code WorkerMultiplexer}.
   */
  public static synchronized WorkerMultiplexer getInstance(WorkerKey key, Path logFile) {
    InstanceInfo instanceInfo =
        multiplexerInstance.computeIfAbsent(
            key,
            k ->
                new InstanceInfo(
                    new WorkerMultiplexer(logFile, k, multiplexerIdCounter.getAndIncrement())));
    instanceInfo.increaseRefCount();
    return instanceInfo.getWorkerMultiplexer();
  }

  static void beforeCommand(Reporter reporter) {
    setReporter(reporter);
  }

  static void afterCommand() {
    setReporter(null);
  }

  /**
   * Sets the reporter for all existing multiplexer instances. This allows reporting problems
   * encountered while fetching an instance, e.g. during WorkerProxy validation.
   */
  private static synchronized void setReporter(@Nullable EventHandler reporter) {
    for (InstanceInfo m : multiplexerInstance.values()) {
      m.workerMultiplexer.setReporter(reporter);
    }
  }

  /** Removes a {@code WorkerProxy} instance and reference count since it is no longer in use. */
  public static synchronized void removeInstance(WorkerKey key) throws UserExecException {
    InstanceInfo instanceInfo = multiplexerInstance.get(key);
    if (instanceInfo == null) {
      throw createUserExecException(
          String.format(
              "Attempting to remove non-existent %s multiplexer instance.", key.getMnemonic()),
          Code.MULTIPLEXER_INSTANCE_REMOVAL_FAILURE);
    }
    instanceInfo.decreaseRefCount();
    if (instanceInfo.getRefCount() == 0) {
      instanceInfo.getWorkerMultiplexer().destroyMultiplexer();
      multiplexerInstance.remove(key);
    }
  }

  @VisibleForTesting
  static WorkerMultiplexer getMultiplexer(WorkerKey key) throws UserExecException {
    InstanceInfo instanceInfo = multiplexerInstance.get(key);
    if (instanceInfo == null) {
      throw createUserExecException(
          "Accessing non-existent multiplexer instance.", Code.MULTIPLEXER_DOES_NOT_EXIST);
    }
    return instanceInfo.getWorkerMultiplexer();
  }

  @VisibleForTesting
  static Integer getRefCount(WorkerKey key) throws UserExecException {
    InstanceInfo instanceInfo = multiplexerInstance.get(key);
    if (instanceInfo == null) {
      throw createUserExecException(
          "Accessing non-existent multiplexer instance.", Code.MULTIPLEXER_DOES_NOT_EXIST);
    }
    return instanceInfo.getRefCount();
  }

  @VisibleForTesting
  static Integer getInstanceCount() {
    return multiplexerInstance.keySet().size();
  }

  private static UserExecException createUserExecException(String message, Code detailedCode) {
    return new UserExecException(
        FailureDetail.newBuilder()
            .setMessage(message)
            .setWorker(FailureDetails.Worker.newBuilder().setCode(detailedCode))
            .build());
  }

  /** Contains the WorkerMultiplexer instance and reference count. */
  static class InstanceInfo {
    private final WorkerMultiplexer workerMultiplexer;
    private Integer refCount;

    public InstanceInfo(WorkerMultiplexer workerMultiplexer) {
      this.workerMultiplexer = workerMultiplexer;
      this.refCount = 0;
    }

    public void increaseRefCount() {
      refCount = refCount + 1;
    }

    public void decreaseRefCount() {
      refCount = refCount - 1;
    }

    public WorkerMultiplexer getWorkerMultiplexer() {
      return workerMultiplexer;
    }

    public Integer getRefCount() {
      return refCount;
    }
  }

  /** Resets the instances. For testing only. */
  @VisibleForTesting
  static void resetForTesting() {
    for (InstanceInfo i : multiplexerInstance.values()) {
      i.workerMultiplexer.destroyMultiplexer();
    }
    multiplexerInstance.clear();
  }

  /** Injects a given WorkerMultiplexer into the instance map with refcount 0. For testing only. */
  @VisibleForTesting
  static synchronized void injectForTesting(WorkerKey key, WorkerMultiplexer multiplexer) {
    multiplexerInstance.put(key, new InstanceInfo(multiplexer));
  }
}
