// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.debug.core.model;

import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.chromium.debug.core.ChromiumDebugPlugin;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.IStreamListener;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IStreamMonitor;
import org.eclipse.debug.core.model.IStreamsProxy;
import org.eclipse.debug.core.model.ITerminate;

/**
 * This process corresponds to a Debugger-Chrome connection and its main
 * purpose is to expose connection log (see process console in UI).
 */
public class ConsolePseudoProcess extends PlatformObject implements IProcess {

  private final ILaunch launch;
  private final Retransmitter outputMonitor;
  private final ITerminate connectionTerminate;
  private final String name;
  private Map<String, String> attributes = null;

  private final IStreamsProxy streamsProxy = new IStreamsProxy() {
    public IStreamMonitor getErrorStreamMonitor() {
      return NullStreamMonitor.INSTANCE;
    }
    public IStreamMonitor getOutputStreamMonitor() {
      return outputMonitor;
    }
    public void write(String input) {
      // ignore
    }
  };

  /**
   * Constructs a ConsolePseudoProcess, adding this process to the given launch.
   *
   * @param launch the parent launch of this process
   * @param name the label used for this process
   */
  public ConsolePseudoProcess(ILaunch launch, String name, Retransmitter retransmitter,
      ITerminate connectionTerminate) {
    this.launch = launch;
    this.name = name;
    this.outputMonitor = retransmitter;
    outputMonitor.consolePseudoProcess = this;
    this.connectionTerminate = connectionTerminate;

    this.launch.addProcess(this);
    fireCreationEvent();
  }

  /**
   * @return writer which directs its contents to process console
   */
  public Writer getOutputWriter() {
    return outputMonitor;
  }

  public String getLabel() {
      return name;
  }

  public ILaunch getLaunch() {
      return launch;
  }

  public boolean isTerminated() {
      return connectionTerminate.isTerminated();
  }

  public void terminate() throws DebugException {
    connectionTerminate.terminate();
  }

  public boolean canTerminate() {
    return connectionTerminate.canTerminate();
  }

  public IStreamsProxy getStreamsProxy() {
    return streamsProxy;
  }

  /*
   * We do not expect intensive usage of attributes for this class. In fact, other option was to
   * keep this method no-op.
   */
  public synchronized void setAttribute(String key, String value) {
    if (attributes == null) {
      attributes = new HashMap<String, String>(1);
    }
    String origVal = attributes.get(key);
    if (origVal != null && origVal.equals(value)) {
      return;
    }

    attributes.put(key, value);
    fireChangeEvent();
  }

  /*
   * We do not expect intensive usage of attributes for this class. In fact, other option was to
   * put a stub here.
   */
  public synchronized String getAttribute(String key) {
    if (attributes == null) {
      return null;
    }
    return attributes.get(key);
  }

  public int getExitValue() throws DebugException {
    if (isTerminated()) {
      return 0;
    }
    throw new DebugException(new Status(IStatus.ERROR, ChromiumDebugPlugin.PLUGIN_ID,
        "Process hasn't been terminated yet"));  //$NON-NLS-1$
  }

  private void fireCreationEvent() {
    fireEvent(new DebugEvent(this, DebugEvent.CREATE));
  }

  private void fireEvent(DebugEvent event) {
    DebugPlugin manager = DebugPlugin.getDefault();
    if (manager != null) {
      manager.fireDebugEventSet(new DebugEvent[] { event });
    }
  }

  private void fireTerminateEvent() {
    outputMonitor.flush();
    fireEvent(new DebugEvent(this, DebugEvent.TERMINATE));
  }

  private void fireChangeEvent() {
    fireEvent(new DebugEvent(this, DebugEvent.CHANGE));
  }

  @Override
  public Object getAdapter(Class adapter) {
    if (adapter.equals(IProcess.class)) {
      return this;
    }
    if (adapter.equals(ILaunch.class)) {
      return getLaunch();
    }
    if (adapter.equals(ILaunchConfiguration.class)) {
      return getLaunch().getLaunchConfiguration();
    }
    return super.getAdapter(adapter);
  }


  private static class NullStreamMonitor implements IStreamMonitor {
    public void addListener(IStreamListener listener) {
    }
    public String getContents() {
      return null;
    }
    public void removeListener(IStreamListener listener) {
    }
    static final NullStreamMonitor INSTANCE = new NullStreamMonitor();
  }

  /**
   * Responsible for getting text as {@link Writer} and retransmitting it
   * as {@link IStreamMonitor} to whoever is interested.
   * However in its initial state it only receives signal (the text) and saves it in a buffer.
   * For {@link Retransmitter} to start giving the signal away one should
   * call {@link #startFlushing} method.
   */
  public static class Retransmitter extends Writer implements IStreamMonitor {
    private StringWriter writer = new StringWriter();
    private boolean isFlushing = false;
    private final List<IStreamListener> listeners = new ArrayList<IStreamListener>(2);
    private volatile ConsolePseudoProcess consolePseudoProcess = null;

    public synchronized void addListener(IStreamListener listener) {
      listeners.add(listener);
    }

    public String getContents() {
      return null;
    }

    public synchronized void removeListener(IStreamListener listener) {
      listeners.remove(listener);
    }

    @Override
    public synchronized void flush() {
      if (!isFlushing) {
        return;
      }
      String text = writer.toString();
      int lastLinePos;
      final boolean flushOnlyFullLines = true;
      if (flushOnlyFullLines) {
        int pos = text.lastIndexOf('\n');
        if (pos == -1) {
          // No full line in the buffer.
          return;
        }
        lastLinePos = pos + 1;
      } else {
        lastLinePos = text.length();
      }
      String readyText = text.substring(0, lastLinePos);
      writer = new StringWriter();
      if (lastLinePos != text.length()) {
        String rest = text.substring(lastLinePos);
        writer.append(rest);
      }
      for (IStreamListener listener : listeners) {
        listener.streamAppended(readyText, this);
      }
    }

    @Override
    public synchronized void close() {
      // do nothing
    }

    @Override
    public synchronized void write(char[] cbuf, int off, int len) {
      writer.write(cbuf, off, len);
    }

    public synchronized void startFlushing() {
      isFlushing = true;
      flush();
    }

    public void processClosed() {
      ConsolePseudoProcess consolePseudoProcess0 = this.consolePseudoProcess;
      if (consolePseudoProcess0 != null) {
        consolePseudoProcess0.fireTerminateEvent();
      }
    }
  }
}
