package org.apache.commons.jcs3.auxiliary.disk.indexed;

/*
 * 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.
 */

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;

import org.apache.commons.jcs3.engine.behavior.IElementSerializer;
import org.apache.commons.jcs3.log.Log;
import org.apache.commons.jcs3.log.LogManager;

/** Provides thread safe access to the underlying random access file. */
public class IndexedDisk implements AutoCloseable
{
    /** The size of the header that indicates the amount of data stored in an occupied block. */
    public static final byte HEADER_SIZE_BYTES = 4;

    /** The serializer. */
    private final IElementSerializer elementSerializer;

    /** The logger */
    private static final Log log = LogManager.getLog(IndexedDisk.class);

    /** The path to the log directory. */
    private final String filepath;

    /** The data file. */
    private final FileChannel fc;

    /**
     * Constructor for the Disk object
     * <p>
     * @param file
     * @param elementSerializer
     * @throws IOException
     */
    public IndexedDisk(final File file, final IElementSerializer elementSerializer)
        throws IOException
    {
        this.filepath = file.getAbsolutePath();
        this.elementSerializer = elementSerializer;
        this.fc = FileChannel.open(file.toPath(),
                StandardOpenOption.CREATE,
                StandardOpenOption.READ,
                StandardOpenOption.WRITE);
    }

    /**
     * This reads an object from the given starting position on the file.
     * <p>
     * The first four bytes of the record should tell us how long it is. The data is read into a byte
     * array and then an object is constructed from the byte array.
     * <p>
     * @return Serializable
     * @param ded
     * @throws IOException
     * @throws ClassNotFoundException
     */
    protected <T> T readObject(final IndexedDiskElementDescriptor ded)
        throws IOException, ClassNotFoundException
    {
        String message = null;
        boolean corrupted = false;
        final long fileLength = fc.size();
        if (ded.pos > fileLength)
        {
            corrupted = true;
            message = "Record " + ded + " starts past EOF.";
        }
        else
        {
            final ByteBuffer datalength = ByteBuffer.allocate(HEADER_SIZE_BYTES);
            fc.read(datalength, ded.pos);
            datalength.flip();
            final int datalen = datalength.getInt();
            if (ded.len != datalen)
            {
                corrupted = true;
                message = "Record " + ded + " does not match data length on disk (" + datalen + ")";
            }
            else if (ded.pos + ded.len > fileLength)
            {
                corrupted = true;
                message = "Record " + ded + " exceeds file length.";
            }
        }

        if (corrupted)
        {
            log.warn("\n The file is corrupt: \n {0}", message);
            throw new IOException("The File Is Corrupt, need to reset");
        }

        final ByteBuffer data = ByteBuffer.allocate(ded.len);
        fc.read(data, ded.pos + HEADER_SIZE_BYTES);
        data.flip();

        return elementSerializer.deSerialize(data.array(), null);
    }

    /**
     * Moves the data stored from one position to another. The descriptor's position is updated.
     * <p>
     * @param ded
     * @param newPosition
     * @throws IOException
     */
    protected void move(final IndexedDiskElementDescriptor ded, final long newPosition)
        throws IOException
    {
        final ByteBuffer datalength = ByteBuffer.allocate(HEADER_SIZE_BYTES);
        fc.read(datalength, ded.pos);
        datalength.flip();
        final int length = datalength.getInt();

        if (length != ded.len)
        {
            throw new IOException("Mismatched memory and disk length (" + length + ") for " + ded);
        }

        // TODO: more checks?

        long readPos = ded.pos;
        long writePos = newPosition;

        // header len + data len
        int remaining = HEADER_SIZE_BYTES + length;
        final ByteBuffer buffer = ByteBuffer.allocate(16384);

        while (remaining > 0)
        {
            // chunk it
            final int chunkSize = Math.min(remaining, buffer.capacity());
            buffer.limit(chunkSize);
            fc.read(buffer, readPos);
            buffer.flip();
            fc.write(buffer, writePos);
            buffer.clear();

            writePos += chunkSize;
            readPos += chunkSize;
            remaining -= chunkSize;
        }

        ded.pos = newPosition;
    }

    /**
     * Writes the given byte array to the Disk at the specified position.
     * <p>
     * @param data
     * @param ded
     * @return true if we wrote successfully
     * @throws IOException
     */
    protected boolean write(final IndexedDiskElementDescriptor ded, final byte[] data)
        throws IOException
    {
        final long pos = ded.pos;
        if (log.isTraceEnabled())
        {
            log.trace("write> pos={0}", pos);
            log.trace("{0} -- data.length = {1}", fc, data.length);
        }

        if (data.length != ded.len)
        {
            throw new IOException("Mismatched descriptor and data lengths");
        }

        final ByteBuffer headerBuffer = ByteBuffer.allocate(HEADER_SIZE_BYTES);
        headerBuffer.putInt(data.length);
        // write the header
        headerBuffer.flip();
        int written = fc.write(headerBuffer, pos);
        assert written == HEADER_SIZE_BYTES;

        //write the data
        final ByteBuffer dataBuffer = ByteBuffer.wrap(data);
        written = fc.write(dataBuffer, pos + HEADER_SIZE_BYTES);

        return written == data.length;
    }

    /**
     * Serializes the object and write it out to the given position.
     * <p>
     * TODO: make this take a ded as well.
     * @param obj
     * @param pos
     * @throws IOException
     */
    protected <T> void writeObject(final T obj, final long pos)
        throws IOException
    {
        final byte[] data = elementSerializer.serialize(obj);
        write(new IndexedDiskElementDescriptor(pos, data.length), data);
    }

    /**
     * Returns the raf length.
     *
     * @return the length of the file.
     * @throws IOException If an I/O error occurs.
     */
    protected long length()
        throws IOException
    {
        return fc.size();
    }

    /**
     * Closes the raf.
     * <p>
     * @throws IOException
     */
    @Override
    public void close()
        throws IOException
    {
        fc.close();
    }

    /**
     * Sets the raf to empty.
     * <p>
     * @throws IOException
     */
    protected synchronized void reset()
        throws IOException
    {
        log.debug("Resetting Indexed File [{0}]", filepath);
        fc.truncate(0);
        fc.force(true);
    }

    /**
     * Truncates the file to a given length.
     * <p>
     * @param length the new length of the file
     * @throws IOException
     */
    protected void truncate(final long length)
        throws IOException
    {
        log.info("Truncating file [{0}] to {1}", filepath, length);
        fc.truncate(length);
    }

    /**
     * This is used for debugging.
     * <p>
     * @return the file path.
     */
    protected String getFilePath()
    {
        return filepath;
    }

    /**
     * Tests if the length is 0.
     * @return true if the if the length is 0.
     * @throws IOException If an I/O error occurs.
     * @since 3.1
     */
    protected boolean isEmpty() throws IOException
    {
        return length() == 0;
    }
}
