/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb.persist;

import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Constructor;

import org.hsqldb.Database;
import org.hsqldb.Trace;
import org.hsqldb.lib.HsqlByteArrayInputStream;
import org.hsqldb.lib.SimpleLog;
import org.hsqldb.lib.Storage;

// fredt@users 20030111 - patch 1.7.2 by bohgammer@users - pad file before seek() beyond end
// some incompatible JVM implementations do not allow seek beyond the existing end of file

/**
 * This class is a wapper for a random access file such as that used for
 * CACHED table storage.
 *
 * @author fredt@users
 * @version  1.8.0
 * @since  1.7.2
 */
class ScaledRAFile implements ScaledRAInterface {

    static final int  DATA_FILE_RAF  = 0;
    static final int  DATA_FILE_NIO  = 1;
    static final int  DATA_FILE_JAR  = 2;
    static final long MAX_NIO_LENGTH = (1L << 28);

    //
    final SimpleLog                appLog;
    final RandomAccessFile         file;
    private final boolean          readOnly;
    final String                   fileName;
    boolean                        isNio;
    boolean                        bufferDirty = true;
    final byte[]                   buffer;
    final HsqlByteArrayInputStream ba;
    long                           bufferOffset;

    //
    long       seekPosition;
    long       realPosition;
    static int cacheHit;

    /**
     * seekPosition is the position in seek() calls or after reading or writing
     * realPosition is the file position
     */
    static Storage newScaledRAFile(Database database, String name,
                                   boolean readonly, int type,
                                   String classname,
                                   String key)
                                   throws FileNotFoundException, IOException {

//#ifdef JAVA4
        if (classname != null) {
            try {
                Class storageClass = null;
                try {
                    ClassLoader classLoader =
                        Thread.currentThread().getContextClassLoader();
                    storageClass =
                        classLoader.loadClass(classname);
                }
                catch (ClassNotFoundException e) {
                    storageClass = Class.forName(classname);
                }
                Constructor constructor =
                    storageClass.getConstructor(new Class[] {
                    String.class, Boolean.class, Object.class
                });

                return (Storage) constructor.newInstance(new Object[] {
                    name, new Boolean(readonly), key
                });
            } catch (Exception e) {
                throw new IOException(
                    Trace.getMessage(
                        Trace.INVALID_STORAGE_CLASS, true,
                        new Object[]{ e.toString() }));
            }
        }

//#endif
        if (type == DATA_FILE_JAR) {
            return new ScaledRAFileInJar(name);
        } else if (type == DATA_FILE_RAF) {
            return new ScaledRAFile(database, name, readonly);
        } else {
            RandomAccessFile file = new RandomAccessFile(name, readonly ? "r"
                                                                        : "rw");

            if (file.length() > MAX_NIO_LENGTH) {
                return new ScaledRAFile(database, name, file, readonly);
            } else {
                file.close();
            }

            try {
                Class.forName("java.nio.MappedByteBuffer");

                Class c =
                    Class.forName("org.hsqldb.persist.ScaledRAFileHybrid");
                Constructor constructor = c.getConstructor(new Class[] {
                    Database.class, String.class, boolean.class
                });

                return (ScaledRAInterface) constructor.newInstance(
                    new Object[] {
                    database, name, new Boolean(readonly)
                });
            } catch (Exception e) {
                return new ScaledRAFile(database, name, readonly);
            }
        }
    }

    ScaledRAFile(Database database, String name, RandomAccessFile file,
                 boolean readonly) throws FileNotFoundException, IOException {

        this.appLog   = database.logger.appLog;
        this.readOnly = readonly;
        this.fileName = name;
        this.file     = file;

        int bufferScale = database.getProperties().getIntegerProperty(
            HsqlDatabaseProperties.hsqldb_raf_buffer_scale, 12, 8, 13);
        int bufferSize = 1 << bufferScale;

        buffer = new byte[bufferSize];
        ba     = new HsqlByteArrayInputStream(buffer);
    }

    ScaledRAFile(Database database, String name,
                 boolean readonly) throws FileNotFoundException, IOException {

        this.appLog   = database.logger.appLog;
        this.readOnly = readonly;
        this.fileName = name;
        this.file     = new RandomAccessFile(name, readonly ? "r"
                                                            : "rw");

        int bufferScale = database.getProperties().getIntegerProperty(
            HsqlDatabaseProperties.hsqldb_raf_buffer_scale, 12);
        int bufferSize = 1 << bufferScale;

        buffer = new byte[bufferSize];
        ba     = new HsqlByteArrayInputStream(buffer);
    }

    public long length() throws IOException {
        return file.length();
    }

    /**
     * Some JVM's do not allow seek beyond end of file, so zeros are written
     * first in that case. Reported by bohgammer@users in Open Disucssion
     * Forum.
     */
    public void seek(long position) throws IOException {

        if (!readOnly && file.length() < position) {
            long tempSize = position - file.length();

            if (tempSize > 1 << 18) {
                tempSize = 1 << 18;
            }

            byte[] temp = new byte[(int) tempSize];

            try {
                long pos = file.length();

                for (; pos < position - tempSize; pos += tempSize) {
                    file.seek(pos);
                    file.write(temp, 0, (int) tempSize);
                }

                file.seek(pos);
                file.write(temp, 0, (int) (position - pos));

                realPosition = position;
            } catch (IOException e) {
                appLog.logContext(e, null);

                throw e;
            }
        }

        seekPosition = position;
    }

    public long getFilePointer() throws IOException {
        return seekPosition;
    }

    private void readIntoBuffer() throws IOException {

        long filePos    = seekPosition;
        long subOffset  = filePos % buffer.length;
        long fileLength = file.length();
        long readLength = fileLength - (filePos - subOffset);

        try {
            if (readLength <= 0) {
                throw new IOException("read beyond end of file");
            }

            if (readLength > buffer.length) {
                readLength = buffer.length;
            }

            file.seek(filePos - subOffset);
            file.readFully(buffer, 0, (int) readLength);

            bufferOffset = filePos - subOffset;
            realPosition = bufferOffset + readLength;
            bufferDirty  = false;
        } catch (IOException e) {
            resetPointer();
            appLog.logContext(e, "" + realPosition + " " + readLength);

            throw e;
        }
    }

    public int read() throws IOException {

        try {
            long fileLength = file.length();

            if (seekPosition >= fileLength) {
                return -1;
            }

            if (bufferDirty || seekPosition < bufferOffset
                    || seekPosition >= bufferOffset + buffer.length) {
                readIntoBuffer();
            } else {
                cacheHit++;
            }

            ba.reset();
            ba.skip(seekPosition - bufferOffset);

            int val = ba.read();

            seekPosition++;

            return val;
        } catch (IOException e) {
            resetPointer();
            appLog.logContext(e, null);

            throw e;
        }
    }

    public long readLong() throws IOException {

        try {
            if (bufferDirty || seekPosition < bufferOffset
                    || seekPosition >= bufferOffset + buffer.length) {
                readIntoBuffer();
            } else {
                cacheHit++;
            }

            ba.reset();

            if (seekPosition - bufferOffset
                    != ba.skip(seekPosition - bufferOffset)) {
                throw new EOFException();
            }

            long val;

            try {
                val = ba.readLong();
            } catch (EOFException e) {
                file.seek(seekPosition);

                val          = file.readLong();
                realPosition = file.getFilePointer();
            }

            seekPosition += 8;

            return val;
        } catch (IOException e) {
            resetPointer();
            appLog.logContext(e, null);

            throw e;
        }
    }

    public int readInt() throws IOException {

        try {
            if (bufferDirty || seekPosition < bufferOffset
                    || seekPosition >= bufferOffset + buffer.length) {
                readIntoBuffer();
            } else {
                cacheHit++;
            }

            ba.reset();

            if (seekPosition - bufferOffset
                    != ba.skip(seekPosition - bufferOffset)) {
                throw new EOFException();
            }

            int val;

            try {
                val = ba.readInt();
            } catch (EOFException e) {
                file.seek(seekPosition);

                val          = file.readInt();
                realPosition = file.getFilePointer();
            }

            seekPosition += 4;

            return val;
        } catch (IOException e) {
            resetPointer();
            appLog.logContext(e, null);

            throw e;
        }
    }

    public void read(byte[] b, int offset, int length) throws IOException {

        try {
            if (bufferDirty || seekPosition < bufferOffset
                    || seekPosition >= bufferOffset + buffer.length) {
                readIntoBuffer();
            } else {
                cacheHit++;
            }

            ba.reset();

            if (seekPosition - bufferOffset
                    != ba.skip(seekPosition - bufferOffset)) {
                throw new EOFException();
            }

            int bytesRead = ba.read(b, offset, length);

            seekPosition += bytesRead;

            if (bytesRead < length) {
                if (seekPosition != realPosition) {
                    file.seek(seekPosition);
                }

                file.readFully(b, offset + bytesRead, length - bytesRead);

                seekPosition += (length - bytesRead);
                realPosition = seekPosition;
            }
        } catch (IOException e) {
            resetPointer();
            appLog.logContext(e, null);

            throw e;
        }
    }

    public void write(byte[] b, int off, int len) throws IOException {

        try {
            if (realPosition != seekPosition) {
                file.seek(seekPosition);

                realPosition = seekPosition;
            }

            if (seekPosition >= bufferOffset
                    && seekPosition < bufferOffset + buffer.length) {
                bufferDirty = true;
            }

            file.write(b, off, len);

            seekPosition += len;
            realPosition = seekPosition;
        } catch (IOException e) {
            resetPointer();
            appLog.logContext(e, null);

            throw e;
        }
    }

    public void writeInt(int i) throws IOException {

        try {
            if (realPosition != seekPosition) {
                file.seek(seekPosition);

                realPosition = seekPosition;
            }

            if (seekPosition >= bufferOffset
                    && seekPosition < bufferOffset + buffer.length) {
                bufferDirty = true;
            }

            file.writeInt(i);

            seekPosition += 4;
            realPosition = seekPosition;
        } catch (IOException e) {
            resetPointer();
            appLog.logContext(e, null);

            throw e;
        }
    }

    public void writeLong(long i) throws IOException {

        try {
            if (realPosition != seekPosition) {
                file.seek(seekPosition);

                realPosition = seekPosition;
            }

            if (seekPosition >= bufferOffset
                    && seekPosition < bufferOffset + buffer.length) {
                bufferDirty = true;
            }

            file.writeLong(i);

            seekPosition += 8;
            realPosition = seekPosition;
        } catch (IOException e) {
            resetPointer();
            appLog.logContext(e, null);

            throw e;
        }
    }

    public void close() throws IOException {
        Trace.printSystemOut("cache hit " + cacheHit);
        file.close();
    }

    public boolean isReadOnly() {
        return readOnly;
    }

    public boolean wasNio() {
        return false;
    }

    public boolean canAccess(int length) {
        return true;
    }

    public boolean canSeek(long position) {
        return true;
    }

    public Database getDatabase() {
        return null;
    }

    private void resetPointer() {

        try {
            bufferDirty = true;

            file.seek(seekPosition);

            realPosition = seekPosition;
        } catch (Throwable e) {}
    }
}
