/* 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.core.ConStoreConstants;
import iitb.con.core.Instance;
import iitb.con.core.Type;
import iitb.con.io.BufferedFileAdapter;
import iitb.con.io.IOAdapter;
import iitb.con.util.ArrayFreeSpaceList;
import iitb.con.util.FreeSpaceList;
import iitb.con.util.MultiKeyHashMap;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;

/**
 * InstanceTable holds the <tt>InstanceMetaItem</tt> as value objects.
 * The super key is the type id and the sub key is the instance id. <p>
 * Instances are managed in the concept-net as a set of three files: <br>
 * ntab file - holds the information about the instance-meta information as InstanceItem <br>
 * net file - holds the instances <br>
 * nfsl file - free blocks list in the .net file <br> <p>
 * The instances are added/updated/removed to the action buffer list and it is committed to the 
 * respective file on calling commit() method.
 * 
 * @author Prathab K
 * @see InstanceMetaItem
 * @see Instance
 */
public class InstanceTable implements ItemTable<Short, Integer, InstanceMetaItem> {

    /** InstanceMetaItem file handle */
    private IOAdapter ntabFile;
    
    /** Concept-net file handle */
    private IOAdapter netFile;
    
    /** Free space list file handle */
    private FreeSpaceList nfsList;
    
    /** Type Table */
    private ItemTable<String, Short, Type> typeTable;
    
    /** Action state list of the instances */
    private ActionBufferList<InstanceMetaItem> instanceBufferList;
    
    /** InstanceItem Table */
    private MultiKeyHashMap<Short, Integer, InstanceMetaItem> instanceItemTable;
    
    /** Instance Item Serializer */
    ItemSerializer<Instance> instanceSerializer;
    
    /**
     * Constructs the InstanceTable.
     * 
     * @param dirName directory name of the concept-net
     * @param mode opening mode (r, rw)
     * @throws FileNotFoundException if file not found
     * @throws IOException if file operation fails
     */
    public InstanceTable(String dirName, String mode) 
    throws FileNotFoundException, IOException
    {
        ntabFile = new BufferedFileAdapter(dirName + ConStoreConstants.INSTANCE_TABLE_FILE, mode);
        typeTable = TypeTableFactory.getTypeTable(dirName + ConStoreConstants.META_FILE, mode);
        netFile  = new BufferedFileAdapter(dirName + ConStoreConstants.NET_FILE, mode);
        nfsList = new ArrayFreeSpaceList(dirName + ConStoreConstants.NET_FSL_FILE, mode);
        instanceBufferList = new ActionBufferList<InstanceMetaItem>();
        instanceItemTable = new MultiKeyHashMap<Short, Integer, InstanceMetaItem>();
        instanceSerializer = new InstanceSerializer(typeTable);
    }
    
    /**
     * Commits the instances to the respective file.<br>
     * Commit operation is performed based on the object and action state
     * that present in the ActionBufferList.
     * 
     * @throws IOException if file operation fails
     */
    public void commit() throws IOException {
        //Remove action
        for(InstanceMetaItem item : instanceBufferList.getRemoveList()) {
            //TODO: optimize the performance of freeing the block
            nfsList.freeTheBlock(item.location, item.size);
            ByteBuffer buf = ByteBuffer.allocate(InstanceMetaItem.headerSize());
            buf.putShort((short)0);
            buf.putShort((short)-1);
            buf.putInt(0);
            buf.putLong(0);
            buf.putInt(0);
            buf.rewind();
            ntabFile.write(buf, item.itemLocation);
            instanceItemTable.remove(item.typeId,item.instanceId);
        }
        
        //Add action
        for(InstanceMetaItem item : instanceBufferList.getAddList()) {
            long location = nfsList.getFreeBlock(item.size);
            item.location = location;
            netFile.write(item.instanceBytes, location);
            item.instance = null;
            item.instanceBytes = null;
            this.setItem(item.typeId,item.instanceId, item);
            writeInstanceMetaItem(item);
        }

        //update action
        for(InstanceMetaItem item : instanceBufferList.getUpdateList()) {
            netFile.write(item.instanceBytes, item.location);
            item.instance = null;
            item.instanceBytes = null;
            this.setItem(item.typeId,item.instanceId, item);
            writeInstanceMetaItem(item);
        }
        
        //meta update action
        for(InstanceMetaItem item : instanceBufferList.getMetaUpdateList()) {
            this.setItem(item.typeId,item.instanceId, item);
            writeInstanceMetaItem(item);
        }
        nfsList.commit();
        instanceBufferList.clear();
    }
    
    /**
     * Writes the InstanceMetaItem to .ntab file
     * @param item InstanceMetaItem
     * @throws IOException if file operation fails
     * @see InstanceMetaItem
     */
    private void writeInstanceMetaItem(InstanceMetaItem item) throws IOException {
        ByteBuffer buf = ByteBuffer.allocate(InstanceMetaItem.headerSize());
        buf.putShort(item.typeId);
        buf.putShort(item.clusterId);
        buf.putInt(item.instanceId);
        buf.putLong(item.location);
        buf.putInt(item.size);
        buf.rewind();
        if(item.itemLocation < 0)
            item.itemLocation = ntabFile.getFileLength();
        ntabFile.write(buf, item.itemLocation);
    }
    
    /**
     * Returns the InstanceMetaItem for the given type id and instance id
     * @param typeId Type id
     * @param instanceId Instance id
     * @return {@link InstanceMetaItem}
     * @see Type
     * @see Instance
     */
    public InstanceMetaItem get(Short typeId, Integer instanceId) {
        return instanceItemTable.get(typeId, instanceId);
    }
    
    /**
     * Returns the hash map with key-value pair as InstanceId , InstanceMetaItem 
     * for the given type id
     * @param typeId Type id
     * @return hash map with key-value pair as InstanceId , InstanceMetaItem 
     */
    public Map<Integer, InstanceMetaItem> get(Short typeId) {
        return instanceItemTable.get(typeId);
    }
    
    /**
     * Returns the InstanceMetaItem for the given instance id
     * @param instanceId Instance id
     * @return {@link InstanceMetaItem}
     * @see Instance
     */
    public InstanceMetaItem get(Integer instanceId) {
        return instanceItemTable.getBySubKey(instanceId);
    }
    
    /**
     * Returns the InstanceMetaItems for the given type id
     * @param o Type id - <tt>Short</tt> type
     * @return InstanceMetaItems as Map with instance id as key and InstanceMetaItem as value
     */
    public Object getObject(Object o) {
        if(o instanceof Short)
            return instanceItemTable.get((Short)o);
        return null;
    }
    
    /**
     * Returns all the InstanceMetaItems from the table
     * @return InstanceMetaItems as List
     * @see InstanceMetaItem
     */
    public List<InstanceMetaItem> getAllItems() {
        return instanceItemTable.getAllItems();
    }
    
    /**
     * Sets the InstanceMetaItem with keys as type id and instance id
     * @param typeId Type id
     * @param instanceId Instance id
     * @param item InstanceMetaItem
     * @see Type
     * @see Instance
     * @see InstanceMetaItem
     */
    public void setItem(Short typeId, Integer instanceId, InstanceMetaItem item ){
        instanceItemTable.put(typeId,instanceId, item);
    }
    
    /**
     * Puts the InstanceMetaItem into the instance table with keys as type id and instance id
     * @param typeId Type id
     * @param instanceId Instance id
     * @param item InstanceMetaItem
     * @see Type
     * @see Instance
     * @see InstanceMetaItem
     */
    public void put(Short typeId, Integer instanceId, InstanceMetaItem item){
        this.put(item);
    }
    
    /**
     * Puts the InstanceMetaItem into the instance table with key as type id
     * @param typeId Type id
     * @param item InstanceMetaItem
     * @see Type
     * @see InstanceMetaItem
     */
    public void put(Short typeId, InstanceMetaItem item) {
        this.put(item);
    }
    
    /**
     * Puts the InstanceMetaItem into the instance table
     * @param item InstanceMetaItem
     * @see InstanceMetaItem
     */
    public void put(InstanceMetaItem item) {
        if(item.instance == null) {
            instanceBufferList.add(item, item.size, ActionBufferList.Action.UPDATE_META);
        } else {
            
            item.instanceBytes = instanceSerializer.serialize(item.instance);
            
            if(!instanceItemTable.containsKey(item.typeId,item.instanceId)) {
                item.size = item.instanceBytes.capacity();
                instanceBufferList.add(item, item.size, ActionBufferList.Action.ADD);
            }
            else{
                if(item.size == item.instanceBytes.capacity())
                    instanceBufferList.add(item, item.size, ActionBufferList.Action.UPDATE);
                else {
                    remove(item);
                    item.size = item.instanceBytes.capacity();
                    instanceBufferList.add(item, item.size, ActionBufferList.Action.ADD);
                }
            }
        }
    }
    
    /**
     * Removes the instance from the instance table with instance id as key
     * @param instanceId Instance id
     * @return <tt>true</tt> on success
     */
    public boolean remove(Integer instanceId) {
        InstanceMetaItem item  = get(instanceId);
        if(item != null)
            return remove(item);
        return false;
    }
    
    /**
     * Removes the instance from the instance table with type id and instance id as keys
     * @param typeId Type id
     * @param instanceId Instance id
     * @return <tt>true</tt> on success
     */
    public boolean remove(Short typeId, Integer instanceId) {
        //return remove(instanceId);
        instanceItemTable.containsKey(typeId, instanceId);
        InstanceMetaItem item = instanceItemTable.get(typeId, instanceId);
        //TODO: error message if item does not exists
        if(item != null)
            return remove(item);
        return false;
    }
    
    /**
     * Removes the instance from the instance table with the given <tt>InstanceMetaItem</tt> handle
     * @param item InstanceMetaItem
     * @return <tt>true</tt> on success
     * @see InstanceMetaItem
     */
    public boolean remove(InstanceMetaItem item){
        InstanceMetaItem tempItem = new InstanceMetaItem(item);
        return instanceBufferList.add(tempItem,tempItem.size,ActionBufferList.Action.REMOVE);
    }
    
    /**
     * Close the file associate with the table
     * @throws IOException if file operations fail
     */
    public void close() throws IOException 
    {
        if(ntabFile != null)
            ntabFile.close();
        if(netFile != null)
            netFile.close();
        if(nfsList != null)
            nfsList.close();
        
        instanceItemTable.clear();
        instanceBufferList.clear();
    }
    
    /**
     * Returns the size of the table i.e. the no. of instance meta items 
     * @return table size
     */
    public int size() {
        return instanceItemTable.size();
    }
}