package jp.sourceforge.nicoro;

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 static jp.sourceforge.nicoro.Log.LOG_TAG;

public class MultiRandomAccessFile implements Closeable {
    private static final boolean DEBUG_LOGD = Release.IS_DEBUG && true;
	private 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;

	RandomAccessFile mFile;
	Object mSync = new Object();

	private long mContentLength;
	long mSeekOffsetWrite;
	private long mSeekOffsetRead;

	private boolean mWasClosed;
	volatile boolean mWrite;

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

		mContentLength = -1L;
		mSeekOffsetWrite = 0L;
		mSeekOffsetRead = 0L;
		mWasClosed = false;
		mWrite = write;
	}

	@Override
	public void close() throws IOException {
		synchronized (mSync) {
			mFile.close();
			mWasClosed = true;
			endWrite();
			mSync.notifyAll();
		}
	}

	public void setLength(long newLength) throws IOException {
		synchronized (mSync) {
			mContentLength = newLength;
			if (mWrite) {
			    if (newLength > 0) {
			        mFile.setLength(newLength);
			    } else {
			        mFile.setLength(0);
			    }
			} else {
				assert newLength <= mFile.length();
			}
		}
	}

	public long length() throws IOException {
		if (mWrite) {
			synchronized (mSync) {
				return lengthNoSync();
			}
		} else {
			return lengthNoSync();
		}
	}
	long lengthNoSync() throws IOException {
//		if (mContentLength >= 0) {
			return mContentLength;
//		} else {
//		    assert false;
//            return mFile.length();
//		}
	}

	public void syncWrite() throws SyncFailedException, IOException {
		if (mWrite) {
			synchronized (mSync) {
				mFile.getFD().sync();
			}
		}
	}

	public long seekWrite(long offset) throws IOException {
		if (offset < 0) {
			return -1L;
		}
		synchronized (mSync) {
			final long contentLength = mContentLength;
			if (contentLength >= 0 && offset > contentLength) {
				return -1L;
			}
			mSeekOffsetWrite = 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 {
		synchronized (mSync) {
			final long contentLength = lengthNoSync();
			final long seekOffsetRead;
			switch (whence) {
			case SEEK_SET:
				seekOffsetRead = offset;
				break;
			case SEEK_CUR:
				seekOffsetRead = mSeekOffsetRead + 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 = seekOffsetRead;
				return seekOffsetRead;
			}
			return -1L;
		}
	}

	public long tellWrite() {
		if (mWrite) {
			synchronized (mSync) {
				return mSeekOffsetWrite;
			}
		} else {
			return mSeekOffsetWrite;
		}
	}

	public long tellRead() {
		synchronized (mSync) {
			return mSeekOffsetRead;
		}
	}

	public void write(byte[] buffer, int offset, int count) throws IOException {
		assert mWrite; // TODO close()の割り込み具合で偶にひっかかるときがある
		synchronized (mSync) {
//			assert (mContentLength >= 0
//					? (mSeekOffsetWrite + count <= mContentLength)
//							: true);
			mFile.seek(mSeekOffsetWrite);
			mFile.write(buffer, offset, count);
			mSeekOffsetWrite += count;
			mSync.notifyAll();
		}
	}

	public int read() throws IOException {
		final int read;
		final long seekOffsetRead;
		synchronized (mSync) {
			seekOffsetRead = mSeekOffsetRead;
			final long length = lengthNoSync();
			if (length >= 0 && seekOffsetRead == length) {
				assert seekOffsetRead == mSeekOffsetWrite;
				// ファイル終端
				read = -1;
			} else {
				while (seekOffsetRead >= mSeekOffsetWrite) {
					try {
						mSync.wait();
					} catch (InterruptedException e) {
						Log.e(LOG_TAG, e.toString(), e);
					}
					if (mWasClosed) {
						return -1;
					}
				}

				mFile.seek(seekOffsetRead);
				read = mFile.read();
				if (read >= 0) {
					++mSeekOffsetRead;
				}
			}
		}
		return read;
	}

    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;
		final long contentLength;
		synchronized (mSync) {
			seekOffsetRead = mSeekOffsetRead;
			seekOffsetWrite = mSeekOffsetWrite;
			contentLength = lengthNoSync();
			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;
				}

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

    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;
        final long contentLength;
        synchronized (mSync) {
            seekOffsetRead = mSeekOffsetRead;
            seekOffsetWrite = mSeekOffsetWrite;
            contentLength = lengthNoSync();
            if (contentLength >= 0 && seekOffsetRead == contentLength) {
                assert seekOffsetRead == seekOffsetWrite;
                // ファイル終端
                throw new EOFException();
            } else {
                while (count > (int) (seekOffsetWrite - seekOffsetRead)) {
                    try {
                        mSync.wait();
                    } catch (InterruptedException e) {
                        if (DEBUG_LOGD) {
                            Log.d(LOG_TAG, e.toString(), e);
                        }
                    }
                    seekOffsetRead = mSeekOffsetRead;
                    seekOffsetWrite = mSeekOffsetWrite;
                }

                mFile.seek(seekOffsetRead);
                mFile.readFully(buffer, offset, count);
                seekOffsetRead += count;
                mSeekOffsetRead = seekOffsetRead;
            }
        }
    }

	public void endWrite() {
		mWrite = false;
	}

	public Object getSync() {
		return mSync;
	}

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

	private class ReadInputStream extends InputStream {
		private long mSeekOffsetReadStream;

		public ReadInputStream() {
			mSeekOffsetReadStream = 0L;
		}

		@Override
	    public int available() throws IOException {
			final int ret = (int) (tellWrite() - mSeekOffsetReadStream);
			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;
			final long seekOffsetRead = mSeekOffsetReadStream;
			synchronized (mSync) {
			    final long length = lengthNoSync();
				if (length >= 0 &&  seekOffsetRead == length) {
					assert seekOffsetRead == mSeekOffsetWrite;
					// ファイル終端
					read = -1;
				} else {
					while (seekOffsetRead >= mSeekOffsetWrite) {
						try {
							mSync.wait();
						} catch (InterruptedException e) {
							Log.e(LOG_TAG, e.toString(), e);
						}
						if (mWasClosed) {
							return -1;
						}
					}

					mFile.seek(seekOffsetRead);
					read = mFile.read();
					if (read >= 0) {
						++mSeekOffsetReadStream;
					}
				}
			}
			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;
			long seekOffsetRead = mSeekOffsetReadStream;
			final long seekOffsetWrite;
			final long contentLength;
			synchronized (mSync) {
				seekOffsetWrite = mSeekOffsetWrite;
				contentLength = lengthNoSync();
				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;
					}

					mFile.seek(seekOffsetRead);
					read = mFile.read(buffer, offset, readCount);
					if (read >= 0) {
						seekOffsetRead += read;
						mSeekOffsetReadStream = 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;
	        if (mWrite) {
				synchronized (mSync) {
			        contentLength = lengthNoSync();
			        seekOffsetWrite = mSeekOffsetWrite;
				}
	        } else {
		        contentLength = lengthNoSync();
		        seekOffsetWrite = mSeekOffsetWrite;
	        }
			long skip;
	        if (n <= 0) {
	        	skip = 0;
	        } else {
				long remain = seekOffsetWrite - mSeekOffsetReadStream;
				if (n > remain) {
					skip = remain;
				} else {
			        skip = n;
				}
				mSeekOffsetReadStream += 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(mSeekOffsetReadStream)
						.append(" seekOffsetWrite=").append(seekOffsetWrite)
						.append(" contentLength=").append(contentLength)
						.toString());
			}
			return skip;
		}
	}
	public InputStream createInputStream() {
		return new ReadInputStream();
	}
}
