/* Copyright (C) 2009  CSE,IIT Bombay  http://www.cse.iitb.ac.in

This file is part of the ConStore open source storage facility for concept-nets.

ConStore is free software and distributed under the 
Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License;
you can copy, distribute and transmit the work
with the work attribution in the manner specified by the author or licensor.
You may not use this work for commercial purposes and may not alter, 
transform, or build upon this work.

Please refer the legal code of the license, available at
http://creativecommons.org/licenses/by-nc-nd/3.0/legalcode

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

package iitb.con.ds;

import iitb.con.caching.SimpleLRUCache;
import iitb.con.core.ConStoreConstants;
import iitb.con.io.BufferedFileAdapter;
import iitb.con.io.IOAdapter;
import iitb.con.util.IntArrayGrid;
import iitb.con.util.Sorted2DIntArray;
import iitb.con.util.Unbound2DIntArray;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * Relation Table hold the relation information of the concept-net as 
 * <left-entity id, relation id, right entity id>. The table can be 
 * searched using left-entity id as it is maintained in the 
 * sorted order. Similarly the search can made against the 
 * relation id and it is searched using the instance ids index.
 * The tuples are maintained in blocks (4KB) and brought to the 
 * main memory on demand. On commit at concept-net level, 
 * the added information are committed to the disk. 
 *
 * @author Prathab K
 *
 */
public class RelationTable {

    /** Relation table (rtab) file */
    private IOAdapter rTabFile;
    
    /** Instance index <instance-id, block-id> */
    private Sorted2DIntArray instanceIndex;
    
    /** Instance id's index file */
    private IOAdapter instanceIndexFile;
    
    /** In-memory management of relation table as grid*/
    private IntArrayGrid grid;
    
    /** Id of the current grid */
    private int currentGridId;
    
    /** Caches the grids */
    private SimpleLRUCache<Integer, IntArrayGrid> cache;
    
    /** Number of columns for the grid */
    private static final int COL_NUM = 3;
    
    /**
     * Concept net directory name is provided to 
     * locate the necessary files. Also initialzed 
     * with cache size
     * @param dirName concept-net directory name
     * @param cacheSize cache size
     * @throws FileNotFoundException
     * @throws IOException
     */
    public RelationTable(String dirName, long cacheSize) 
    throws FileNotFoundException, IOException
    {
        String idxFileName = dirName + ConStoreConstants.RELATION_IDX_FILE;
        String rtabFileName = dirName + ConStoreConstants.RELATION_TABLE_FILE;
        File file = new File(idxFileName);
        int length = (int)file.length();
        instanceIndexFile = new BufferedFileAdapter(idxFileName , "rw", length);
        ByteBuffer buffer = instanceIndexFile.readIntoBuffer(0);
        instanceIndex = new Sorted2DIntArray(buffer);
        
        file = new File(rtabFileName);
        length = (int)file.length();
        int blockId = length / IntArrayGrid.BLOCK_SIZE;
        rTabFile = new BufferedFileAdapter(rtabFileName, "rw");
        currentGridId = blockId;

        cache = new SimpleLRUCache<Integer, IntArrayGrid>((int)(cacheSize / IntArrayGrid.BLOCK_SIZE));
        grid = getGridBlock(blockId);
        
    }

    /**
     * Inserts the relation tuple into the relation table
     * @param leftId left entity id
     * @param relationId relation id
     * @param rightId right entity id
     * @return <tt>true</tt> on success
     */
    public boolean insert(int leftId, int relationId, int rightId) {
        int n = grid.add(leftId, relationId, rightId);
        if(n < 0) {
            newGrid();
            grid.add(leftId, relationId, rightId);
        }

        instanceIndex.add(leftId, grid.id);
        instanceIndex.add(relationId, grid.id);
        return true;
    }
    
    /**
     * Updates the relation tuple in the relation table.
     * Relation id is used as key to update the tuple. 
     * @param leftId left entity id
     * @param relationId relation id
     * @param rightId right entity id
     * @return <tt>true</tt> on success
     */
    public boolean update(int leftId, int relationId, int rightId) {
        IntArrayGrid[] grids = getGrid(leftId);
        for(IntArrayGrid grid : grids) {
            if(grid.setRow(leftId, relationId, rightId)) {
                commitGrid(grid);
                return true;
            }
        }
        return false;
    }
    
    /**
     * Removes the relation tuples based on the 
     * left entity id.
     * @param leftId left entity id
     * @return <tt>true</tt> on success
     */
    public boolean removeInstance(int leftId) {
        IntArrayGrid[] grids = getGrid(leftId);
        if(grids != null) {
            for(IntArrayGrid grid : grids) {
                if(grid.setRow(leftId, LEFT_ENTITY_IDX, -1)) {
                    commitGrid(grid);
                    instanceIndex.remove(leftId);
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * Removes the relation tuple based on the relation id
     * @param relationId relation id
     * @return <tt>true</tt> on success
     */
    public boolean removeRelation(int relationId) {
        IntArrayGrid[] grids = getGrid(relationId);
        if(grids != null) {
            for(IntArrayGrid grid : grids) {
                if(grid.setRow(relationId, RELATION_IDX, -1, -1, -1)) {
                    commitGrid(grid);
                    instanceIndex.remove(relationId);
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * Returns the relation tuples based on the 
     * left entity id.
     * @param leftEntityId left entity id
     * @return <tt>true</tt> on success
     */
    public Unbound2DIntArray getRelationTuplesByEntityId(int leftEntityId) {
        IntArrayGrid[] grids = getGrid(leftEntityId);
        Unbound2DIntArray result = null;
        if(grids != null) {
            result = new  Unbound2DIntArray(5, COL_NUM); //5 is initial size
            for(int i = 0; i< grids.length ; i++) {
                int[][] r = grids[i].getRows(leftEntityId, LEFT_ENTITY_IDX);
                if(r != null) {
                    for(int j = 0; j < r.length ; j++)
                        result.add(r[j][LEFT_ENTITY_IDX], r[j][RELATION_IDX], r[j][RIGHT_ENTITY_IDX]);
                }
            }
        }
        return result;
    }

    /**
     * Returns the relation tuple based on the realtion id.
     * @param relationId relation id
     * @return <tt>true</tt> on success
     */
    public Unbound2DIntArray getRelationTupleByRelationId(int relationId) {
        IntArrayGrid[] grids = getGrid(relationId);
        Unbound2DIntArray result = null;
        if(grids != null) {
            result = new  Unbound2DIntArray(5, COL_NUM); //5 is initial size
            for(int i = 0; i< grids.length ; i++) {
                int[][] r = grids[i].getRows(relationId, RELATION_IDX);
                if(r != null) {
                    for(int j = 0; j < r.length ; j++)
                        result.add(r[j][LEFT_ENTITY_IDX], r[j][RELATION_IDX], r[j][RIGHT_ENTITY_IDX]);
                }
            }
        }
        return result;
    }
    
    /**
     * Returns the left entity id for the given relation id
     * @param relationId relation id
     * @return left entity id of the relation
     */
    public int getLeftEntityId(int relationId) {
        Unbound2DIntArray result = getRelationTupleByRelationId(relationId);
        if(result != null) {
            if(result.length > 0) {
                int[] r = result.get(0);
                return r[LEFT_ENTITY_IDX];
            }
        }
        return -1;
    }
    
    /**
     * Returns the right entity id for the given relation id
     * @param relationId relation id
     * @return left entity id of the relation
     */
    public int getRightEntityId(int relationId) {
        Unbound2DIntArray result = getRelationTupleByRelationId(relationId);
        if(result != null) {
            if(result.length > 0) {
                int[] r = result.get(0);
                return r[RIGHT_ENTITY_IDX]; 
            }
        }
        return -1;
    }
    
    /**
     * Returns all the relation ids of the current array grid
     * @return relation ids as integer array
     */
    public int[] getAllRelationIds() {
        return grid.getCol(RELATION_IDX); 
    }
    
    /**
     * Creates a new array grid
     */
    private void newGrid() {
        commitGrid(grid);
        currentGridId++;
        grid = new IntArrayGrid(currentGridId, COL_NUM);
    }
    
    /**
     * Returns the array grid based on the id
     * @param leftId left entity id
     * @return {@link IntArrayGrid}
     */
    private IntArrayGrid[] getGrid(int leftId) {
        IntArrayGrid[] grids = null;
        int[][] r = instanceIndex.get(leftId);
        if(r != null) {
            grids = new IntArrayGrid[r.length];
            for(int i = 0; i < r.length; i++)
                grids[i] = getGridBlock(r[i][1]);
        }
        
        return grids;
            
    }
    
    /**
     * Returns the array grid based on the block id
     * @param blockId block id
     * @return {@link IntArrayGrid}
     */
    private IntArrayGrid getGridBlock(int blockId) {
        IntArrayGrid grid = cache.get(blockId);
        if(grid != null) {
            cache.put(blockId, grid);
            return grid;
        }
        
        try {
            ByteBuffer buf = rTabFile.readIntoBuffer(IntArrayGrid.BLOCK_SIZE * blockId);
            grid = new IntArrayGrid(blockId, COL_NUM, buf);
            cache.put(blockId, grid);
            return grid;
        }catch(IOException ie) {
            ie.printStackTrace();
        }
        return null;
    }
    
    /**
     * Commits the contents of array grid to the disk 
     * @param grid array grid
     * @see IntArrayGrid
     */
    private void commitGrid(IntArrayGrid grid) {
        try {
            rTabFile.write(grid.toBuffer(), IntArrayGrid.BLOCK_SIZE * grid.id);
        }catch(IOException ie) {
            ie.printStackTrace();
        }
    }
    
    /**
     * Commits the instance id's index
     */
    private void commitInstanceIndex() {
        try {
            instanceIndexFile.write(instanceIndex.toBuffer(), 0);
        }catch(IOException ie) {
            ie.printStackTrace();
        }
    }
    
    /**
     * Commits the relation table changes
     */
    public void commit() {
        commitGrid(grid);
        commitInstanceIndex();
    }
    
    /**
     * Closes the relation table associated files
     */
    public void close() {
        try {
            rTabFile.close();
            instanceIndexFile.close();
        }catch(IOException ie) {
            ie.printStackTrace();
        }
    }
    
    /*public void dumpIndex() {
        instanceIndex.dump();
    }*/
    
    /** Left entity index */
    public static final int LEFT_ENTITY_IDX = 0;
    
    /** Relation index */
    public static final int RELATION_IDX = 1;
    
    /** Right entity index */
    public static final int RIGHT_ENTITY_IDX = 2;
}