/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/*
 * Android Notice
 * In this class the address length was changed from long to int.
 * This is due to performance optimizations for the device.
 *
 * Also this class was copied from a newer version of harmony.
 */

package org.apache.harmony.nio.internal;

import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.MappedByteBufferAdapter;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import org.apache.harmony.luni.platform.IFileSystem;
import org.apache.harmony.luni.platform.Platform;
import org.apache.harmony.luni.platform.PlatformAddress;
import org.apache.harmony.luni.platform.PlatformAddressFactory;

/*
 * The file channel impl class is the bridge between the logical channels
 * described by the NIO channel framework, and the file system implementation
 * provided by the port layer.
 *
 * This class is non-API, but implements the API of the FileChannel interface.
 */
public abstract class FileChannelImpl extends FileChannel {

    // Reference to the portable file system code.
    private static final IFileSystem fileSystem = Platform.getFileSystem();

    private static final int ALLOC_GRANULARITY;

    static {
        try {
            ALLOC_GRANULARITY = fileSystem.getAllocGranularity();
        } catch (IOException e) {
            throw new Error(e);
        }
    }

    // Handle to the open file
    private final int handle;

    // The object that will track all outstanding locks on this channel.
    private final LockManager lockManager = new LockManager();

    private static class RepositioningLock {}
    private final Object repositioningLock = new RepositioningLock();

    private final Object stream;

    /*
     * Create a new file channel implementation class that wraps the given file
     * handle and operates in the specified mode.
     *
     */
    public FileChannelImpl(Object stream, int handle) {
        super();
        this.handle = handle;
        this.stream = stream;
    }

    /*
     * Helper method to throw an exception if the channel is already closed.
     * Note that we don't bother to synchronize on this test since the file may
     * be closed by operations beyond our control anyways.
     */
    protected final void openCheck() throws ClosedChannelException {
        if (!isOpen()) {
            throw new ClosedChannelException();
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see java.nio.channels.spi.AbstractInterruptibleChannel#implCloseChannel()
     */
    protected void implCloseChannel() throws IOException {
        if (stream instanceof Closeable) {
            ((Closeable) stream).close();
        }
    }

    protected FileLock basicLock(long position, long size, boolean shared,
            boolean wait) throws IOException {
        if (position < 0 || size < 0) {
            throw new IllegalArgumentException("Lock position and size must be non-negative");
        }
        int lockType = shared ? IFileSystem.SHARED_LOCK_TYPE : IFileSystem.EXCLUSIVE_LOCK_TYPE;
        FileLock pendingLock = new FileLockImpl(this, position, size, shared);
        lockManager.addLock(pendingLock);

        if (fileSystem.lock(handle, position, size, lockType, wait)) {
            return pendingLock;
        }

        // Lock acquisition failed
        lockManager.removeLock(pendingLock);
        return null;
    }

    /*
     * Acquire a lock on the receiver, blocks if the lock cannot be obtained
     * immediately.
     *
     * @see java.nio.channels.FileChannel#lock(long, long, boolean)
     */
    public final FileLock lock(long position, long size, boolean shared)
            throws IOException {
        openCheck();
        FileLock resultLock = null;
        {
            boolean completed = false;
            try {
                begin();
                resultLock = basicLock(position, size, shared, true);
                completed = true;
            } finally {
                end(completed);
            }
        }
        return resultLock;
    }

    /*
     * Attempts to acquire the given lock, but does not block. If the lock
     * cannot be acquired the method returns null.
     *
     * @see java.nio.channels.FileChannel#tryLock(long, long, boolean)
     */
    public final FileLock tryLock(long position, long size, boolean shared)
            throws IOException {
        openCheck();
        return basicLock(position, size, shared, false);
    }

    /*
     * Non-API method to release a given lock on a file channel. Assumes that
     * the lock will mark itself invalid after successful unlocking.
     */
    void release(FileLock lock) throws IOException {
        openCheck();
        fileSystem.unlock(handle, lock.position(), lock.size());
        lockManager.removeLock(lock);
    }

    @Override public void force(boolean metadata) throws IOException {
        openCheck();
        fileSystem.fsync(handle, metadata);
    }

    public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;

    protected final MappedByteBuffer mapImpl(MapMode mapMode, long position, long size)
            throws IOException {
        if (position + size > size()) {
            fileSystem.truncate(handle, position + size);
        }
        long alignment = position - position % ALLOC_GRANULARITY;
        int offset = (int) (position - alignment);
        PlatformAddress address = PlatformAddressFactory.allocMap(handle,
                alignment, size + offset, mapMode);
        return new MappedByteBufferAdapter(address, (int) size, offset, mapMode);
    }

    /*
     * Returns the current file position.
     */
    public long position() throws IOException {
        openCheck();
        return fileSystem.seek(handle, 0L, IFileSystem.SEEK_CUR);
    }

    /*
     * Sets the file pointer.
     */
    public FileChannel position(long newPosition) throws IOException {
        openCheck();
        if (newPosition < 0) {
            throw new IllegalArgumentException("New position must be non-negative");
        }

        synchronized (repositioningLock) {
            fileSystem.seek(handle, newPosition, IFileSystem.SEEK_SET);
        }
        return this;
    }

    public int read(ByteBuffer buffer, long position) throws IOException {
        FileChannelImpl.checkWritable(buffer);
        if (position < 0) {
            throw new IllegalArgumentException();
        }
        openCheck();
        if (!buffer.hasRemaining()) {
            return 0;
        }
        synchronized (repositioningLock) {
            int bytesRead = 0;
            long preReadPosition = position();
            position(position);
            try {
                bytesRead = read(buffer);
            } finally {
                position(preReadPosition);
            }
            return bytesRead;
        }
    }

    public int read(ByteBuffer buffer) throws IOException {
        FileChannelImpl.checkWritable(buffer);
        openCheck();
        if (!buffer.hasRemaining()) {
            return 0;
        }
        boolean completed = false;
        int bytesRead = 0;
        synchronized (repositioningLock) {
            if (buffer.isDirect()) {
                DirectBuffer directBuffer = (DirectBuffer) buffer;
                int address = directBuffer.getEffectiveAddress().toInt();
                try {
                    begin();
                    /*
                     * if (bytesRead <= EOF) dealt by read completed = false;
                     */
                    bytesRead = (int) fileSystem.readDirect(handle, address,
                            buffer.position(), buffer.remaining());
                    completed = true;
                } finally {
                    end(completed && bytesRead >= 0);
                }
            } else {
                try {
                    begin();
                    /*
                     * if (bytesRead <= EOF) dealt by read completed = false;
                     */
                    bytesRead = (int) fileSystem.read(handle, buffer.array(),
                            buffer.arrayOffset() + buffer.position(), buffer
                                    .remaining());
                    completed = true;
                } finally {
                    end(completed && bytesRead >= 0);
                }
            }
            if (bytesRead > 0) {
                buffer.position(buffer.position() + bytesRead);
            }
        }
        return bytesRead;
    }

    public long read(ByteBuffer[] buffers, int offset, int length) throws IOException {
        if (offset < 0 || length < 0 || offset + length > buffers.length) {
            throw new IndexOutOfBoundsException();
        }
        openCheck();
        int count = FileChannelImpl.calculateTotalRemaining(buffers, offset, length, true);
        if (count == 0) {
            return 0;
        }
        ByteBuffer[] directBuffers = new ByteBuffer[length];
        int[] handles = new int[length];
        int[] offsets = new int[length];
        int[] lengths = new int[length];
        for (int i = 0; i < length; i++) {
            ByteBuffer buffer = buffers[i + offset];
            if (!buffer.isDirect()) {
                buffer = ByteBuffer.allocateDirect(buffer.remaining());
                directBuffers[i] = buffer;
                offsets[i] = 0;
            } else {
                offsets[i] = buffer.position();
            }
            handles[i] = ((DirectBuffer) buffer).getEffectiveAddress().toInt();
            lengths[i] = buffer.remaining();
        }
        long bytesRead = 0;
        {
            boolean completed = false;
            try {
                begin();
                synchronized (repositioningLock) {
                    bytesRead = fileSystem.readv(handle, handles, offsets,
                            lengths, length);

                }
                completed = true;
                /*
                 * if (bytesRead < EOF) //dealt by readv? completed = false;
                 */
            } finally {
                end(completed);
            }
        }
        int end = offset + length;
        long bytesRemaining = bytesRead;
        for (int i = offset; i < end && bytesRemaining > 0; i++) {
            if (buffers[i].isDirect()) {
                if (lengths[i] < bytesRemaining) {
                    int pos = buffers[i].limit();
                    buffers[i].position(pos);
                    bytesRemaining -= lengths[i];
                } else {
                    int pos = (int) bytesRemaining;
                    buffers[i].position(pos);
                    break;
                }
            } else {
                ByteBuffer buf = directBuffers[i - offset];
                if (bytesRemaining < buf.remaining()) {
                    // this is the last step.
                    int pos = buf.position();
                    buffers[i].put(buf);
                    buffers[i].position(pos + (int) bytesRemaining);
                    bytesRemaining = 0;
                } else {
                    bytesRemaining -= buf.remaining();
                    buffers[i].put(buf);
                }
            }
        }
        return bytesRead;
    }

    /*
     * Returns the current file size, as an integer number of bytes.
     */
    public long size() throws IOException {
        openCheck();
        return fileSystem.length(handle);
    }

    public long transferFrom(ReadableByteChannel src, long position, long count)
            throws IOException {
        openCheck();
        if (!src.isOpen()) {
            throw new ClosedChannelException();
        }
        if (position < 0 || count < 0 || count > Integer.MAX_VALUE) {
            throw new IllegalArgumentException();
        }
        if (position > size()) {
            return 0;
        }

        ByteBuffer buffer = null;

        try {
            if (src instanceof FileChannel) {
                FileChannel fileSrc = (FileChannel) src;
                long size = fileSrc.size();
                long filePosition = fileSrc.position();
                count = Math.min(count, size - filePosition);
                buffer = fileSrc.map(MapMode.READ_ONLY, filePosition, count);
                fileSrc.position(filePosition + count);
            } else {
                buffer = ByteBuffer.allocateDirect((int) count);
                src.read(buffer);
                buffer.flip();
            }
            return write(buffer, position);
        } finally {
            // unmap the buffer
            if (buffer != null) {
                // all children of FileChannelImpl currently returns
                // an instance of DirectBuffer from map() method
                ((DirectBuffer) buffer).free();
            }
        }
    }

    public long transferTo(long position, long count, WritableByteChannel target)
            throws IOException {
        openCheck();
        if (!target.isOpen()) {
            throw new ClosedChannelException();
        }
        if (target instanceof ReadOnlyFileChannel) {
            throw new NonWritableChannelException();
        }
        if (position < 0 || count < 0) {
            throw new IllegalArgumentException();
        }

        if (count == 0 || position >= size()) {
            return 0;
        }
        ByteBuffer buffer = null;
        count = Math.min(count, size() - position);
        if (target instanceof SocketChannelImpl) {
            // only socket can be transfered by system call
            return kernelTransfer(handle, ((SocketChannelImpl) target).getFD(),
                    position, count);
        }

        try {
            buffer = map(MapMode.READ_ONLY, position, count);
            return target.write(buffer);
        } finally {
            // unmap the buffer
            if (buffer != null) {
                // all children of FileChannelImpl currently returns
                // an instance of DirectBuffer from map() method
                ((DirectBuffer) buffer).free();
            }
        }
    }

    private long kernelTransfer(int l, FileDescriptor fd, long position,
            long count) throws IOException {
        boolean completed = false;
        try {
            begin();
            long ret = fileSystem.transfer(l, fd, position, count);
            completed = true;
            return ret;
        } finally {
            end(completed);
        }
    }

    public FileChannel truncate(long size) throws IOException {
        openCheck();
        if (size < 0) {
            throw new IllegalArgumentException();
        }
        if (size < size()) {
            synchronized (repositioningLock) {
                long position = position();
                fileSystem.truncate(handle, size);
                /*
                 * FIXME: currently the port library always modifies the
                 * position to given size. not sure it is a bug or intended
                 * behaviour, so I always reset the position to proper value as
                 * Java Spec.
                 */
                position(position > size ? size : position);
            }
        }
        return this;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.nio.channels.WritableByteChannel#write(java.nio.ByteBuffer)
     */

    public int write(ByteBuffer buffer, long position) throws IOException {
        if (null == buffer) {
            throw new NullPointerException();
        }
        if (position < 0) {
            throw new IllegalArgumentException();
        }
        openCheck();
        if (!buffer.hasRemaining()) {
            return 0;
        }
        int bytesWritten = 0;
        synchronized (repositioningLock) {
            long preWritePosition = position();
            position(position);
            try {
                bytesWritten = writeImpl(buffer);
            } finally {
                position(preWritePosition);
            }
        }
        return bytesWritten;
    }

    public int write(ByteBuffer buffer) throws IOException {
        openCheck();
        return writeImpl(buffer);
    }

    private int writeImpl(ByteBuffer buffer) throws IOException {
        int bytesWritten;
        boolean completed = false;
        synchronized (repositioningLock) {
            if (buffer.isDirect()) {
                DirectBuffer directBuffer = (DirectBuffer) buffer;
                int address = directBuffer.getEffectiveAddress().toInt();
                try {
                    begin();
                    bytesWritten = (int) fileSystem.writeDirect(handle,
                            address, buffer.position(), buffer.remaining());
                    completed = true;
                } finally {
                    end(completed);
                }
            } else {
                try {
                    begin();
                    bytesWritten = (int) fileSystem.write(handle, buffer
                            .array(), buffer.arrayOffset() + buffer.position(),
                            buffer.remaining());
                    completed = true;
                } finally {
                    end(completed);
                }
            }
            if (bytesWritten > 0) {
                buffer.position(buffer.position() + bytesWritten);
            }
        }
        return bytesWritten;
    }

    public long write(ByteBuffer[] buffers, int offset, int length) throws IOException {
        if (offset < 0 || length < 0 || (offset + length) > buffers.length) {
            throw new IndexOutOfBoundsException();
        }
        openCheck();
        int count = FileChannelImpl.calculateTotalRemaining(buffers, offset, length, false);
        if (count == 0) {
            return 0;
        }
        int[] handles = new int[length];
        int[] offsets = new int[length];
        int[] lengths = new int[length];
        // BEGIN android-changed
        // list of allocated direct ByteBuffers to prevent them from being GC-ed
        DirectBuffer[] allocatedBufs = new DirectBuffer[length];

        for (int i = 0; i < length; i++) {
            ByteBuffer buffer = buffers[i + offset];
            if (!buffer.isDirect()) {
                ByteBuffer directBuffer = ByteBuffer.allocateDirect(buffer.remaining());
                directBuffer.put(buffer);
                directBuffer.flip();
                buffer = directBuffer;
                allocatedBufs[i] = (DirectBuffer) directBuffer;
                offsets[i] = 0;
            } else {
                offsets[i] = buffer.position();
                allocatedBufs[i] = null;
            }
            handles[i] = ((DirectBuffer) buffer).getEffectiveAddress().toInt();
            lengths[i] = buffer.remaining();
        }
        // END android-changed

        long bytesWritten = 0;
        boolean completed = false;
        synchronized (repositioningLock) {
            try {
                begin();
                bytesWritten = fileSystem.writev(handle, handles, offsets,
                        lengths, length);
                completed = true;
            } finally {
                end(completed);
                // BEGIN android-added
                // free temporary direct buffers
                for (int i = 0; i < length; ++i) {
                    if (allocatedBufs[i] != null) {
                        allocatedBufs[i].free();
                    }
                }
                // END android-added
            }
        }

        long bytesRemaining = bytesWritten;
        for (int i = offset; i < length + offset; i++) {
            if (bytesRemaining > buffers[i].remaining()) {
                int pos = buffers[i].limit();
                buffers[i].position(pos);
                bytesRemaining -= buffers[i].remaining();
            } else {
                int pos = buffers[i].position() + (int) bytesRemaining;
                buffers[i].position(pos);
                break;
            }
        }
        return bytesWritten;
    }

    static void checkWritable(ByteBuffer buffer) {
        if (buffer.isReadOnly()) {
            throw new IllegalArgumentException("read-only buffer");
        }
    }

    /**
     * @param copyingIn true if we're copying data into the buffers (typically
     * because the caller is a file/network read operation), false if we're
     * copying data out of the buffers (for a file/network write operation).
     */
    static int calculateTotalRemaining(ByteBuffer[] buffers, int offset, int length, boolean copyingIn) {
        int count = 0;
        for (int i = offset; i < offset + length; ++i) {
            count += buffers[i].remaining();
            if (copyingIn) {
                checkWritable(buffers[i]);
            }
        }
        return count;
    }

    public int getHandle() {
        return handle;
    }
}
