/* 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.net;

import iitb.con.core.Attribute;
import iitb.con.core.ConStoreConstants;
import iitb.con.core.Entity;
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.InstanceTableFactory;
import iitb.con.ds.ItemTable;
import iitb.con.ds.Node;
import iitb.con.ds.RelationTable;
import iitb.con.ds.TypeTableFactory;
import iitb.con.indexing.IndexBuilder;
import iitb.con.net.codegenerator.CodeGenerator;
import iitb.con.query.ItemQuery;
import iitb.con.query.Query;
import iitb.con.util.FileUtil;
import iitb.con.util.PropertiesConfig;
import iitb.con.util.Result;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;

/**
 * FileConceptNet extends and provides the implementation for the {@link ConceptNet} class.  
 * It manages the {@link Type} and {@link Instance} in the concept-nets.<br>
 * It provides methods to add, update, and remove the types and instances in the concept-nets.<br>
 * It also provides a high level retrieval support through <tt>Query</tt> object.<br>
 * 
 * @author Prathab K
 * 
 * @see Type
 * @see Instance
 * @see Entity
 * @see Relation
 *
 */
public class FileConceptNet extends ConceptNet {

    private boolean readOnly;
    
    private boolean typeDirtFlag;
    private boolean instanceDirtFlag;
    
    private ItemTable<String, Short, Type> typeTable;
    private ItemTable<Short, Integer, InstanceMetaItem> instanceTable;
    private RelationTable relationTable;
    private List<Type> typeList;
    //private Query itemQuery;
    
    private short typeId = -1;
    private int instanceId = -1;
    private Properties properties;
    
    /**
     * Initializes the Concept-Net.<p>
     * The <tt>Type Table</tt> and <tt>Instance Table</tt> are loaded into the memory
     * and other necessary files are opened in the specified mode.
     * @param name concept-net name
     * @param mode opening mode; can read (r) or read-write (rw)
     * @throws FileNotFoundException if concept-net does not exists then throws exception
     * @throws IOException if any file operations fail
     */
    public FileConceptNet(String name, String mode)
    throws FileNotFoundException, IOException {
        properties = PropertiesConfig.loadProperties(ConceptNetDir + ConStoreConstants.PROPERTIES_FILE);
        if(mode.equals("r")) {
            readOnly = true;
        }else if(mode.equals("rw")) {
            readOnly = false;
            typeId = Short.parseShort(properties.getProperty("TYPE_ID"));
            instanceId = Integer.parseInt(properties.getProperty("INSTANCE_ID"));
            typeList = new ArrayList<Type>();
        }else{
            throw new IOException("Invalid opening mode");
        }
        ConStoreConstants.IO_BUFFER_SIZE = Integer.parseInt(properties.getProperty("IO_BUFFER_SIZE"));
        ConStoreConstants.QUERY_CACHE_SIZE = Long.parseLong(properties.getProperty("QUERY_CACHE_SIZE"));
        ConStoreConstants.INSTANCE_CACHE_SIZE = Long.parseLong(properties.getProperty("INSTANCE_CACHE_SIZE"));
        typeTable = TypeTableFactory.getTypeTable(ConceptNetDir + ConStoreConstants.META_FILE, mode);
        instanceTable = InstanceTableFactory.getInstanceTable(ConceptNetDir, mode);
        relationTable = new RelationTable(ConceptNetDir,
                                                    ConStoreConstants.INSTANCE_CACHE_SIZE);
        //itemQuery = new ItemQuery(ConceptNetDir, typeTable, instanceTable, relationTable);
        typeDirtFlag = false;
        instanceDirtFlag = false;
    }
    
    /**
     * Inducts <tt>Type</tt> into the concept-net
     * @param type {@link Type}
     * @return {@link Type}
     */
    public Result induct(Type type) {
        if(type !=null) {
            if(type.name != null && !type.name.equals("")) {
                if(typeTable.get(type.name, null) == null) {
                    type.id = ++typeId;
                    typeTable.put(type);
                    typeDirtFlag = true;
                    typeList.add(type);
                    return new Result(true,"Type '" + type.name + "' inducted successfully");
                }else {
                    return new Result(false,"Type with the name '" + type.name 
                            + "' already exists");
                }
            }else{
                return new Result(false,"Invalid Type name '" + type.name + "'");
            }
        }else{
            return new Result(false,"Null Object");
        }
    }
    
    /**
     * Returns the <tt>Type</tt> of specified name.
     * Returns <tt>NULL</tt> if type does not exists.
     * @param name type name
     * @return {@link Type}
     */
    public Type getType(String name) {
        return typeTable.get(name, null);
    }

    /**
     * Returns <tt>true</tt> if type exists in the concept-net
     * @param name type name
     * @return <tt>true</tt> if type exists in the concept-net
     */
    public boolean hasType(String name) {
        if(typeTable.get(name,null) != null)
            return true;
        else
            return false;
    }
    
    /**
     * Adds the instance to the concept-net.<br>
     * The instance can be <tt>EntityInstance</tt> or <tt>RelationInstance</tt>
     * @param instance {@link Instance}
     * @return the added instance's id
     */
    public int add(Instance instance) {
        InstanceMetaItem metaItem = createInstaneMeta(instance);
        return storeInstance(instance, metaItem);
    }
    
    private int storeInstance(Instance instance, InstanceMetaItem metaItem) {
        instanceTable.put(metaItem);
        if(instance instanceof RelationInstance)
            addRelation((RelationInstance)instance);
        instanceDirtFlag= true;
        return metaItem.instanceId;
    }

    /**
     * Updates the instance in the concept-net.
     * @param instance {@link Instance}
     * @return {@link Result}
     */
    public Result update(Instance instance) {
        InstanceMetaItem instanceItem = instanceTable.get(instance.id);
        if(instanceItem != null) {
            instanceItem.instance = instance;
            instanceTable.put(instanceItem);
            if(instance instanceof RelationInstance)
                updateRelation((RelationInstance)instance);
            instanceDirtFlag = true;
            return new Result(true);
        }else {
            return new Result(false, "Instance does not exists");
        }
    }
    
    /**
     * Removes the instance from the concept-net.
     * @param instance {@link Instance}
     * @return {@link Result}
     */
    public Result remove(Instance instance) {
        if(instance instanceof RelationInstance)
            removeRelation((RelationInstance)instance);
        else
            relationTable.removeInstance(instance.id);
        instanceTable.remove(instance.typeId, instance.id);
        instanceDirtFlag = true;
        return new Result(true);
    }
    
    /**
     * Removes the existing <code>Instance</code> from 
     * the concept-nets.<br>
     * @param instanceId instance's id
     * @return {@link Result}
     */
    public Result remove(int instanceId) {
        instanceTable.remove(instanceId);
        //TODO Checking to be done is instanceId is entity or relation
        relationTable.removeInstance(instanceId);
        relationTable.removeRelation(instanceId);
        
        instanceDirtFlag = true;
        return new Result(true);
    }

    private void addRelation(RelationInstance instance) {
        Type type = getType(instance.getClass().getSimpleName());
        instance.setType(type);
        relationTable.insert(instance.leftInstanceId, instance.id, instance.rightInstanceId);
        //updateClusterLabel(instance.leftInstanceId,instance.rightInstanceId);
        if(instance.getDirection() == Relation.BIDIRECTIONAL)
            relationTable.insert(instance.rightInstanceId, instance.id, instance.leftInstanceId);
    }
    
    private void updateClusterLabel(int leftInstanceId, int rightInstanceId) {
        InstanceMetaItem leftItem = instanceTable.get(leftInstanceId);
        InstanceMetaItem rightItem = instanceTable.get(rightInstanceId);
        if(leftItem != null && rightItem != null) {
            short leftLabel = leftItem.clusterId;
            short rightLabel = rightItem.clusterId;
            if(leftLabel == 1 && rightLabel == 1) {
                rightItem.clusterId = ++leftLabel;
                instanceTable.put(rightItem);
            } else if(leftLabel != 1 && rightLabel == 1) {
                rightItem.clusterId = ++leftLabel;
                instanceTable.put(rightItem);
            } else if(leftLabel == 1 && rightLabel != 1) {
                leftItem.clusterId = --rightLabel;
                instanceTable.put(leftItem);
            }else if(leftLabel != 1 && rightLabel != 1) {
                if(rightLabel > (leftLabel + 1)) {
                    rightItem.clusterId = ++leftLabel;
                    instanceTable.put(rightItem);
                }
            } 
        }
    }
    
    private void updateRelation(RelationInstance instance) {
        relationTable.update(instance.leftInstanceId, instance.id, instance.rightInstanceId);
    }
    
    private void removeRelation(RelationInstance instance) {
        relationTable.removeRelation(instance.id);
    }
    /**
     * Creates Index of the specified type's attribute.
     * @param <K> data type object
     * @param typeName type name
     * @param attributeName attribute name
     * @param dataType data type
     * @return {@link Result}
     */
    public <K extends Comparable<K>> Result createIndex(String typeName, String attributeName, K dataType) {
        Type type = typeTable.get(typeName, null);
        
        if(type == null)
            return new Result(false,"Type does not exists");
        
        Query itemQuery = this.query();
        
        List<Instance> instances = itemQuery.getInstances(type.id);
        
        List<Attribute> attributes = type.getAllAttributes();
        Attribute attribute = null;
        List<Node<K>> nodes = new ArrayList<Node<K>>();
        
        for(Attribute attr : attributes) {
            if(attr.name.equals(attributeName)) {
                attribute = attr; break;
            }
        }
        
        if(attribute != null) {
            if(attribute.repeating) {
                for(Instance instance : instances) {
                    List<K> list = (List<K>) instance.getAttributeValue(attribute.name);
                    for(K key: list)
                        nodes.add(new Node<K>(key,instance.id));
                }
            }else{
                for(Instance instance : instances) {
                    K key = (K) instance.getAttributeValue(attribute.name);
                    nodes.add(new Node<K>(key,instance.id));
                }
            }
            closeQuery(itemQuery);
            String fileName = IndexBuilder.getIndexFileName(type.id, attributeName);
            if(IndexBuilder.createIndex(fileName, "rw", nodes))
                return new Result(true, "Index created successfully");
        }else {
            return new Result(false, "Attribute not found");
        }
        return new Result(false, "Index creation failed");
    }
    
    public void addInstance(InstanceMetaItem item) {
        instanceTable.put(item);
        instanceDirtFlag= true;
    }
    
    public int addInstance(Instance instance, short clusterId) {
        InstanceMetaItem metaItem = createInstaneMeta(instance);
        metaItem.clusterId = clusterId;
        return storeInstance(instance, metaItem);
    }
    
    private InstanceMetaItem createInstaneMeta(Instance instance) {
        if(instance.id <=0) instance.id = ++instanceId;
        InstanceMetaItem instanceItem = new InstanceMetaItem();
        Type type = typeTable.get(instance.getClass().getSimpleName(), null);
        instanceItem.typeId = type.id;
        instanceItem.instanceId = instance.id;
        instanceItem.instance = instance;
        return instanceItem;
    }
    
    /**
     * Drops the index for the specified type attribute.
     * @param type <tt>Type</tt> object
     * @param attributeName attribute name
     * @return {@link Result}
     */
    public Result dropIndex(Type type, String attributeName) {
        String fileName = IndexBuilder.getIndexFileName(type.id, attributeName);
        return new Result(IndexBuilder.dropIndex(fileName), "Index dropped successfully");
    }
    
    /**
     * Returns the <tt>Query</tt> object. User can query the concept-net
     * using the methods of the <tt>Query</tt> object.
     * @return {@link Query}
     */
    public Query query(){
        try {
            return new ItemQuery(ConceptNetDir, typeTable, instanceTable, relationTable);
        }catch(IOException ie) {
            ie.printStackTrace();
        }
        return null;
        //return itemQuery;
    }

    /**
     * Commits the changes to the concept-net.<p>
     * The operations like induct, add, update, remove, etc., does not affect the
     * concept-net unless it is committed. 
     * @return {@link Result}
     */
    public Result commit() {
        if(!readOnly) {
            if(typeDirtFlag) {
                
                if(!commitType().Success) {
                    return new Result(false,"Error in commiting the types"); 
                }
                generateCode();
                typeList.clear();
                typeDirtFlag = false;
            }
            
            if(instanceDirtFlag) {
                if(!commitInstance().Success) {
                    return new Result(false,"Error in commiting the instances"); 
                }
                instanceDirtFlag = false;
            }
        }else{
            return new Result(false, "Read-Only mode, cannot be commited");
        }
        return new Result(true,"Commit Success");   
    }
    
    /**
     * Commits the types to the concept-net
     * @return {@link Result}
     */
    private Result commitType() {
        try {
            typeTable.commit();
            properties.setProperty("TYPE_ID", String.valueOf(typeId));
            PropertiesConfig.storeProperties(ConceptNetDir + ConStoreConstants.PROPERTIES_FILE, 
                                                properties);
            return new Result(true);
        }catch(IOException ie) {
            log.log(Level.SEVERE, "Type Commit Exception", ie);
            return new Result(false,"Type Commit Exception",ie);
        }
    }
    
    /**
     * Commits the instances to the concept-net
     * @return {@link Result}
     */
    private Result commitInstance() {
        try {
            instanceTable.commit();
            relationTable.commit();
            properties.setProperty("INSTANCE_ID", String.valueOf(instanceId));
            PropertiesConfig.storeProperties(ConceptNetDir + ConStoreConstants.PROPERTIES_FILE, 
                    properties);
            return new Result(true);
        }catch(IOException ie) {
            log.log(Level.SEVERE, "Instance Commit Exception", ie);
            return new Result(false,"Instance Commit Exception",ie);
        }
    }
    
    /**
     * Generates the Java code for types that are inducted to the concept-net.
     * The generated Java class file will be used to instantiate the type.
     */
    private void generateCode(){
        CodeGenerator cg = CodeGenerator.getInstance();
        for(Type type: typeList) {
            if(type instanceof Relation) {
                Relation r = (Relation) type;
                cg.generateCode(ConceptNetDir, r, typeTable.get(r.getLeftEntityId()).name,
                        typeTable.get(r.getRightEntityId()).name);
            }
            else {
                cg.generateCode(ConceptNetDir, (Entity)type);
            }
        }
    }
    
    /**
     * Reinitializes the instances related files for clustering, defragementation purposes.
     * Also backup the old data before re-initializing
     * @param backupName name of the backup
     */
    public void reInitializeNetFiles(String backupName) {
        if(backupName == null)
            backupName = new Long(System.currentTimeMillis()).toString();

        File filePath = new File(ConceptNetDir + BAK);
        if(!filePath.exists()) 
            filePath.mkdirs();
        
        String path = ConceptNetDir + BAK + backupName + "/";
        FileUtil.createDir(path);
        
        FileUtil.renameMove(ConceptNetDir + ConStoreConstants.NET_FILE, 
                path + ConStoreConstants.NET_FILE);
        FileUtil.renameMove(ConceptNetDir + ConStoreConstants.INSTANCE_TABLE_FILE, 
                path + ConStoreConstants.INSTANCE_TABLE_FILE);
        FileUtil.renameMove(ConceptNetDir + ConStoreConstants.NET_FSL_FILE, 
                path + ConStoreConstants.NET_FSL_FILE);
        FileUtil.renameMove(ConceptNetDir + ConStoreConstants.RELATION_TABLE_FILE, 
                path + ConStoreConstants.RELATION_TABLE_FILE);
        FileUtil.renameMove(ConceptNetDir + ConStoreConstants.RELATION_IDX_FILE, 
                path + ConStoreConstants.RELATION_IDX_FILE);
        
        FileUtil.createFile(ConceptNetDir + ConStoreConstants.NET_FILE);
        FileUtil.createFile(ConceptNetDir + ConStoreConstants.INSTANCE_TABLE_FILE);
        FileUtil.createFile(ConceptNetDir + ConStoreConstants.RELATION_TABLE_FILE);
        FileUtil.createFile(ConceptNetDir + ConStoreConstants.RELATION_IDX_FILE);
        String fslFileName = ConceptNetDir + ConStoreConstants.NET_FSL_FILE;
        FileUtil.createFile(fslFileName);
        initializeFslFile(fslFileName);
    }
    
    /**
     * Closes the concept-net.
     * @return <tt>true</tt> on success
     */
    public boolean close() {
        if(!readOnly) {
            try{
                typeTable.close();
                instanceTable.close();
                relationTable.close();
                //itemQuery.close();
                TypeTableFactory.shutDown();
                InstanceTableFactory.shutDown();
            }catch(IOException ie){
                log.log(Level.SEVERE, "ConceptNet file close exception", ie);
                return false;
            }
        }
        return true;
    }
    
    private void closeQuery(Query itemQuery) {
        if(itemQuery != null) {
            try{
                itemQuery.close();
            }catch(IOException ie){
                log.log(Level.SEVERE, "Query files close exception", ie);
            }
        }
    }
    /**
     * Display the statistical information about the concept-net.
     */
    public void showStaistics() {
        Query itemQuery = this.query();
        long totalEntities = 0;
        long totalRelations = 0;
        
        System.out.println("IO_BUFFER_SIZE : " + ConStoreConstants.IO_BUFFER_SIZE);
        System.out.println("QUERY_CACHE_SIZE : " + ConStoreConstants.QUERY_CACHE_SIZE);
        System.out.println("INSTANCE_CACHE_SIZE : " + ConStoreConstants.INSTANCE_CACHE_SIZE);
        //TODO: optimize with count values
        for(Type t : typeTable.getAllItems()) {
            if(t instanceof Entity) {
                System.out.print("\nEntity : ");
                System.out.print(t.id + " - ");
                System.out.print(t.name);
                int n = itemQuery.getEntityInstances(t.id).size();
                System.out.print(" - " + n);
                totalEntities += n;
            } else if(t instanceof Relation) {
                System.out.print("\nRelation: ");
                System.out.print(t.id + " - ");
                System.out.print(t.name);
                int n = itemQuery.getRelationInstances(t.id).size();
                System.out.print(" - " + n);
                totalRelations += n;
            }
        }
        
        System.out.println("\n******************************************");
        System.out.println("No. of Types : " + typeTable.size());
        System.out.println("No. of Entity Instances :" + totalEntities);
        System.out.println("No. of Relation Instances :" + totalRelations);
        System.out.println("No. of Instances :" + instanceTable.size());
        closeQuery(itemQuery);
    }

    
    public final static String BAK = "bak/";
}