package com.peircean.glusterfs;

import com.peircean.libgfapi_jni.internal.GLFS;
import com.peircean.libgfapi_jni.internal.GlusterOpenOption;
import com.peircean.libgfapi_jni.internal.UtilJNI;
import com.peircean.libgfapi_jni.internal.structs.stat;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.*;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.util.*;

/**
 * @author <a href="http://about.me/louiszuckerman">Louis Zuckerman</a>
 */
@Data
@NoArgsConstructor(access = AccessLevel.PACKAGE)
public class GlusterFileChannel extends FileChannel {
	public static final Map<StandardOpenOption, Integer> optionMap = new HashMap<StandardOpenOption, Integer>();

	static {
		optionMap.put(StandardOpenOption.APPEND, GlusterOpenOption.O_APPEND);
		optionMap.put(StandardOpenOption.CREATE, GlusterOpenOption.O_CREAT);
		optionMap.put(StandardOpenOption.CREATE_NEW, GlusterOpenOption.O_CREAT | GlusterOpenOption.O_EXCL);
		optionMap.put(StandardOpenOption.DSYNC, GlusterOpenOption.O_DSYNC);
		optionMap.put(StandardOpenOption.READ, GlusterOpenOption.O_RDONLY);
		optionMap.put(StandardOpenOption.WRITE, GlusterOpenOption.O_RDWR);
		optionMap.put(StandardOpenOption.TRUNCATE_EXISTING, GlusterOpenOption.O_TRUNC);
	}

	private GlusterFileSystem fileSystem;
	private GlusterPath path;
	private Set<? extends OpenOption> options = new HashSet<>();
	private FileAttribute<?> attrs[] = null;
	private long fileptr;
	private long position;
	private boolean closed = false;

	void init(GlusterFileSystem fileSystem, Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
		this.fileSystem = fileSystem;
		if (!path.isAbsolute()) {
			throw new IllegalStateException("Only absolute paths are supported at this time");
		}
		this.path = (GlusterPath) path;
		this.options = options;

		int flags = parseOptions(options);
		int mode = GlusterFileAttributes.parseAttrs(attrs);

		String pathString = path.toUri().getPath();
		boolean createNew = options.contains(StandardOpenOption.CREATE_NEW);
		if (options.contains(StandardOpenOption.CREATE) || createNew) {
			fileptr = GLFS.glfs_creat(fileSystem.getVolptr(), pathString, flags, mode);
		}

		if (createNew && 0 == fileptr) {
			throw new FileAlreadyExistsException(path.toString());
		}

		if (0 >= fileptr) {
			fileptr = GLFS.glfs_open(fileSystem.getVolptr(), pathString, flags);
		}

		if (0 >= fileptr) {
			throw new IOException("Unable to create or open file '" + pathString + "' on volume '" + fileSystem.toString() + "'");
		}
	}

	int parseOptions(Set<? extends OpenOption> options) {
		int opt = 0;
		for (OpenOption o : options) {
			if (!optionMap.containsKey(o)) {
				throw new UnsupportedOperationException("Option " + o + " is not supported at this time");
			}
			opt |= optionMap.get(o);
		}
		return opt;
	}

	@Override
	public int read(ByteBuffer byteBuffer) throws IOException {
		guardClosed();
		byte[] bytes = byteBuffer.array();
		long read = GLFS.glfs_read(fileptr, bytes, bytes.length, 0);
		position += read;
		return (int) read;
	}

	@Override
	public long read(ByteBuffer[] byteBuffers, int i, int i2) throws IOException {
		return 0;  //To change body of implemented methods use File | Settings | File Templates.
	}

    @Override
    public int write(ByteBuffer byteBuffer) throws IOException {
        guardClosed();
        byte[] buf = byteBuffer.array();
        int written = GLFS.glfs_write(fileptr, buf, buf.length, 0);
        if (written < 0) {
            throw new IOException(UtilJNI.strerror());
        }
        position += written;
        byteBuffer.position(written);
        return written;
    }

    @Override
    public long write(ByteBuffer[] byteBuffers, int offset, int length) throws IOException {
        guardClosed();
        if (offset < 0 || offset > byteBuffers.length) {
            throw new IndexOutOfBoundsException("Offset provided is invalid.");
        }
        if (length < 0 || length > byteBuffers.length - offset) {
            throw new IndexOutOfBoundsException("Length provided is invalid");
        }
        if (!options.contains(StandardOpenOption.WRITE)) {
            throw new NonWritableChannelException();
        }

        long totalWritten = 0L;

        for (int i = offset; i < length + offset; i++) {
            int remaining = byteBuffers[i].remaining();
            while (remaining > 0) {
                byte[] bytes = byteBuffers[i].array();
                int written = GLFS.glfs_write(fileptr, bytes, remaining, 0);
                if (written < 0) {
                    throw new IOException();
                }
                position += written;
                byteBuffers[i].position(written);
                totalWritten += written;
                remaining = byteBuffers[i].remaining();
            }
        }
        return totalWritten;
    }

	@Override
	public long position() throws IOException {
		guardClosed();
		return position;
	}

	@Override
	public FileChannel position(long offset) throws IOException {
		guardClosed();
		if (offset < 0) {
			throw new IllegalArgumentException("offset can't be negative");
		}
		int whence = 0; //SEEK_SET
		int seek = GLFS.glfs_lseek(fileptr, offset, whence);
		position = offset;
		return this;
	}

	void guardClosed() throws ClosedChannelException {
		if (closed) {
			throw new ClosedChannelException();
		}
	}

	@Override
	public long size() throws IOException {
		stat stat = new stat();
		int retval = GLFS.glfs_fstat(fileptr, stat);
		if (0 != retval) {
			throw new IOException("fstat failed");
		}
		return stat.st_size;
	}

	@Override
	public FileChannel truncate(long l) throws IOException {
		return null;  //To change body of implemented methods use File | Settings | File Templates.
	}

	@Override
	public void force(boolean b) throws IOException {
		guardClosed();
		int fsync = GLFS.glfs_fsync(fileptr);
		if (0 != fsync) {
			throw new IOException("Unable to fsync");
		}
	}

	@Override
	public long transferTo(long l, long l2, WritableByteChannel writableByteChannel) throws IOException {
		return 0;  //To change body of implemented methods use File | Settings | File Templates.
	}

	@Override
	public long transferFrom(ReadableByteChannel readableByteChannel, long l, long l2) throws IOException {
		return 0;  //To change body of implemented methods use File | Settings | File Templates.
	}

	@Override
	public int read(ByteBuffer byteBuffer, long l) throws IOException {
		return 0;  //To change body of implemented methods use File | Settings | File Templates.
	}

    @Override
    public int write(ByteBuffer byteBuffer, long position) throws IOException {
        guardClosed();
        if (position < 0) {
            throw new IllegalArgumentException();
        }
        if (!options.contains(StandardOpenOption.WRITE)) {
            throw new NonWritableChannelException();
        }
        if (position >= size()) {
            byte[] bytes = byteBuffer.array();
            byte[] temp = Arrays.copyOf(bytes, bytes.length + (int) (position - size()));
            byteBuffer = ByteBuffer.wrap(temp);
        }
        int whence = 0; //SEEK_SET
        int seek = GLFS.glfs_lseek(fileptr, position, whence);
        if (seek < 0) {
            throw new IOException();
        }
        byte[] bytes = byteBuffer.array();
        long written = GLFS.glfs_write(fileptr, bytes, bytes.length, 0);
        seek = GLFS.glfs_lseek(fileptr, this.position, whence);
        if (seek < 0) {
            throw new IOException();
        }
        return (int) written;
    }

	@Override
	public MappedByteBuffer map(MapMode mapMode, long l, long l2) throws IOException {
		return null;  //To change body of implemented methods use File | Settings | File Templates.
	}

	@Override
	public FileLock lock(long l, long l2, boolean b) throws IOException {
		return null;  //To change body of implemented methods use File | Settings | File Templates.
	}

	@Override
	public FileLock tryLock(long l, long l2, boolean b) throws IOException {
		return null;  //To change body of implemented methods use File | Settings | File Templates.
	}

	@Override
	protected void implCloseChannel() throws IOException {
		if (!closed) {
			int close = GLFS.glfs_close(fileptr);
			if (0 != close) {
				throw new IOException("Close returned nonzero");
			}
			closed = true;
		}
	}

}
