/*
 * Copyright 2013 Cloudera Inc.
 *
 * 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 org.kitesdk.morphline.hadoop.rcfile;

import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;

import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.PositionedReadable;
import org.apache.hadoop.fs.Seekable;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.util.Progressable;

/**
 * Very simple Filesystem Implementation which serves an InputStream for a given
 * path. This is used to serve the underlying input stream from a FileSystem
 * Interface. Only open() and getFileStatus() is implemented.
 */
public final class SingleStreamFileSystem extends FileSystem {
  private final FSDataInputStream inputStream;
  private final Path path;
  private final FileStatus fileStatus;

  public SingleStreamFileSystem(InputStream inputStream, Path path)
      throws IOException {
    this.inputStream = new FSDataInputStream(new ForwardOnlySeekable(inputStream));
    this.path = path;
    // Since this is a stream, we dont know the length of the stream. Setting it
    // to the maximum size
    this.fileStatus = new FileStatus(Long.MAX_VALUE, false, 0, 0, 0, path);
  }

  @Override
  public URI getUri() {
    throw new UnsupportedOperationException("not implemented!");
  }

  @Override
  public FSDataInputStream open(final Path f, final int bufferSize)
      throws IOException {
    if (f.equals(path)) {
      return inputStream;
    }
    throw new UnsupportedOperationException("Path " + f.getName()
        + " is not found");
  }

  @Override
  public FSDataOutputStream create(final Path f, final FsPermission permission,
      final boolean overwrite, final int bufferSize, final short replication,
      final long blockSize, final Progressable progress) throws IOException {
    throw new UnsupportedOperationException("not implemented!");
  }

  @Override
  public FSDataOutputStream append(final Path f, final int bufferSize,
      final Progressable progress) throws IOException {
    throw new UnsupportedOperationException("not implemented!");
  }

  @Override
  public boolean rename(final Path src, final Path dst) throws IOException {
    throw new UnsupportedOperationException("not implemented!");
  }

  @Override
  public boolean delete(final Path f, final boolean recursive)
      throws IOException {
    throw new UnsupportedOperationException("not implemented!");
  }

  @Override
  public FileStatus[] listStatus(final Path f) throws FileNotFoundException,
      IOException {
    throw new UnsupportedOperationException("not implemented!");
  }

  @Override
  public void setWorkingDirectory(final Path new_dir) {
    throw new UnsupportedOperationException("not implemented!");
  }

  @Override
  public Path getWorkingDirectory() {
    throw new UnsupportedOperationException("not implemented!");
  }

  @Override
  public boolean mkdirs(final Path f, final FsPermission permission)
      throws IOException {
    throw new UnsupportedOperationException("not implemented!");
  }

  @Override
  public FileStatus getFileStatus(final Path f) throws IOException {
    if (path.equals(f)) {
      return fileStatus;
    }
    throw new UnsupportedOperationException("Path " + f.getName()
        + " is not found");
  }

  @Override
  public boolean delete(Path path) throws IOException {
    throw new UnsupportedOperationException("not implemented!");
  }

  ///////////////////////////////////////////////////////////////////////////////
  // Nested classes:
  ///////////////////////////////////////////////////////////////////////////////
  /**
   * Forward-only Seekable InputStream to use for reading SequenceFiles.  Will throw an exception if
   * an attempt is made to seek backwards.
   */
  private static class ForwardOnlySeekable extends InputStream implements Seekable, PositionedReadable {
    private ForwardOnlySeekableInputStream fosInputStream;

    public ForwardOnlySeekable(InputStream inputStream) {
      this.fosInputStream = new ForwardOnlySeekableInputStream(inputStream);
    }

    /**
     * Seek to the given offset from the start of the file.
     * The next read() will be from that location.  Can't
     * seek past the end of the file.
     */
    public void seek(long pos) throws IOException {
      fosInputStream.seek(pos);
    }

    /**
     * Return the current offset from the start of the file
     */
    public long getPos() throws IOException {
      return fosInputStream.tell();
    }

    /**
     * Seeks a different copy of the data.  Returns true if
     * found a new source, false otherwise.
     */
    public boolean seekToNewSource(long targetPos) throws IOException {
      throw new UnsupportedOperationException("not implemented!");
    }

    /**
     * Read upto the specified number of bytes, from a given
     * position within a file, and return the number of bytes read. This does not
     * change the current offset of a file, and is thread-safe.
     */
    public int read(long position, byte[] buffer, int offset, int length)
        throws IOException {
      throw new UnsupportedOperationException("not implemented!");
    }

    /**
     * Read the specified number of bytes, from a given
     * position within a file. This does not
     * change the current offset of a file, and is thread-safe.
     */
    public void readFully(long position, byte[] buffer, int offset, int length)
        throws IOException {
      throw new UnsupportedOperationException("not implemented!");
    }

    /**
     * Read number of bytes equal to the length of the buffer, from a given
     * position within a file. This does not
     * change the current offset of a file, and is thread-safe.
     */
    public void readFully(long position, byte[] buffer) throws IOException {
      throw new UnsupportedOperationException("not implemented!");
    }

    public int read() throws IOException {
      byte [] b = new byte[1];
      int len = fosInputStream.read(b, 0, 1);
      int ret = (len == -1)? -1 : b[0] & 0xFF;
      return ret;
    }
  }


  ///////////////////////////////////////////////////////////////////////////////
  // Nested classes:
  ///////////////////////////////////////////////////////////////////////////////
  /**
   * A SeekableInput backed by an {@link InputStream} that can only advance
   * forward, not backwards.
   */
  private static final class ForwardOnlySeekableInputStream { // implements SeekableInput {

    private final InputStream in;
    private long pos = 0;

    public ForwardOnlySeekableInputStream(InputStream in) {
      this.in = in;
    }

//    @Override
    public long tell() throws IOException {
      return pos;
    }

//    @Override
    public int read(byte b[], int off, int len) throws IOException {
      int n = in.read(b, off, len);
      if (n > 0) {
        pos += n;
      }
      return n;
    }

//    @Override
    public long length() throws IOException {
      throw new UnsupportedOperationException("Random access is not supported");
    }

//    @Override
    public void seek(long p) throws IOException {
      long todo = p - pos;
      if (todo < 0) {
        throw new UnsupportedOperationException("Seeking backwards is not supported");
      }
      skip(todo);
    }

    private long skip(long len) throws IOException {
      // borrowed from org.apache.hadoop.io.IOUtils.skipFully()
      len = Math.max(0, len);
      long todo = len;
      while (todo > 0) {
        long ret = in.skip(todo);
        if (ret == 0) {
          // skip may return 0 even if we're not at EOF.  Luckily, we can
          // use the read() method to figure out if we're at the end.
          int b = in.read();
          if (b == -1) {
            throw new EOFException( "Premature EOF from inputStream after " +
                "skipping " + (len - todo) + " byte(s).");
          }
          ret = 1;
        }
        todo -= ret;
        pos += ret;
      }
      return len;
    }

//    @Override
    public void close() throws IOException {
      in.close();
    }

  }
}