/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2004-2006, GeoTools Project Managment Committee (PMC)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.index.quadtree.fs;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;

import org.geotools.index.quadtree.Node;
import org.geotools.index.quadtree.StoreException;

import com.vividsolutions.jts.geom.Envelope;


/**
 * DOCUMENT ME!
 *
 * @author Tommaso Nolli
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/plugin/shapefile/src/main/java/org/geotools/index/quadtree/fs/FileSystemNode.java $
 */
public class FileSystemNode extends Node {
    private ScrollingBuffer buffer;
    private ByteOrder order;
    private int subNodeStartByte;
    private int subNodesLength;
    private int numSubNodes;

    /**
     * DOCUMENT ME!
     *
     * @param bounds
     * @param channel DOCUMENT ME!
     * @param order DOCUMENT ME!
     * @param startByte DOCUMENT ME!
     * @param subNodesLength DOCUMENT ME!
     */
    FileSystemNode(Envelope bounds, int id, Node parent, ScrollingBuffer buffer, int startByte, int subNodesLength) {
        super(bounds, id, parent);
        this.buffer = buffer;
        this.subNodeStartByte = startByte;
        this.subNodesLength = subNodesLength;
    }

    public Node copy() throws IOException {
    	FileSystemNode copy = new FileSystemNode(getBounds(), id, getParent(), buffer, subNodeStartByte, subNodesLength );
    	copy.numShapesId=numShapesId;
    	copy.shapesId=new int [numShapesId];
    	System.arraycopy(shapesId, 0, copy.shapesId, 0, numShapesId);
    	copy.numSubNodes=numSubNodes;
		return copy;
	}
    /**
     * DOCUMENT ME!
     *
     * @return Returns the numSubNodes.
     */
    public int getNumSubNodes() {
        return this.numSubNodes;
    }

    /**
     * DOCUMENT ME!
     *
     * @param numSubNodes The numSubNodes to set.
     */
    public void setNumSubNodes(int numSubNodes) {
        this.numSubNodes = numSubNodes;
    }

    /**
     * DOCUMENT ME!
     *
     * @return Returns the subNodeStartByte.
     */
    public int getSubNodeStartByte() {
        return this.subNodeStartByte;
    }

    /**
     * DOCUMENT ME!
     *
     * @return Returns the subNodesLength.
     */
    public int getSubNodesLength() {
        return this.subNodesLength;
    }

    /**
     * @see org.geotools.index.quadtree.Node#getSubNode(int)
     */
    public Node getSubNode(int pos) throws StoreException {
        if (this.subNodes.size() > pos) {
            return super.getSubNode(pos);
        }

        try {
            FileSystemNode subNode = null;

            // Getting prec subNode...
            int offset = this.subNodeStartByte;

            if (pos > 0) {
                subNode = (FileSystemNode) getSubNode(pos - 1);
                offset = subNode.getSubNodeStartByte()
                    + subNode.getSubNodesLength();
            }

            buffer.goTo(offset);
            for (int i = 0, ii = subNodes.size(); i < ((pos + 1) - ii); i++) {
                subNode = readNode(pos, this, buffer);
                this.addSubNode(subNode);
            }
        } catch (IOException e) {
            throw new StoreException(e);
        }

        return super.getSubNode(pos);
    }
    
    /**
     * DOCUMENT ME!
     *
     * @param channel
     * @param order DOCUMENT ME!
     *
     *
     * @throws IOException
     */
    public static FileSystemNode readNode(int id, Node parent, FileChannel channel, ByteOrder order)
        throws IOException {
        ScrollingBuffer buffer = new ScrollingBuffer(channel, order);
        return readNode(id, parent, buffer);
    }

    static FileSystemNode readNode(int id, Node parent, ScrollingBuffer buf)
        throws IOException {
        // offset 
        int offset = buf.getInt();
        double x1;
        double y1;
        double x2;
        double y2;
        
        // envelope
        x1 = buf.getDouble();
        y1 = buf.getDouble();
        x2 = buf.getDouble();
        y2 = buf.getDouble();
        Envelope env = new Envelope(x1, x2, y1, y2);

        // shapes in this node
        int numShapesId = buf.getInt();
        int[] ids = new int[numShapesId];
        buf.getIntArray(ids);
        int numSubNodes = buf.getInt();

        // let's create the new node
        FileSystemNode node = new FileSystemNode(env, id, parent, buf, (int) buf.getPosition(), offset);
        node.setShapesId(ids);
        node.setNumSubNodes(numSubNodes);

        return node;
    }
    
    /**
     * A utility class to access file contents by using a single scrolling buffer reading
     * file contents with a minimum of 8kb per access
     */
    private static class ScrollingBuffer {
        FileChannel channel;
        ByteOrder order;
        ByteBuffer buffer;
        /** the initial position of the buffer in the channel */
        long bufferStart;
        
        
        public ScrollingBuffer(FileChannel channel, ByteOrder order) throws IOException {
           this.channel = channel;
           this.order = order;
           this.bufferStart = channel.position();
           
           // start with an 8kb buffer
           this.buffer = ByteBuffer.allocateDirect(8 * 1024);
           this.buffer.order(order);
           channel.read(buffer);
           buffer.flip();
        }
        
        public int getInt()  throws IOException {
            if(buffer.remaining() < 4)
                refillBuffer(4);
            return buffer.getInt();
        }
        
        public double getDouble()  throws IOException {
            if(buffer.remaining() < 8)
                refillBuffer(8);
            return buffer.getDouble();
        }
        
        public void getIntArray(int[] array)  throws IOException {
            int size = array.length * 4;
            if(buffer.remaining() < size)
                refillBuffer(size);
            // read the array using a view
            IntBuffer intView = buffer.asIntBuffer();
            intView.limit(array.length);
            intView.get(array);
            // don't forget to update the original buffer position, since the view is independent
            buffer.position(buffer.position() + size);
        }
        
        /**
         * 
         * @param requiredSize
         * @throws IOException
         */
        void refillBuffer(int requiredSize) throws IOException {
            // compute the actual position up to we have read something
            long currentPosition = bufferStart + buffer.position();
            // if the buffer is not big enough enlarge it
            if(buffer.capacity() < requiredSize) {
                int size = buffer.capacity();
                while(size < requiredSize) size *= 2;
                buffer = ByteBuffer.allocateDirect(size);
                buffer.order(order);
            }
            readBuffer(currentPosition);
        }

        private void readBuffer(long currentPosition) throws IOException {
            channel.position(currentPosition);
            buffer.clear();
            channel.read(buffer);
            buffer.flip();
            bufferStart = currentPosition;
        }

        /**
         * Jumps the buffer to the specified position in the file
         * @param newPosition
         * @throws IOException
         */
        public void goTo(long newPosition) throws IOException {
            // if the new position is already in the buffer, just move the buffer position
            // otherwise we have to reload it
            if(newPosition >= bufferStart && newPosition <= bufferStart + buffer.limit()) {
                buffer.position((int) (newPosition - bufferStart));
            } else {
                readBuffer(newPosition);
            }
        }
        
        /**
         * Returns the absolute position of the next byte that will be read
         * @return
         */
        public long getPosition() {
            return bufferStart + buffer.position();
        }
    }
}
