package iitb.con.query;

import iitb.con.caching.CacheObject;
import iitb.con.caching.MRUCache;
import iitb.con.core.Attribute;
import iitb.con.core.ConStoreConstants;
import iitb.con.core.Entity;
import iitb.con.core.EntityInstance;
import iitb.con.core.Instance;
import iitb.con.core.Relation;
import iitb.con.core.RelationInstance;
import iitb.con.core.Type;
import iitb.con.ds.InstanceMetaItem;
import iitb.con.ds.InstanceSerializer;
import iitb.con.ds.ItemSerializer;
import iitb.con.ds.ItemTable;
import iitb.con.ds.RelationTable;
import iitb.con.indexing.IndexBuilder;
import iitb.con.indexing.IndexCache;
import iitb.con.util.Unbound2DIntArray;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author Prathab K
 *
 */
public class ItemQuery implements Query {
    
    /** Type table having the information of types in concept-net */
    private ItemTable<String, Short, Type> typeTable;
    
    /** Instance table having the information about the instances in concept-net */
    private ItemTable<Short, Integer, InstanceMetaItem> instanceTable;
    
    /** Relation table having the information about the relations in concept-net */
    private RelationTable relationTable;
    
    /** Instance Serializer */
    private ItemSerializer<Instance> instanceSerializer;
    
    /** Instance reader to read the instances from concept-net */
    private ItemReader<Instance> instanceReader;
    
    /** MRU query cache */
    private MRUCache<String> queryCache;
    
    /** MRU instance cache */
    private MRUCache<Integer> instanceCache;

    
    /**
     * Constructs the ItemQuery object and initializes with meta-information of concept-net
     * @param cnetDirName concept-net directory name
     * @param typeTable type table reference
     * @param instanceTable instance table reference
     * @throws FileNotFoundException if concept-net file not found
     * @throws IOException if file operation fails
     */
    public ItemQuery(String cnetDirName, 
            ItemTable<String, Short, Type> typeTable, 
            ItemTable<Short, Integer, InstanceMetaItem> instanceTable,
            RelationTable relationTable) 
    throws FileNotFoundException, IOException{
        
        this.typeTable = typeTable;
        this.instanceTable = instanceTable;
        this.relationTable = relationTable;
        
        instanceSerializer = new InstanceSerializer(typeTable);
        instanceReader = new ItemReader<Instance>(cnetDirName + ConStoreConstants.NET_FILE,
                                                                        instanceSerializer);

        queryCache = new MRUCache<String>(ConStoreConstants.QUERY_CACHE_SIZE);
        instanceCache = new MRUCache<Integer>(ConStoreConstants.INSTANCE_CACHE_SIZE);
    }
    
    //Type or Instance Id Retrieval
    
    /**
     * Returns the type id of the specified instance
     * @param instanceId instance id
     * @return type id if exists, else returns -1
     */
    public short getTypeId(int instanceId) { 
        InstanceMetaItem item = instanceTable.get(instanceId);
        if(item != null)
            return item.typeId;
        return -1;
    }
    
    /**
     * Returns the type id of the specified type name
     * @param typeName type name
     * @return type id if exists, else returns -1
     */
    public short getTypeId(String typeName) { 
        Type t = typeTable.get(typeName, null);
        if(t != null) 
            return t.id;
        return -1;
    }
    
    /**
     * Returns the type name for the specified type id
     * @param typeId Type id
     * @return type name if exists, else <tt>null</tt>
     * @see Type
     */
    public String getTypeName(short typeId) {
        Type t = typeTable.get(typeId);
        if(t != null) 
            return t.name;
        return null;
    }
    
    /**
     * Returns all type ids
     * @return type ids as list
     */
    public List<Short> getAllTypeIds(){
        List<Short> idList = new ArrayList<Short>();
        List<Type> types = typeTable.getAllItems();
        for(Type type : types) {
            idList.add(type.id);
        }
        return idList;
    }
    
    /**
     * Returns all entity type ids
     * @return type ids as list
     */
    public List<Short> getAllEntityTypeIds(){
        List<Short> idList = new ArrayList<Short>();
        List<Type> types = typeTable.getAllItems();
        for(Type type : types) {
            if(type instanceof Entity)
                idList.add(type.id);
        }
        return idList;
    }
    
    /**
     * Returns all relation type ids
     * @return type ids as list
     */
    public List<Short> getAllRelationTypeIds(){
        List<Short> idList = new ArrayList<Short>();
        List<Type> types = typeTable.getAllItems();
        for(Type type : types) {
            if(type instanceof Relation)
                idList.add(type.id);
        }
        return idList;
    }
    
    /**
     * Returns the instance ids for the specified <tt>Type</tt> id
     * @param typeId type id
     * @return instance ids of the relevant type
     */
    public List<Integer> getInstanceIds(short typeId) { 
        Map<Integer,InstanceMetaItem> metaItems = getMetaInstances(typeId);
        
        List<Integer> result = new ArrayList<Integer>();
        
        if(metaItems != null) {
            for(InstanceMetaItem item : metaItems.values()) {
                result.add(item.instanceId);
            }
        }
        return result;
    }
    
    /**
     * Returns the instance ids for the specified <tt>Type</tt> name
     * @param typeName type name
     * @return instance ids of the relevant type
     */
    public List<Integer> getInstanceIds(String typeName) { 
        return getInstanceIds(getTypeId(typeName));
    }
    
    /**
     * Returns the existing <code>Instance</code> ids from 
     * the concept-nets.<br>
     * The instance id is retrieved based on the type with the
     * specified attribute name and its value. <br>
     * Returns <code>NULL</code>, if <code>Instance</code> does not exists.
     * @param typeName type name
     * @param attributeName instance's attribute name
     * @param value the attribute value
     * @return instance ids as  list
     */
    public <K extends Comparable<K>> List<Integer> getInstanceIds(String typeName, 
            String attributeName, K value) { 
        Type type = typeTable.get(typeName, null);
        if(type == null)
            return null;
        
        Attribute attribute = type.getAttribute(attributeName);
        if(attribute == null) 
            return null;
        
        List<Integer> instanceIdList = new ArrayList<Integer>();
        int[] indexedInstanceIds = null;
        
        if(IndexBuilder.hasIndex(type.id, attributeName)) {
            IndexCache indexCache = IndexCache.getInstance();
            indexedInstanceIds = indexCache.getInstancesIds(type.id, attributeName, value);
            if(indexedInstanceIds != null) {
                for(int i=0 ; i < indexedInstanceIds.length; i++) {
                    InstanceMetaItem item = instanceTable.get(indexedInstanceIds[i]);
                    if(item != null) {
                        instanceIdList.add(item.instanceId);
                    }
                }
            }
        }else {
            List<Integer> instanceIds = getInstanceIds(type.id);
            if(instanceIds != null) {
                for(Integer id : instanceIds) {
                    InstanceMetaItem item = instanceTable.get(id);
                    if(item != null) {
                        boolean toBeCached = false;
                        Object val = null;
                        CacheObject cacheObj = queryCache.get(new String(id + attributeName));
                        try {
                            if(cacheObj == null) {
                                val = instanceReader.getItemAttributeValue(item.location, 
                                                                        item.size, attributeName);
                                toBeCached = true;
                            }else {
                                val = cacheObj.object;
                            }
                        }catch(IOException ie) {
                            ie.printStackTrace();
                            return null;
                        }
                        int valSize = 0;
                        if(val != null) {
                            if(attribute.repeating) {
                                List<K> valList = (List<K>) val;
                                for(K v : valList) {
                                    if(value.compareTo(v) == 0) {
                                        instanceIdList.add(item.instanceId);
                                    }
                                    valSize +=8; // 8 is approx size TODO: calculate actual size
                                }
                            }else {
                                K attrValue = (K)val;
                                if(value.compareTo(attrValue) == 0){
                                    instanceIdList.add(item.instanceId);
                                }
                                valSize +=8; // 8 is approx size
                            }
                            if(toBeCached) {
                                cacheObj = new CacheObject(val, valSize);
                                queryCache.put(new String(item.instanceId + attributeName), cacheObj);
                            }
                        }else {
                            if(value == null){
                                instanceIdList.add(item.instanceId);
                            }
                        }
                    }
                }
            }
        }
        return instanceIdList;
    }
    
    /**
     * Returns the left associated entity id of the given relation id
     * @param relationInstanceId relation instance id
     * @return entity instance id if exists, else returns -1
     */
    public int getLeftEntityId(int relationInstanceId) {
        return relationTable.getLeftEntityId(relationInstanceId);
    }
    
    /**
     * Returns the right associated entity id of the given relation id
     * @param relationInstanceId relation instance id
     * @return entity instance id if exists, else returns -1
     */
    public int getRightEntityId(int relationInstanceId) { 
        return relationTable.getRightEntityId(relationInstanceId);
    }
    
    /**
     * Returns the instance ids based on the relation association of the specified instance
     * @param instanceId entity instance id
     * @return entity instance ids which have relations with the specified instance
     */
    public List<Integer> getRelatedInstanceIds(int instanceId) { 
        List<Integer> result = new ArrayList<Integer>();
        Unbound2DIntArray instanceIds = relationTable.getRelationTuplesByEntityId(instanceId);
        if(instanceIds != null) { // this approach is followed for better performance
            for(int i = 0; i < instanceIds.length; i++) {
                int[] a = instanceIds.get(i);
                result.add(a[RelationTable.RIGHT_ENTITY_IDX]);
            }
        }
        return result;
    }
    
    /**
     * Returns the related Instance ids for the specified entity instance id and 
     * filtered by relation type name
     * @param instanceId entity instance id whose relations are to be fetched
     * @param relationTypeName relation type name to filter the instance's relations
     * @return Instance's relations Id as <tt>Integer List</tt> 
     */
    public List<Integer> getRelatedInstanceIds(int instanceId, String relationTypeName) { 
        List<Integer> result = new ArrayList<Integer>();
        Unbound2DIntArray instanceIds = relationTable.getRelationTuplesByEntityId(instanceId);
        
        Map<Integer, InstanceMetaItem> instanceMap = 
            instanceTable.get(getTypeId(relationTypeName));
        
        if(instanceIds != null && instanceMap != null) {
            for(int i = 0; i < instanceIds.length; i++) {
                int[] a = instanceIds.get(i);
                InstanceMetaItem item = instanceMap.get(a[RelationTable.RELATION_IDX]); 
                if(item != null)
                    result.add(a[RelationTable.RIGHT_ENTITY_IDX]);
            }
        }
        return result;
        
    }
    
    /**
     * Returns the associated relations Ids of an entity instance id
     * @param instanceId entity instance id whose relations are to be fetched
     * @return Instance's relations Id as <tt>Integer List</tt> 
     */
    public List<Integer> getRelationIds(int instanceId) { 
        List<Integer> result = new ArrayList<Integer>();
        Unbound2DIntArray instanceIds = relationTable.getRelationTuplesByEntityId(instanceId);
        if(instanceIds != null) { // this approach is followed for better performance
            for(int i = 0; i < instanceIds.length; i++) {
                int[] a = instanceIds.get(i);
                result.add(a[RelationTable.RELATION_IDX]); // index '1' indicates the relation id in the tuple
            }
        }
        return result;
    }
    
    /**
     * Returns the associated relations Ids of an entity instance id and
     * filtered by relation type name
     * @param instanceId entity instance id whose relations are to be fetched
     * @param relationTypeName relation type name to filter the instance's relations
     * @return Instance's relations Id as <tt>Integer List</tt> 
     */
    public List<Integer> getRelationIds(int instanceId, String relationTypeName) { 
        List<Integer> idList = new ArrayList<Integer>();
        Map<Integer, InstanceMetaItem> instanceMap = 
            instanceTable.get(getTypeId(relationTypeName));
        
        List<Integer> relIds = getRelationIds(instanceId);
        
        if(instanceMap != null) {
            for(Integer id : relIds) {
                InstanceMetaItem item = instanceMap.get(id);
                if(item != null)
                    idList.add(id);
            }
        }
        return idList;
    }
    
    /**
     * Returns all the Entity and Relation Instances Id
     * @return Instance Ids as <tt>List</tt>
     */
    public List<Integer> getAllInstanceIds() { 
        List<Integer> result = new ArrayList<Integer>();
        List<InstanceMetaItem> metaItems = instanceTable.getAllItems();
        if(metaItems != null) {
            for(InstanceMetaItem item : metaItems)
                result.add(item.instanceId);
        }
        
        return result;
    }
    
    /**
     * Returns all the Entity Instances Id
     * @return Instance Ids as <tt>List</tt>
     */
    public List<Integer> getAllEntityInstanceIds() { 
        List<Integer> idList = new ArrayList<Integer>();
        List<Short> typeIds = getAllEntityTypeIds();
        if(typeIds != null) {
            for(Short id : typeIds) {
                List<Integer> instanceIds = getInstanceIds(id);
                if(instanceIds != null) {
                    for(Integer i : instanceIds)
                        idList.add(i);
                }
            }
        }
        return idList;
    }
    
    /**
     * Returns all the Relation Instances Id
     * @return Instance Ids as <tt>List</tt>
     */
    public List<Integer> getAllRelationInstanceIds() { 
        List<Integer> idList = new ArrayList<Integer>();
        List<Short> typeIds = getAllRelationTypeIds();
        if(typeIds != null) {
            for(Short id : typeIds) {
                List<Integer> instanceIds = getInstanceIds(id);
                if(instanceIds != null) {
                    for(Integer i : instanceIds)
                        idList.add(i);
                }
            }
        }
        return idList;
    }
    
    //Type retrieval
    
    /**
     * Returns <tt>Type<tt> for the specified type name
     * @return {@link Type} if exists, else <tt>null</tt>
     */
    public Type getType(String typeName) { 
        return typeTable.get(typeName, null);
    }
    
    //Instance retrieval
    
    /**
     * Returns the instance for the specified instance id
     * @param instanceId instance id
     * @return @link{Instance}
     */
    public Instance getInstance(int instanceId) { 
        InstanceMetaItem item = instanceTable.get(instanceId);
        try {
            if(item != null) {
                Instance instance = readInstance(item);
                return instance;
            }
        }catch(IOException ie) {
            ie.printStackTrace();
        }
        return null;
    }
    
    
    /**
     * Returns the existing <code>Instance</code> from 
     * the concept-nets.<br>
     * The instance is retrieved based on the type with the
     * specified attribute name and its value. <br>
     * Returns <code>NULL</code>, if <code>Instance</code> does not exists.
     * @param typeName type name
     * @param attributeName instance's attribute name
     * @param value the attribute value
     * @return {@link iitb.con.core.Instance} list
     */
    public <K extends Comparable<K>> List<Instance> getInstances(String typeName, 
            String attributeName, K value) {
        List<Integer> idList = getInstanceIds(typeName, attributeName, value);
        if(idList != null)
            return getInstances(idList);
        return null;
    }

    /**
     * Returns the instances for the specified instance id
     * @param idList list of instance ids
     * @return @link{Instance}
     */
    public List<Instance> getInstances(List<Integer> idList) { 
        List<Instance> instanceList = new ArrayList<Instance>();
        try {
            for(Integer id : idList) {
                InstanceMetaItem item = instanceTable.get(id);
                Instance instance = readInstance(item);
                if(instance != null) 
                    instanceList.add(instance);
            }
        }catch(IOException ie) {
            ie.printStackTrace();
            return null;
        }
        return instanceList;
    }
    
    /**
     * Returns all the instances for the specified <tt>Type</tt> 
     * @param typeId type id
     * @return instance list of the given type
     * @see Instance
     */
    public List<Instance> getInstances(Short typeId) {
        List<Integer> idList = getInstanceIds(typeId);
        if(idList != null) {
            return getInstances(idList);
        }
        return null;
        
    }
    
    /**
     * Returns all the instances for the specified <tt>Type</tt> 
     * @param typeName type name
     * @return instance list of the given type
     * @see Instance
     */
    public List<Instance> getInstances(String typeName) {
        Type type = getType(typeName);
        return getInstances(type.id);
    }
    
    /**
     * Returns all the entity instances of specified <tt>Type</tt>
     * @param typeId type id
     * @return Entity instance list of the given type
     * @see EntityInstance
     */
    public List<EntityInstance> getEntityInstances(Short typeId){
        List<Integer> idList = getInstanceIds(typeId);
        if(idList != null) {
            return getEntityInstances(idList);
        }
        return null;
    }
    
    /**
     * Returns all the entity instances of specified <tt>Type</tt>
     * @param typeName type name
     * @return Entity instance list of the given type
     * @see EntityInstance
     */
    public List<EntityInstance> getEntityInstances(String typeName){
        return getEntityInstances(getTypeId(typeName));
    }
    
    /**
     * Returns all the relation instances of specified <tt>Type</tt>
     * @param typeId type id
     * @return Relation instance list of the given type
     * @see RelationInstance
     */
    public List<RelationInstance> getRelationInstances(Short typeId){
        List<Integer> idList = getInstanceIds(typeId);
        if(idList != null) {
            return getRelationInstances(idList);
        }
        return null;
    }
    
    /**
     * Returns all the relation instances of specified <tt>Type</tt>
     * @param typeName type name
     * @return Relation instance list of the given type
     * @see RelationInstance
     */
    public List<RelationInstance> getRelationInstances(String typeName){
        return getRelationInstances(getTypeId(typeName));
    }
    
    /**
     * Returns all the <tt>EntitiyInstances</tt> in the concept-net 
     * @return entity instance list
     * @see EntityInstance
     */
    public List<EntityInstance> getAllEntityInstances(){
        List<EntityInstance> entityInstances = new ArrayList<EntityInstance>();
        List<Short> entityIds = getAllEntityTypeIds();
        for(Short id : entityIds) {
            Map<Integer,InstanceMetaItem> instances = getMetaInstances(id);
            
            try {
                if(instances != null) {
                    for(InstanceMetaItem item : instances.values()) {
                        Instance instance = readInstance(item);
                        if(instance instanceof EntityInstance) {
                            entityInstances.add((EntityInstance) instance);
                        }
                    }
                }
            }catch(IOException io) {
                io.printStackTrace();
            }
        }
        return entityInstances;
    }
    
    /**
     * Returns all the <tt>RelationInstances</tt> in the concept-net 
     * @return relation instance list
     * @see RelationInstance
     */
    public List<RelationInstance> getAllRelationInstances(){
        List<RelationInstance> relationInstances = new ArrayList<RelationInstance>();
        List<Short> relationIds = getAllRelationTypeIds();
        for(Short id : relationIds) {
            Map<Integer,InstanceMetaItem> instances = getMetaInstances(id);
            
            try {
                if(instances != null) {
                    for(InstanceMetaItem item : instances.values()) {
                        Instance instance = readInstance(item);
                        if(instance instanceof RelationInstance) {
                            relationInstances.add((RelationInstance) instance);
                        }
                    }
                }
            }catch(IOException io) {
                io.printStackTrace();
            }
        }
        return relationInstances;
    }
    
    //low-level access methods
    /**
     * Returns the instance meta item of specified instance id
     * @param instanceId instance's id
     * @return {@link InstanceMetaItem}
     */
    public InstanceMetaItem getInstanceMetaItem(int instanceId) {
        return instanceTable.get(instanceId);
    }
    
    //non query methods
    
    /**
     * Closes the files that associated with the query object
     * @throws IOException if file operation fails
     */
    public void close() throws IOException {
        instanceReader.close();
        IndexCache.getInstance().clear();
    }

    //private methods
    
    /**
     * Returns the instances for the specified type id
     * @param typeId type id
     * @return Instance Table Items with instance id key as Map
     *@see InstanceMetaItem 
     */
    private Map<Integer,InstanceMetaItem> getMetaInstances(Short typeId) {
        return (Map<Integer,InstanceMetaItem>) instanceTable.getObject(typeId);
    }
    
    /**
     * Reads the instance from the file
     * @param item {@link InstanceMetaItem}
     * @return instance
     * @throws IOException on file error
     */
    private Instance readInstance(InstanceMetaItem item) throws IOException {
        Instance instance = null;
        CacheObject cacheObj = instanceCache.get(item.instanceId);
        if(cacheObj == null) {
            instance = instanceReader.getItem(item.location, item.size);
            if(instance != null)
                instance.id = item.instanceId;
            cacheObj = new CacheObject(instance, item.size);
            instanceCache.put(item.instanceId, cacheObj);
        }
        instance = (Instance) cacheObj.object;
        return instance;
    }
    
    /*private Instance readInstance(InstanceMetaItem item) throws IOException {
        Instance instance = instanceCache.get(item.instanceId);
        if(instance == null) {
            //System.out.println("Miss");
            instance = instanceReader.getItem(item.location, item.size);
            if(instance != null)
                instance.id = item.instanceId;
            if(item.clusterId > 0) instanceCache.put(instance, item);
            //preFetchInstances(item.instanceId);
        } else {
            //System.out.println("Hit");
        }
        return instance;
    }*/
    
/*
    private Instance readInstance(InstanceMetaItem item) throws IOException {
        Instance instance = lruCache.get(item.instanceId);
        if(instance == null) {
            System.out.println("Miss");
            instance = instanceReader.getItem(item.location, item.size);
            if(instance != null)
                instance.id = item.instanceId;
        }else {
            System.out.println("Hit");
        }
        
        lruCache.put(instance.id, instance);
        return instance;
    }*/
    
    /*private Instance readInstance(InstanceMetaItem item) throws IOException {
        Instance instance = instanceReader.getItem(item.location, item.size);
        if(instance != null) 
            instance.id = item.instanceId;
        return instance;
    }*/
    
    /*private Instance readInstance(InstanceMetaItem item) throws IOException {
        Instance instance = instanceCache.get(item.instanceId);
        if(instance == null) {
            instance = instanceReader.getItem(item.location, item.size);
            if(instance != null)
                instance.id = item.instanceId;
            if(item.clusterId > 0) instanceCache.put(instance, item);
        }
        //lruCache.put(instance.id, instance);
        return instance;
    }*/
    
    /*private void preFetchInstances(Integer instanceId) throws IOException {
        Unbound2DIntArray instanceIds = relationTable.getRelatedInstanceIds(instanceId);
        if(instanceIds != null) { 
            for(int i = 0; i < instanceIds.length; i++) {
                int[] a = instanceIds.get(i);
                int rid = a[RelationTable.RIGHT_ENTITY_IDX];
                long freeSpace = instanceCache.freeSpace() / 2;
                InstanceMetaItem item = instanceTable.get(rid);
                Instance instance = instanceReader.getItem(item.location, item.size);
                if(instance != null) {
                    instance.id = item.instanceId;
                    instanceCache.put(instance, item);
                }
            }
        }
    }*/
    
    private List<EntityInstance> getEntityInstances(List<Integer> idList) { 
        List<EntityInstance> instanceList = new ArrayList<EntityInstance>();
        try {
            for(Integer id : idList) {
                InstanceMetaItem item = instanceTable.get(id);
                Instance instance = readInstance(item);
                if(instance != null && instance instanceof EntityInstance) 
                    instanceList.add((EntityInstance)instance);
            }
        }catch(IOException ie) {
            ie.printStackTrace();
            return null;
        }
        return instanceList;
    }
    
    private List<RelationInstance> getRelationInstances(List<Integer> idList) { 
        List<RelationInstance> instanceList = new ArrayList<RelationInstance>();
        try {
            for(Integer id : idList) {
                InstanceMetaItem item = instanceTable.get(id);
                Instance instance = readInstance(item);
                if(instance != null && instance instanceof RelationInstance) 
                    instanceList.add((RelationInstance)instance);
            }
        }catch(IOException ie) {
            ie.printStackTrace();
            return null;
        }
        return instanceList;
    }
}