package jp.sourceforge.nicoro;

import android.os.SystemClock;

import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

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

public class MultiRandomAccessFile extends AbstractMultiRandomAccessFile {
//	private static final int READ_CACHE_SIZE = 256 * 1024;
    private static final int READ_CACHE_SIZE = 64 * 1024;
//    private static final int READ_CACHE_SIZE = 32 * 1024;

	private ReadCache mReadCache = new ReadCache(READ_CACHE_SIZE);

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

    @Override
    protected void writeImpl(long offsetSeekWrite, byte[] buffer, int offset, int count) throws IOException {
        mFile.seek(offsetSeekWrite);
        mFile.write(buffer, offset, count);
    }

	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 == getOffsetWritten();
				// ファイル終端
				read = -1;
			} else {
                if (!mReadCache.isInBuffer(seekOffsetRead)) {
                    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;
                        }
                    }
                    mReadCache.readCache(seekOffsetRead);
                }
                read = mReadCache.read(seekOffsetRead);
//                mLockFile.lock();
//                try {
//                    mFile.seek(seekOffsetRead);
//                    read = mFile.read();
//                } finally {
//                    mLockFile.unlock();
//                }
                if (read >= 0) {
                    mSeekOffsetRead.incrementAndGet();
                }
			}
		}
		return read;
	}

    @Override
    public int readImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException {
        if (!mReadCache.isInBuffer(seekOffsetRead)) {
            mReadCache.readCache(seekOffsetRead);
        }
        return mReadCache.read(seekOffsetRead, buffer, offset, count);
//      mLockFile.lock();
//      try {
//          mFile.seek(seekOffsetRead);
//          return mFile.read(buffer, offset, count);
//      } finally {
//          mLockFile.unlock();
//      }
    }

    @Override
    public void readFullyImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException {
        mReadCache.readFully(seekOffsetRead, buffer, offset, count);
//      mLockFile.lock();
//      try {
//          mFile.seek(seekOffsetRead);
//          mFile.readFully(buffer, offset, count);
//      } finally {
//          mLockFile.unlock();
//      }
    }

    @Override
	public int readTemporary(long offset, byte[] head) throws IOException {
	    mLockFile.lock();
	    try {
	        mFile.seek(offset);
	        int read = mFile.read(head);
	        return read;
	    } finally {
	        mLockFile.unlock();
	    }
	}

	private class ReadCache {
	    private byte[] buffer;
	    private long cacheOffset;
	    private int cacheSize;

	    ReadCache(int bufSize) {
	        buffer = new byte[bufSize];
	        cacheOffset = 0;
	        cacheSize = 0;
	    }

	    boolean isInBuffer(long offsetRead) {
            return (offsetRead >= cacheOffset
                    && offsetRead < cacheOffset + cacheSize);
	    }
	    int read(long offsetRead) {
            return buffer[(int) (offsetRead - cacheOffset)];
	    }
	    int read(long offsetRead, byte[] buf, int bufOffset, int bufCount) {
	        int start = (int) (offsetRead - cacheOffset);
	        int readCount = Math.min(bufCount, cacheSize - start);
	        System.arraycopy(buffer, start, buf, bufOffset, readCount);
	        return readCount;
	    }

        void readFully(long offsetRead, byte[] buf, int bufOffset, int bufCount)
        throws IOException {
            if (!isInBuffer(offsetRead)) {
                readCache(offsetRead);
            }
            int read = read(offsetRead, buf, bufOffset, bufCount);
            offsetRead += read;
            bufOffset += read;
            bufCount -= read;
            while (bufCount > 0) {
                readCache(offsetRead);
                read = read(offsetRead, buf, bufOffset, bufCount);
                offsetRead += read;
                bufOffset += read;
                bufCount -= read;
            }
            assert bufCount == 0;
        }

        void readCache(long offsetRead) throws IOException {
            long startTime = 0;
            if (DEBUG_LOGV) {
                startTime = SystemClock.elapsedRealtime();
            }
            long offsetWritten = getOffsetWritten();
            int readCount = buffer.length;
            final int remainFileLength = (int) (offsetWritten - offsetRead);
            if (readCount > remainFileLength) {
                readCount = remainFileLength;
            }

            int read;
            mLockFile.lock();
            try {
                mFile.seek(offsetRead);
                read = mFile.read(buffer, 0, readCount);
            } finally {
                mLockFile.unlock();
            }
            if (read >= 0) {
                cacheOffset = offsetRead;
                cacheSize = read;
            } else {
                throw new EOFException();
            }
            if (DEBUG_LOGV) {
                Log.v(LOG_TAG, Log.buf().append(getClass().getSimpleName())
                        .append("#readCache() time=")
                        .append(SystemClock.elapsedRealtime() - startTime)
                        .append("ms")
                        .toString());
            }
        }
    }

	private class ReadInputStream extends AbstractReadInputStream {
        @Override
		protected int readImpl(long seekOffsetRead) throws IOException {
            mLockFile.lock();
            try {
                mFile.seek(seekOffsetRead);
                return mFile.read();
            } finally {
                mLockFile.unlock();
            }
        }

		@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;
            int remainFileLength = -1;
		    try {
    			synchronized (mSeekOffsetReadStream) {
        			seekOffsetRead = mSeekOffsetReadStream.get();
        			if (contentLength >= 0 && seekOffsetRead == contentLength) {
        				assert seekOffsetRead == getOffsetWritten();
        				// ファイル終端
        				read = -1;
        			} else {
        				remainFileLength = (int) (getOffsetWritten() - seekOffsetRead);
        				if (readCount > remainFileLength) {
        					readCount = remainFileLength;
        				}
        				final int remainBufferLength = buffer.length - offset;
        				if (readCount > remainBufferLength) {
        					readCount = remainBufferLength;
        				}
//        				if (readCount < 0) {
//        				    if (DEBUG_LOGD) {
//        				        Log.d(LOG_TAG, "readCount becomes 0!");
//        				    }
//        				    readCount = 0;
//        				}

        	            mLockFile.lock();
        	            try {
        					mFile.seek(seekOffsetRead);
        					read = mFile.read(buffer, offset, readCount);
        	            } finally {
        	                mLockFile.unlock();
        	            }
        				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;
		    } catch (IOException e) {
		        throw e;
		    } catch (Exception e) {
		        // FIXME クラッシュ解析向け
		        String detailMessage = "original count=" + count
    		        + ", readCount=" + readCount
    		        + ", remainFileLength=" + remainFileLength;
		        throw new RuntimeException(detailMessage, e);
		    }
		}

        @Override
		protected int readImpl(long seekOffsetRead, byte[] buffer, int offset, int count) throws IOException {
            mLockFile.lock();
            try {
                mFile.seek(seekOffsetRead);
                return mFile.read(buffer, offset, count);
            } finally {
                mLockFile.unlock();
            }
        }
	}
	public InputStream createInputStream() {
		return new ReadInputStream();
	}
}
