package com.beijunyi.parallelgit.filesystem.io;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.file.OpenOption;
import java.util.Collection;
import javax.annotation.Nonnull;

import static java.lang.System.arraycopy;
import static java.nio.file.StandardOpenOption.*;

public class GfsSeekableByteChannel implements SeekableByteChannel {

  private final FileNode file;
  private final boolean readable;
  private final boolean writable;
  private ByteBuffer buffer;
  private volatile boolean closed = false;

  GfsSeekableByteChannel(FileNode file, Collection<? extends OpenOption> options) throws IOException {
    this.file = file;
    buffer = ByteBuffer.wrap(options.contains(TRUNCATE_EXISTING) ? new byte[0] : file.getData());
    readable = options.contains(READ);
    writable = options.contains(WRITE);
    if(options.contains(APPEND)) buffer.position(buffer.limit());
  }

  @Override
  public int read(ByteBuffer dst) throws ClosedChannelException {
    checkClosed();
    checkReadAccess();
    synchronized(this) {
      return copyBytes(dst, buffer);
    }
  }

  @Override
  public int write(ByteBuffer src) throws ClosedChannelException {
    checkClosed();
    checkWriteAccess();
    synchronized(this) {
      if(buffer.remaining() < src.remaining()) {
        int position = buffer.position();
        byte[] bytes = new byte[position + src.remaining()];
        arraycopy(buffer.array(), 0, bytes, 0, position);
        buffer = ByteBuffer.wrap(bytes);
        buffer.position(position);
      }
      return copyBytes(buffer, src);
    }
  }

  @Override
  public long position() throws ClosedChannelException {
    checkClosed();
    return buffer.position();
  }

  private static int toInt(long num) {
    if(num < 0 || num >= Integer.MAX_VALUE)
      throw new IllegalArgumentException("Input must be between 0 and " + Integer.MAX_VALUE);
    return (int) num;
  }

  @Override
  public GfsSeekableByteChannel position(long newPosition) throws ClosedChannelException {
    checkClosed();
    synchronized(this) {
      buffer.position(toInt(newPosition));
    }
    return this;
  }

  @Override
  public long size() throws ClosedChannelException {
    checkClosed();
    return buffer.limit();
  }

  @Override
  public GfsSeekableByteChannel truncate(long size) throws NonWritableChannelException, ClosedChannelException, IllegalArgumentException {
    checkClosed();
    checkWriteAccess();
    synchronized(this) {
      buffer.limit(toInt(size));
    }
    return this;
  }

  @Override
  public boolean isOpen() {
    return !closed;
  }

  @Nonnull
  public byte[] getBytes() {
    synchronized(this) {
      byte[] bytes = new byte[buffer.limit()];
      arraycopy(buffer.array(), 0, bytes, 0, bytes.length);
      return bytes;
    }

  }

  @Override
  public void close() {
    if(closed)
      return;
    synchronized(this) {
      if(!closed) {
        closed = true;
        file.setBytes(getBytes());
      }
    }
  }

  private void checkClosed() throws ClosedChannelException {
    if(!isOpen()) throw new ClosedChannelException();
  }

  private void checkReadAccess() throws NonReadableChannelException {
    if(!readable) throw new NonReadableChannelException();
  }

  private void checkWriteAccess() throws NonWritableChannelException {
    if(!writable) throw new NonWritableChannelException();
  }

  private static int copyBytes(ByteBuffer dst, ByteBuffer src) {
    int remaining = Math.min(src.remaining(), dst.remaining());
    for(int i = 0; i < remaining; i++)
      dst.put(src.get());
    return remaining;
  }

}
