package com.velocitypowered.natives.compression;

import static com.velocitypowered.natives.compression.CompressorUtils.ZLIB_BUFFER_SIZE;
import static com.velocitypowered.natives.compression.CompressorUtils.ensureMaxSize;

import com.google.common.base.Preconditions;
import io.netty.buffer.ByteBuf;
import java.util.zip.DataFormatException;

public class NativeVelocityCompressor implements VelocityCompressor {

  public static final VelocityCompressorFactory FACTORY = NativeVelocityCompressor::new;

  private final NativeZlibInflate inflate = new NativeZlibInflate();
  private final long inflateCtx;
  private final NativeZlibDeflate deflate = new NativeZlibDeflate();
  private final long deflateCtx;
  private boolean disposed = false;

  private NativeVelocityCompressor(int level) {
    this.inflateCtx = inflate.init();
    this.deflateCtx = deflate.init(level);
  }

  @Override
  public void inflate(ByteBuf source, ByteBuf destination, int max) throws DataFormatException {
    ensureNotDisposed();
    source.memoryAddress();
    destination.memoryAddress();

    while (!inflate.finished && source.isReadable()) {
      if (!destination.isWritable()) {
        ensureMaxSize(destination, max);
        destination.ensureWritable(ZLIB_BUFFER_SIZE);
      }
      int produced = inflate.process(inflateCtx, source.memoryAddress() + source.readerIndex(),
          source.readableBytes(), destination.memoryAddress() + destination.writerIndex(),
          destination.writableBytes());
      source.readerIndex(source.readerIndex() + inflate.consumed);
      destination.writerIndex(destination.writerIndex() + produced);
    }

    inflate.reset(inflateCtx);
    inflate.consumed = 0;
    inflate.finished = false;
  }

  @Override
  public void deflate(ByteBuf source, ByteBuf destination) throws DataFormatException {
    ensureNotDisposed();
    source.memoryAddress();
    destination.memoryAddress();

    while (!deflate.finished) {
      if (!destination.isWritable()) {
        destination.ensureWritable(ZLIB_BUFFER_SIZE);
      }
      int produced = deflate.process(deflateCtx, source.memoryAddress() + source.readerIndex(),
          source.readableBytes(),
          destination.memoryAddress() + destination.writerIndex(), destination.writableBytes(),
          true);
      source.readerIndex(source.readerIndex() + deflate.consumed);
      destination.writerIndex(destination.writerIndex() + produced);
    }

    deflate.reset(deflateCtx);
    deflate.consumed = 0;
    deflate.finished = false;
  }

  private void ensureNotDisposed() {
    Preconditions.checkState(!disposed, "Object already disposed");
  }

  @Override
  public void dispose() {
    if (!disposed) {
      inflate.free(inflateCtx);
      deflate.free(deflateCtx);
    }
    disposed = true;
  }

  @Override
  public boolean isNative() {
    return true;
  }
}