/* Copyright (c) 2001-2016, 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.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.UnsupportedEncodingException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

import org.hsqldb.Database;
import org.hsqldb.DatabaseType;
import org.hsqldb.HsqlException;
import org.hsqldb.HsqlNameManager.HsqlName;
import org.hsqldb.Session;
import org.hsqldb.SessionInterface;
import org.hsqldb.Statement;
import org.hsqldb.Table;
import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.lib.ArrayUtil;
import org.hsqldb.lib.HashMappedList;
import org.hsqldb.lib.HsqlByteArrayInputStream;
import org.hsqldb.lib.LineGroupReader;
import org.hsqldb.map.ValuePool;
import org.hsqldb.navigator.RowSetNavigator;
import org.hsqldb.result.Result;
import org.hsqldb.result.ResultLob;
import org.hsqldb.result.ResultMetaData;
import org.hsqldb.types.BinaryData;
import org.hsqldb.types.BlobData;
import org.hsqldb.types.BlobDataID;
import org.hsqldb.types.ClobData;
import org.hsqldb.types.ClobDataID;
import org.hsqldb.types.Collation;
import org.hsqldb.types.Types;

/**
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 * @version 2.3.4
 * @since 1.9.0
 */
public class LobManager {

    static final String resourceFileName =
        "/org/hsqldb/resources/lob-schema.sql";
    static final String[] starters = new String[]{ "/*" };

    //
    Database         database;
    LobStore         lobStore;
    Session          sysLobSession;
    volatile boolean storeModified;
    byte[]           byteBuffer;

    //
    Inflater inflater;
    Deflater deflater;
    byte[]   dataBuffer;

    //
    boolean cryptLobs;
    boolean compressLobs;
    int     lobBlockSize;
    int     largeLobBlockSize    = SessionInterface.lobStreamBlockSize;
    int     totalBlockLimitCount = Integer.MAX_VALUE;

    //
    Statement getLob;
    Statement getSpanningBlocks;
    Statement deleteLobCall;
    Statement deleteLobPartCall;
    Statement divideLobPartCall;
    Statement createLob;
    Statement createLobPartCall;
    Statement createSingleLobPartCall;
    Statement updateLobLength;
    Statement updateLobUsage;
    Statement getNextLobId;
    Statement deleteUnusedLobs;
    Statement mergeUnusedSpace;
    Statement getLobUseLimit;
    Statement getLobCount;
    Statement getSpanningParts;
    Statement getLastPart;
    Statement createPart;

    //
    boolean usageChanged;

    //
    ReadWriteLock lock      = new ReentrantReadWriteLock();
    Lock          writeLock = lock.writeLock();

    // LOBS columns
    private interface LOBS {

        int BLOCK_ADDR   = 0;
        int BLOCK_COUNT  = 1;
        int BLOCK_OFFSET = 2;
        int LOB_ID       = 3;
    }

    private interface LOB_IDS {

        int LOB_ID          = 0;
        int LOB_LENGTH      = 1;
        int LOB_USAGE_COUNT = 2;
        int LOB_TYPE        = 3;
    }

    private interface GET_LOB_PART {

        int LOB_ID       = 0;
        int BLOCK_OFFSET = 1;
        int BLOCK_LIMIT  = 2;
    }

    private interface DIVIDE_BLOCK {
        int BLOCK_OFFSET = 0;
        int LOB_ID       = 1;
    }

    private interface DELETE_BLOCKS {

        int LOB_ID       = 0;
        int BLOCK_OFFSET = 1;
        int BLOCK_LIMIT  = 2;
        int TX_ID        = 3;
    }

    private interface ALLOC_BLOCKS {

        int BLOCK_COUNT  = 0;
        int BLOCK_OFFSET = 1;
        int LOB_ID       = 2;
    }

    private interface UPDATE_USAGE {
        int BLOCK_COUNT = 0;
        int LOB_ID      = 1;
    }

    private interface UPDATE_LENGTH {
        int LOB_LENGTH = 0;
        int LOB_ID     = 1;
    }

    private interface ALLOC_PART {

        int BLOCK_COUNT  = 0;
        int BLOCK_OFFSET = 1;
        int PART_OFFSET  = 2;
        int PART_LENGTH  = 3;
        int PART_BYTES   = 4;
        int LOB_ID       = 5;
    }

    private static final String existsBlocksSQL =
        "SELECT * FROM SYSTEM_LOBS.BLOCKS LIMIT 1";
    private static final String initialiseBlocksSQL =
        "INSERT INTO SYSTEM_LOBS.BLOCKS VALUES(?,?,?)";
    private static final String getLobSQL =
        "SELECT * FROM SYSTEM_LOBS.LOB_IDS WHERE LOB_IDS.LOB_ID = ?";
    private static final String getLobPartSQL =
        "SELECT * FROM SYSTEM_LOBS.LOBS WHERE LOBS.LOB_ID = ? AND BLOCK_OFFSET + BLOCK_COUNT > ? AND BLOCK_OFFSET < ? ORDER BY BLOCK_OFFSET";
    private static final String deleteLobPartCallSQL =
        "CALL SYSTEM_LOBS.DELETE_BLOCKS(?,?,?,?)";
    private static final String createLobSQL =
        "INSERT INTO SYSTEM_LOBS.LOB_IDS VALUES(?, ?, ?, ?)";
    private static final String updateLobLengthSQL =
        "UPDATE SYSTEM_LOBS.LOB_IDS SET LOB_LENGTH = ? WHERE LOB_IDS.LOB_ID = ?";
    private static final String createLobPartCallSQL =
        "CALL SYSTEM_LOBS.ALLOC_BLOCKS(?, ?, ?)";
    private static final String createSingleLobPartCallSQL =
        "CALL SYSTEM_LOBS.ALLOC_SINGLE_BLOCK(?, ?, ?)";
    private static final String divideLobPartCallSQL =
        "CALL SYSTEM_LOBS.DIVIDE_BLOCK(?, ?)";
    private static final String updateLobUsageSQL =
        "UPDATE SYSTEM_LOBS.LOB_IDS SET LOB_USAGE_COUNT = LOB_USAGE_COUNT + ? WHERE LOB_IDS.LOB_ID = ?";
    private static final String getNextLobIdSQL =
        "VALUES NEXT VALUE FOR SYSTEM_LOBS.LOB_ID";
    private static final String deleteLobCallSQL =
        "CALL SYSTEM_LOBS.DELETE_LOB(?, ?)";
    private static final String deleteUnusedCallSQL =
        "CALL SYSTEM_LOBS.DELETE_UNUSED_LOBS(?,?)";
    private static final String mergeUnusedSpaceSQL =
        "CALL SYSTEM_LOBS.MERGE_EMPTY_BLOCKS()";
    private static final String getLobUseLimitSQL =
        "SELECT * FROM SYSTEM_LOBS.LOBS WHERE BLOCK_ADDR = (SELECT MAX(BLOCK_ADDR) FROM SYSTEM_LOBS.LOBS)";
    private static final String getLobCountSQL =
        "SELECT COUNT(*) FROM SYSTEM_LOBS.LOB_IDS";

    //
    //BLOCK_COUNT INT NOT NULL, BLOCK_OFFSET INT, PART_OFFSET BIGINT NOT NULL, PART_LENGTH BIGINT NOT NULL, PART_BYTES BIGINT NOT NULL, LOB_ID BIGINT,
    private static final String getPartsSQL =
        "SELECT BLOCK_COUNT, BLOCK_OFFSET, PART_OFFSET, PART_LENGTH, PART_BYTES, LOB_ID "
        + "FROM SYSTEM_LOBS.PARTS "
        + "WHERE LOB_ID = ?  AND PART_OFFSET + PART_LENGTH > ? AND PART_OFFSET < ? ORDER BY BLOCK_OFFSET";
    private static final String getLastPartSQL =
        "SELECT BLOCK_COUNT, BLOCK_OFFSET, PART_OFFSET, PART_LENGTH, PART_BYTES, LOB_ID "
        + "FROM SYSTEM_LOBS.PARTS "
        + "WHERE LOB_ID = ? ORDER BY LOB_ID DESC, BLOCK_OFFSET DESC LIMIT 1";
    private static final String createPartSQL =
        "INSERT INTO SYSTEM_LOBS.PARTS VALUES ?,?,?,?,?,?";

    public LobManager(Database database) {
        this.database = database;
    }

    public void lock() {
        writeLock.lock();
    }

    public void unlock() {
        writeLock.unlock();
    }

    public void createSchema() {

        sysLobSession = database.sessionManager.getSysLobSession();

        InputStream fis = (InputStream) AccessController.doPrivileged(
            new PrivilegedAction() {

            public InputStream run() {
                return getClass().getResourceAsStream(resourceFileName);
            }
        });
        InputStreamReader reader = null;

        try {
            reader = new InputStreamReader(fis, "ISO-8859-1");
        } catch (UnsupportedEncodingException e) {
            reader = new InputStreamReader(fis);
        }

        LineNumberReader lineReader = new LineNumberReader(reader);
        LineGroupReader  lg = new LineGroupReader(lineReader, starters);
        HashMappedList   map        = lg.getAsMap();

        lg.close();

        String    sql       = (String) map.get("/*lob_schema_definition*/");
        Statement statement = sysLobSession.compileStatement(sql);
        Result    result    = statement.execute(sysLobSession);

        if (result.isError()) {
            throw result.getException();
        }

        HsqlName name =
            database.schemaManager.getSchemaHsqlName("SYSTEM_LOBS");
        Table table = database.schemaManager.getUserTable("BLOCKS",
            "SYSTEM_LOBS");

        compileStatements();
    }

    public void compileStatements() {

        writeLock.lock();

        try {
            getLob            = sysLobSession.compileStatement(getLobSQL);
            getSpanningBlocks = sysLobSession.compileStatement(getLobPartSQL);
            createLob         = sysLobSession.compileStatement(createLobSQL);
            createLobPartCall =
                sysLobSession.compileStatement(createLobPartCallSQL);
            createSingleLobPartCall =
                sysLobSession.compileStatement(createSingleLobPartCallSQL);
            divideLobPartCall =
                sysLobSession.compileStatement(divideLobPartCallSQL);
            deleteLobCall = sysLobSession.compileStatement(deleteLobCallSQL);
            deleteLobPartCall =
                sysLobSession.compileStatement(deleteLobPartCallSQL);
            updateLobLength =
                sysLobSession.compileStatement(updateLobLengthSQL);
            updateLobUsage = sysLobSession.compileStatement(updateLobUsageSQL);
            getNextLobId   = sysLobSession.compileStatement(getNextLobIdSQL);
            deleteUnusedLobs =
                sysLobSession.compileStatement(deleteUnusedCallSQL);
            mergeUnusedSpace =
                sysLobSession.compileStatement(mergeUnusedSpaceSQL);
            getLobUseLimit = sysLobSession.compileStatement(getLobUseLimitSQL);
            getLobCount    = sysLobSession.compileStatement(getLobCountSQL);

            //
            getSpanningParts = sysLobSession.compileStatement(getPartsSQL);
            getLastPart      = sysLobSession.compileStatement(getLastPartSQL);
            createPart       = sysLobSession.compileStatement(createPartSQL);
        } finally {
            writeLock.unlock();
        }
    }

    private void initialiseLobSpace() {

        Statement statement = sysLobSession.compileStatement(existsBlocksSQL);
        Result    result    = statement.execute(sysLobSession);

        if (result.isError()) {
            throw result.getException();
        }

        RowSetNavigator navigator = result.getNavigator();
        int             size      = navigator.getSize();

        if (size > 0) {
            return;
        }

        statement = sysLobSession.compileStatement(initialiseBlocksSQL);

        Object[] params = new Object[3];

        params[0] = ValuePool.INTEGER_0;
        params[1] = ValuePool.getInt(totalBlockLimitCount);
        params[2] = ValuePool.getLong(0);

        sysLobSession.executeCompiledStatement(statement, params, 0);
    }

    public void open() {

        lobBlockSize = database.logger.getLobBlockSize();
        cryptLobs    = database.logger.cryptLobs;
        compressLobs = database.logger.propCompressLobs;

        if (compressLobs || cryptLobs) {
            int largeBufferBlockSize = largeLobBlockSize + 4 * 1024;

            inflater   = new Inflater();
            deflater   = new Deflater(Deflater.BEST_SPEED);
            dataBuffer = new byte[largeBufferBlockSize];
        }

        if (database.getType() == DatabaseType.DB_RES) {
            lobStore = new LobStoreInJar(database, lobBlockSize);
        } else if (database.getType() == DatabaseType.DB_FILE) {
            lobStore = new LobStoreRAFile(database, lobBlockSize);

            if (!database.isFilesReadOnly()) {
                byteBuffer = new byte[lobBlockSize];

                initialiseLobSpace();
            }
        } else {
            lobStore   = new LobStoreMem(lobBlockSize);
            byteBuffer = new byte[lobBlockSize];

            initialiseLobSpace();
        }
    }

    public void close() {

        if (lobStore != null) {
            lobStore.close();
        }

        lobStore = null;
    }

    public LobStore getLobStore() {

        if (lobStore == null) {
            open();
        }

        return lobStore;
    }

    //
    private Long getNewLobID() {

        Result result = getNextLobId.execute(sysLobSession);

        if (result.isError()) {
            return Long.valueOf(0);
        }

        RowSetNavigator navigator = result.getNavigator();
        boolean         next      = navigator.next();

        if (!next) {
            navigator.release();

            return Long.valueOf(0);
        }

        Object[] data = navigator.getCurrent();

        navigator.release();

        return (Long) data[0];
    }

    private Object[] getLobHeader(long lobID) {

        ResultMetaData meta   = getLob.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[0] = ValuePool.getLong(lobID);

        sysLobSession.sessionContext.pushDynamicArguments(params);

        Result result = getLob.execute(sysLobSession);

        sysLobSession.sessionContext.pop();

        if (result.isError()) {
            throw result.getException();
        }

        RowSetNavigator navigator = result.getNavigator();
        boolean         next      = navigator.next();
        Object[]        data      = null;

        if (next) {
            data = navigator.getCurrent();
        }

        navigator.release();

        return data;
    }

    public BlobData getBlob(long lobID) {

        writeLock.lock();

        try {
            Object[] data = getLobHeader(lobID);

            if (data == null) {
                return null;
            }

            BlobData blob = new BlobDataID(lobID);

            return blob;
        } finally {
            writeLock.unlock();
        }
    }

    public ClobData getClob(long lobID) {

        writeLock.lock();

        try {
            Object[] data = getLobHeader(lobID);

            if (data == null) {
                return null;
            }

            ClobData clob = new ClobDataID(lobID);

            return clob;
        } finally {
            writeLock.unlock();
        }
    }

    public long createBlob(Session session, long length) {

        writeLock.lock();

        try {
            Long           lobID  = getNewLobID();
            ResultMetaData meta   = createLob.getParametersMetaData();
            Object[]       params = new Object[meta.getColumnCount()];

            params[LOB_IDS.LOB_ID]          = lobID;
            params[LOB_IDS.LOB_LENGTH]      = ValuePool.getLong(length);
            params[LOB_IDS.LOB_USAGE_COUNT] = ValuePool.INTEGER_0;
            params[LOB_IDS.LOB_TYPE]        = ValuePool.getInt(Types.SQL_BLOB);

            Result result = sysLobSession.executeCompiledStatement(createLob,
                params, 0);

            usageChanged = true;

            return lobID.longValue();
        } finally {
            writeLock.unlock();
        }
    }

    public long createClob(Session session, long length) {

        writeLock.lock();

        try {
            Long           lobID  = getNewLobID();
            ResultMetaData meta   = createLob.getParametersMetaData();
            Object[]       params = new Object[meta.getColumnCount()];

            params[LOB_IDS.LOB_ID]          = lobID;
            params[LOB_IDS.LOB_LENGTH]      = ValuePool.getLong(length);
            params[LOB_IDS.LOB_USAGE_COUNT] = ValuePool.INTEGER_0;
            params[LOB_IDS.LOB_TYPE]        = ValuePool.getInt(Types.SQL_CLOB);

            Result result = sysLobSession.executeCompiledStatement(createLob,
                params, 0);

            usageChanged = true;

            return lobID.longValue();
        } finally {
            writeLock.unlock();
        }
    }

    public Result deleteLob(long lobID) {

        writeLock.lock();

        try {
            ResultMetaData meta   = deleteLobCall.getParametersMetaData();
            Object[]       params = new Object[meta.getColumnCount()];

            params[0] = ValuePool.getLong(lobID);
            params[1] = ValuePool.getLong(0);

            Result result =
                sysLobSession.executeCompiledStatement(deleteLobCall, params,
                    0);

            usageChanged = true;

            return result;
        } finally {
            writeLock.unlock();
        }
    }

    public void setUsageChanged() {
        usageChanged = true;
    }

    public Result deleteUnusedLobs() {

        writeLock.lock();

        try {
            if (lobStore == null || byteBuffer == null || !usageChanged) {
                return Result.updateZeroResult;
            }

            Session[] sessions   = database.sessionManager.getAllSessions();
            long      firstLobID = Long.MAX_VALUE;

            for (int i = 0; i < sessions.length; i++) {
                if (sessions[i].isClosed()) {
                    continue;
                }

                long sessionLobID = sessions[i].sessionData.getFirstLobID();

                if (sessionLobID != 0 && sessionLobID < firstLobID) {
                    firstLobID = sessionLobID;
                }
            }

            ResultMetaData meta   = deleteUnusedLobs.getParametersMetaData();
            Object[]       params = new Object[meta.getColumnCount()];

            params[0] = new Long(firstLobID);

            Result result =
                sysLobSession.executeCompiledStatement(deleteUnusedLobs,
                    params, 0);

            if (result.isError()) {
                return result;
            }

            if (params[1] != null) {
                int total = ((Number) params[1]).intValue();

                if (total < 1) {
                    return Result.updateZeroResult;
                }
            }

            result = sysLobSession.executeCompiledStatement(mergeUnusedSpace,
                    ValuePool.emptyObjectArray, 0);

            if (result.isError()) {
                return result;
            }

            result = sysLobSession.executeCompiledStatement(getLobUseLimit,
                    ValuePool.emptyObjectArray, 0);

            if (result.isError()) {
                return result;
            }

            // result is empty when there is no lob, or one row
            usageChanged = false;

            long            sizeLimit = 0;
            RowSetNavigator navigator = result.getNavigator();
            boolean         next      = navigator.next();

            if (next) {
                Object[] data = navigator.getCurrent();

                if (data[LOBS.BLOCK_ADDR] == null
                        || data[LOBS.BLOCK_COUNT] == null) {
                    return Result.updateOneResult;
                }

                sizeLimit = ((Integer) data[LOBS.BLOCK_ADDR]).intValue()
                            + ((Integer) data[LOBS.BLOCK_COUNT]).intValue();
                sizeLimit *= lobBlockSize;
            }

            long currentLength = lobStore.getLength();

            if (currentLength > sizeLimit) {
                database.logger.logInfoEvent("lob file truncated to usage");
                lobStore.setLength(sizeLimit);

                try {
                    lobStore.synch();
                } catch (Throwable t) {}
            } else if (currentLength < sizeLimit) {
                database.logger.logInfoEvent(
                    "lob file reported smaller than usage");
            }

            return Result.updateOneResult;
        } finally {
            writeLock.unlock();
        }
    }

    public Result getLength(long lobID) {

        writeLock.lock();

        try {
            Object[] data = getLobHeader(lobID);

            if (data == null) {
                throw Error.error(ErrorCode.X_0F502);
            }

            long length = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();
            int  type   = ((Integer) data[LOB_IDS.LOB_TYPE]).intValue();

            return ResultLob.newLobSetResponse(lobID, length);
        } catch (HsqlException e) {
            return Result.newErrorResult(e);
        } finally {
            writeLock.unlock();
        }
    }

    public int compare(BlobData a, byte[] b) {

        writeLock.lock();

        try {
            Object[] data    = getLobHeader(a.getId());
            long     aLength = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();
            int[][] aAddresses = getBlockAddresses(a.getId(), 0,
                                                   Integer.MAX_VALUE);
            int aIndex  = 0;
            int bOffset = 0;
            int aOffset = 0;

            if (aLength == 0) {
                return b.length == 0 ? 0
                                     : -1;
            }

            if (b.length == 0) {
                return 1;
            }

            while (true) {
                int aBlockOffset = aAddresses[aIndex][LOBS.BLOCK_ADDR]
                                   + aOffset;
                byte[] aBytes = getLobStore().getBlockBytes(aBlockOffset, 1);

                for (int i = 0; i < aBytes.length; i++) {
                    if (bOffset + i >= b.length) {
                        if (aLength == b.length) {
                            return 0;
                        }

                        return 1;
                    }

                    if (aBytes[i] == b[bOffset + i]) {
                        continue;
                    }

                    return (((int) aBytes[i]) & 0xff)
                           > (((int) b[bOffset + i]) & 0xff) ? 1
                                                             : -1;
                }

                aOffset++;

                bOffset += lobBlockSize;

                if (aOffset == aAddresses[aIndex][LOBS.BLOCK_COUNT]) {
                    aOffset = 0;

                    aIndex++;
                }

                if (aIndex == aAddresses.length) {
                    break;
                }

                if (bOffset >= b.length) {
                    break;
                }
            }

            if (aLength == b.length) {
                return 0;
            }

            return aLength > b.length ? 1
                                      : -1;
        } finally {
            writeLock.unlock();
        }
    }

    public int compare(BlobData a, BlobData b) {

        if (a.getId() == b.getId()) {
            return 0;
        }

        writeLock.lock();

        try {
            if (compressLobs || cryptLobs) {
                return compareBytesCompressed(a.getId(), b.getId());
            } else {
                return compareBytesNormal(a.getId(), b.getId());
            }
        } finally {
            writeLock.unlock();
        }
    }

    /** @todo - implement as compareText() */
    public int compare(Collation collation, ClobData a, String b) {

        writeLock.lock();

        try {
            Object[] data    = getLobHeader(a.getId());
            long     aLength = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();
            int[][] aAddresses = getBlockAddresses(a.getId(), 0,
                                                   Integer.MAX_VALUE);
            int aIndex  = 0;
            int bOffset = 0;
            int aOffset = 0;

            if (aLength == 0) {
                return b.length() == 0 ? 0
                                       : -1;
            }

            if (b.length() == 0) {
                return 1;
            }

            while (true) {
                int aBlockOffset = aAddresses[aIndex][LOBS.BLOCK_ADDR]
                                   + aOffset;
                byte[] aBytes = getLobStore().getBlockBytes(aBlockOffset, 1);
                long aLimit =
                    aLength
                    - ((long) aAddresses[aIndex][LOBS.BLOCK_OFFSET] + aOffset)
                      * lobBlockSize / 2;

                if (aLimit > lobBlockSize / 2) {
                    aLimit = lobBlockSize / 2;
                }

                String aString = new String(ArrayUtil.byteArrayToChars(aBytes),
                                            0, (int) aLimit);
                int bLimit = b.length() - bOffset;

                if (bLimit > lobBlockSize / 2) {
                    bLimit = lobBlockSize / 2;
                }

                String bString = b.substring(bOffset, bOffset + bLimit);
                int    diff    = collation.compare(aString, bString);

                if (diff != 0) {
                    return diff;
                }

                aOffset++;

                bOffset += lobBlockSize / 2;

                if (aOffset == aAddresses[aIndex][LOBS.BLOCK_COUNT]) {
                    aOffset = 0;

                    aIndex++;
                }

                if (aIndex == aAddresses.length) {
                    break;
                }

                if (bOffset >= b.length()) {
                    break;
                }
            }

            if (aLength == b.length()) {
                return 0;
            }

            return aLength > b.length() ? 1
                                        : -1;
        } finally {
            writeLock.unlock();
        }
    }

    public int compare(Collation collation, ClobData a, ClobData b) {

        if (a.getId() == b.getId()) {
            return 0;
        }

        writeLock.lock();

        try {
            if (compressLobs || cryptLobs) {
                return compareTextCompressed(collation, a.getId(), b.getId());
            } else {
                return compareTextNormal(collation, a.getId(), b.getId());
            }
        } finally {
            writeLock.unlock();
        }
    }

    private int compareBytesNormal(long aID, long bID) {

        Object[] data    = getLobHeader(aID);
        long     aLength = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();

        data = getLobHeader(bID);

        long    bLength    = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();
        int[][] aAddresses = getBlockAddresses(aID, 0, Integer.MAX_VALUE);
        int[][] bAddresses = getBlockAddresses(bID, 0, Integer.MAX_VALUE);
        int     aIndex     = 0;
        int     bIndex     = 0;
        int     aOffset    = 0;
        int     bOffset    = 0;

        if (aLength == 0) {
            return bLength == 0 ? 0
                                : -1;
        }

        if (bLength == 0) {
            return 1;
        }

        while (true) {
            int aBlockOffset = aAddresses[aIndex][LOBS.BLOCK_ADDR] + aOffset;
            int bBlockOffset = bAddresses[bIndex][LOBS.BLOCK_ADDR] + bOffset;
            byte[] aBytes    = getLobStore().getBlockBytes(aBlockOffset, 1);
            byte[] bBytes    = getLobStore().getBlockBytes(bBlockOffset, 1);
            int    result    = ArrayUtil.compare(aBytes, bBytes);

            if (result != 0) {
                return result;
            }

            aOffset++;
            bOffset++;

            if (aOffset == aAddresses[aIndex][LOBS.BLOCK_COUNT]) {
                aOffset = 0;

                aIndex++;
            }

            if (bOffset == bAddresses[bIndex][LOBS.BLOCK_COUNT]) {
                bOffset = 0;

                bIndex++;
            }

            if (aIndex == aAddresses.length) {
                break;
            }

            if (bIndex == bAddresses.length) {
                break;
            }
        }

        if (aLength == bLength) {
            return 0;
        }

        return aLength > bLength ? 1
                                 : -1;
    }

    /** @todo - word-separator and end block zero issues */
    private int compareTextNormal(Collation collation, long aID, long bID) {

        Object[] data    = getLobHeader(aID);
        long     aLength = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();

        data = getLobHeader(bID);

        long    bLength    = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();
        int[][] aAddresses = getBlockAddresses(aID, 0, Integer.MAX_VALUE);
        int[][] bAddresses = getBlockAddresses(bID, 0, Integer.MAX_VALUE);
        int     aIndex     = 0;
        int     bIndex     = 0;
        int     aOffset    = 0;
        int     bOffset    = 0;

        if (aLength == 0) {
            return bLength == 0 ? 0
                                : -1;
        }

        if (bLength == 0) {
            return 1;
        }

        while (true) {
            int aBlockOffset = aAddresses[aIndex][LOBS.BLOCK_ADDR] + aOffset;
            int bBlockOffset = bAddresses[bIndex][LOBS.BLOCK_ADDR] + bOffset;
            byte[] aBytes    = getLobStore().getBlockBytes(aBlockOffset, 1);
            byte[] bBytes    = getLobStore().getBlockBytes(bBlockOffset, 1);
            long aLimit =
                aLength
                - ((long) aAddresses[aIndex][LOBS.BLOCK_OFFSET] + aOffset)
                  * lobBlockSize / 2;

            if (aLimit > lobBlockSize / 2) {
                aLimit = lobBlockSize / 2;
            }

            long bLimit =
                bLength
                - ((long) bAddresses[bIndex][LOBS.BLOCK_OFFSET] + bOffset)
                  * lobBlockSize / 2;

            if (bLimit > lobBlockSize / 2) {
                bLimit = lobBlockSize / 2;
            }

            String aString = new String(ArrayUtil.byteArrayToChars(aBytes), 0,
                                        (int) aLimit);
            String bString = new String(ArrayUtil.byteArrayToChars(bBytes), 0,
                                        (int) bLimit);
            int diff = collation.compare(aString, bString);

            if (diff != 0) {
                return diff;
            }

            aOffset++;
            bOffset++;

            if (aOffset == aAddresses[aIndex][LOBS.BLOCK_COUNT]) {
                aOffset = 0;

                aIndex++;
            }

            if (bOffset == bAddresses[bIndex][LOBS.BLOCK_COUNT]) {
                bOffset = 0;

                bIndex++;
            }

            if (aIndex == aAddresses.length) {
                break;
            }

            if (bIndex == bAddresses.length) {
                break;
            }
        }

        if (aLength == bLength) {
            return 0;
        }

        return aLength > bLength ? 1
                                 : -1;
    }

    /**
     * Used for SUBSTRING
     */
    public Result getLob(long lobID, long offset, long length) {

        if (offset == 0) {
            return createDuplicateLob(lobID, length);
        }

        throw Error.runtimeError(ErrorCode.U_S0500, "LobManager");
    }

    public Result createDuplicateLob(long lobID) {

        Result result = getLength(lobID);

        if (result.isError()) {
            return result;
        }

        return createDuplicateLob(lobID,
                                  ((ResultLob) result).getBlockLength());
    }

    public Result createDuplicateLob(long lobID, long newLength) {

        if (byteBuffer == null) {
            throw Error.error(ErrorCode.DATA_IS_READONLY);
        }

        writeLock.lock();

        try {
            Object[] data = getLobHeader(lobID);

            if (data == null) {
                return Result.newErrorResult(Error.error(ErrorCode.X_0F502));
            }

            Long     newLobID = getNewLobID();
            Object[] params   = new Object[data.length];

            params[LOB_IDS.LOB_ID]          = newLobID;
            params[LOB_IDS.LOB_LENGTH]      = Long.valueOf(newLength);
            params[LOB_IDS.LOB_USAGE_COUNT] = ValuePool.INTEGER_0;
            params[LOB_IDS.LOB_TYPE]        = data[LOB_IDS.LOB_TYPE];

            Result result = sysLobSession.executeCompiledStatement(createLob,
                params, 0);

            if (result.isError()) {
                return result;
            }

            usageChanged = true;

            if (newLength == 0) {
                return ResultLob.newLobSetResponse(newLobID.longValue(),
                                                   newLength);
            }

            long byteLength = newLength;
            int  lobType    = ((Integer) data[LOB_IDS.LOB_TYPE]).intValue();

            if (lobType == Types.SQL_CLOB) {
                byteLength *= 2;
            }

            int newBlockCount = (int) (byteLength / lobBlockSize);

            if (byteLength % lobBlockSize != 0) {
                newBlockCount++;
            }

            createBlockAddresses(newLobID.longValue(), 0, newBlockCount);

            // copy the contents
            int[][] sourceBlocks = getBlockAddresses(lobID, 0,
                Integer.MAX_VALUE);
            int[][] targetBlocks = getBlockAddresses(newLobID.longValue(), 0,
                Integer.MAX_VALUE);

            try {
                copyBlockSet(sourceBlocks, targetBlocks);
            } catch (HsqlException e) {
                return Result.newErrorResult(e);
            }

            // clear the end block unused space
            int endOffset = (int) (byteLength % lobBlockSize);

            if (endOffset != 0) {
                int[] block = targetBlocks[targetBlocks.length - 1];
                int blockOffset = block[LOBS.BLOCK_ADDR]
                                  + block[LOBS.BLOCK_COUNT] - 1;
                byte[] bytes = getLobStore().getBlockBytes(blockOffset, 1);

                ArrayUtil.fillArray(bytes, endOffset, (byte) 0);
                getLobStore().setBlockBytes(bytes, blockOffset, 1);
            }

            lobStore.synch();

            return ResultLob.newLobSetResponse(newLobID.longValue(),
                                               newLength);
        } finally {
            writeLock.unlock();
        }
    }

    /** @todo - currently unused and returns whole length */
    public Result getTruncateLength(long lobID) {

        writeLock.lock();

        try {
            Object[] data = getLobHeader(lobID);

            if (data == null) {
                throw Error.error(ErrorCode.X_0F502);
            }

            long length = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();
            int  type   = ((Integer) data[LOB_IDS.LOB_TYPE]).intValue();

            return ResultLob.newLobSetResponse(lobID, length);
        } finally {
            writeLock.unlock();
        }
    }

    private void copyBlockSet(int[][] source, int[][] target) {

        int sourceIndex  = 0;
        int targetIndex  = 0;
        int sourceOffset = 0;
        int targetOffset = 0;

        while (true) {
            byte[] bytes = getLobStore().getBlockBytes(
                source[sourceIndex][LOBS.BLOCK_ADDR] + sourceOffset, 1);

            getLobStore().setBlockBytes(bytes,
                                        target[targetIndex][LOBS.BLOCK_ADDR]
                                        + targetOffset, 1);

            sourceOffset++;
            targetOffset++;

            if (sourceOffset == source[sourceIndex][LOBS.BLOCK_COUNT]) {
                sourceOffset = 0;

                sourceIndex++;
            }

            if (targetOffset == target[targetIndex][LOBS.BLOCK_COUNT]) {
                targetOffset = 0;

                targetIndex++;
            }

            if (sourceIndex == source.length) {
                break;
            }

            if (targetIndex == target.length) {
                break;
            }
        }

        storeModified = true;
    }

    public Result getChars(long lobID, long offset, int length) {

        Result result;

        writeLock.lock();

        try {
            if (compressLobs || cryptLobs) {
                result = getBytesCompressed(lobID, offset * 2, length * 2,
                                            true);
            } else {
                result = getBytesNormal(lobID, offset * 2, length * 2);
            }
        } finally {
            writeLock.unlock();
        }

        if (result.isError()) {
            return result;
        }

        byte[] bytes = ((ResultLob) result).getByteArray();
        char[] chars = ArrayUtil.byteArrayToChars(bytes);

        return ResultLob.newLobGetCharsResponse(lobID, offset, chars);
    }

    public Result getBytes(long lobID, long offset, int length) {

        writeLock.lock();

        try {
            if (compressLobs || cryptLobs) {
                return getBytesCompressed(lobID, offset, length, false);
            } else {
                return getBytesNormal(lobID, offset, length);
            }
        } finally {
            writeLock.unlock();
        }
    }

    private Result getBytesNormal(long lobID, long offset, int length) {

        int blockOffset     = (int) (offset / lobBlockSize);
        int byteBlockOffset = (int) (offset % lobBlockSize);
        int blockLimit      = (int) ((offset + length) / lobBlockSize);
        int byteLimitOffset = (int) ((offset + length) % lobBlockSize);

        if (byteLimitOffset == 0) {
            byteLimitOffset = lobBlockSize;
        } else {
            blockLimit++;
        }

        if (length == 0) {
            return ResultLob.newLobGetBytesResponse(lobID, offset,
                    BinaryData.zeroLengthBytes);
        }

        int    dataBytesPosition = 0;
        byte[] dataBytes         = new byte[length];
        int[][] blockAddresses = getBlockAddresses(lobID, blockOffset,
            blockLimit);

        if (blockAddresses.length == 0) {
            return Result.newErrorResult(Error.error(ErrorCode.X_0F502));
        }

        //
        int i = 0;
        int blockCount = blockAddresses[i][LOBS.BLOCK_COUNT]
                         + blockAddresses[i][LOBS.BLOCK_OFFSET] - blockOffset;

        if (blockAddresses[i][LOBS.BLOCK_COUNT]
                + blockAddresses[i][LOBS.BLOCK_OFFSET] > blockLimit) {
            blockCount -= (blockAddresses[i][LOBS.BLOCK_COUNT]
                           + blockAddresses[i][LOBS.BLOCK_OFFSET]
                           - blockLimit);
        }

        byte[] bytes;

        try {
            bytes = getLobStore().getBlockBytes(
                blockAddresses[i][LOBS.BLOCK_ADDR]
                - blockAddresses[i][LOBS.BLOCK_OFFSET]
                + blockOffset, blockCount);
        } catch (HsqlException e) {
            return Result.newErrorResult(e);
        }

        int subLength = lobBlockSize * blockCount - byteBlockOffset;

        if (subLength > length) {
            subLength = length;
        }

        System.arraycopy(bytes, byteBlockOffset, dataBytes, dataBytesPosition,
                         subLength);

        dataBytesPosition += subLength;

        i++;

        for (; i < blockAddresses.length && dataBytesPosition < length; i++) {
            blockCount = blockAddresses[i][LOBS.BLOCK_COUNT];

            if (blockAddresses[i][LOBS.BLOCK_COUNT]
                    + blockAddresses[i][LOBS.BLOCK_OFFSET] > blockLimit) {
                blockCount -= (blockAddresses[i][LOBS.BLOCK_COUNT]
                               + blockAddresses[i][LOBS.BLOCK_OFFSET]
                               - blockLimit);
            }

            try {
                bytes = getLobStore().getBlockBytes(
                    blockAddresses[i][LOBS.BLOCK_ADDR], blockCount);
            } catch (HsqlException e) {
                return Result.newErrorResult(e);
            }

            subLength = lobBlockSize * blockCount;

            if (subLength > length - dataBytesPosition) {
                subLength = length - dataBytesPosition;
            }

            System.arraycopy(bytes, 0, dataBytes, dataBytesPosition,
                             subLength);

            dataBytesPosition += subLength;
        }

        return ResultLob.newLobGetBytesResponse(lobID, offset, dataBytes);
    }

    private Result setBytesBA(long lobID, long offset, byte[] dataBytes,
                              int dataLength, boolean isClob) {

        if (dataLength == 0) {
            return ResultLob.newLobSetResponse(lobID, 0);
        }

        writeLock.lock();

        try {
            if (compressLobs || cryptLobs) {
                return setBytesBACompressed(lobID, offset, dataBytes,
                                            dataLength, isClob);
            } else {
                return setBytesBANormal(lobID, offset, dataBytes, dataLength);
            }
        } finally {
            writeLock.unlock();
        }
    }

    private Result setBytesBANormal(long lobID, long offset, byte[] dataBytes,
                                    int dataLength) {

        boolean newBlocks       = false;
        int     blockOffset     = (int) (offset / lobBlockSize);
        int     byteBlockOffset = (int) (offset % lobBlockSize);
        int     blockLimit      = (int) ((offset + dataLength) / lobBlockSize);
        int     byteLimitOffset = (int) ((offset + dataLength) % lobBlockSize);

        if (byteLimitOffset == 0) {
            byteLimitOffset = lobBlockSize;
        } else {
            blockLimit++;
        }

        int[][] blockAddresses = getBlockAddresses(lobID, blockOffset,
            blockLimit);
        int existingLimit = blockOffset;

        if (blockAddresses.length > 0) {
            existingLimit =
                blockAddresses[blockAddresses.length - 1][LOBS.BLOCK_OFFSET]
                + blockAddresses[blockAddresses.length - 1][LOBS.BLOCK_COUNT];
        }

        if (existingLimit < blockLimit) {
            createBlockAddresses(lobID, existingLimit,
                                 blockLimit - existingLimit);

            blockAddresses = getBlockAddresses(lobID, blockOffset, blockLimit);
            newBlocks      = true;
        }

        int currentDataOffset = 0;
        int currentDataLength = dataLength;

        try {
            for (int i = 0; i < blockAddresses.length; i++) {
                long currentBlockOffset =
                    (long) blockAddresses[i][LOBS.BLOCK_OFFSET] * lobBlockSize;
                long currentBlockLength =
                    (long) blockAddresses[i][LOBS.BLOCK_COUNT] * lobBlockSize;
                long currentBlockPosition =
                    (long) blockAddresses[i][LOBS.BLOCK_ADDR] * lobBlockSize;
                int padding = 0;

                if (offset > currentBlockOffset) {
                    currentBlockLength   -= (offset - currentBlockOffset);
                    currentBlockPosition += (offset - currentBlockOffset);
                }

                if (currentDataLength < currentBlockLength) {
                    if (newBlocks) {
                        padding =
                            (int) ((currentBlockLength - currentDataLength)
                                   % lobBlockSize);
                    }

                    currentBlockLength = currentDataLength;
                }

                getLobStore().setBlockBytes(dataBytes, currentBlockPosition,
                                            currentDataOffset,
                                            (int) currentBlockLength);

                if (padding != 0) {
                    ArrayUtil.fillArray(byteBuffer, 0, (byte) 0);
                    getLobStore().setBlockBytes(byteBuffer,
                                                currentBlockPosition
                                                + currentBlockLength, 0,
                                                    padding);
                }

                currentDataOffset += currentBlockLength;
                currentDataLength -= currentBlockLength;
            }
        } catch (HsqlException e) {
            return Result.newErrorResult(e);
        }

        lobStore.synch();

        storeModified = true;

        return ResultLob.newLobSetResponse(lobID, dataLength);
    }

    private Result setBytesIS(long lobID, InputStream inputStream,
                              long length, boolean isClob) {

        if (length == 0) {
            return ResultLob.newLobSetResponse(lobID, 0);
        }

        if (compressLobs || cryptLobs) {
            return setBytesISCompressed(lobID, inputStream, length, isClob);
        } else {
            return setBytesISNormal(lobID, inputStream, length);
        }
    }

    private Result setBytesISNormal(long lobID, InputStream inputStream,
                                    long length) {

        long writeLength     = 0;
        int  blockLimit      = (int) (length / lobBlockSize);
        int  byteLimitOffset = (int) (length % lobBlockSize);

        if (byteLimitOffset == 0) {
            byteLimitOffset = lobBlockSize;
        } else {
            blockLimit++;
        }

        createBlockAddresses(lobID, 0, blockLimit);

        int[][] blockAddresses = getBlockAddresses(lobID, 0, blockLimit);

        for (int i = 0; i < blockAddresses.length; i++) {
            for (int j = 0; j < blockAddresses[i][LOBS.BLOCK_COUNT]; j++) {
                int localLength = lobBlockSize;

                ArrayUtil.fillArray(byteBuffer, 0, (byte) 0);

                if (i == blockAddresses.length - 1
                        && j == blockAddresses[i][LOBS.BLOCK_COUNT] - 1) {
                    localLength = byteLimitOffset;
                }

                try {
                    int count = 0;

                    while (localLength > 0) {
                        int read = inputStream.read(byteBuffer, count,
                                                    localLength);

                        if (read == -1) {
                            return Result.newErrorResult(new EOFException());
                        }

                        localLength -= read;
                        count       += read;
                    }

                    writeLength += count;

                    // read more
                } catch (IOException e) {

                    // deallocate
                    return Result.newErrorResult(e);
                }

                try {
                    getLobStore().setBlockBytes(
                        byteBuffer, blockAddresses[i][LOBS.BLOCK_ADDR] + j, 1);
                } catch (HsqlException e) {
                    return Result.newErrorResult(e);
                }
            }
        }

        storeModified = true;

        lobStore.synch();

        return ResultLob.newLobSetResponse(lobID, writeLength);
    }

    /**
     * returns new LOB length
     */
    public Result setBytes(long lobID, long offset, byte[] dataBytes,
                           int dataLength) {

        if (byteBuffer == null) {
            throw Error.error(ErrorCode.DATA_IS_READONLY);
        }

        writeLock.lock();

        try {
            Object[] data = getLobHeader(lobID);

            if (data == null) {
                return Result.newErrorResult(Error.error(ErrorCode.X_0F502));
            }

            long length = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();

            if (dataLength == 0) {
                return ResultLob.newLobSetResponse(lobID, length);
            }

            Result result = setBytesBA(lobID, offset, dataBytes, dataLength,
                                       false);

            if (result.isError()) {
                return result;
            }

            if (offset + dataLength > length) {
                length = offset + dataLength;
                result = setLength(lobID, length);

                if (result.isError()) {
                    return result;
                }
            }

            return ResultLob.newLobSetResponse(lobID, length);
        } finally {
            writeLock.unlock();
        }
    }

    public Result setBytesForNewBlob(long lobID, InputStream inputStream,
                                     long length) {

        if (length == 0) {
            return ResultLob.newLobSetResponse(lobID, 0);
        }

        if (byteBuffer == null) {
            throw Error.error(ErrorCode.DATA_IS_READONLY);
        }

        writeLock.lock();

        try {
            Result result = setBytesIS(lobID, inputStream, length, false);

            return result;
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * returns new full length of Clob
     */
    public Result setChars(long lobID, long offset, char[] chars,
                           int dataLength) {

        if (byteBuffer == null) {
            throw Error.error(ErrorCode.DATA_IS_READONLY);
        }

        writeLock.lock();

        try {
            Object[] data = getLobHeader(lobID);

            if (data == null) {
                return Result.newErrorResult(Error.error(ErrorCode.X_0F502));
            }

            long length = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();

            if (dataLength == 0) {
                return ResultLob.newLobSetResponse(lobID, length);
            }

            byte[] bytes = ArrayUtil.charArrayToBytes(chars, dataLength);
            Result result = setBytesBA(lobID, offset * 2, bytes,
                                       dataLength * 2, true);

            if (result.isError()) {
                return result;
            }

            if (offset + dataLength > length) {
                length = offset + dataLength;
                result = setLength(lobID, length);

                if (result.isError()) {
                    return result;
                }
            }

            return ResultLob.newLobSetResponse(lobID, length);
        } finally {
            writeLock.unlock();
        }
    }

    public Result setCharsForNewClob(long lobID, InputStream inputStream,
                                     long length) {

        if (length == 0) {
            return ResultLob.newLobSetResponse(lobID, 0);
        }

        if (byteBuffer == null) {
            throw Error.error(ErrorCode.DATA_IS_READONLY);
        }

        writeLock.lock();

        try {
            Result result = setBytesIS(lobID, inputStream, length * 2, false);

            if (result.isError()) {
                return result;
            }

            long newLength = ((ResultLob) result).getBlockLength();

            if (newLength < length) {
                Result trunc = truncate(lobID, newLength);
            }

            return result;
        } finally {
            writeLock.unlock();
        }
    }

    public Result truncate(long lobID, long offset) {

        if (byteBuffer == null) {
            throw Error.error(ErrorCode.DATA_IS_READONLY);
        }

        writeLock.lock();

        try {
            Object[] data = getLobHeader(lobID);

            if (data == null) {
                return Result.newErrorResult(Error.error(ErrorCode.X_0F502));
            }

            long length     = ((Long) data[LOB_IDS.LOB_LENGTH]).longValue();
            long byteLength = offset;

            if (((Integer) data[LOB_IDS.LOB_TYPE]).intValue()
                    == Types.SQL_CLOB) {
                byteLength *= 2;
            }

            int blockOffset = (int) ((byteLength + lobBlockSize - 1)
                                     / lobBlockSize);
            ResultMetaData meta   = deleteLobPartCall.getParametersMetaData();
            Object[]       params = new Object[meta.getColumnCount()];

            params[DELETE_BLOCKS.LOB_ID]       = ValuePool.getLong(lobID);
            params[DELETE_BLOCKS.BLOCK_OFFSET] = Integer.valueOf(blockOffset);
            params[DELETE_BLOCKS.BLOCK_LIMIT]  = ValuePool.INTEGER_MAX;
            params[DELETE_BLOCKS.TX_ID] =
                ValuePool.getLong(sysLobSession.getTransactionTimestamp());

            Result result =
                sysLobSession.executeCompiledStatement(deleteLobPartCall,
                    params, 0);

            setLength(lobID, offset);

            return ResultLob.newLobTruncateResponse(lobID, offset);
        } finally {
            writeLock.unlock();
        }
    }

    private Result setLength(long lobID, long length) {

        ResultMetaData meta   = updateLobLength.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[UPDATE_LENGTH.LOB_LENGTH] = ValuePool.getLong(length);
        params[UPDATE_LENGTH.LOB_ID]     = ValuePool.getLong(lobID);

        Result result = sysLobSession.executeCompiledStatement(updateLobLength,
            params, 0);

        return result;
    }

    /**
     * Executes in user session. No lock
     */
    public Result adjustUsageCount(Session session, long lobID, int delta) {

        ResultMetaData meta   = updateLobUsage.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[UPDATE_USAGE.BLOCK_COUNT] = ValuePool.getInt(delta);
        params[UPDATE_USAGE.LOB_ID]      = ValuePool.getLong(lobID);

        session.sessionContext.pushDynamicArguments(params);

        Result result = updateLobUsage.execute(session);

        usageChanged = true;

        session.sessionContext.pop();

        return result;
    }

    private int[][] getBlockAddresses(long lobID, int offset, int limit) {

        ResultMetaData meta   = getSpanningBlocks.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[GET_LOB_PART.LOB_ID]       = ValuePool.getLong(lobID);
        params[GET_LOB_PART.BLOCK_OFFSET] = ValuePool.getInt(offset);
        params[GET_LOB_PART.BLOCK_LIMIT]  = ValuePool.getInt(limit);

        sysLobSession.sessionContext.pushDynamicArguments(params);

        Result result = getSpanningBlocks.execute(sysLobSession);

        sysLobSession.sessionContext.pop();

        RowSetNavigator navigator = result.getNavigator();
        int             size      = navigator.getSize();
        int[][]         blocks    = new int[size][3];

        for (int i = 0; i < size; i++) {
            navigator.absolute(i);

            Object[] data = navigator.getCurrent();

            blocks[i][LOBS.BLOCK_ADDR] =
                ((Integer) data[LOBS.BLOCK_ADDR]).intValue();
            blocks[i][LOBS.BLOCK_COUNT] =
                ((Integer) data[LOBS.BLOCK_COUNT]).intValue();
            blocks[i][LOBS.BLOCK_OFFSET] =
                ((Integer) data[LOBS.BLOCK_OFFSET]).intValue();
        }

        navigator.release();

        return blocks;
    }

    private void deleteBlockAddresses(long lobID, int offset, int limit) {

        ResultMetaData meta   = deleteLobPartCall.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[DELETE_BLOCKS.LOB_ID]       = ValuePool.getLong(lobID);
        params[DELETE_BLOCKS.BLOCK_OFFSET] = ValuePool.getInt(offset);
        params[DELETE_BLOCKS.BLOCK_LIMIT]  = ValuePool.getInt(limit);
        params[DELETE_BLOCKS.TX_ID] =
            ValuePool.getLong(sysLobSession.getTransactionTimestamp());

        Result result =
            sysLobSession.executeCompiledStatement(deleteLobPartCall, params,
                0);
    }

    private void divideBlockAddresses(long lobID, int offset) {

        ResultMetaData meta   = divideLobPartCall.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[DIVIDE_BLOCK.BLOCK_OFFSET] = ValuePool.getInt(offset);
        params[DIVIDE_BLOCK.LOB_ID]       = ValuePool.getLong(lobID);

        Result result =
            sysLobSession.executeCompiledStatement(divideLobPartCall, params,
                0);
    }

    private Result createBlockAddresses(long lobID, int offset, int count) {

        ResultMetaData meta   = createLobPartCall.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[ALLOC_BLOCKS.BLOCK_COUNT]  = ValuePool.getInt(count);
        params[ALLOC_BLOCKS.BLOCK_OFFSET] = ValuePool.getInt(offset);
        params[ALLOC_BLOCKS.LOB_ID]       = ValuePool.getLong(lobID);

        Result result =
            sysLobSession.executeCompiledStatement(createLobPartCall, params,
                0);

        return result;
    }

    private Result createFullBlockAddresses(long lobID, int offset,
            int count) {

        ResultMetaData meta = createSingleLobPartCall.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[ALLOC_BLOCKS.BLOCK_COUNT]  = ValuePool.getInt(count);
        params[ALLOC_BLOCKS.BLOCK_OFFSET] = ValuePool.getInt(offset);
        params[ALLOC_BLOCKS.LOB_ID]       = ValuePool.getLong(lobID);

        Result result =
            sysLobSession.executeCompiledStatement(createSingleLobPartCall,
                params, 0);

        return result;
    }

    private Result createPart(long lobID, long partOffset, int dataLength,
                              int byteLength, int blockOffset,
                              int blockCount) {

        ResultMetaData meta   = createPart.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[ALLOC_PART.BLOCK_COUNT]  = ValuePool.getInt(blockCount);
        params[ALLOC_PART.BLOCK_OFFSET] = ValuePool.getInt(blockOffset);
        params[ALLOC_PART.PART_OFFSET]  = ValuePool.getLong(partOffset);
        params[ALLOC_PART.PART_LENGTH]  = ValuePool.getLong(dataLength);
        params[ALLOC_PART.PART_BYTES]   = ValuePool.getLong(byteLength);
        params[ALLOC_PART.LOB_ID]       = ValuePool.getLong(lobID);

        Result result = sysLobSession.executeCompiledStatement(createPart,
            params, 0);

        return result;
    }

    private int getBlockAddress(int[][] blockAddresses, int blockOffset) {

        for (int i = 0; i < blockAddresses.length; i++) {
            if (blockAddresses[i][LOBS.BLOCK_OFFSET]
                    + blockAddresses[i][LOBS.BLOCK_COUNT] > blockOffset) {
                return blockAddresses[i][LOBS.BLOCK_ADDR]
                       - blockAddresses[i][LOBS.BLOCK_OFFSET] + blockOffset;
            }
        }

        return -1;
    }

    public int getLobCount() {

        writeLock.lock();

        try {
            sysLobSession.sessionContext.pushDynamicArguments(new Object[]{});

            Result result = getLobCount.execute(sysLobSession);

            sysLobSession.sessionContext.pop();

            RowSetNavigator navigator = result.getNavigator();
            boolean         next      = navigator.next();

            if (!next) {
                navigator.release();

                return 0;
            }

            Object[] data = navigator.getCurrent();

            return ((Number) data[0]).intValue();
        } finally {
            writeLock.unlock();
        }
    }

    public void synch() {

        if (storeModified) {
            if (lobStore != null) {
                writeLock.lock();

                try {
                    try {
                        lobStore.synch();
                    } catch (Throwable t) {}

                    storeModified = false;
                } finally {
                    writeLock.unlock();
                }
            }
        }
    }

    private long[][] getParts(long lobID, long offset, long limit) {

        ResultMetaData meta   = getSpanningParts.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[GET_LOB_PART.LOB_ID]       = ValuePool.getLong(lobID);
        params[GET_LOB_PART.BLOCK_OFFSET] = ValuePool.getLong(offset);
        params[GET_LOB_PART.BLOCK_LIMIT]  = ValuePool.getLong(limit);

        sysLobSession.sessionContext.pushDynamicArguments(params);

        Result result = getSpanningParts.execute(sysLobSession);

        sysLobSession.sessionContext.pop();

        RowSetNavigator navigator = result.getNavigator();
        int             size      = navigator.getSize();
        long[][]        blocks    = new long[size][6];

        for (int i = 0; i < size; i++) {
            navigator.absolute(i);

            Object[] data = navigator.getCurrent();

            for (int j = 0; j < blocks[i].length; j++) {
                blocks[i][j] = ((Number) data[j]).longValue();
            }
        }

        navigator.release();

        return blocks;
    }

    private void inflate(byte[] data, int length, boolean isClob) {

        if (cryptLobs) {
            length = database.logger.getCrypto().decode(data, 0, length, data,
                    0);
        }

        try {
            inflater.setInput(data, 0, length);

            length = inflater.inflate(dataBuffer);

            inflater.reset();
        } catch (DataFormatException e) {

            //
        } catch (Throwable e) {}

        int limit = (int) ArrayUtil.getBinaryMultipleCeiling(length,
            lobBlockSize);

        for (int i = length; i < limit; i++) {
            dataBuffer[i] = 0;
        }
    }

    private int deflate(byte[] data, int offset, int length, boolean isClob) {

        deflater.setInput(data, offset, length);
        deflater.finish();

        length = deflater.deflate(dataBuffer);

        deflater.reset();

        if (cryptLobs) {
            length = database.logger.getCrypto().encode(dataBuffer, 0, length,
                    dataBuffer, 0);
        }

        int limit = (int) ArrayUtil.getBinaryMultipleCeiling(length,
            lobBlockSize);

        for (int i = length; i < limit; i++) {
            dataBuffer[i] = 0;
        }

        return length;
    }

    private int compareBytesCompressed(long aID, long bID) {

        long[][] aParts = getParts(aID, 0, Long.MAX_VALUE);
        long[][] bParts = getParts(bID, 0, Long.MAX_VALUE);

        for (int i = 0; i < aParts.length && i < bParts.length; i++) {
            int aPartLength = (int) aParts[i][ALLOC_PART.PART_LENGTH];

            getPartBytesCompressedInBuffer(aID, aParts[i], false);

            byte[] aPartBytes = new byte[aPartLength];

            System.arraycopy(dataBuffer, 0, aPartBytes, 0, aPartLength);

            int bPartLength = (int) bParts[i][ALLOC_PART.PART_LENGTH];

            getPartBytesCompressedInBuffer(aID, bParts[i], false);

            int result = ArrayUtil.compare(aPartBytes, 0, aPartLength,
                                           byteBuffer, bPartLength);

            if (result != 0) {
                return result;
            }
        }

        if (aParts.length == bParts.length) {
            return 0;
        }

        return aParts.length > bParts.length ? 1
                                             : -1;
    }

    private int compareTextCompressed(Collation collation, long aID,
                                      long bID) {

        long[][] aParts = getParts(aID, 0, Long.MAX_VALUE);
        long[][] bParts = getParts(bID, 0, Long.MAX_VALUE);

        for (int i = 0; i < aParts.length && i < bParts.length; i++) {
            int aPartLength = (int) aParts[i][ALLOC_PART.PART_LENGTH];

            getPartBytesCompressedInBuffer(aID, aParts[i], true);

            String aString = new String(ArrayUtil.byteArrayToChars(dataBuffer,
                aPartLength));
            int bPartLength = (int) bParts[i][ALLOC_PART.PART_LENGTH];

            getPartBytesCompressedInBuffer(bID, bParts[i], true);

            String bString = new String(ArrayUtil.byteArrayToChars(dataBuffer,
                bPartLength));
            int diff = collation.compare(aString, bString);

            if (diff != 0) {
                return diff;
            }
        }

        if (aParts.length == bParts.length) {
            return 0;
        }

        return aParts.length > bParts.length ? 1
                                             : -1;
    }

    private Result setBytesISCompressed(long lobID, InputStream inputStream,
                                        long length, boolean isClob) {

        long   offset = 0;
        byte[] tempBuffer;

        if (length < largeLobBlockSize) {
            tempBuffer = new byte[(int) length];
        } else {
            tempBuffer = new byte[largeLobBlockSize];
        }

        while (true) {
            int localLength = tempBuffer.length;

            if (localLength > length - offset) {
                localLength = (int) (length - offset);
            }

            int count = 0;

            try {
                while (localLength > 0) {
                    int read = inputStream.read(tempBuffer, count,
                                                localLength);

                    if (read == -1) {
                        return Result.newErrorResult(new EOFException());
                    }

                    localLength -= read;
                    count       += read;
                }

                // read more
            } catch (IOException e) {

                // deallocate
                return Result.newErrorResult(e);
            }

            Result result = setBytesBACompressedPart(lobID, offset,
                tempBuffer, count, isClob);

            if (result.isError()) {
                return result;
            }

            offset += count;

            if (offset == length) {
                break;
            }
        }

        storeModified = true;

        return ResultLob.newLobSetResponse(lobID, length);
    }

    private Result setBytesBACompressed(long lobID, long offset,
                                        byte[] dataBytes, int dataLength,
                                        boolean isClob) {

        if (dataLength == 0) {
            return ResultLob.newLobSetResponse(lobID, 0);
        }

        if (dataLength <= largeLobBlockSize) {
            return setBytesBACompressedPart(lobID, offset, dataBytes,
                                            dataLength, isClob);
        }

        HsqlByteArrayInputStream is = new HsqlByteArrayInputStream(dataBytes,
            0, dataLength);

        return setBytesISCompressed(lobID, is, dataLength, isClob);
    }

    /**
     * Only for loading parts of the same lob, not for overwriting parts of existing lob
     */
    private Result setBytesBACompressedPart(long lobID, long offset,
            byte[] dataBytes, int dataLength, boolean isClob) {

        // get block offset after existing blocks and compressed block
        long[] lastPart = getLastPart(lobID);
        int blockOffset = (int) lastPart[ALLOC_PART.BLOCK_OFFSET]
                          + (int) lastPart[ALLOC_PART.BLOCK_COUNT];

        // check position
        long limit = lastPart[ALLOC_PART.PART_OFFSET]
                     + lastPart[ALLOC_PART.PART_LENGTH];

        if (limit != offset || limit % largeLobBlockSize != 0) {
            return Result.newErrorResult(Error.error(ErrorCode.X_0A501,
                    "compressed lobs"));
        }

        int byteLength = deflate(dataBytes, 0, dataLength, isClob);
        int blockCount = (byteLength + lobBlockSize - 1) / lobBlockSize;
        Result result = createFullBlockAddresses(lobID, blockOffset,
            blockCount);

        if (result.isError()) {
            return result;
        }

        result = createPart(lobID, offset, dataLength, byteLength,
                            blockOffset, blockCount);

        if (result.isError()) {
            return result;
        }

        long blockByteOffset = blockOffset * (long) lobBlockSize;
        int blockByteLength =
            (int) ArrayUtil.getBinaryMultipleCeiling(byteLength, lobBlockSize);

        setBytesBANormal(lobID, blockByteOffset, dataBuffer, blockByteLength);

        storeModified = true;

        return ResultLob.newLobSetResponse(lobID, dataLength);
    }

    private Result getBytesCompressed(long lobID, long offset, int length,
                                      boolean isClob) {

        byte[]   dataBytes = new byte[length];
        long[][] parts     = getParts(lobID, offset, offset + length);

        for (int i = 0; i < parts.length; i++) {
            long[] part       = parts[i];
            long   partOffset = part[ALLOC_PART.PART_OFFSET];
            int    partLength = (int) part[ALLOC_PART.PART_LENGTH];
            Result result = getPartBytesCompressedInBuffer(lobID, part,
                isClob);

            if (result.isError()) {
                return result;
            }

            ArrayUtil.copyBytes(partOffset, dataBuffer, 0, partLength, offset,
                                dataBytes, length);
        }

        return ResultLob.newLobGetBytesResponse(lobID, offset, dataBytes);
    }

    private Result getPartBytesCompressedInBuffer(long lobID, long[] part,
            boolean isClob) {

        long blockOffset     = part[ALLOC_PART.BLOCK_OFFSET];
        long partOffset      = part[ALLOC_PART.PART_OFFSET];
        long partLength      = part[ALLOC_PART.PART_LENGTH];
        int  partBytesLength = (int) part[ALLOC_PART.PART_BYTES];
        long blockByteOffset = blockOffset * lobBlockSize;
        Result result = getBytesNormal(lobID, blockByteOffset,
                                       partBytesLength);

        if (result.isError()) {
            return result;
        }

        byte[] byteBlock = ((ResultLob) result).getByteArray();

        inflate(byteBlock, partBytesLength, isClob);

        return ResultLob.newLobSetResponse(lobID, partLength);
    }

    private long[] getLastPart(long lobID) {

        ResultMetaData meta   = getLastPart.getParametersMetaData();
        Object[]       params = new Object[meta.getColumnCount()];

        params[GET_LOB_PART.LOB_ID] = ValuePool.getLong(lobID);

        sysLobSession.sessionContext.pushDynamicArguments(params);

        Result result = getLastPart.execute(sysLobSession);

        sysLobSession.sessionContext.pop();

        RowSetNavigator navigator = result.getNavigator();
        int             size      = navigator.getSize();
        long[]          blocks    = new long[6];

        if (size == 0) {
            blocks[ALLOC_PART.LOB_ID] = lobID;
        } else {
            navigator.absolute(0);

            Object[] data = navigator.getCurrent();

            for (int j = 0; j < blocks.length; j++) {
                blocks[j] = ((Number) data[j]).longValue();
            }
        }

        navigator.release();

        return blocks;
    }
}
