/*
 * Copyright (C) 2014 Square, 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 okio;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

import static okio.Util.checkOffsetAndCount;

final class RealBufferedSource implements BufferedSource {
  public final Buffer buffer;
  public final Source source;
  private boolean closed;

  public RealBufferedSource(Source source, Buffer buffer) {
    if (source == null) throw new IllegalArgumentException("source == null");
    this.buffer = buffer;
    this.source = source;
  }

  public RealBufferedSource(Source source) {
    this(source, new Buffer());
  }

  @Override public Buffer buffer() {
    return buffer;
  }

  @Override public long read(Buffer sink, long byteCount) throws IOException {
    if (sink == null) throw new IllegalArgumentException("sink == null");
    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    if (closed) throw new IllegalStateException("closed");

    if (buffer.size == 0) {
      long read = source.read(buffer, Segment.SIZE);
      if (read == -1) return -1;
    }

    long toRead = Math.min(byteCount, buffer.size);
    return buffer.read(sink, toRead);
  }

  @Override public boolean exhausted() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    return buffer.exhausted() && source.read(buffer, Segment.SIZE) == -1;
  }

  @Override public void require(long byteCount) throws IOException {
    if (!request(byteCount)) throw new EOFException();
  }

  @Override public boolean request(long byteCount) throws IOException {
    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    if (closed) throw new IllegalStateException("closed");
    while (buffer.size < byteCount) {
      if (source.read(buffer, Segment.SIZE) == -1) return false;
    }
    return true;
  }

  @Override public byte readByte() throws IOException {
    require(1);
    return buffer.readByte();
  }

  @Override public ByteString readByteString() throws IOException {
    buffer.writeAll(source);
    return buffer.readByteString();
  }

  @Override public ByteString readByteString(long byteCount) throws IOException {
    require(byteCount);
    return buffer.readByteString(byteCount);
  }

  @Override public byte[] readByteArray() throws IOException {
    buffer.writeAll(source);
    return buffer.readByteArray();
  }

  @Override public byte[] readByteArray(long byteCount) throws IOException {
    require(byteCount);
    return buffer.readByteArray(byteCount);
  }

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

  @Override public void readFully(byte[] sink) throws IOException {
    try {
      require(sink.length);
    } catch (EOFException e) {
      // The underlying source is exhausted. Copy the bytes we got before rethrowing.
      int offset = 0;
      while (buffer.size > 0) {
        int read = buffer.read(sink, offset, (int) buffer.size);
        if (read == -1) throw new AssertionError();
        offset += read;
      }
      throw e;
    }
    buffer.readFully(sink);
  }

  @Override public int read(byte[] sink, int offset, int byteCount) throws IOException {
    checkOffsetAndCount(sink.length, offset, byteCount);

    if (buffer.size == 0) {
      long read = source.read(buffer, Segment.SIZE);
      if (read == -1) return -1;
    }

    int toRead = (int) Math.min(byteCount, buffer.size);
    return buffer.read(sink, offset, toRead);
  }

  @Override public void readFully(Buffer sink, long byteCount) throws IOException {
    try {
      require(byteCount);
    } catch (EOFException e) {
      // The underlying source is exhausted. Copy the bytes we got before rethrowing.
      sink.writeAll(buffer);
      throw e;
    }
    buffer.readFully(sink, byteCount);
  }

  @Override public long readAll(Sink sink) throws IOException {
    if (sink == null) throw new IllegalArgumentException("sink == null");

    long totalBytesWritten = 0;
    while (source.read(buffer, Segment.SIZE) != -1) {
      long emitByteCount = buffer.completeSegmentByteCount();
      if (emitByteCount > 0) {
        totalBytesWritten += emitByteCount;
        sink.write(buffer, emitByteCount);
      }
    }
    if (buffer.size() > 0) {
      totalBytesWritten += buffer.size();
      sink.write(buffer, buffer.size());
    }
    return totalBytesWritten;
  }

  @Override public String readUtf8() throws IOException {
    buffer.writeAll(source);
    return buffer.readUtf8();
  }

  @Override public String readUtf8(long byteCount) throws IOException {
    require(byteCount);
    return buffer.readUtf8(byteCount);
  }

  @Override public String readString(Charset charset) throws IOException {
    if (charset == null) throw new IllegalArgumentException("charset == null");

    buffer.writeAll(source);
    return buffer.readString(charset);
  }

  @Override public String readString(long byteCount, Charset charset) throws IOException {
    require(byteCount);
    if (charset == null) throw new IllegalArgumentException("charset == null");
    return buffer.readString(byteCount, charset);
  }

  @Override public String readUtf8Line() throws IOException {
    long newline = indexOf((byte) '\n');

    if (newline == -1) {
      return buffer.size != 0 ? readUtf8(buffer.size) : null;
    }

    return buffer.readUtf8Line(newline);
  }

  @Override public String readUtf8LineStrict() throws IOException {
    long newline = indexOf((byte) '\n');
    if (newline == -1L) {
      Buffer data = new Buffer();
      buffer.copyTo(data, 0, Math.min(32, buffer.size()));
      throw new EOFException("\\n not found: size=" + buffer.size()
          + " content=" + data.readByteString().hex() + "...");
    }
    return buffer.readUtf8Line(newline);
  }

  @Override public int readUtf8CodePoint() throws IOException {
    require(1);

    byte b0 = buffer.getByte(0);
    if ((b0 & 0xe0) == 0xc0) {
      require(2);
    } else if ((b0 & 0xf0) == 0xe0) {
      require(3);
    } else if ((b0 & 0xf8) == 0xf0) {
      require(4);
    }

    return buffer.readUtf8CodePoint();
  }

  @Override public short readShort() throws IOException {
    require(2);
    return buffer.readShort();
  }

  @Override public short readShortLe() throws IOException {
    require(2);
    return buffer.readShortLe();
  }

  @Override public int readInt() throws IOException {
    require(4);
    return buffer.readInt();
  }

  @Override public int readIntLe() throws IOException {
    require(4);
    return buffer.readIntLe();
  }

  @Override public long readLong() throws IOException {
    require(8);
    return buffer.readLong();
  }

  @Override public long readLongLe() throws IOException {
    require(8);
    return buffer.readLongLe();
  }

  @Override public long readDecimalLong() throws IOException {
    require(1);

    for (int pos = 0; request(pos + 1); pos++) {
      byte b = buffer.getByte(pos);
      if ((b < '0' || b > '9') && (pos != 0 || b != '-')) {
        // Non-digit, or non-leading negative sign.
        if (pos == 0) {
          throw new NumberFormatException(String.format(
              "Expected leading [0-9] or '-' character but was %#x", b));
        }
        break;
      }
    }

    return buffer.readDecimalLong();
  }

  @Override public long readHexadecimalUnsignedLong() throws IOException {
    require(1);

    for (int pos = 0; request(pos + 1); pos++) {
      byte b = buffer.getByte(pos);
      if ((b < '0' || b > '9') && (b < 'a' || b > 'f') && (b < 'A' || b > 'F')) {
        // Non-digit, or non-leading negative sign.
        if (pos == 0) {
          throw new NumberFormatException(String.format(
              "Expected leading [0-9a-fA-F] character but was %#x", b));
        }
        break;
      }
    }

    return buffer.readHexadecimalUnsignedLong();
  }

  @Override public void skip(long byteCount) throws IOException {
    if (closed) throw new IllegalStateException("closed");
    while (byteCount > 0) {
      if (buffer.size == 0 && source.read(buffer, Segment.SIZE) == -1) {
        throw new EOFException();
      }
      long toSkip = Math.min(byteCount, buffer.size());
      buffer.skip(toSkip);
      byteCount -= toSkip;
    }
  }

  @Override public long indexOf(byte b) throws IOException {
    return indexOf(b, 0);
  }

  @Override public long indexOf(byte b, long fromIndex) throws IOException {
    if (closed) throw new IllegalStateException("closed");
    while (fromIndex >= buffer.size) {
      if (source.read(buffer, Segment.SIZE) == -1) return -1L;
    }
    long index;
    while ((index = buffer.indexOf(b, fromIndex)) == -1) {
      fromIndex = buffer.size;
      if (source.read(buffer, Segment.SIZE) == -1) return -1L;
    }
    return index;
  }

  @Override public long indexOf(ByteString bytes) throws IOException {
    return indexOf(bytes, 0);
  }

  @Override public long indexOf(ByteString bytes, long fromIndex) throws IOException {
    if (bytes.size() == 0) throw new IllegalArgumentException("bytes is empty");
    while (true) {
      fromIndex = indexOf(bytes.getByte(0), fromIndex);
      if (fromIndex == -1) {
        return -1;
      }
      if (rangeEquals(fromIndex, bytes)) {
        return fromIndex;
      }
      fromIndex++;
    }
  }

  @Override public long indexOfElement(ByteString targetBytes) throws IOException {
    return indexOfElement(targetBytes, 0);
  }

  @Override public long indexOfElement(ByteString targetBytes, long fromIndex) throws IOException {
    if (closed) throw new IllegalStateException("closed");
    while (fromIndex >= buffer.size) {
      if (source.read(buffer, Segment.SIZE) == -1) return -1L;
    }
    long index;
    while ((index = buffer.indexOfElement(targetBytes, fromIndex)) == -1) {
      fromIndex = buffer.size;
      if (source.read(buffer, Segment.SIZE) == -1) return -1L;
    }
    return index;
  }

  private boolean rangeEquals(long offset, ByteString bytes) throws IOException {
    return request(offset + bytes.size()) && buffer.rangeEquals(offset, bytes);
  }

  @Override public InputStream inputStream() {
    return new InputStream() {
      @Override public int read() throws IOException {
        if (closed) throw new IOException("closed");
        if (buffer.size == 0) {
          long count = source.read(buffer, Segment.SIZE);
          if (count == -1) return -1;
        }
        return buffer.readByte() & 0xff;
      }

      @Override public int read(byte[] data, int offset, int byteCount) throws IOException {
        if (closed) throw new IOException("closed");
        checkOffsetAndCount(data.length, offset, byteCount);

        if (buffer.size == 0) {
          long count = source.read(buffer, Segment.SIZE);
          if (count == -1) return -1;
        }

        return buffer.read(data, offset, byteCount);
      }

      @Override public int available() throws IOException {
        if (closed) throw new IOException("closed");
        return (int) Math.min(buffer.size, Integer.MAX_VALUE);
      }

      @Override public void close() throws IOException {
        RealBufferedSource.this.close();
      }

      @Override public String toString() {
        return RealBufferedSource.this + ".inputStream()";
      }
    };
  }

  @Override public void close() throws IOException {
    if (closed) return;
    closed = true;
    source.close();
    buffer.clear();
  }

  @Override public Timeout timeout() {
    return source.timeout();
  }

  @Override public String toString() {
    return "buffer(" + source + ")";
  }
}
