/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed 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.
 */

package com.android.apksig.internal.util;

import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * {@link DataSource} backed by a {@link RandomAccessFile}.
 */
public class RandomAccessFileDataSource implements DataSource {

    private static final int MAX_READ_CHUNK_SIZE = 65536;

    private final RandomAccessFile mFile;
    private final long mOffset;
    private final long mSize;

    /**
     * Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
     * specified the whole file. Changes to the contents of the file, including the size of the
     * file, will be visible in this data source.
     */
    public RandomAccessFileDataSource(RandomAccessFile file) {
        mFile = file;
        mOffset = 0;
        mSize = -1;
    }

    /**
     * Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
     * specified region of the provided file. Changes to the contents of the file will be visible in
     * this data source.
     *
     * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative.
     */
    public RandomAccessFileDataSource(RandomAccessFile file, long offset, long size) {
        if (offset < 0) {
            throw new IndexOutOfBoundsException("offset: " + size);
        }
        if (size < 0) {
            throw new IndexOutOfBoundsException("size: " + size);
        }
        mFile = file;
        mOffset = offset;
        mSize = size;
    }

    @Override
    public long size() {
        if (mSize == -1) {
            try {
                return mFile.length();
            } catch (IOException e) {
                return 0;
            }
        } else {
            return mSize;
        }
    }

    @Override
    public RandomAccessFileDataSource slice(long offset, long size) {
        long sourceSize = size();
        checkChunkValid(offset, size, sourceSize);
        if ((offset == 0) && (size == sourceSize)) {
            return this;
        }

        return new RandomAccessFileDataSource(mFile, mOffset + offset, size);
    }

    @Override
    public void feed(long offset, long size, DataSink sink) throws IOException {
        long sourceSize = size();
        checkChunkValid(offset, size, sourceSize);
        if (size == 0) {
            return;
        }

        long chunkOffsetInFile = mOffset + offset;
        long remaining = size;
        byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)];
        while (remaining > 0) {
            int chunkSize = (int) Math.min(remaining, buf.length);
            synchronized (mFile) {
                mFile.seek(chunkOffsetInFile);
                mFile.readFully(buf, 0, chunkSize);
            }
            sink.consume(buf, 0, chunkSize);
            chunkOffsetInFile += chunkSize;
            remaining -= chunkSize;
        }
    }

    @Override
    public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
        long sourceSize = size();
        checkChunkValid(offset, size, sourceSize);
        if (size == 0) {
            return;
        }
        if (size > dest.remaining()) {
            throw new BufferOverflowException();
        }

        long offsetInFile = mOffset + offset;
        int remaining = size;
        int prevLimit = dest.limit();
        try {
            // FileChannel.read(ByteBuffer) reads up to dest.remaining(). Thus, we need to adjust
            // the buffer's limit to avoid reading more than size bytes.
            dest.limit(dest.position() + size);
            FileChannel fileChannel = mFile.getChannel();
            while (remaining > 0) {
                int chunkSize;
                synchronized (mFile) {
                    fileChannel.position(offsetInFile);
                    chunkSize = fileChannel.read(dest);
                }
                offsetInFile += chunkSize;
                remaining -= chunkSize;
            }
        } finally {
            dest.limit(prevLimit);
        }
    }

    @Override
    public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
        if (size < 0) {
            throw new IndexOutOfBoundsException("size: " + size);
        }
        ByteBuffer result = ByteBuffer.allocate(size);
        copyTo(offset, size, result);
        result.flip();
        return result;
    }

    private static void checkChunkValid(long offset, long size, long sourceSize) {
        if (offset < 0) {
            throw new IndexOutOfBoundsException("offset: " + offset);
        }
        if (size < 0) {
            throw new IndexOutOfBoundsException("size: " + size);
        }
        if (offset > sourceSize) {
            throw new IndexOutOfBoundsException(
                    "offset (" + offset + ") > source size (" + sourceSize + ")");
        }
        long endOffset = offset + size;
        if (endOffset < offset) {
            throw new IndexOutOfBoundsException(
                    "offset (" + offset + ") + size (" + size + ") overflow");
        }
        if (endOffset > sourceSize) {
            throw new IndexOutOfBoundsException(
                    "offset (" + offset + ") + size (" + size
                            + ") > source size (" + sourceSize  +")");
        }
    }
}