package org.msgpack.jruby;


import java.nio.ByteBuffer;

import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyObject;
import org.jruby.RubyHash;
import org.jruby.RubyIO;
import org.jruby.RubyInteger;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.util.ByteList;

import org.jcodings.Encoding;


@JRubyClass(name="MessagePack::Buffer")
public class Buffer extends RubyObject {
  private RubyHash options;
  private IRubyObject io;
  private ByteBuffer buffer;
  private boolean writeMode;
  private Encoding binaryEncoding;

  private static final int CACHE_LINE_SIZE = 64;
  private static final int ARRAY_HEADER_SIZE = 24;

  public Buffer(Ruby runtime, RubyClass type) {
    super(runtime, type);
  }

  static class BufferAllocator implements ObjectAllocator {
    public IRubyObject allocate(Ruby runtime, RubyClass type) {
      return new Buffer(runtime, type);
    }
  }

  @JRubyMethod(name = "initialize", optional = 2)
  public IRubyObject initialize(ThreadContext ctx, IRubyObject[] args) {
    if (args.length > 0) {
      if (args[0].respondsTo("read") && args[0].respondsTo("write")) {
        this.io = args[0];
      }
      if (args[args.length - 1] instanceof RubyHash) {
        this.options = (RubyHash) args[args.length - 1];
      }
    }
    this.buffer = ByteBuffer.allocate(CACHE_LINE_SIZE - ARRAY_HEADER_SIZE);
    this.writeMode = true;
    this.binaryEncoding = ctx.getRuntime().getEncodingService().getAscii8bitEncoding();
    return this;
  }

  private void ensureRemainingCapacity(int c) {
    if (!writeMode) {
      buffer.compact();
      writeMode = true;
    }
    if (buffer.remaining() < c) {
      int newLength = Math.max(buffer.capacity() + (buffer.capacity() >> 1), buffer.capacity() + c);
      newLength += CACHE_LINE_SIZE - ((ARRAY_HEADER_SIZE + newLength) % CACHE_LINE_SIZE);
      buffer = ByteBuffer.allocate(newLength).put(buffer.array(), 0, buffer.position());
    }
  }

  private void ensureReadMode() {
    if (writeMode) {
      buffer.flip();
      writeMode = false;
    }
  }

  private int rawSize() {
    if (writeMode) {
      return buffer.position();
    } else {
      return buffer.limit() - buffer.position();
    }
  }

  @JRubyMethod(name = "clear")
  public IRubyObject clear(ThreadContext ctx) {
    buffer.clear();
    return ctx.getRuntime().getNil();
  }

  @JRubyMethod(name = "size")
  public IRubyObject size(ThreadContext ctx) {
    return ctx.getRuntime().newFixnum(rawSize());
  }

  @JRubyMethod(name = "empty?")
  public IRubyObject isEmpty(ThreadContext ctx) {
    return rawSize() == 0 ? ctx.getRuntime().getTrue() : ctx.getRuntime().getFalse();
  }

  private IRubyObject bufferWrite(ThreadContext ctx, IRubyObject str) {
    ByteList bytes = str.asString().getByteList();
    int length = bytes.length();
    ensureRemainingCapacity(length);
    buffer.put(bytes.unsafeBytes(), bytes.begin(), length);
    return ctx.getRuntime().newFixnum(length);

  }

  @JRubyMethod(name = "write", alias = {"<<"})
  public IRubyObject write(ThreadContext ctx, IRubyObject str) {
    if (io == null) {
      return bufferWrite(ctx, str);
    } else {
      return io.callMethod(ctx, "write", str);
    }
  }

  private void feed(ThreadContext ctx) {
    if (io != null) {
      bufferWrite(ctx, io.callMethod(ctx, "read"));
    }
  }

  private IRubyObject readCommon(ThreadContext ctx, IRubyObject[] args, boolean raiseOnUnderflow) {
    feed(ctx);
    int length = rawSize();
    if (args != null && args.length == 1) {
      length = (int) args[0].convertToInteger().getLongValue();
    }
    if (raiseOnUnderflow && rawSize() < length) {
      throw ctx.getRuntime().newEOFError();
    }
    int readLength = Math.min(length, rawSize());
    if (readLength == 0 && length > 0) {
      return ctx.getRuntime().getNil();
    } else if (readLength == 0) {
      return ctx.getRuntime().newString();
    } else {
      ensureReadMode();
      byte[] bytes = new byte[readLength];
      buffer.get(bytes);
      ByteList byteList = new ByteList(bytes, binaryEncoding);
      return ctx.getRuntime().newString(byteList);
    }
  }

  @JRubyMethod(name = "read", optional = 1)
  public IRubyObject read(ThreadContext ctx, IRubyObject[] args) {
    return readCommon(ctx, args, false);
  }

  @JRubyMethod(name = "read_all", optional = 1)
  public IRubyObject readAll(ThreadContext ctx, IRubyObject[] args) {
    return readCommon(ctx, args, true);
  }

  private IRubyObject skipCommon(ThreadContext ctx, IRubyObject _length, boolean raiseOnUnderflow) {
    feed(ctx);
    int length = (int) _length.convertToInteger().getLongValue();
    if (raiseOnUnderflow && rawSize() < length) {
      throw ctx.getRuntime().newEOFError();
    }
    ensureReadMode();
    int skipLength = Math.min(length, rawSize());
    buffer.position(buffer.position() + skipLength);
    return ctx.getRuntime().newFixnum(skipLength);
  }

  @JRubyMethod(name = "skip")
  public IRubyObject skip(ThreadContext ctx, IRubyObject length) {
    return skipCommon(ctx, length, false);
  }

  @JRubyMethod(name = "skip_all")
  public IRubyObject skipAll(ThreadContext ctx, IRubyObject length) {
    return skipCommon(ctx, length, true);
  }

  @JRubyMethod(name = "to_s", alias = {"to_str"})
  public IRubyObject toS(ThreadContext ctx) {
    ensureReadMode();
    int length = buffer.limit() - buffer.position();
    ByteList str = new ByteList(buffer.array(), buffer.position(), length, binaryEncoding, true);
    return ctx.getRuntime().newString(str);
  }

  @JRubyMethod(name = "to_a")
  public IRubyObject toA(ThreadContext ctx) {
    return ctx.getRuntime().newArray(toS(ctx));
  }

  @JRubyMethod(name = "io")
  public IRubyObject getIo(ThreadContext ctx) {
    return io == null ? ctx.getRuntime().getNil() : io;
  }

  @JRubyMethod(name = "flush")
  public IRubyObject flush(ThreadContext ctx) {
    if (io == null) {
      return ctx.getRuntime().getNil();
    } else {
      return io.callMethod(ctx, "flush");
    }
  }

  @JRubyMethod(name = "close")
  public IRubyObject close(ThreadContext ctx) {
    if (io == null) {
      return ctx.getRuntime().getNil();
    } else {
      return io.callMethod(ctx, "close");
    }
  }

  @JRubyMethod(name = "write_to")
  public IRubyObject writeTo(ThreadContext ctx, IRubyObject io) {
    return io.callMethod(ctx, "write", readCommon(ctx, null, false));
  }
}
