/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.netty.buffer;

import io.netty.util.internal.PlatformDependent;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteOrder;
import java.util.concurrent.atomic.AtomicLong;

/**
 * The underlying class we use for little-endian access to memory. Is used
 * underneath DrillBufs to abstract away the Netty classes and underlying Netty
 * memory management.
 */

public final class UnsafeDirectLittleEndian extends WrappedByteBuf {
  private static final boolean NATIVE_ORDER = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN;
  private static final AtomicLong ID_GENERATOR = new AtomicLong(0);

  public final long id = ID_GENERATOR.incrementAndGet();
  private final AbstractByteBuf wrapped;
  private final long memoryAddress;

  private final AtomicLong bufferCount;
  private final AtomicLong bufferSize;
  private final long initCap;

  UnsafeDirectLittleEndian(DuplicatedByteBuf buf) {
    this(buf, true, null, null);
  }

  UnsafeDirectLittleEndian(LargeBuffer buf) {
    this(buf, true, null, null);
  }

  UnsafeDirectLittleEndian(PooledUnsafeDirectByteBuf buf, AtomicLong bufferCount, AtomicLong bufferSize) {
    this(buf, true, bufferCount, bufferSize);
  }

  private UnsafeDirectLittleEndian(AbstractByteBuf buf, boolean fake, AtomicLong bufferCount, AtomicLong bufferSize) {
    super(buf);
    if (!NATIVE_ORDER) {
      throw new IllegalStateException("Drill only runs on LittleEndian systems.");
    }

    this.bufferCount = bufferCount;
    this.bufferSize = bufferSize;

    // initCap is used if we're tracking memory release. If we're in non-debug mode, we'll skip this.
    this.initCap = ASSERT_ENABLED ? buf.capacity() : -1;

    this.wrapped = buf;
    this.memoryAddress = buf.memoryAddress();
  }

  private long addr(int index) {
      return memoryAddress + index;
  }

  @Override
  public long getLong(int index) {
    //wrapped.checkIndex(index, 8);
    return PlatformDependent.getLong(addr(index));
  }

  @Override
  public float getFloat(int index) {
        return Float.intBitsToFloat(getInt(index));
    }

  @Override
  public ByteBuf slice() {
    return slice(this.readerIndex(), readableBytes());
  }

  @Override
  public ByteBuf slice(int index, int length) {
    return new SlicedByteBuf(this, index, length);
  }

  @Override
  public ByteBuf order(ByteOrder endianness) {
    return this;
  }

  @Override
  public double getDouble(int index) {
    return Double.longBitsToDouble(getLong(index));
  }

  @Override
  public char getChar(int index) {
    return (char) getShort(index);
  }

  @Override
  public long getUnsignedInt(int index) {
    return getInt(index) & 0xFFFFFFFFL;
  }

  @Override
  public int getInt(int index) {
    int v = PlatformDependent.getInt(addr(index));
    return v;
  }

  @Override
  public int getUnsignedShort(int index) {
    return getShort(index) & 0xFFFF;
  }

  @Override
  public short getShort(int index) {
    short v = PlatformDependent.getShort(addr(index));
    return v;
  }

  @Override
  public ByteBuf setShort(int index, int value) {
    wrapped.checkIndex(index, 2);
    _setShort(index, value);
    return this;
  }

  @Override
  public ByteBuf setInt(int index, int value) {
    wrapped.checkIndex(index, 4);
    _setInt(index, value);
    return this;
  }

  @Override
  public ByteBuf setLong(int index, long value) {
    wrapped.checkIndex(index, 8);
    _setLong(index, value);
    return this;
  }

  @Override
  public ByteBuf setChar(int index, int value) {
    setShort(index, value);
    return this;
  }

  @Override
  public ByteBuf setFloat(int index, float value) {
    setInt(index, Float.floatToRawIntBits(value));
    return this;
  }

  @Override
  public ByteBuf setDouble(int index, double value) {
    setLong(index, Double.doubleToRawLongBits(value));
    return this;
  }

  // Clone of the super class checkIndex, but this version returns a boolean rather
  // than throwing an exception.

  protected boolean hasCapacity(int index, int fieldLength) {
    assert fieldLength >= 0;
    return (! (index < 0 || index > capacity() - fieldLength));
  }

  /**
   * Write bytes into the buffer at the given index, if space is available.
   * @param index location to write
   * @param src bytes to write
   * @param srcIndex start of data in the source array
   * @param length length of the data to write
   * @return true if the value was written, false if the value was not
   * written because the value would overflow the buffer
   */

  public boolean setBytesBounded(int index, byte[] src, int srcIndex, int length) {
    if (! hasCapacity(index, length)) {
      return false;
    }
    PlatformDependent.copyMemory(src, srcIndex, addr(index), length);
    return true;
  }

  // Version of the super class setBytes(), but with bounds checking done as a boolean,
  // not assertion. This version requires a direct source buffer.

  public boolean setBytesBounded(int index, UnsafeDirectLittleEndian src, int srcIndex, int length) {
    if (! hasCapacity(index, length)) {
      return false;
    }
    PlatformDependent.copyMemory(src.memoryAddress() + srcIndex, addr(index), length);
    return true;
  }

  @Override
  public ByteBuf writeShort(int value) {
    wrapped.ensureWritable(2);
    _setShort(wrapped.writerIndex, value);
    wrapped.writerIndex += 2;
    return this;
  }

  @Override
  public ByteBuf writeInt(int value) {
    wrapped.ensureWritable(4);
    _setInt(wrapped.writerIndex, value);
    wrapped.writerIndex += 4;
    return this;
  }

  @Override
  public ByteBuf writeLong(long value) {
    wrapped.ensureWritable(8);
    _setLong(wrapped.writerIndex, value);
    wrapped.writerIndex += 8;
    return this;
  }

  @Override
  public ByteBuf writeChar(int value) {
    writeShort(value);
    return this;
  }

  @Override
  public ByteBuf writeFloat(float value) {
    writeInt(Float.floatToRawIntBits(value));
    return this;
  }

  @Override
  public ByteBuf writeDouble(double value) {
    writeLong(Double.doubleToRawLongBits(value));
    return this;
  }

  private void _setShort(int index, int value) {
    PlatformDependent.putShort(addr(index), (short) value);
  }

  private void _setInt(int index, int value) {
    PlatformDependent.putInt(addr(index), value);
  }

  private void _setLong(int index, long value) {
    PlatformDependent.putLong(addr(index), value);
  }

  @Override
  public byte getByte(int index) {
    return PlatformDependent.getByte(addr(index));
  }

  @Override
  public ByteBuf setByte(int index, int value) {
    PlatformDependent.putByte(addr(index), (byte) value);
    return this;
  }

  @Override
  public boolean release() {
    return release(1);
  }

  @Override
  public boolean release(int decrement) {
    final boolean released = super.release(decrement);
    if (ASSERT_ENABLED && released && bufferCount != null && bufferSize != null) {
      bufferCount.decrementAndGet();
      bufferSize.addAndGet(-initCap);
    }
    return released;
  }

  @Override
  public int setBytes(int index, InputStream in, int length) throws IOException {
    wrapped.checkIndex(index, length);
    byte[] tmp = new byte[length];
    int readBytes = in.read(tmp);
    if (readBytes > 0) {
      PlatformDependent.copyMemory(tmp, 0, addr(index), readBytes);
    }
    return readBytes;
  }

  @Override
  public ByteBuf getBytes(int index, OutputStream out, int length) throws IOException {
    wrapped.checkIndex(index, length);
    if (length != 0) {
      byte[] tmp = new byte[length];
      PlatformDependent.copyMemory(addr(index), tmp, 0, length);
      out.write(tmp);
    }
    return this;
  }

  @Override
  public int hashCode() {
    return System.identityHashCode(this);
  }

  public static final boolean ASSERT_ENABLED;

  static {
    boolean isAssertEnabled = false;
    assert isAssertEnabled = true;
    ASSERT_ENABLED = isAssertEnabled;
  }
}
