
package jp.sourceforge.nicoro;

import static jp.sourceforge.nicoro.Log.LOG_TAG;

import android.os.SystemClock;

import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.io.SyncFailedException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

abstract public class AbstractMultiRandomAccessFile implements Closeable {
    protected static final boolean DEBUG_LOGV = Release.IS_DEBUG & false;
    protected static final boolean DEBUG_LOGD = Release.IS_DEBUG & true;
    protected static final boolean DEBUG_LOGD_INPUT_STREAM = Release.IS_DEBUG & false;

    public static final int SEEK_SET = 0;
    public static final int SEEK_CUR = 1;
    public static final int SEEK_END = 2;

    protected RandomAccessFile mFile;
    protected final ReentrantLock mLockFile = new ReentrantLock();
    protected final Condition mConditionFile = mLockFile.newCondition();

    protected final AtomicLong mContentLength;
    protected final AtomicLong mSeekOffsetWrite;
    protected final AtomicLong mSeekOffsetRead;

    protected volatile boolean mWasClosed;
    protected volatile boolean mWrite;

    private File mFilePath;

    public AbstractMultiRandomAccessFile(File file, boolean write)
    throws FileNotFoundException {
        super();
        String mode;
        if (write) {
            mode = "rw";
        } else {
            mode = "r";
        }
        mFilePath = file;
        mFile = new RandomAccessFile(file, mode);

        mContentLength = new AtomicLong(-1L);
        mSeekOffsetWrite = new AtomicLong(0L);
        mSeekOffsetRead = new AtomicLong(0L);
        mWasClosed = false;
        mWrite = write;
    }

    @Override
    public void close() throws IOException {
        mLockFile.lock();
        try {
            mFile.close();
            mWasClosed = true;
            endWrite();
            mConditionFile.signalAll();
        } finally {
            mLockFile.unlock();
        }
    }

    public void setLength(long newLength) throws IOException {
        synchronized (mContentLength) {
            mContentLength.set(newLength);
            if (mWrite) {
                mLockFile.lock();
                try {
                    if (newLength > 0) {
                        mFile.setLength(newLength);
                    } else {
                        mFile.setLength(0);
                    }
                } finally {
                    mLockFile.unlock();
                }
            } else {
                assert newLength <= mFile.length();
            }
        }
    }

    public long length() throws IOException {
        return mContentLength.get();
    }

    public void syncWrite() throws SyncFailedException, IOException {
        if (mWrite) {
            mLockFile.lock();
            try {
                mFile.getFD().sync();
            } finally {
                mLockFile.unlock();
            }
        }
    }

    public long seekWrite(long offset) throws IOException {
        if (offset < 0) {
            return -1L;
        }
        final long contentLength = mContentLength.get();
        if (contentLength >= 0 && offset > contentLength) {
            return -1L;
        }
        synchronized (mSeekOffsetWrite) {
            mSeekOffsetWrite.set(offset);
            return offset;
        }
    }

    public long seekRead(long offset) throws IOException {
        return seekRead(offset, SEEK_SET);
    }

    /**
    *
    * @param offset
    * @param whence
    * <ul>
    * <li>SEEK_SET
    * <li>SEEK_CUR
    * <li>SEEK_END
    * </ul>
    * @return 現在の読み込みファイル位置、エラー発生時は-1を返す
    * @throws IOException
    */
    public long seekRead(long offset, int whence) throws IOException {
        final long contentLength = mContentLength.get();
        synchronized (mSeekOffsetRead) {
            final long seekOffsetRead;
            switch (whence) {
            case SEEK_SET:
                seekOffsetRead = offset;
                break;
            case SEEK_CUR:
                seekOffsetRead = mSeekOffsetRead.get() + offset;
                break;
            case SEEK_END:
                if (contentLength < 0) {
                    return -1L;
                }
                seekOffsetRead = contentLength + offset;
                break;
            default:
                assert false : whence;
                return -1L;
            }
            if (seekOffsetRead >= 0 &&
                    (contentLength < 0 || seekOffsetRead <= contentLength)) {
                mSeekOffsetRead.set(seekOffsetRead);
                return seekOffsetRead;
            }
            return -1L;
        }
    }

    public long tellWrite() {
        return mSeekOffsetWrite.get();
    }

    public long tellRead() {
        return mSeekOffsetRead.get();
    }

    public void write(byte[] buffer, int offset, int count) throws IOException {
        long startTime = 0;
        if (DEBUG_LOGV) {
            startTime = SystemClock.elapsedRealtime();
        }
        assert mWrite; // TODO close()の割り込み具合で偶にひっかかるときがある

//        mLockSeekOffsetWrite.readLock().lock();
//        mLockFile.lock();
//        try {
//            long offsetSeekWrite = mSeekOffsetWrite;
//            try {
//                writeImpl(offsetSeekWrite, buffer, offset, count);
//            } finally {
//                mLockSeekOffsetWrite.readLock().unlock();
//            }
//            mLockSeekOffsetWrite.writeLock().lock();
//            mSeekOffsetWrite += count;
//            mLockSeekOffsetWrite.writeLock().unlock();
//            mConditionFile.signalAll();
//        } finally {
//            mLockFile.unlock();
//        }

        synchronized (mSeekOffsetWrite) {
            mLockFile.lock();
            try {
                long offsetSeekWrite = mSeekOffsetWrite.get();
                writeImpl(offsetSeekWrite, buffer, offset, count);
                mSeekOffsetWrite.addAndGet(count);
                mConditionFile.signalAll();
            } finally {
                mLockFile.unlock();
            }
        }

        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append(getClass().getSimpleName())
                    .append("#write() time=")
                    .append(SystemClock.elapsedRealtime() - startTime)
                    .append("ms")
                    .toString());
        }
    }

    /**
     * mLockSeekOffsetWrite と mLockFile のlock付き
     * @param buffer
     * @param offset
     * @param count
     * @throws IOException
     */
    abstract protected void writeImpl(long offsetSeekWrite, byte[] buffer, int offset, int count) throws IOException;

    abstract public int read() throws IOException;

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

    public int read(byte[] buffer, int offset, int count) throws IOException {
        final int read;
        int readCount = count;
        long seekOffsetRead;
        final long seekOffsetWrite = mSeekOffsetWrite.get();
        final long contentLength = mContentLength.get();
        synchronized (mSeekOffsetRead) {
            seekOffsetRead = mSeekOffsetRead.get();
            if (contentLength >= 0 && seekOffsetRead == contentLength) {
                assert seekOffsetRead == seekOffsetWrite;
                // ファイル終端
                read = -1;
            } else {
                final int remainFileLength = (int) (seekOffsetWrite - seekOffsetRead);
                if (readCount > remainFileLength) {
                    readCount = remainFileLength;
                }
                final int remainBufferLength = buffer.length - offset;
                if (readCount > remainBufferLength) {
                    readCount = remainBufferLength;
                }

                read = readImpl(seekOffsetRead, buffer, offset, readCount);
                if (read >= 0) {
                    seekOffsetRead += read;
                    mSeekOffsetRead.set(seekOffsetRead);
                }
            }
        }
        return read;
    }

    abstract public int readImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException;

    public void readFully(byte[] buffer) throws IOException {
        readFully(buffer, 0, buffer.length);
    }

    public void readFully(byte[] buffer, int offset, int count) throws IOException {
        long seekOffsetRead;
        long seekOffsetWrite = mSeekOffsetWrite.get();
        final long contentLength = mContentLength.get();
        synchronized (mSeekOffsetRead) {
            seekOffsetRead = mSeekOffsetRead.get();
            if (contentLength >= 0 && seekOffsetRead + count > contentLength) {
                // ファイル終端
                throw new EOFException();
            } else {
                while (count > (int) (seekOffsetWrite - seekOffsetRead)) {
                    mLockFile.lock();
                    try {
                        mConditionFile.await();
                    } catch (InterruptedException e) {
                        if (DEBUG_LOGD) {
                            Log.d(LOG_TAG, e.toString(), e);
                        }
                    } finally {
                        mLockFile.unlock();
                    }
                    seekOffsetWrite = mSeekOffsetWrite.get();
                }

                readFullyImpl(seekOffsetRead, buffer, offset, count);
                seekOffsetRead += count;
                mSeekOffsetRead.set(seekOffsetRead);
            }
        }
    }

    abstract public void readFullyImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException;

    public void endWrite() {
        mWrite = false;
    }

    abstract public int readTemporary(long offset, byte[] head) throws IOException;

    public boolean needWaitToRead() throws IOException {
        if (mWrite) {
            final long seekOffsetRead = mSeekOffsetRead.get();
            final long length = mContentLength.get();
            if (length < 0 || seekOffsetRead != length) {
                return seekOffsetRead >= mSeekOffsetWrite.get();
            }
        }
        return false;
    }

    public boolean equalFilePath(String path) {
        return equalFilePath(new File(path));
    }

    public boolean equalFilePath(File path) {
        return mFilePath.equals(path);
    }

    abstract protected class AbstractReadInputStream extends InputStream {
        protected AtomicLong mSeekOffsetReadStream;

        protected AbstractReadInputStream() {
            mSeekOffsetReadStream = new AtomicLong(0L);
        }

        @Override
        public int available() throws IOException {
            final int ret = (int) (tellWrite() - mSeekOffsetReadStream.get());
            if (DEBUG_LOGD_INPUT_STREAM) {
                Log.d(LOG_TAG, Log.buf().append(getClass().getName())
                        .append("#available() return=")
                        .append(ret).toString());
            }
            return ret;
        }

        @Override
        public void close() {
            if (DEBUG_LOGD_INPUT_STREAM) {
                Log.d(LOG_TAG, Log.buf().append(getClass().getName())
                        .append("#close()").toString());
            }
            // 何もしない
        }

        @Override
        public int read() throws IOException {
            final int read;
            synchronized (mSeekOffsetReadStream) {
                final long seekOffsetRead = mSeekOffsetReadStream.get();
                final long length = mContentLength.get();
                if (length >= 0 &&  seekOffsetRead == length) {
                    assert seekOffsetRead == mSeekOffsetWrite.get();
                    // ファイル終端
                    read = -1;
                } else {
                    while (true) {
                        long seekOffsetWrite = mSeekOffsetWrite.get();
                        if (seekOffsetRead < seekOffsetWrite) {
                            break;
                        }
                        mLockFile.lock();
                        try {
                            mConditionFile.await();
                        } catch (InterruptedException e) {
                            Log.e(LOG_TAG, e.toString(), e);
                        } finally {
                            mLockFile.unlock();
                        }
                        if (mWasClosed) {
                            return -1;
                        }
                    }

                    read = readImpl(seekOffsetRead);
                    if (read >= 0) {
                        mSeekOffsetReadStream.incrementAndGet();
                    }
                }
            }
            if (DEBUG_LOGD_INPUT_STREAM) {
                Log.d(LOG_TAG, Log.buf().append(getClass().getName())
                        .append("#read() return=")
                        .append(read).toString());
            }
            return read;
        }

        @Override
        public int read(byte[] buffer, int offset, int count) throws IOException {
            final int read;
            int readCount = count;
            final long seekOffsetWrite = mSeekOffsetWrite.get();
            final long contentLength = mContentLength.get();
            long seekOffsetRead;
            synchronized (mSeekOffsetReadStream) {
                seekOffsetRead = mSeekOffsetReadStream.get();
                if (contentLength >= 0 && seekOffsetRead == contentLength) {
                    assert seekOffsetRead == seekOffsetWrite;
                    // ファイル終端
                    read = -1;
                } else {
                    final int remainFileLength = (int) (seekOffsetWrite - seekOffsetRead);
                    if (readCount > remainFileLength) {
                        if (remainFileLength >= 0) {
                            readCount = remainFileLength;
                        } else {
                            Log.e(LOG_TAG, Log.buf().append("Invalid seekOffset: write=")
                                    .append(seekOffsetWrite).append(" read=")
                                    .append(seekOffsetRead).toString());
                            readCount = 0;
                        }
                    }
                    final int remainBufferLength = buffer.length - offset;
                    if (readCount > remainBufferLength) {
                        readCount = remainBufferLength;
                    }

                    read = readImpl(seekOffsetRead, buffer, offset, readCount);
                    if (read >= 0) {
                        seekOffsetRead += read;
                        mSeekOffsetReadStream.set(seekOffsetRead);
                    }
                }
            }
            if (DEBUG_LOGD_INPUT_STREAM) {
                Log.d(LOG_TAG, Log.buf().append(getClass().getName())
                        .append("#read(")
                        .append(buffer.toString()).append(',').append(offset)
                        .append(',').append(count)
                        .append(") readCount=").append(readCount)
                        .append(" return=").append(read)
                        .append(" seekOffsetRead=").append(seekOffsetRead)
                        .append(" seekOffsetWrite=").append(seekOffsetWrite)
                        .append(" contentLength=").append(contentLength)
                        .toString());
            }
            return read;
        }

        @Override
        public long skip(long n) throws IOException {
            long contentLength;
            long seekOffsetWrite;
            contentLength = mContentLength.get();
            seekOffsetWrite = mSeekOffsetWrite.get();
            long skip;
            long seekOffsetReadStream;
            if (n <= 0) {
                skip = 0;
                seekOffsetReadStream = mSeekOffsetReadStream.get();
            } else {
                synchronized (mSeekOffsetReadStream) {
                    long remain = seekOffsetWrite - mSeekOffsetReadStream.get();
                    if (n > remain) {
                        skip = remain;
                    } else {
                        skip = n;
                    }
                    seekOffsetReadStream = mSeekOffsetReadStream.addAndGet(skip);
                }
            }
            if (DEBUG_LOGD_INPUT_STREAM) {
                Log.d(LOG_TAG, Log.buf().append(getClass().getName())
                        .append("#skip(")
                        .append(n).append(") return=").append(skip)
                        .append(" seekOffsetRead=").append(seekOffsetReadStream)
                        .append(" seekOffsetWrite=").append(seekOffsetWrite)
                        .append(" contentLength=").append(contentLength)
                        .toString());
            }
            return skip;
        }

        abstract protected int readImpl(long seekOffsetRead) throws IOException;
        abstract protected int readImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException;
    }

    abstract public InputStream createInputStream();
}