
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();
        }
    }

    /**
     * ファイルの長さを設定する
     * @param newLength
     * @throws IOException
     */
    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();
            }
        }
    }

    /**
     * ファイルの長さを取得する
     * @return ファイルの長さ。未設定の場合は-1
     * @throws IOException
     */
    public long length() throws IOException {
        return mContentLength.get();
    }

    /**
     * 書き込みキャッシュに溜まっているデータをデバイス上のファイルに反映させる
     * @throws SyncFailedException
     * @throws IOException
     */
    public void syncWrite() throws SyncFailedException, IOException {
        if (mWrite) {
            mLockFile.lock();
            try {
                mFile.getFD().sync();
            } finally {
                mLockFile.unlock();
            }
        }
    }

    /**
     * 書き込み用のシーク位置を変更する
     * @param offset ファイルの先頭から見たシーク位置
     * @return 現在の書き込みファイル位置、エラー発生時は-1を返す
     * @throws IOException
     */
    public long seekWrite(long offset) throws IOException {
        return seekWrite(offset, SEEK_SET);
    }

    /**
     * 書き込み用のシーク位置を変更する
     * @param offset シーク位置
     * @param whence offsetの意味
     * <ul>
     * <li>{@link #SEEK_SET} ファイルの先頭から
     * <li>{@link #SEEK_CUR} 現在の書き込み用シーク位置から
     * <li>{@link #SEEK_END} ファイルの終端から
     * </ul>
     * @return 現在の書き込みファイル位置、エラー発生時は-1を返す
     * @throws IOException
     */
    public long seekWrite(long offset, int whence) throws IOException {
        final long contentLength = mContentLength.get();
        synchronized (mSeekOffsetWrite) {
            final long seekOffsetWrite;
            switch (whence) {
            case SEEK_SET:
                seekOffsetWrite = offset;
                break;
            case SEEK_CUR:
                seekOffsetWrite = mSeekOffsetWrite.get() + offset;
                break;
            case SEEK_END:
                if (contentLength < 0) {
                    return -1L;
                }
                seekOffsetWrite = contentLength + offset;
                break;
            default:
                assert false : whence;
                return -1L;
            }
            if (seekOffsetWrite >= 0 &&
                    (contentLength < 0 || seekOffsetWrite <= contentLength)) {
                mSeekOffsetWrite.set(seekOffsetWrite);
                return seekOffsetWrite;
            }
            return -1L;
        }
    }

    /**
     * 読み込み用のシーク位置を変更する
     * @param offset ファイルの先頭から見たシーク位置
     * @return 現在の読み込みファイル位置、エラー発生時は-1を返す
     * @throws IOException
     */
    public long seekRead(long offset) throws IOException {
        return seekRead(offset, SEEK_SET);
    }

    /**
     * 読み込み用のシーク位置を変更する
     * @param offset シーク位置
     * @param whence offsetの意味
     * <ul>
     * <li>{@link #SEEK_SET} ファイルの先頭から
     * <li>{@link #SEEK_CUR} 現在の読み込み用シーク位置から
     * <li>{@link #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;
        }
    }

    /**
     * 書き込みファイル位置を取得する
     * @return
     */
    public long tellWrite() {
        return mSeekOffsetWrite.get();
    }

    /**
     * 読み込みファイル位置を取得する
     * @return
     */
    public long tellRead() {
        return mSeekOffsetRead.get();
    }

    /**
     * ファイルにデータを書き込む
     * @param buffer データ
     * @param offset bufferの読み込み開始位置
     * @param count 書き込むバイト数
     * @throws IOException
     */
    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());
        }
    }

    /**
     * ファイル書き込みのメイン処理。
     * {@link #mLockSeekOffsetWrite} と {@link #mLockFile} のlock付きで呼び出される
     * @param offsetSeekWrite 書き込み開始位置
     * @param buffer
     * @param offset
     * @param count
     * @throws IOException
     * @see #write(byte[] buffer, int offset, int count)
     */
    abstract protected void writeImpl(long offsetSeekWrite, byte[] buffer, int offset, int count) throws IOException;

    /**
     * ファイルを1バイト読み込む
     * @return 読み込んだデータ。ファイル終端に到達していたら-1
     * @throws IOException
     */
    abstract public int read() throws IOException;

    /**
     * ファイルを読み込む
     * @param buffer ファイルの読み込み先。配列の先頭からサイズぶん読み込もうとする
     * @return 読み込んだバイト数。ファイル終端に到達していたら-1
     * @throws IOException
     */
    public int read(byte[] buffer) throws IOException {
        return read(buffer, 0, buffer.length);
    }

    /**
     * ファイルを読み込む
     * @param buffer ファイルの読み込み先
     * @param offset bufferの書き込み開始位置
     * @param count 書き込むバイト数
     * @return 読み込んだバイト数。ファイル終端に到達していたら-1
     * @throws IOException
     */
    public int read(byte[] buffer, int offset, int count) throws IOException {
        final int read;
        int readCount = count;
        long seekOffsetRead;
        final long contentLength = mContentLength.get();
        synchronized (mSeekOffsetRead) {
            seekOffsetRead = mSeekOffsetRead.get();
            if (contentLength >= 0 && seekOffsetRead == contentLength) {
                assert seekOffsetRead == getOffsetWritten();
                // ファイル終端
                read = -1;
            } else {
                final int remainFileLength = (int) (getOffsetWritten() - 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;
    }

    /**
     * ファイル読み込みのメイン処理。
     * {@link #mSeekOffsetRead} のlock付きで呼び出される
     * @param seekOffsetRead 読み込み開始位置
     * @param buffer
     * @param offset
     * @param count
     * @throws IOException
     * @see #read(byte[] buffer, int offset, int count)
     */
    abstract public int readImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException;

    /**
     * ファイルをbufferを満たすまで読み込む
     * @param buffer ファイルの読み込み先
     * @throws IOException
     */
    public void readFully(byte[] buffer) throws IOException {
        readFully(buffer, 0, buffer.length);
    }

    /**
     * ファイルをbufferを満たすまで読み込む
     * @param buffer ファイルの読み込み先
     * @param offset bufferの書き込み開始位置
     * @param count 書き込むバイト数
     * @throws IOException
     */
    public void readFully(byte[] buffer, int offset, int count) throws IOException {
        long seekOffsetRead;
        final long contentLength = mContentLength.get();
        synchronized (mSeekOffsetRead) {
            seekOffsetRead = mSeekOffsetRead.get();
            if (contentLength >= 0 && seekOffsetRead + count > contentLength) {
                // ファイル終端
                throw new EOFException();
            } else {
                long offsetWritten = getOffsetWritten();
                while (count > (int) (offsetWritten - seekOffsetRead)) {
                    if (!mWrite) {
                        throw new IOException("file was written but size is invalid");
                    }
                    mLockFile.lock();
                    try {
                        mConditionFile.await();
                    } catch (InterruptedException e) {
                        if (DEBUG_LOGD) {
                            Log.d(LOG_TAG, e.toString(), e);
                        }
                    } finally {
                        mLockFile.unlock();
                    }
                    offsetWritten = getOffsetWritten();
                }

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

    /**
     * ファイル完全読み込みのメイン処理。
     * {@link #mSeekOffsetRead} のlock付きで呼び出される
     * @param seekOffsetRead 読み込み開始位置
     * @param buffer
     * @param offset
     * @param count
     * @throws IOException
     * @see #readFully(byte[] buffer, int offset, int count)
     */
    abstract public void readFullyImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException;

    /**
     * ファイルの書き込みが終了したことを知らせる
     */
    public void endWrite() {
        mWrite = false;
    }

    /**
     * シーク位置は変更せずにファイルを任意の位置から読み込む
     * @param offset 読み込むファイルのオフセット位置
     * @param head ファイルの読み込み先。配列の大きさがそのまま読み込むバイト数指定になる
     * @return 実際に読み込んだバイト数。-1ならファイル終点到達
     * @throws IOException
     */
    abstract public int readTemporary(long offset, byte[] head) throws IOException;

    /**
     * ファイルの読み込みに待ち時間が必要か確認する
     * @return
     * @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;
    }

    /**
     * ファイルのパスが等しいか確認する
     * @param path
     * @return
     */
    public boolean equalFilePath(String path) {
        return equalFilePath(new File(path));
    }

    /**
     * ファイルのパスが等しいか確認する
     * @param path
     * @return
     */
    public boolean equalFilePath(File path) {
        return mFilePath.equals(path);
    }

    /**
     * ファイルが並行して書き込み中か確認する
     * @return
     */
    public boolean isWriting() {
        return mWrite;
    }

    /**
     * ファイルが既にclose済みか確認する
     * @return
     */
    public boolean wasClosed() {
        return mWasClosed;
    }

    /**
     * 既に書き込み済みの位置を取得する
     * @return
     */
    protected long getOffsetWritten() {
        final long offsetWritten;
        if (mWrite) {
            offsetWritten = mSeekOffsetWrite.get();
        } else {
            offsetWritten = mContentLength.get();
            assert offsetWritten > 0;
        }
        return offsetWritten;
    }

    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 == getOffsetWritten();
                    // ファイル終端
                    read = -1;
                } else {
                    while (true) {
                        long offsetWritten = getOffsetWritten();
                        if (seekOffsetRead < offsetWritten) {
                            break;
                        }
                        if (!mWrite) {
                            throw new IOException("file was written but size is invalid");
                        }
                        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 contentLength = mContentLength.get();
            long seekOffsetRead;
            synchronized (mSeekOffsetReadStream) {
                seekOffsetRead = mSeekOffsetReadStream.get();
                if (contentLength >= 0 && seekOffsetRead == contentLength) {
                    assert seekOffsetRead == getOffsetWritten();
                    // ファイル終端
                    read = -1;
                } else {
                    long offsetWritten = getOffsetWritten();
                    final int remainFileLength = (int) (offsetWritten - seekOffsetRead);
                    if (readCount > remainFileLength) {
                        if (remainFileLength >= 0) {
                            readCount = remainFileLength;
                        } else {
                            Log.e(LOG_TAG, Log.buf().append("Invalid seekOffset: write=")
                                    .append(offsetWritten).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(mSeekOffsetWrite.get())
                        .append(" contentLength=").append(contentLength)
                        .toString());
            }
            return read;
        }

        @Override
        public long skip(long n) throws IOException {
            long skip;
            long seekOffsetReadStream;
            if (n <= 0) {
                skip = 0;
                seekOffsetReadStream = mSeekOffsetReadStream.get();
            } else {
                long offsetWritten = getOffsetWritten();
                synchronized (mSeekOffsetReadStream) {
                    long remain = offsetWritten - 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(mSeekOffsetWrite.get())
                        .append(" contentLength=").append(mContentLength.get())
                        .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;
    }

    /**
     * このファイルを並行して読み込める {@link #InputStream} を作成する
     * @return
     */
    abstract public InputStream createInputStream();
}