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

package org.apache.cayenne.util;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Blob;
import java.sql.SQLException;

import org.apache.cayenne.CayenneRuntimeException;

/**
 * A Blob implementation that stores content in memory.
 * <p>
 * <i>This implementation is based on jdbcBlob from HSQLDB (copyright HSQL Development
 * Group).</i>
 * </p>
 * 
 * @since 1.2
 * @author Andrus Adamchik, based on HSQLDB jdbcBlob.
 */
public class MemoryBlob implements Blob {

    volatile byte[] data;

    public MemoryBlob() {
        this(new byte[0]);
    }

    /**
     * Constructs a new MemoryBlob instance wrapping the given octet sequence.
     * 
     * @param data the octet sequence representing the Blob value
     * @throws CayenneRuntimeException if the argument is null
     */
    public MemoryBlob(byte[] data) {

        if (data == null) {
            throw new CayenneRuntimeException("Null data");
        }

        this.data = data;
    }

    /**
     * Returns the number of bytes in the <code>BLOB</code> value designated by this
     * <code>Blob</code> object.
     * 
     * @return length of the <code>BLOB</code> in bytes
     * @exception SQLException if there is an error accessing the length of the
     *                <code>BLOB</code>
     */
    public long length() throws SQLException {
        return data.length;
    }

    /**
     * Retrieves all or part of the <code>BLOB</code> value that this <code>Blob</code>
     * object represents, as an array of bytes. This <code>byte</code> array contains up
     * to <code>length</code> consecutive bytes starting at position <code>pos</code>.
     * <p>
     * The official specification is ambiguous in that it does not precisely indicate the
     * policy to be observed when pos > this.length() - length. One policy would be to
     * retrieve the octets from pos to this.length(). Another would be to throw an
     * exception. This implementation observes the later policy.
     * 
     * @param pos the ordinal position of the first byte in the <code>BLOB</code> value
     *            to be extracted; the first byte is at position 1
     * @param length the number of consecutive bytes to be copied
     * @return a byte array containing up to <code>length</code> consecutive bytes from
     *         the <code>BLOB</code> value designated by this <code>Blob</code>
     *         object, starting with the byte at position <code>pos</code>
     * @exception SQLException if there is an error accessing the <code>BLOB</code>
     *                value
     */
    public byte[] getBytes(long pos, final int length) throws SQLException {

        final byte[] ldata = data;
        final int dlen = ldata.length;

        pos--;

        if (pos < 0 || pos > dlen) {
            throw new SQLException("Invalid pos: " + (pos + 1));
        }

        if (length < 0 || length > dlen - pos) {
            throw new SQLException("length: " + length);
        }

        final byte[] out = new byte[length];
        System.arraycopy(ldata, (int) pos, out, 0, length);
        return out;
    }

    /**
     * Retrieves the <code>BLOB</code> value designated by this <code>Blob</code>
     * instance as a stream.
     * 
     * @return a stream containing the <code>BLOB</code> data
     * @exception SQLException if there is an error accessing the <code>BLOB</code>
     *                value
     */
    public InputStream getBinaryStream() throws SQLException {
        return new ByteArrayInputStream(data);
    }

    /**
     * Retrieves the byte position at which the specified byte array <code>pattern</code>
     * begins within the <code>BLOB</code> value that this <code>Blob</code> object
     * represents. The search for <code>pattern</code> begins at position
     * <code>start</code>.
     * <p>
     * 
     * @param pattern the byte array for which to search
     * @param start the position at which to begin searching; the first position is 1
     * @return the position at which the pattern appears, else -1
     * @exception SQLException if there is an error accessing the <code>BLOB</code>
     */
    public long position(final byte[] pattern, long start) throws SQLException {

        final byte[] ldata = data;
        final int dlen = ldata.length;

        if (start > dlen || pattern == null) {
            return -1;
        }
        else if (start < 1) {
            start = 0;
        }
        else {
            start--;
        }

        final int plen = pattern.length;

        if (plen == 0 || start > dlen - plen) {
            return -1;
        }

        final int stop = dlen - plen;
        final byte b0 = pattern[0];

        outer_loop: for (int i = (int) start; i <= stop; i++) {
            if (ldata[i] != b0) {
                continue;
            }

            int len = plen;
            int doffset = i;
            int poffset = 0;

            while (len-- > 0) {
                if (ldata[doffset++] != pattern[poffset++]) {
                    continue outer_loop;
                }
            }

            return i + 1;
        }

        return -1;
    }

    /**
     * Retrieves the byte position in the <code>BLOB</code> value designated by this
     * <code>Blob</code> object at which <code>pattern</code> begins. The search
     * begins at position <code>start</code>.
     * 
     * @param pattern the <code>Blob</code> object designating the <code>BLOB</code>
     *            value for which to search
     * @param start the position in the <code>BLOB</code> value at which to begin
     *            searching; the first position is 1
     * @return the position at which the pattern begins, else -1
     * @exception SQLException if there is an error accessing the <code>BLOB</code>
     *                value
     */
    public long position(final Blob pattern, long start) throws SQLException {

        final byte[] ldata = data;
        final int dlen = ldata.length;

        if (start > dlen || pattern == null) {
            return -1;
        }
        else if (start < 1) {
            start = 0;
        }
        else {
            start--;
        }

        final long plen = pattern.length();

        if (plen == 0 || start > dlen - plen) {
            return -1;
        }

        // by now, we know plen <= Integer.MAX_VALUE
        final int iplen = (int) plen;
        byte[] bap;

        if (pattern instanceof MemoryBlob) {
            bap = ((MemoryBlob) pattern).data;
        }
        else {
            bap = pattern.getBytes(1, iplen);
        }

        final int stop = dlen - iplen;
        final byte b0 = bap[0];

        outer_loop: for (int i = (int) start; i <= stop; i++) {
            if (ldata[i] != b0) {
                continue;
            }

            int len = iplen;
            int doffset = i;
            int poffset = 0;

            while (len-- > 0) {
                if (ldata[doffset++] != bap[poffset++]) {
                    continue outer_loop;
                }
            }

            return i + 1;
        }

        return -1;
    }

    /**
     * Always throws an exception.
     */
    public int setBytes(long pos, byte[] bytes) throws SQLException {
        throw new SQLException("Not supported");
    }

    /**
     * Always throws an exception.
     */
    public int setBytes(long pos, byte[] bytes, int offset, int len) throws SQLException {
        throw new SQLException("Not supported");
    }

    /**
     * Always throws an exception.
     */
    public OutputStream setBinaryStream(long pos) throws SQLException {
        throw new SQLException("Not supported");
    }

    /**
     * Truncates the <code>BLOB</code> value that this <code>Blob</code> object
     * represents to be <code>len</code> bytes in length.
     * 
     * @param len the length, in bytes, to which the <code>BLOB</code> value that this
     *            <code>Blob</code> object represents should be truncated
     * @exception SQLException if there is an error accessing the <code>BLOB</code>
     *                value
     */
    public void truncate(final long len) throws SQLException {

        final byte[] ldata = data;

        if (len < 0 || len > ldata.length) {
            throw new SQLException("Invalid length: " + Long.toString(len));
        }

        if (len == ldata.length) {
            return;
        }

        byte[] newData = new byte[(int) len];
        System.arraycopy(ldata, 0, newData, 0, (int) len);
        data = newData;
    }
}
