package jp.sourceforge.nicoro;

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

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.SyncFailedException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MultiRandomAccessFileMmap extends AbstractMultiRandomAccessFile {
    private static final long FILE_EXPAND_SIZE = 32 * 1024 * 1024;

    private MappedByteBuffer mMmap;
    private MappedByteBuffer mMmapRead;
    private FileChannel mChannel;

    public MultiRandomAccessFileMmap(String file, boolean write)
    throws FileNotFoundException {
        this(new File(file), write);
    }
    public MultiRandomAccessFileMmap(File file, boolean write)
    throws FileNotFoundException {
        super(file, write);
    }

    @Override
    public void close() throws IOException {
        mMmap = null;
        mMmapRead = null;
        mChannel = null;
        super.close();
    }

    @Override
    public void setLength(long newLength) throws IOException {
        super.setLength(newLength);

        FileChannel.MapMode mode;
        if (mWrite) {
            mode = FileChannel.MapMode.READ_WRITE;
        } else {
            mode = FileChannel.MapMode.READ_ONLY;
        }
        long size;
        if (newLength > 0) {
            size = newLength;
        } else {
            size = FILE_EXPAND_SIZE;
        }
        FileChannel channel = mFile.getChannel();
        mChannel = channel;
        mMmap = channel.map(mode, 0, size);
        mMmapRead = (MappedByteBuffer) mMmap.duplicate();
//        mMmapRead = channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
    }

    @Override
    public void syncWrite() throws SyncFailedException, IOException {
        if (mWrite) {
            if (mMmap != null) {
//                mLockFile.lock();
                try {
                    mMmap.force();
                } finally {
//                    mLockFile.unlock();
                }
            }
        }
        super.syncWrite();
    }

    @Override
    public long seekWrite(long offset) throws IOException {
        long ret = super.seekWrite(offset);

        expandMmapIfNeeded(ret);

        return ret;
    }

    @Override
    protected void writeImpl(long offsetSeekWrite, byte[] buffer, int offset, int count) throws IOException {
        expandMmapIfNeeded(offsetSeekWrite + count);

        mMmap.position((int) offsetSeekWrite);
        mMmap.put(buffer, offset, count);
    }

    @Override
    public int read() throws IOException {
        final int read;
        final long seekOffsetRead;
        final long length = mContentLength.get();
        synchronized (mSeekOffsetRead) {
            seekOffsetRead = mSeekOffsetRead.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;
                    }
                }
//                mLockFile.lock();
                try {
//                    mMmap.position((int) seekOffsetRead);
//                    read = mMmap.get() & 0xff;
                    mMmapRead.position((int) seekOffsetRead);
                    read = mMmapRead.get() & 0xff;
                } finally {
//                    mLockFile.unlock();
                }
                if (read >= 0) {
                    mSeekOffsetRead.incrementAndGet();
                }
            }
        }
        return read;
    }

    @Override
    public int readImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException {
//        mLockFile.lock();
        try {
//            mMmap.position((int) seekOffsetRead);
//            mMmap.get(buffer, offset, count);
            mMmapRead.position((int) seekOffsetRead);
            mMmapRead.get(buffer, offset, count);
            return count;
        } finally {
//            mLockFile.unlock();
        }
    }

    @Override
    public void readFullyImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException {
//        mLockFile.lock();
        try {
//            mMmap.position((int) seekOffsetRead);
//            mMmap.get(buffer, offset, count);
            mMmapRead.position((int) seekOffsetRead);
            mMmapRead.get(buffer, offset, count);
        } finally {
//            mLockFile.unlock();
        }
    }

    @Override
    public void endWrite() {
        super.endWrite();
    }

    @Override
    public int readTemporary(long offset, byte[] head) throws IOException {
//        mLockFile.lock();
        try {
//            mMmap.position((int) offset);
//            mMmap.get(head);
            mMmapRead.position((int) offset);
            mMmapRead.get(head);
            return head.length;
        } finally {
//            mLockFile.unlock();
        }
    }

    private class ReadInputStream extends AbstractReadInputStream {
        @Override
        protected int readImpl(long seekOffsetRead) throws IOException {
//            mLockFile.lock();
            try {
//                mMmap.position((int) seekOffsetRead);
//                return mMmap.get() & 0xff;
                mMmapRead.position((int) seekOffsetRead);
                return mMmapRead.get() & 0xff;
            } finally {
//                mLockFile.unlock();
            }
        }

        @Override
        protected int readImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException {
//            mLockFile.lock();
            try {
//                mMmap.position((int) seekOffsetRead);
//                mMmap.get(buffer, offset, count);
                mMmapRead.position((int) seekOffsetRead);
                mMmapRead.get(buffer, offset, count);
                return count;
            } finally {
//                mLockFile.unlock();
            }
        }
    }

    @Override
    public InputStream createInputStream() {
        return new ReadInputStream();
    }

    private void expandMmapIfNeeded(long offset) throws IOException {
        if (offset >= 0 && mMmap != null) {
            assert mChannel != null;
            int capacity = mMmap.capacity();
            int newCapacity = capacity;
            while (offset > newCapacity) {
                newCapacity += FILE_EXPAND_SIZE;
            }
            if (newCapacity > capacity) {
                FileChannel.MapMode mode;
                if (mWrite) {
                    mode = FileChannel.MapMode.READ_WRITE;
                } else {
                    mode = FileChannel.MapMode.READ_ONLY;
                }
                mLockFile.lock();
                try {
                    mMmap = mChannel.map(mode, 0, newCapacity);
                    mMmapRead = (MappedByteBuffer) mMmap.duplicate();
//                    mMmapRead = mChannel.map(FileChannel.MapMode.READ_ONLY, 0, newCapacity);
                } finally {
                    mLockFile.unlock();
                }
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, Log.buf().append(getClass().getSimpleName())
                            .append("#expandMmapIfNeeded: offset=").append(offset)
                            .append(" capacity=").append(capacity)
                            .append(" newCapacity=").append(capacity)
                            .toString());
                }
            }
        }
    }
}
