package com.lts.core.commons.io;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

public class BufferedRandomAccessFile extends RandomAccessFile {

    private static final int DEFAULT_BUFFER_BIT_LEN = 10;

    protected byte buf[];
    protected int bufBitLen;
    protected int bufSize;
    protected long bufMask;
    protected boolean bufDirty;
    protected int bufUsedSize;
    protected long curPos;

    protected long bufStartPos;
    protected long bufEndPos;
    protected long fileEndPos;

    protected boolean append;
    protected String filename;
    protected long initFileLen;

    public BufferedRandomAccessFile(String name) throws IOException {
        this(name, "r", DEFAULT_BUFFER_BIT_LEN);
    }

    public BufferedRandomAccessFile(File file) throws IOException {
        this(file.getPath(), "r", DEFAULT_BUFFER_BIT_LEN);
    }

    public BufferedRandomAccessFile(String name, int bufBitLen) throws IOException {
        this(name, "r", bufBitLen);
    }

    public BufferedRandomAccessFile(File file, int bufBitLen) throws IOException {
        this(file.getPath(), "r", bufBitLen);
    }

    public BufferedRandomAccessFile(String name, String mode) throws IOException {
        this(name, mode, DEFAULT_BUFFER_BIT_LEN);
    }

    public BufferedRandomAccessFile(File file, String mode) throws IOException {
        this(file.getPath(), mode, DEFAULT_BUFFER_BIT_LEN);
    }

    public BufferedRandomAccessFile(String name, String mode, int bufBitLen) throws IOException {
        super(name, mode);
        this.init(name, mode, bufBitLen);
    }

    public BufferedRandomAccessFile(File file, String mode, int bufBitLen) throws IOException {
        this(file.getPath(), mode, bufBitLen);
    }

    private void init(String name, String mode, int bufbitlen) throws IOException {

        this.append = !mode.equals("r");

        this.filename = name;
        this.initFileLen = super.length();
        this.fileEndPos = this.initFileLen - 1;
        this.curPos = super.getFilePointer();

        if (bufbitlen < 0) {
            throw new IllegalArgumentException("bufBitLen size must >= 0");
        }

        this.bufBitLen = bufbitlen;
        this.bufSize = 1 << bufbitlen;
        this.buf = new byte[this.bufSize];
        this.bufMask = ~((long) this.bufSize - 1L);
        this.bufDirty = false;
        this.bufUsedSize = 0;
        this.bufStartPos = -1;
        this.bufEndPos = -1;
    }

    private void flushBuf() throws IOException {
        if (this.bufDirty) {
            if (super.getFilePointer() != this.bufStartPos) {
                super.seek(this.bufStartPos);
            }
            super.write(this.buf, 0, this.bufUsedSize);
            this.bufDirty = false;
        }
    }

    private int fillBuf() throws IOException {
        super.seek(this.bufStartPos);
        this.bufDirty = false;
        return super.read(this.buf);
    }

    public byte read(long pos) throws IOException {
        if (pos < this.bufStartPos || pos > this.bufEndPos) {
            this.flushBuf();
            this.seek(pos);

            if ((pos < this.bufStartPos) || (pos > this.bufEndPos)) {
                throw new IOException();
            }
        }
        this.curPos = pos;
        return this.buf[(int) (pos - this.bufStartPos)];
    }

    public boolean write(byte bw) throws IOException {
        return this.write(bw, this.curPos);
    }

    public boolean append(byte bw) throws IOException {
        return this.write(bw, this.fileEndPos + 1);
    }

    public boolean write(byte bw, long pos) throws IOException {

        if ((pos >= this.bufStartPos) && (pos <= this.bufEndPos)) { // write pos in buf
            this.buf[(int) (pos - this.bufStartPos)] = bw;
            this.bufDirty = true;

            if (pos == this.fileEndPos + 1) { // write pos is append pos
                this.fileEndPos++;
                this.bufUsedSize++;
            }
        } else { // write pos not in buf
            this.seek(pos);

            if ((pos >= 0) && (pos <= this.fileEndPos) && (this.fileEndPos != 0)) { // write pos is modify file
                this.buf[(int) (pos - this.bufStartPos)] = bw;

            } else if (((pos == 0) && (this.fileEndPos == 0)) || (pos == this.fileEndPos + 1)) { // write pos is append pos
                this.buf[0] = bw;
                this.fileEndPos++;
                this.bufUsedSize = 1;
            } else {
                throw new IndexOutOfBoundsException();
            }
            this.bufDirty = true;
        }
        this.curPos = pos;
        return true;
    }

    public void write(byte b[], int off, int len) throws IOException {

        long writeEndPos = this.curPos + len - 1;

        if (writeEndPos <= this.bufEndPos) { // b[] in cur buf
            System.arraycopy(b, off, this.buf, (int) (this.curPos - this.bufStartPos), len);
            this.bufDirty = true;
            this.bufUsedSize = (int) (writeEndPos - this.bufStartPos + 1);//(int)(this.curPos - this.bufStartPos + len - 1);

        } else { // b[] not in cur buf
            super.seek(this.curPos);
            super.write(b, off, len);
        }

        if (writeEndPos > this.fileEndPos)
            this.fileEndPos = writeEndPos;

        this.seek(writeEndPos + 1);
    }

    public int read(byte b[], int off, int len) throws IOException {

        long readEndPos = this.curPos + len - 1;

        if (readEndPos <= this.bufEndPos && readEndPos <= this.fileEndPos) { // read in buf
            System.arraycopy(this.buf, (int) (this.curPos - this.bufStartPos), b, off, len);
        } else { // read b[] size > buf[]

            if (readEndPos > this.fileEndPos) { // read b[] part in file
                len = (int) (this.length() - this.curPos + 1);
            }

            super.seek(this.curPos);
            len = super.read(b, off, len);
            readEndPos = this.curPos + len - 1;
        }
        this.seek(readEndPos + 1);
        return len;
    }

    public void write(byte b[]) throws IOException {
        this.write(b, 0, b.length);
    }

    public int read(byte b[]) throws IOException {
        return this.read(b, 0, b.length);
    }

    public void seek(long pos) throws IOException {

        if ((pos < this.bufStartPos) || (pos > this.bufEndPos)) { // seek pos not in buf
            this.flushBuf();

            if ((pos >= 0) && (pos <= this.fileEndPos) && (this.fileEndPos != 0)) { // seek pos in file (file length > 0)
                this.bufStartPos = pos & this.bufMask;
                this.bufUsedSize = this.fillBuf();

            } else if (((pos == 0) && (this.fileEndPos == 0)) || (pos == this.fileEndPos + 1)) { // seek pos is append pos

                this.bufStartPos = pos;
                this.bufUsedSize = 0;
            }
            this.bufEndPos = this.bufStartPos + this.bufSize - 1;
        }
        this.curPos = pos;
    }

    public long length() throws IOException {
        return this.max(this.fileEndPos + 1, this.initFileLen);
    }

    public void setLength(long newLength) throws IOException {
        if (newLength > 0) {
            this.fileEndPos = newLength - 1;
        } else {
            this.fileEndPos = 0;
        }
        super.setLength(newLength);
    }

    public long getFilePointer() throws IOException {
        return this.curPos;
    }

    private long max(long a, long b) {
        if (a > b) return a;
        return b;
    }

    public void close() throws IOException {
        this.flushBuf();
        super.close();
    }
}

