/*

   Derby - Class org.apache.derby.impl.jdbc.LOBStreamControl

   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.derby.impl.jdbc;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UTFDataFormatException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import org.apache.derby.iapi.error.StandardException;
import org.apache.derby.iapi.reference.Property;
import org.apache.derby.iapi.reference.SQLState;
import org.apache.derby.iapi.services.i18n.MessageService;
import org.apache.derby.iapi.services.monitor.Monitor;
import org.apache.derby.iapi.store.raw.data.DataFactory;
import org.apache.derby.io.StorageFile;
import org.apache.derby.iapi.error.ExceptionUtil;
import org.apache.derby.shared.common.reference.MessageId;

/**
 * This class acts as a layer of blob/clob repository (in memory or file).
 * The max bytes of data stored in memory depends on the way this
 * class is created. If the class is created with initial data, the buffer
 * size is set to the size of the byte array supplied, but no larger than
 * MAX_BUF_SIZE. If no initial data is supplied, or if the initial data size
 * is less than DEFAULT_BUF_SIZE, the buffer size is set to DEFAULT_BUF_SIZE.
 * When write increases the data beyond this value a temporary file is created
 * and data is moved into that. If truncate reduces the size of the file below
 * initial buffer size, the data is moved into memory.
 *
 * This class also creates InputStream and OutputStream which can be used to access
 * blob data irrespective of if its in memory or in file.
 */

final class LOBStreamControl {
    private LOBFile tmpFile;
    private byte [] dataBytes = new byte [0];
    private boolean isBytes = true;
    private final int bufferSize;
    private final EmbedConnection conn;
    private long updateCount;
    private static final int DEFAULT_BUF_SIZE = 4096;
    private static final int MAX_BUF_SIZE = 32768;

    /**
     * Creates an empty LOBStreamControl.
     * @param conn Connection for this lob
     */
    LOBStreamControl (EmbedConnection conn) {
        this.conn = conn;
        updateCount = 0;
        //default buffer size
        bufferSize = DEFAULT_BUF_SIZE;
    }

    /**
     * Creates a LOBStreamControl and initializes with a bytes array.
     * @param conn Connection for this lob
     * @param data initial value
     */
    LOBStreamControl (EmbedConnection conn, byte [] data)
            throws IOException, StandardException {
        this.conn = conn;
        updateCount = 0;
        bufferSize =
            Math.min(Math.max(DEFAULT_BUF_SIZE, data.length), MAX_BUF_SIZE);
        write (data, 0, data.length, 0);
    }

    private void init(byte [] b, long len)
            throws IOException, StandardException {
        Object monitor = findService(
                Property.DATABASE_MODULE, conn.getDBName());
        final DataFactory df = (DataFactory) findServiceModule(
                monitor, DataFactory.MODULE);
        try {
            AccessController.doPrivileged (new PrivilegedExceptionAction<Object>() {
                public Object run() throws IOException {
                    //create a temporary file
                    StorageFile lobFile =
                        df.getStorageFactory().createTemporaryFile("lob", null);
                    if (df.databaseEncrypted()) {
                        tmpFile = new EncryptedLOBFile (lobFile, df);
                    } else {
                        tmpFile = new LOBFile (lobFile);
                    }
                    return null;
                }
            });
        }
        catch (PrivilegedActionException pae) {
            throw (IOException) pae.getCause();
        }

        conn.addLobFile(tmpFile);
        isBytes = false;
        //now this call will write into the file
        if (len != 0)
            write(b, 0, (int) len, 0);
        dataBytes = null;
    }

    private long updateData(byte[] bytes, int offset, int len, long pos)
            throws StandardException {
        if (dataBytes == null) {
            if ((int) pos == 0) {
                dataBytes = new byte [len];
                System.arraycopy(bytes, offset, dataBytes, (int) pos, len);
                return len;
            }
            else {
                //invalid postion
                throw StandardException.newException(
                        SQLState.BLOB_POSITION_TOO_LARGE, pos);
            }
        }
        else {
            if (pos > dataBytes.length) {
                //invalid postion
                throw StandardException.newException(
                        SQLState.BLOB_POSITION_TOO_LARGE, pos);
            }
            else {
                if (pos + len < dataBytes.length) {
                    System.arraycopy(bytes, offset, dataBytes, (int) pos, len);
                }
                else {
                    byte [] tmpBytes = new byte [len + (int) pos];
                    System.arraycopy(dataBytes, 0 , tmpBytes, 0, (int) pos);
                    System.arraycopy(bytes, offset, tmpBytes, (int) pos, len);
                    dataBytes = tmpBytes;
                }
            }
            return pos + len;
        }
    }

    private void isValidPostion(long pos)
            throws IOException, StandardException {
        if (pos < 0)
            throw StandardException.newException(
                SQLState.BLOB_NONPOSITIVE_LENGTH, (pos + 1));
        if (pos > Integer.MAX_VALUE)
            throw StandardException.newException(
                    SQLState.BLOB_POSITION_TOO_LARGE, (pos + 1));

        if (isBytes) {
            if (dataBytes == null) {
                if (pos != 0)
                    throw StandardException.newException(
                            SQLState.BLOB_POSITION_TOO_LARGE, (pos + 1));
            } else if (dataBytes.length < pos)
                throw StandardException.newException(
                        SQLState.BLOB_POSITION_TOO_LARGE, (pos + 1));
        } else {
            if (pos > tmpFile.length())
                throw StandardException.newException(
                        SQLState.BLOB_POSITION_TOO_LARGE, (pos + 1));
        }
    }

    private void isValidOffset(int off, int length) throws StandardException {
        if (off < 0 || off > length)
            throw StandardException.newException(
                    SQLState.BLOB_INVALID_OFFSET, off);
    }

    /**
     * Writes one byte.
     * @param b byte
     * @param pos
     * @return new position
     * @throws IOException if writing to the LOB file fails
     * @throws StandardException if encrypting/decrypting the LOB file fails
     */
    synchronized long write(int b, long pos)
            throws IOException, StandardException {
        isValidPostion(pos);
        updateCount++;
        if (isBytes) {
            if (pos < bufferSize) {
                byte [] bytes = {(byte) b};
                updateData(bytes, 0, 1, pos);
                return pos + 1;
            } else {
                init(dataBytes, pos);
            }
        }
        tmpFile.seek(pos);
        tmpFile.write(b);
        return tmpFile.getFilePointer();
    }

    /**
     * Writes {@code len} bytes from the specified byte array to the LOB.
     *
     * @param b byte array
     * @param off offset from where to read from the byte array
     * @param len number of bytes to be written
     * @param pos starting position
     * @return The position after the bytes have been written to the LOB.
     * @throws IOException if writing to the LOB fails
     * @throws StandardException if writing to the LOB fails
     * @throws IndexOutOfBoundsException if {@code len} is larger than
     *       {@code b.length - off}
     */
    synchronized long write(byte[] b, int off, int len, long pos)
            throws IOException, StandardException {
        isValidPostion(pos);
        try {
            isValidOffset(off, b.length);
        } catch (StandardException e) {
            if (e.getSQLState().equals(
                    ExceptionUtil.getSQLStateFromIdentifier(
                                  SQLState.BLOB_INVALID_OFFSET)))
                    throw new ArrayIndexOutOfBoundsException (e.getMessage());
            throw e;
        }
        updateCount++;
        if (isBytes) {
            if (pos + len <= bufferSize)
                return updateData(b, off, len, pos);
            else {
                init(dataBytes, pos);
            }
        }
        tmpFile.seek(pos);
        tmpFile.write(b, off, len);
        return tmpFile.getFilePointer();
    }

    /**
     * Reads one byte.
     * @param pos position from where to read
     * @return byte
     * @throws IOException if reading the LOB file fails
     * @throws StandardException if decrypting an encrypted LOB file fails
     */
    synchronized int read(long pos)
            throws IOException, StandardException {
        isValidPostion(pos);
        if (isBytes) {
            if (dataBytes.length == pos)
                return -1;
            return dataBytes [(int) pos] & 0xff;
        }
        if (tmpFile.getFilePointer() != pos)
            tmpFile.seek(pos);
        try {
            return tmpFile.readByte() & 0xff;
        }
        catch (EOFException eof) {
            return -1;
        }
    }

    private int readBytes(byte [] b, int off, int len, long pos) {
        if (pos >= dataBytes.length)
            return -1;
        int lengthFromPos = dataBytes.length - (int) pos;
        int actualLength = len > lengthFromPos ? lengthFromPos : len;
        System.arraycopy(dataBytes, (int) pos, b, off, actualLength);
        return actualLength;
    }

    /**
     * Reads bytes starting from 'position' into bytes array.
     * starting from 'offset'
     * @param buff array into the bytes will be copied
     * @param off offset from where the array has to be populated
     * @param len number of bytes to read
     * @param pos initial position before reading
     * @return number new position
     * @throws IOException if reading the LOB file fails
     * @throws StandardException if decrypting an encrypted LOB file fails
     */
    synchronized int read(byte[] buff, int off, int len, long pos)
            throws IOException, StandardException {
        isValidPostion(pos);
        isValidOffset(off, buff.length);
        if (isBytes) {
            return readBytes(buff, off, len, pos);
        }
        tmpFile.seek(pos);
        return tmpFile.read (buff, off, len);
    }

    /**
     * returns input stream linked with this object.
     * @param pos initial postion
     * @return InputStream
     */
    InputStream getInputStream(long pos) {
        return new LOBInputStream(this, pos);
    }

    /**
     * returns output stream linked with this object
     * @param pos initial postion
     * @return OutputStream
     */
    OutputStream getOutputStream(long pos) {
        return new LOBOutputStream(this, pos);
    }

    /**
     * Returns length of data.
     * @return length
     * @throws IOException
     */
    long getLength() throws IOException {
        if (isBytes)
            return dataBytes.length;
        return tmpFile.length();
    }

    /**
     * Resets the size.
     * @param size new size should be smaller than existing size
     * @throws IOException
     */
    synchronized void truncate(long size)
            throws IOException, StandardException {
        isValidPostion(size);
        if (isBytes) {
            byte [] tmpByte = new byte [(int) size];
            System.arraycopy(dataBytes, 0, tmpByte, 0, (int) size);
            dataBytes = tmpByte;
        } else {
            if (size < bufferSize) {
                dataBytes = new byte [(int) size];
                read(dataBytes, 0, dataBytes.length, 0);
                isBytes = true;
                releaseTempFile(tmpFile);
                tmpFile = null;
            } else {
                tmpFile.setLength(size);
            }
        }
    }

    /**
     * Copies bytes from stream to local storage.
     * <p>
     * Note that specifying the length as {@code Long.MAX_VALUE} results in
     * reading data from the stream until EOF is reached, but no length checking
     * will be performed.
     *
     * @param inStream the stream to copy from
     * @param length number of bytes to be copied, or {@code Long.MAX_VALUE} to
     *      copy everything until EOF is reached
     * @throws IOException if reading or writing a LOB file fails
     * @throws StandardException if encrypting or decrypting a LOB file fails
     */
    synchronized void copyData(InputStream inStream, long length)
            throws IOException, StandardException {
        byte [] data = new byte [bufferSize];
        long sz = 0;
        while (sz < length) {
            int len = (int) Math.min (length - sz, bufferSize);
            len = inStream.read(data, 0, len);
            if (len == -1) {
                if (length != Long.MAX_VALUE) {
                    // We reached EOF before all the requested bytes are read.
                    throw new EOFException(MessageService.getTextMessage(
                            MessageId.STREAM_PREMATURE_EOF,
                            length, sz));
                } else {
                    // End of data, but no length checking.
                    break;
                }
            }
            write(data, 0, len, sz);
            sz += len;
        }
        // If we copied until EOF, and we read more data than the length of the
        // marker, see if we have a Derby end-of-stream marker.
        long curLength = getLength();
        if (length == Long.MAX_VALUE && curLength > 2) {
            byte[] eos = new byte[3];
            // Read the three last bytes, marker is 0xE0 0x00 0x00.
            read(eos, 0, 3, curLength -3);
            if ((eos[0] & 0xFF) == 0xE0 && (eos[1] & 0xFF) == 0x00 &&
                    (eos[2] & 0xFF) == 0x00) {
                // Remove Derby end-of-stream-marker.
                truncate(curLength -3);
            }
        }
    }

    /**
     * Copies UTF-8 encoded chars from a stream to local storage.
     * <p>
     * Note that specifying the length as {@code Long.MAX_VALUE} results in
     * reading data from the stream until EOF is reached, but no length checking
     * will be performed.
     *
     * @param utf8Stream the stream to copy from
     * @param charLength number of chars to be copied, or {@code Long.MAX_VALUE}
     *      to copy everything until EOF is reached
     * @return The number of characters copied.
     * @throws EOFException if EOF is reached prematurely
     * @throws IOException thrown on a number of error conditions
     * @throws StandardException if reading, writing or truncating the
     *      {@code LOBStreamControl}-object fails
     * @throws UTFDataFormatException if an invalid UTF-8 encoding is detected
     */
    synchronized long copyUtf8Data(final InputStream utf8Stream,
                                   final long charLength)
            throws IOException, StandardException {
        long charCount = 0; // Number of chars read
        int offset = 0;     // Where to start looking for the start of a char
        int read = 0;       // Number of bytes read
        final byte[] buf = new byte[bufferSize];
        while (charCount < charLength) {
            int readNow = utf8Stream.read(buf, 0,
                            (int)Math.min(buf.length, charLength - charCount));
            if (readNow == -1) {
                break;
            }
            // Count the characters.
            while (offset < readNow) {
                int c = buf[offset] & 0xFF;
                if ((c & 0x80) == 0x00) { // 8th bit not set (top bit)
                    offset++;
                } else if ((c & 0x60) == 0x40) { // 7th bit set, 6th bit unset
                    // Found char of two byte width.
                    offset += 2;
                } else if ((c & 0x70) == 0x60) { // 7th & 6th bit set, 5th unset
                    // Found char of three byte width.
                    offset += 3;
                } else {
                    // This shouldn't happen, as the data is coming from the
                    // store and is supposed to be well-formed.
                    // If it happens, fail and print some internal information.
                    throw new UTFDataFormatException("Invalid UTF-8 encoding: "
                            + Integer.toHexString(c) + ", charCount=" +
                            charCount + ", offset=" + offset);
                }
                charCount++;
            }
            offset -= readNow; // Starting offset for next iteration
            write(buf, 0, readNow, read);
            read += readNow;
        }
        // See if an EOF-marker ended the stream. Don't check if we have fewer
        // bytes than the marker length.
        long curLength = getLength();
        if (curLength > 2) {
            byte[] eos = new byte[3];
            // Read the three last bytes, marker is 0xE0 0x00 0x00.
            read(eos, 0, 3, curLength -3);
            if ((eos[0] & 0xFF) == 0xE0 && (eos[1] & 0xFF) == 0x00 &&
                    (eos[2] & 0xFF) == 0x00) {
                // Remove Derby end-of-stream-marker.
                truncate(curLength -3);
                charCount--;
            }
        }
        if (charLength != Long.MAX_VALUE && charCount != charLength) {
            throw new EOFException(MessageService.getTextMessage(
                    MessageId.STREAM_PREMATURE_EOF,
                    charLength, charCount));
        }
        return charCount;
    }

    //
    // This method in java.lang.Object was deprecated as of build 167
    // of JDK 9. See DERBY-6932.
    //
    @SuppressWarnings("deprecation")
    protected void finalize() throws Throwable {
        free();
    }

    private void deleteFile(final StorageFile file) {
        AccessController.doPrivileged(new PrivilegedAction<Object>() {
            public Object run() {
                file.delete();
                return null;
            }
        });
    }

    /**
     * Invalidates all the variables and closes file handle if open.
     * @throws IOException if closing the file fails
     */
    void free() throws IOException {
        dataBytes = null;
        if (tmpFile != null) {
            releaseTempFile(tmpFile);
            tmpFile = null;
        }
    }

    /**
     * Close and release all resources held by a temporary file. The file will
     * also be deleted from the file system and removed from the list of
     * {@code LOBFile}s in {@code EmbedConnection}.
     *
     * @param file the temporary file
     * @throws IOException if the file cannot be closed or deleted
     */
    private void releaseTempFile(LOBFile file) throws IOException {
        // Remove the file from the list of open files *first*, then close it.
        //
        // Why? This code may be called from finalize(), and may end up running
        // at the same time the transaction is committed or rolled back. If two
        // threads call RandomAccessFile.close() at the same time, Java 5 could
        // fail (see DERBY-6092). By removing it from the list before closing
        // it, we make sure that EmbedConnection.clearLOBMapping() won't see
        // it if we get to the file first. Conversely, if clearLOBMapping()
        // gets to it first, the call to removeLobFile() will block until
        // clearLOBMapping() is done, so we won't attempt to close the file
        // until after clearLOBMapping() is done, rather than at the same time.
        //
        // Calling close() concurrently is safe on Java 6 and newer, after the
        // fix for http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6322678 .
        conn.removeLobFile(file);
        file.close();
        deleteFile(file.getStorageFile());
    }
    
    /**
     * Replaces a block of bytes in the middle of the LOB with a another block
     * of bytes, which may be of a different size.
     * <p>
     * The new byte array may not be be of same length as the original,
     * thus it may result in resizing the total lob.
     *
     * @param buf byte array which will be written inplace of old block
     * @param stPos inclusive starting position of current block
     * @param endPos exclusive end position of current block
     * @return Current position after write.
     * @throws IOException if writing to the temporary file fails
     * @throws StandardException
     */
    synchronized long replaceBytes (byte [] buf, long stPos, long endPos)
            throws IOException, StandardException {
        long length = getLength();
        if (isBytes) {
            long finalLength = length - endPos + stPos + buf.length;
            if (finalLength > bufferSize) {
                byte [] tmpBytes = dataBytes;
                init (tmpBytes, stPos);
                write (buf, 0, buf.length, getLength());
                if (endPos < length)
                    write (tmpBytes, (int) endPos, 
                            (int) (length - endPos), getLength());
            }
            else {
                byte [] tmpByte = new byte [(int) finalLength];
                System.arraycopy (dataBytes, 0, tmpByte, 0, (int) stPos);
                System.arraycopy (buf, 0, tmpByte, (int) stPos, (int) buf.length);
                if (endPos < length)
                    System.arraycopy (dataBytes, (int) endPos, tmpByte, 
                            (int) (stPos + buf.length), (int) (length - endPos));
                dataBytes = tmpByte;            
            }
        }
        else {
            //save over file handle and 
            //create new file with 0 size
            
            byte tmp [] = new byte [0];
            LOBFile oldFile = tmpFile;
            init (tmp, 0);
            byte [] tmpByte = new byte [1024];
            long sz = stPos;
            oldFile.seek(0);
            while (sz != 0) {
                int readLen = (int) Math.min (1024, sz);                
                int actualLength = oldFile.read (tmpByte, 0, readLen);
                if (actualLength == -1)
                    break;
                tmpFile.write (tmpByte, 0, actualLength);
                sz -= actualLength;
            }
            tmpFile.write (buf);
            oldFile.seek (endPos);
            int rdLen;
            if (endPos < length) {
                do {
                    rdLen = oldFile.read (tmpByte, 0, 1024);
                    if (rdLen == -1)
                        break;
                    tmpFile.write (tmpByte, 0, rdLen);
                }while (true);
            }            
            releaseTempFile(oldFile);
        }
        updateCount++;
        return stPos + buf.length;
    }

    /**
     * Returns the running sequence number to check if the lob is updated since
     * last access.
     *
     * @return The current update sequence number.
     */
    long getUpdateCount() {
        return updateCount;
    }
    /**
     * Privileged startup. Must be private so that user code
     * can't call this entry point.
     */
    private  static  Object findServiceModule( final Object serviceModule, final String factoryInterface)
        throws StandardException
    {
        try {
            return AccessController.doPrivileged
                (
                 new PrivilegedExceptionAction<Object>()
                 {
                     public Object run()
                         throws StandardException
                     {
                         return Monitor.findServiceModule( serviceModule, factoryInterface );
                     }
                 }
                 );
        } catch (PrivilegedActionException pae)
        {
            throw StandardException.plainWrapException( pae );
        }
    }

    /**
     * Privileged service lookup. Must be private so that user code
     * can't call this entry point.
     */
    private static  Object findService( final String factoryInterface, final String serviceName )
    {
        return AccessController.doPrivileged
            (
             new PrivilegedAction<Object>()
             {
                 public Object run()
                 {
                     return Monitor.findService( factoryInterface, serviceName );
                 }
             }
             );
    }
    
}
