/*
 * 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.geode.internal.process;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.Validate.isTrue;
import static org.apache.commons.lang3.Validate.notNull;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.geode.internal.process.ControlFileWatchdog.ControlRequestHandler;
import org.apache.geode.lang.AttachAPINotFoundException;

/**
 * Controls a process using files.
 *
 * @since GemFire 8.0
 */
class FileProcessController implements ProcessController {

  static final long DEFAULT_STATUS_TIMEOUT_MILLIS = 60 * 1000;

  private final long statusTimeoutMillis;
  private final FileControllerParameters parameters;
  private final int pid;

  /**
   * Constructs an instance for controlling a local process.
   *
   * @param parameters details about the controllable process
   * @param pid process id identifying the process to control
   *
   * @throws IllegalArgumentException if pid is not a positive integer
   */
  FileProcessController(final FileControllerParameters parameters, final int pid) {
    this(parameters, pid, DEFAULT_STATUS_TIMEOUT_MILLIS, MILLISECONDS);
  }

  /**
   * Constructs an instance for controlling a local process.
   *
   * @param parameters details about the controllable process
   * @param pid process id identifying the process to control
   * @param timeout the timeout that operations must complete within
   * @param units the units of the timeout
   *
   * @throws IllegalArgumentException if pid is not a positive integer
   */
  FileProcessController(final FileControllerParameters parameters, final int pid,
      final long timeout, final TimeUnit units) {
    notNull(parameters, "Invalid parameters '" + parameters + "' specified");
    isTrue(pid > 0, "Invalid pid '" + pid + "' specified");
    isTrue(timeout >= 0, "Invalid timeout '" + timeout + "' specified");
    notNull(units, "Invalid units '" + units + "' specified");

    this.pid = pid;
    this.parameters = parameters;
    statusTimeoutMillis = units.toMillis(timeout);
  }

  /**
   * Constructs an instance for controlling a local process.
   *
   * @param parameters details about the controllable process
   */
  FileProcessController(final FileControllerParameters parameters) {
    this(parameters, DEFAULT_STATUS_TIMEOUT_MILLIS, MILLISECONDS);
  }

  /**
   * Constructs an instance for controlling a local process.
   *
   * @param parameters details about the controllable process
   * @param timeout the timeout that operations must complete within
   * @param units the units of the timeout
   */
  FileProcessController(final FileControllerParameters parameters,
      final long timeout, final TimeUnit units) {
    notNull(parameters, "Invalid parameters '" + parameters + "' specified");
    isTrue(timeout >= 0, "Invalid timeout '" + timeout + "' specified");
    notNull(units, "Invalid units '" + units + "' specified");

    this.pid = 0;
    this.parameters = parameters;
    this.statusTimeoutMillis = units.toMillis(timeout);
  }

  @Override
  public int getProcessId() {
    return pid;
  }

  @Override
  public String status()
      throws UnableToControlProcessException, IOException, InterruptedException, TimeoutException {
    return status(parameters.getDirectory(), parameters.getProcessType().getStatusRequestFileName(),
        parameters.getProcessType().getStatusFileName());
  }

  @Override
  public void stop() throws UnableToControlProcessException, IOException {
    stop(parameters.getDirectory(), parameters.getProcessType().getStopRequestFileName());
  }

  @Override
  public void checkPidSupport() {
    throw new AttachAPINotFoundException(
        "The Attach API classes could not be found on the classpath.  Please include JDK tools.jar on the classpath or add the JDK tools.jar to the jre/lib/ext directory.");
  }

  long getStatusTimeoutMillis() {
    return statusTimeoutMillis;
  }

  private void stop(final File workingDir, final String stopRequestFileName) throws IOException {
    File stopRequestFile = new File(workingDir, stopRequestFileName);
    if (!stopRequestFile.exists()) {
      stopRequestFile.createNewFile();
    }
  }

  private String status(final File workingDir, final String statusRequestFileName,
      final String statusFileName) throws IOException, InterruptedException, TimeoutException {
    // monitor for statusFile
    File statusFile = new File(workingDir, statusFileName);
    AtomicReference<String> statusRef = new AtomicReference<>();

    ControlRequestHandler statusHandler = () -> {
      // read the statusFile
      StringBuilder lines = new StringBuilder();
      try (BufferedReader reader = new BufferedReader(new FileReader(statusFile))) {
        reader.lines().forEach(lines::append);
      } finally {
        statusRef.set(lines.toString());
      }
    };

    ControlFileWatchdog statusFileWatchdog =
        new ControlFileWatchdog(workingDir, statusFileName, statusHandler, true);
    statusFileWatchdog.start();

    File statusRequestFile = new File(workingDir, statusRequestFileName);
    if (!statusRequestFile.exists()) {
      statusRequestFile.createNewFile();
    }

    // if timeout invoke stop and then throw TimeoutException
    long start = System.currentTimeMillis();
    while (statusFileWatchdog.isAlive()) {
      Thread.sleep(10);
      if (System.currentTimeMillis() >= start + statusTimeoutMillis) {
        statusFileWatchdog.stop();
        throw new TimeoutException("Timed out waiting for process to create " + statusFile);
      }
    }

    String lines = statusRef.get();
    if (isBlank(lines)) {
      throw new IllegalStateException("Status file '" + statusFile + "' is blank");
    }
    return lines;
  }
}
