/*
 * Copyright 2015 the original author or authors.
 *
 * 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 io.atomix.catalyst.buffer;

import io.atomix.catalyst.buffer.util.Memory;
import io.atomix.catalyst.util.reference.ReferenceManager;

import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteOrder;
import java.nio.InvalidMarkException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;

import static io.atomix.catalyst.buffer.Bytes.BOOLEAN;
import static io.atomix.catalyst.buffer.Bytes.BYTE;
import static io.atomix.catalyst.buffer.Bytes.SHORT;

/**
 * Abstract buffer implementation.
 *
 * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
 */
public abstract class AbstractBuffer implements Buffer {
  static final long DEFAULT_INITIAL_CAPACITY = 4096;

  protected final Bytes bytes;
  private long offset;
  private long initialCapacity;
  private long capacity;
  private long maxCapacity;
  private long position;
  private long limit = -1;
  private long mark = -1;
  private final AtomicInteger references = new AtomicInteger();
  private final ReferenceManager<Buffer> referenceManager;
  private SwappedBuffer swap;

  protected AbstractBuffer(Bytes bytes, ReferenceManager<Buffer> referenceManager) {
    this(bytes, 0, 0, 0, referenceManager);
  }

  protected AbstractBuffer(Bytes bytes, long offset, long initialCapacity, long maxCapacity, ReferenceManager<Buffer> referenceManager) {
    if (bytes == null)
      throw new NullPointerException("bytes cannot be null");
    if (offset < 0)
      throw new IndexOutOfBoundsException("offset out of bounds of the underlying byte array");
    this.bytes = bytes;
    this.offset = offset;
    this.capacity = 0;
    this.initialCapacity = initialCapacity;
    this.maxCapacity = maxCapacity;
    capacity(initialCapacity);
    this.referenceManager = referenceManager;
    references.set(1);
  }

  /**
   * Resets the buffer's internal offset and capacity.
   */
  protected AbstractBuffer reset(long offset, long capacity, long maxCapacity) {
    this.offset = offset;
    this.capacity = 0;
    this.initialCapacity = capacity;
    this.maxCapacity = maxCapacity;
    capacity(initialCapacity);
    references.set(0);
    rewind();
    return this;
  }

  @Override
  public Buffer acquire() {
    references.incrementAndGet();
    return this;
  }

  @Override
  public boolean release() {
    if (references.decrementAndGet() == 0) {
      if (referenceManager != null)
        referenceManager.release(this);
      else
        bytes.close();
      return true;
    }
    return false;
  }

  @Override
  public int references() {
    return references.get();
  }

  @Override
  public Bytes bytes() {
    return bytes;
  }

  @Override
  public ByteOrder order() {
    return bytes.order();
  }

  @Override
  public Buffer order(ByteOrder order) {
    if (order == null)
      throw new NullPointerException("order cannot be null");
    if (order == order())
      return this;
    if (swap != null)
      return swap;
    swap = new SwappedBuffer(this, offset, capacity, maxCapacity, referenceManager);
    return swap;
  }

  @Override
  public boolean isDirect() {
    return bytes.isDirect();
  }

  @Override
  public boolean isFile() {
    return bytes.isFile();
  }

  @Override
  public boolean isReadOnly() {
    return false;
  }

  @Override
  public Buffer asReadOnlyBuffer() {
    return new ReadOnlyBuffer(this, referenceManager)
      .reset(offset, capacity, maxCapacity)
      .position(position)
      .limit(limit);
  }

  @Override
  public Buffer compact() {
    compact(offset(position), offset, (limit != -1 ? limit : capacity) - offset(position));
    return clear();
  }

  /**
   * Compacts the given bytes.
   */
  protected abstract void compact(long from, long to, long length);

  @Override
  public Buffer slice() {
    long maxCapacity = this.maxCapacity - position;
    long capacity = Math.min(Math.min(initialCapacity, maxCapacity), bytes.size() - offset(position));
    if (limit != -1)
      capacity = maxCapacity = limit - position;
    return new SlicedBuffer(this, bytes, offset(position), capacity, maxCapacity);
  }

  @Override
  public Buffer slice(long length) {
    checkSlice(position, length);
    return new SlicedBuffer(this, bytes, offset(position), length, length);
  }

  @Override
  public Buffer slice(long offset, long length) {
    checkSlice(offset, length);
    return new SlicedBuffer(this, bytes, offset(offset), length, length);
  }

  @Override
  public long offset() {
    return offset;
  }

  @Override
  public long capacity() {
    return capacity;
  }

  /**
   * Updates the buffer capacity.
   */
  public Buffer capacity(long capacity) {
    if (capacity > maxCapacity) {
      throw new IllegalArgumentException("capacity cannot be greater than maximum capacity");
    } else if (capacity < this.capacity) {
      throw new IllegalArgumentException("capacity cannot be decreased");
    } else if (capacity != this.capacity) {
      // It's possible that the bytes could already meet the requirements of the capacity.
      if (offset(capacity) > bytes.size()) {
        bytes.resize(Memory.Util.toPow2(offset(capacity)));
      }
      this.capacity = capacity;
    }
    return this;
  }

  @Override
  public long maxCapacity() {
    return maxCapacity;
  }

  @Override
  public long position() {
    return position;
  }

  @Override
  public Buffer position(long position) {
    if (limit != -1 && position > limit) {
      throw new IllegalArgumentException("position cannot be greater than limit");
    } else if (limit == -1 && position > maxCapacity) {
      throw new IllegalArgumentException("position cannot be greater than capacity");
    }
    if (position > capacity)
      capacity(Math.min(maxCapacity, Memory.Util.toPow2(position)));
    this.position = position;
    return this;
  }

  /**
   * Returns the real offset of the given relative offset.
   */
  private long offset(long offset) {
    return this.offset + offset;
  }

  @Override
  public long limit() {
    return limit;
  }

  @Override
  public Buffer limit(long limit) {
    if (limit > maxCapacity)
      throw new IllegalArgumentException("limit cannot be greater than buffer capacity");
    if (limit < -1)
      throw new IllegalArgumentException("limit cannot be negative");
    if (limit != -1 && offset(limit) > bytes.size())
      bytes.resize(offset(limit));
    this.limit = limit;
    return this;
  }

  @Override
  public long remaining() {
    return (limit == -1 ? maxCapacity : limit) - position;
  }

  @Override
  public boolean hasRemaining() {
    return remaining() > 0;
  }

  @Override
  public Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
  }

  @Override
  public Buffer mark() {
    this.mark = position;
    return this;
  }

  @Override
  public Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
  }

  @Override
  public Buffer reset() {
    if (mark == -1)
      throw new InvalidMarkException();
    position = mark;
    return this;
  }

  @Override
  public Buffer skip(long length) {
    if (length > remaining())
      throw new IndexOutOfBoundsException("length cannot be greater than remaining bytes in the buffer");
    position += length;
    return this;
  }

  @Override
  public Buffer clear() {
    position = 0;
    limit = -1;
    mark = -1;
    return this;
  }

  /**
   * Checks that the offset is within the bounds of the buffer.
   */
  protected void checkOffset(long offset) {
    if (offset(offset) < this.offset) {
      throw new IndexOutOfBoundsException();
    } else if (limit == -1) {
      if (offset > maxCapacity)
        throw new IndexOutOfBoundsException();
    } else {
      if (offset > limit)
        throw new IndexOutOfBoundsException();
    }
  }

  /**
   * Checks bounds for a slice.
   */
  protected long checkSlice(long offset, long length) {
    checkOffset(offset);
    if (limit == -1) {
      if (offset + length > capacity) {
        if (capacity < maxCapacity) {
          capacity(calculateCapacity(offset + length));
        } else {
          throw new BufferUnderflowException();
        }
      }
    } else {
      if (offset + length > limit)
        throw new BufferUnderflowException();
    }
    return offset(offset);
  }

  /**
   * Checks bounds for a read for the given length.
   */
  protected long checkRead(long length) {
    checkRead(position, length);
    long previousPosition = this.position;
    this.position = previousPosition + length;
    return offset(previousPosition);
  }

  /**
   * Checks bounds for a read.
   */
  protected long checkRead(long offset, long length) {
    checkOffset(offset);
    if (limit == -1) {
      if (offset + length > capacity) {
        if (capacity < maxCapacity) {
          if (this.offset + offset + length <= bytes.size()) {
            capacity = bytes.size() - this.offset;
          } else {
            capacity(calculateCapacity(offset + length));
          }
        } else {
          throw new BufferUnderflowException();
        }
      }
    } else {
      if (offset + length > limit)
        throw new BufferUnderflowException();
    }
    return offset(offset);
  }

  /**
   * Checks bounds for a write of the given length.
   */
  protected long checkWrite(long length) {
    checkWrite(position, length);
    long previousPosition = this.position;
    this.position = previousPosition + length;
    return offset(previousPosition);
  }

  /**
   * Checks bounds for a write.
   */
  protected long checkWrite(long offset, long length) {
    checkOffset(offset);
    if (limit == -1) {
      if (offset + length > capacity) {
        if (capacity < maxCapacity) {
          capacity(calculateCapacity(offset + length));
        } else {
          throw new BufferOverflowException();
        }
      }
    } else {
      if (offset + length > limit)
        throw new BufferOverflowException();
    }
    return offset(offset);
  }

  /**
   * Calculates the next capacity that meets the given minimum capacity.
   */
  private long calculateCapacity(long minimumCapacity) {
    long newCapacity = Math.min(Math.max(capacity, 2), minimumCapacity);
    while (newCapacity < Math.min(minimumCapacity, maxCapacity)) {
      newCapacity <<= 1;
    }
    return Math.min(newCapacity, maxCapacity);
  }

  @Override
  public Buffer zero() {
    bytes.zero(offset);
    return this;
  }

  @Override
  public Buffer zero(long offset) {
    checkOffset(offset);
    bytes.zero(offset(offset));
    return this;
  }

  @Override
  public Buffer zero(long offset, long length) {
    checkOffset(offset);
    bytes.zero(offset(offset), length);
    return this;
  }

  @Override
  public Buffer read(Buffer buffer) {
    long length = Math.min(buffer.remaining(), remaining());
    read(buffer.bytes(), buffer.offset() + buffer.position(), length);
    buffer.position(buffer.position() + length);
    return this;
  }

  @Override
  public Buffer read(Bytes bytes) {
    this.bytes.read(checkRead(bytes.size()), bytes, 0, bytes.size());
    return this;
  }

  @Override
  public Buffer read(Bytes bytes, long offset, long length) {
    this.bytes.read(checkRead(length), bytes, offset, length);
    return this;
  }

  @Override
  public Buffer read(long srcOffset, Bytes bytes, long dstOffset, long length) {
    this.bytes.read(checkRead(srcOffset, length), bytes, dstOffset, length);
    return this;
  }

  @Override
  public Buffer read(byte[] bytes) {
    this.bytes.read(checkRead(bytes.length), bytes, 0, bytes.length);
    return this;
  }

  @Override
  public Buffer read(byte[] bytes, long offset, long length) {
    this.bytes.read(checkRead(length), bytes, offset, length);
    return this;
  }

  @Override
  public Buffer read(long srcOffset, byte[] bytes, long dstOffset, long length) {
    this.bytes.read(checkRead(srcOffset, length), bytes, dstOffset, length);
    return this;
  }

  @Override
  public int readByte() {
    return bytes.readByte(checkRead(BYTE));
  }

  @Override
  public int readByte(long offset) {
    return bytes.readByte(checkRead(offset, BYTE));
  }

  @Override
  public int readUnsignedByte() {
    return bytes.readUnsignedByte(checkRead(BYTE));
  }

  @Override
  public int readUnsignedByte(long offset) {
    return bytes.readUnsignedByte(checkRead(offset, BYTE));
  }

  @Override
  public char readChar() {
    return bytes.readChar(checkRead(Bytes.CHARACTER));
  }

  @Override
  public char readChar(long offset) {
    return bytes.readChar(checkRead(offset, Bytes.CHARACTER));
  }

  @Override
  public short readShort() {
    return bytes.readShort(checkRead(SHORT));
  }

  @Override
  public short readShort(long offset) {
    return bytes.readShort(checkRead(offset, SHORT));
  }

  @Override
  public int readUnsignedShort() {
    return bytes.readUnsignedShort(checkRead(SHORT));
  }

  @Override
  public int readUnsignedShort(long offset) {
    return bytes.readUnsignedShort(checkRead(offset, SHORT));
  }

  @Override
  public int readMedium() {
    return bytes.readMedium(checkRead(3));
  }

  @Override
  public int readMedium(long offset) {
    return bytes.readMedium(checkRead(offset, 3));
  }

  @Override
  public int readUnsignedMedium() {
    return bytes.readUnsignedMedium(checkRead(3));
  }

  @Override
  public int readUnsignedMedium(long offset) {
    return bytes.readUnsignedMedium(checkRead(offset, 3));
  }

  @Override
  public int readInt() {
    return bytes.readInt(checkRead(Bytes.INTEGER));
  }

  @Override
  public int readInt(long offset) {
    return bytes.readInt(checkRead(offset, Bytes.INTEGER));
  }

  @Override
  public long readUnsignedInt() {
    return bytes.readUnsignedInt(checkRead(Bytes.INTEGER));
  }

  @Override
  public long readUnsignedInt(long offset) {
    return bytes.readUnsignedInt(checkRead(offset, Bytes.INTEGER));
  }

  @Override
  public long readLong() {
    return bytes.readLong(checkRead(Bytes.LONG));
  }

  @Override
  public long readLong(long offset) {
    return bytes.readLong(checkRead(offset, Bytes.LONG));
  }

  @Override
  public float readFloat() {
    return bytes.readFloat(checkRead(Bytes.FLOAT));
  }

  @Override
  public float readFloat(long offset) {
    return bytes.readFloat(checkRead(offset, Bytes.FLOAT));
  }

  @Override
  public double readDouble() {
    return bytes.readDouble(checkRead(Bytes.DOUBLE));
  }

  @Override
  public double readDouble(long offset) {
    return bytes.readDouble(checkRead(offset, Bytes.DOUBLE));
  }

  @Override
  public boolean readBoolean() {
    return bytes.readBoolean(checkRead(BYTE));
  }

  @Override
  public boolean readBoolean(long offset) {
    return bytes.readBoolean(checkRead(offset, BYTE));
  }

  @Override
  public String readString(Charset charset) {
    if (readBoolean(position)) {
      byte[] bytes = new byte[readUnsignedShort(position + BOOLEAN)];
      read(position + BOOLEAN + SHORT, bytes, 0, bytes.length);
      this.position += BOOLEAN + SHORT + bytes.length;
      return new String(bytes, charset);
    } else {
      this.position += BOOLEAN;
    }
    return null;
  }

  @Override
  public String readString(long offset, Charset charset) {
    if (readBoolean(offset)) {
      byte[] bytes = new byte[readUnsignedShort(offset + BOOLEAN)];
      read(offset + BOOLEAN + SHORT, bytes, 0, bytes.length);
      return new String(bytes, charset);
    }
    return null;
  }

  @Override
  public String readString() {
    return readString(Charset.defaultCharset());
  }

  @Override
  public String readString(long offset) {
    return readString(offset, Charset.defaultCharset());
  }

  @Override
  public String readUTF8() {
    return readString(StandardCharsets.UTF_8);
  }

  @Override
  public String readUTF8(long offset) {
    return readString(offset, StandardCharsets.UTF_8);
  }

  @Override
  public Buffer write(Buffer buffer) {
    long length = Math.min(buffer.remaining(), remaining());
    write(buffer.bytes(), buffer.offset() + buffer.position(), length);
    buffer.position(buffer.position() + length);
    return this;
  }

  @Override
  public Buffer write(Bytes bytes) {
    this.bytes.write(checkWrite(bytes.size()), bytes, 0, bytes.size());
    return this;
  }

  @Override
  public Buffer write(Bytes bytes, long offset, long length) {
    this.bytes.write(checkWrite(length), bytes, offset, length);
    return this;
  }

  @Override
  public Buffer write(long offset, Bytes bytes, long srcOffset, long length) {
    this.bytes.write(checkWrite(offset, length), bytes, srcOffset, length);
    return this;
  }

  @Override
  public Buffer write(byte[] bytes) {
    this.bytes.write(checkWrite(bytes.length), bytes, 0, bytes.length);
    return this;
  }

  @Override
  public Buffer write(byte[] bytes, long offset, long length) {
    this.bytes.write(checkWrite(length), bytes, offset, length);
    return this;
  }

  @Override
  public Buffer write(long offset, byte[] bytes, long srcOffset, long length) {
    this.bytes.write(checkWrite(offset, length), bytes, srcOffset, length);
    return this;
  }

  @Override
  public Buffer writeByte(int b) {
    bytes.writeByte(checkWrite(BYTE), b);
    return this;
  }

  @Override
  public Buffer writeByte(long offset, int b) {
    bytes.writeByte(checkWrite(offset, BYTE), b);
    return this;
  }

  @Override
  public Buffer writeUnsignedByte(int b) {
    bytes.writeUnsignedByte(checkWrite(BYTE), b);
    return this;
  }

  @Override
  public Buffer writeUnsignedByte(long offset, int b) {
    bytes.writeUnsignedByte(checkWrite(offset, BYTE), b);
    return this;
  }

  @Override
  public Buffer writeChar(char c) {
    bytes.writeChar(checkWrite(Bytes.CHARACTER), c);
    return this;
  }

  @Override
  public Buffer writeChar(long offset, char c) {
    bytes.writeChar(checkWrite(offset, Bytes.CHARACTER), c);
    return this;
  }

  @Override
  public Buffer writeShort(short s) {
    bytes.writeShort(checkWrite(SHORT), s);
    return this;
  }

  @Override
  public Buffer writeShort(long offset, short s) {
    bytes.writeShort(checkWrite(offset, SHORT), s);
    return this;
  }

  @Override
  public Buffer writeUnsignedShort(int s) {
    bytes.writeUnsignedShort(checkWrite(SHORT), s);
    return this;
  }

  @Override
  public Buffer writeUnsignedShort(long offset, int s) {
    bytes.writeUnsignedShort(checkWrite(offset, SHORT), s);
    return this;
  }

  @Override
  public Buffer writeMedium(int m) {
    bytes.writeMedium(checkWrite(3), m);
    return this;
  }

  @Override
  public Buffer writeMedium(long offset, int m) {
    bytes.writeMedium(checkWrite(offset, 3), m);
    return this;
  }

  @Override
  public Buffer writeUnsignedMedium(int m) {
    bytes.writeUnsignedMedium(checkWrite(3), m);
    return this;
  }

  @Override
  public Buffer writeUnsignedMedium(long offset, int m) {
    bytes.writeUnsignedMedium(checkWrite(offset, 3), m);
    return this;
  }

  @Override
  public Buffer writeInt(int i) {
    bytes.writeInt(checkWrite(Bytes.INTEGER), i);
    return this;
  }

  @Override
  public Buffer writeInt(long offset, int i) {
    bytes.writeInt(checkWrite(offset, Bytes.INTEGER), i);
    return this;
  }

  @Override
  public Buffer writeUnsignedInt(long i) {
    bytes.writeUnsignedInt(checkWrite(Bytes.INTEGER), i);
    return this;
  }

  @Override
  public Buffer writeUnsignedInt(long offset, long i) {
    bytes.writeUnsignedInt(checkWrite(offset, Bytes.INTEGER), i);
    return this;
  }

  @Override
  public Buffer writeLong(long l) {
    bytes.writeLong(checkWrite(Bytes.LONG), l);
    return this;
  }

  @Override
  public Buffer writeLong(long offset, long l) {
    bytes.writeLong(checkWrite(offset, Bytes.LONG), l);
    return this;
  }

  @Override
  public Buffer writeFloat(float f) {
    bytes.writeFloat(checkWrite(Bytes.FLOAT), f);
    return this;
  }

  @Override
  public Buffer writeFloat(long offset, float f) {
    bytes.writeFloat(checkWrite(offset, Bytes.FLOAT), f);
    return this;
  }

  @Override
  public Buffer writeDouble(double d) {
    bytes.writeDouble(checkWrite(Bytes.DOUBLE), d);
    return this;
  }

  @Override
  public Buffer writeDouble(long offset, double d) {
    bytes.writeDouble(checkWrite(offset, Bytes.DOUBLE), d);
    return this;
  }

  @Override
  public Buffer writeBoolean(boolean b) {
    bytes.writeBoolean(checkWrite(BYTE), b);
    return this;
  }

  @Override
  public Buffer writeBoolean(long offset, boolean b) {
    bytes.writeBoolean(checkWrite(offset, BYTE), b);
    return this;
  }

  @Override
  public Buffer writeString(String s, Charset charset) {
    if (s == null) {
      return writeBoolean(checkWrite(BOOLEAN), Boolean.FALSE);
    } else {
      byte[] bytes = s.getBytes(charset);
      checkWrite(position, BOOLEAN + SHORT + bytes.length);
      writeBoolean(Boolean.TRUE)
        .writeUnsignedShort(bytes.length)
        .write(bytes, 0, bytes.length);
      return this;
    }
  }

  @Override
  public Buffer writeString(long offset, String s, Charset charset) {
    if (s == null) {
      return writeBoolean(checkWrite(offset, BOOLEAN), Boolean.FALSE);
    } else {
      byte[] bytes = s.getBytes(charset);
      checkWrite(offset, BOOLEAN + SHORT + bytes.length);
      writeBoolean(offset, Boolean.TRUE)
        .writeUnsignedShort(offset + BOOLEAN, bytes.length)
        .write(offset + BOOLEAN + SHORT, bytes, 0, bytes.length);
      return this;
    }
  }

  @Override
  public Buffer writeString(String s) {
    return writeString(s, Charset.defaultCharset());
  }

  @Override
  public Buffer writeString(long offset, String s) {
    return writeString(offset, s, Charset.defaultCharset());
  }

  @Override
  public Buffer writeUTF8(String s) {
    return writeString(s, StandardCharsets.UTF_8);
  }

  @Override
  public Buffer writeUTF8(long offset, String s) {
    return writeString(offset, s, StandardCharsets.UTF_8);
  }

  @Override
  public Buffer flush() {
    bytes.flush();
    return this;
  }

  @Override
  public void close() {
    references.set(0);
    if (referenceManager != null) {
      referenceManager.release(this);
    } else {
      bytes.close();
    }
  }

}
