/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * 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.trilead.ssh2;

import com.googlecode.android_scripting.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * A <code>StreamGobbler</code> is an InputStream that uses an internal worker thread to constantly
 * consume input from another InputStream. It uses a buffer to store the consumed data. The buffer
 * size is automatically adjusted, if needed.
 * <p>
 * This class is sometimes very convenient - if you wrap a session's STDOUT and STDERR InputStreams
 * with instances of this class, then you don't have to bother about the shared window of STDOUT and
 * STDERR in the low level SSH-2 protocol, since all arriving data will be immediatelly consumed by
 * the worker threads. Also, as a side effect, the streams will be buffered (e.g., single byte
 * read() operations are faster).
 * <p>
 * Other SSH for Java libraries include this functionality by default in their STDOUT and STDERR
 * InputStream implementations, however, please be aware that this approach has also a downside:
 * <p>
 * If you do not call the StreamGobbler's <code>read()</code> method often enough and the peer is
 * constantly sending huge amounts of data, then you will sooner or later encounter a low memory
 * situation due to the aggregated data (well, it also depends on the Java heap size). Joe Average
 * will like this class anyway - a paranoid programmer would never use such an approach.
 * <p>
 * The term "StreamGobbler" was taken from an article called "When Runtime.exec() won't", see
 * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html.
 *
 * @version $Id: StreamGobbler.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $
 */

public class StreamGobbler extends InputStream {
  class GobblerThread extends Thread {
    @Override
    public void run() {

      while (true) {
        try {
          byte[] saveBuffer = null;

          int avail = is.read(buffer, write_pos, buffer.length - write_pos);

          synchronized (synchronizer) {
            if (avail <= 0) {
              isEOF = true;
              synchronizer.notifyAll();
              break;
            }
            write_pos += avail;

            int space_available = buffer.length - write_pos;

            if (space_available == 0) {
              if (read_pos > 0) {
                saveBuffer = new byte[read_pos];
                System.arraycopy(buffer, 0, saveBuffer, 0, read_pos);
                System.arraycopy(buffer, read_pos, buffer, 0, buffer.length - read_pos);
                write_pos -= read_pos;
                read_pos = 0;
              } else {
                write_pos = 0;
                saveBuffer = buffer;
              }
            }

            synchronizer.notifyAll();
          }

          writeToFile(saveBuffer);

        } catch (IOException e) {
          synchronized (synchronizer) {
            exception = e;
            synchronizer.notifyAll();
            break;
          }
        }
      }
    }
  }

  private InputStream is;
  private GobblerThread t;

  private Object synchronizer = new Object();

  private boolean isEOF = false;
  private boolean isClosed = false;
  private IOException exception = null;

  private byte[] buffer;
  private int read_pos = 0;
  private int write_pos = 0;
  private final FileOutputStream mLogStream;
  private final int mBufferSize;

  public StreamGobbler(InputStream is, File log, int buffer_size) {
    this.is = is;
    mBufferSize = buffer_size;
    FileOutputStream out = null;
    try {
      out = new FileOutputStream(log, false);
    } catch (IOException e) {
      Log.e(e);
    }
    mLogStream = out;
    buffer = new byte[mBufferSize];
    t = new GobblerThread();
    t.setDaemon(true);
    t.start();
  }

  public void writeToFile(byte[] buffer) {
    if (mLogStream != null && buffer != null) {
      try {
        mLogStream.write(buffer);
      } catch (IOException e) {
        Log.e(e);
      }
    }
  }

  @Override
  public int read() throws IOException {
    synchronized (synchronizer) {
      if (isClosed) {
        throw new IOException("This StreamGobbler is closed.");
      }

      while (read_pos == write_pos) {
        if (exception != null) {
          throw exception;
        }

        if (isEOF) {
          return -1;
        }

        try {
          synchronizer.wait();
        } catch (InterruptedException e) {
        }
      }

      int b = buffer[read_pos++] & 0xff;

      return b;
    }
  }

  @Override
  public int available() throws IOException {
    synchronized (synchronizer) {
      if (isClosed) {
        throw new IOException("This StreamGobbler is closed.");
      }

      return write_pos - read_pos;
    }
  }

  @Override
  public int read(byte[] b) throws IOException {
    return read(b, 0, b.length);
  }

  @Override
  public void close() throws IOException {
    synchronized (synchronizer) {
      if (isClosed) {
        return;
      }
      isClosed = true;
      isEOF = true;
      synchronizer.notifyAll();
      is.close();
    }
  }

  @Override
  public int read(byte[] b, int off, int len) throws IOException {
    if (b == null) {
      throw new NullPointerException();
    }

    if ((off < 0) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0) || (off > b.length)) {
      throw new IndexOutOfBoundsException();
    }

    if (len == 0) {
      return 0;
    }

    synchronized (synchronizer) {
      if (isClosed) {
        throw new IOException("This StreamGobbler is closed.");
      }

      while (read_pos == write_pos) {
        if (exception != null) {
          throw exception;
        }

        if (isEOF) {
          return -1;
        }

        try {
          synchronizer.wait();
        } catch (InterruptedException e) {
        }
      }

      int avail = write_pos - read_pos;

      avail = (avail > len) ? len : avail;

      System.arraycopy(buffer, read_pos, b, off, avail);

      read_pos += avail;

      return avail;
    }
  }
}