/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * 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.intellij.execution.process;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public abstract class ProcessHandler extends UserDataHolderBase {
  private static final Logger LOG = Logger.getInstance("#com.intellij.execution.process.ProcessHandler");
  /**
   * todo: replace with an overridable method [nik]
   *
   * @deprecated
   */
  public static final Key<Boolean> SILENTLY_DESTROY_ON_CLOSE = Key.create("SILENTLY_DESTROY_ON_CLOSE");
  public static final Key<Boolean> TERMINATION_REQUESTED = Key.create("TERMINATION_REQUESTED");

  private final List<ProcessListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();

  private static final int STATE_INITIAL = 0;
  private static final int STATE_RUNNING = 1;
  private static final int STATE_TERMINATING = 2;
  private static final int STATE_TERMINATED = 3;

  private final AtomicInteger myState = new AtomicInteger(STATE_INITIAL);

  private final Semaphore myWaitSemaphore;
  private final ProcessListener myEventMulticaster;
  private final TasksRunner myAfterStartNotifiedRunner;

  @Nullable private volatile Integer myExitCode = null;

  protected ProcessHandler() {
    myEventMulticaster = createEventMulticaster();
    myWaitSemaphore = new Semaphore();
    myWaitSemaphore.down();
    myAfterStartNotifiedRunner = new TasksRunner();
    myListeners.add(myAfterStartNotifiedRunner);
  }

  public void startNotify() {
    if (myState.compareAndSet(STATE_INITIAL, STATE_RUNNING)) {
      myEventMulticaster.startNotified(new ProcessEvent(this));
    }
    else {
      LOG.error("startNotify called already");
    }
  }

  protected abstract void destroyProcessImpl();

  protected abstract void detachProcessImpl();

  public abstract boolean detachIsDefault();

  /**
   * Wait for process execution.
   *
   * @return true if target process has actually ended; false if we stopped watching the process execution and don't know if it has completed.
   */
  public boolean waitFor() {
    try {
      myWaitSemaphore.waitFor();
      return true;
    }
    catch (ProcessCanceledException e) {
      return false;
    }
  }

  public boolean waitFor(long timeoutInMilliseconds) {
    try {
      return myWaitSemaphore.waitFor(timeoutInMilliseconds);
    }
    catch (ProcessCanceledException e) {
      return false;
    }
  }

  public void destroyProcess() {
    myAfterStartNotifiedRunner.execute(new Runnable() {
      @Override
      public void run() {
        if (myState.compareAndSet(STATE_RUNNING, STATE_TERMINATING)) {
          fireProcessWillTerminate(true);
          destroyProcessImpl();
        }
      }
    });
  }

  public void detachProcess() {
    myAfterStartNotifiedRunner.execute(new Runnable() {
      @Override
      public void run() {
        if (myState.compareAndSet(STATE_RUNNING, STATE_TERMINATING)) {
          fireProcessWillTerminate(false);
          detachProcessImpl();
        }
      }
    });
  }

  public boolean isProcessTerminated() {
    return myState.get() == STATE_TERMINATED;
  }

  public boolean isProcessTerminating() {
    return myState.get() == STATE_TERMINATING;
  }

  /**
   * @return exit code if the process has already finished, null otherwise
   */
  @Nullable
  public Integer getExitCode() {
    return myExitCode;
  }

  public void addProcessListener(final ProcessListener listener) {
    myListeners.add(listener);
  }

  public void removeProcessListener(final ProcessListener listener) {
    myListeners.remove(listener);
  }

  protected void notifyProcessDetached() {
    notifyTerminated(0, false);
  }

  protected void notifyProcessTerminated(final int exitCode) {
    notifyTerminated(exitCode, true);
  }

  private void notifyTerminated(final int exitCode, final boolean willBeDestroyed) {
    myAfterStartNotifiedRunner.execute(new Runnable() {
      @Override
      public void run() {
        LOG.assertTrue(isStartNotified(), "Start notify is not called");

        if (myState.compareAndSet(STATE_RUNNING, STATE_TERMINATING)) {
          try {
            fireProcessWillTerminate(willBeDestroyed);
          }
          catch (Throwable e) {
            if (!isCanceledException(e)) {
              LOG.error(e);
            }
          }
        }

        if (myState.compareAndSet(STATE_TERMINATING, STATE_TERMINATED)) {
          try {
            myExitCode = exitCode;
            myEventMulticaster.processTerminated(new ProcessEvent(ProcessHandler.this, exitCode));
          }
          catch (Throwable e) {
            if (!isCanceledException(e)) {
              LOG.error(e);
            }
          }
          finally {
            myWaitSemaphore.up();
          }
        }
      }
    });
  }

  public void notifyTextAvailable(final String text, final Key outputType) {
    final ProcessEvent event = new ProcessEvent(this, text);
    myEventMulticaster.onTextAvailable(event, outputType);
  }

  @Nullable
  public abstract OutputStream getProcessInput();

  private void fireProcessWillTerminate(final boolean willBeDestroyed) {
    LOG.assertTrue(isStartNotified(), "All events should be fired after startNotify is called");
    myEventMulticaster.processWillTerminate(new ProcessEvent(this), willBeDestroyed);
  }

  public boolean isStartNotified() {
    return myState.get() > STATE_INITIAL;
  }

  public boolean isSilentlyDestroyOnClose() {
    return false;
  }

  private ProcessListener createEventMulticaster() {
    final Class<ProcessListener> listenerClass = ProcessListener.class;
    return (ProcessListener)Proxy.newProxyInstance(listenerClass.getClassLoader(), new Class[]{listenerClass}, new InvocationHandler() {
      @Override
      public Object invoke(Object object, Method method, Object[] params) throws Throwable {
        for (ProcessListener listener : myListeners) {
          try {
            method.invoke(listener, params);
          }
          catch (Throwable e) {
            if (!isCanceledException(e)) {
              LOG.error(e);
            }
          }
        }
        return null;
      }
    });
  }

  private static boolean isCanceledException(Throwable e) {
    final boolean value = e instanceof InvocationTargetException && e.getCause() instanceof ProcessCanceledException;
    if (value) {
      LOG.info(e);
    }
    return value;
  }

  private final class TasksRunner extends ProcessAdapter {
    private final List<Runnable> myPendingTasks = new ArrayList<Runnable>();

    @Override
    public void startNotified(ProcessEvent event) {
      removeProcessListener(this);
      // at this point it is guaranteed that nothing will be added to myPendingTasks
      runPendingTasks();
    }

    public void execute(@NotNull Runnable task) {
      if (isStartNotified()) {
        task.run();
      }
      else {
        synchronized (myPendingTasks) {
          myPendingTasks.add(task);
        }
        if (isStartNotified()) {
          runPendingTasks();
        }
      }
    }

    private void runPendingTasks() {
      final Runnable[] tasks;
      synchronized (myPendingTasks) {
        tasks = myPendingTasks.toArray(new Runnable[myPendingTasks.size()]);
        myPendingTasks.clear();
      }
      for (Runnable task : tasks) {
        task.run();
      }
    }
  }
}
