/*
Copyright (C) 2000-2010  Ministere de la culture et de la communication (France), AJLSM
See LICENCE file
 */
package fr.gouv.culture.sdx.utils.database;

import java.io.File;
import java.io.IOException;
import java.util.Enumeration;

import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.parameters.ParameterException;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.TermQuery;

import fr.gouv.culture.sdx.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.utils.Utilities;
import fr.gouv.culture.sdx.utils.constants.ContextKeys;
import fr.gouv.culture.sdx.utils.constants.Node;
import fr.gouv.culture.sdx.utils.logging.LoggingUtils;
import fr.gouv.culture.sdx.utils.lucene.LuceneDataStore;
import fr.gouv.culture.sdx.utils.save.SaveParameters;
import fr.gouv.culture.sdx.utils.save.Saveable;

/**
 * An implementation of database operations using Lucene.
 *
 * <p>
 * For a detailed description of the database abstraction in SDX, please see
 * the Database interface.
 * <p>
 * When creating a LuceneDatabase object, these steps should be followed :
 * <ol>
 *   <li>Create the object with a File object pointing at the directory where the index is or will be.
 *   <li>Use the enableLogging method to enable logging of errors/events.
 *   <li>Use the init() method to make sure that the database is initialized and usable.
 * </ol>
 *
 */
public class LuceneDatabase extends LuceneDataStore implements Database {

    protected String DATABASE_DIR_NAME = "_lucene";
    protected String id = null;


    public LuceneDatabase() {
    }

    /**
     * Gets or creates a _database stored at a given dir.
     *
     * <p>
     * One should call the enableLogging() method before using this database,
     * otherwise errors won't be logged.
     * <p>
     * Most of all, you should call the init() method to make sure
     * that the database can be used.
     *
     *@param   dir     The directory where the Lucene database is stored.
     */
    public LuceneDatabase(File dir) throws SDXException {
        super(dir);
    }

    /**
     * Initializes the Lucene database.
     *
     * <p>
     * It the database exists, nothing is done here. If it is doesn't
     * exist, it will be created.
     */
    public void init() throws SDXException {

        if (this.fsdFile == null) {
            String basePath = Utilities.getStringFromContext(ContextKeys.SDX.Application.DATABASE_DIRECTORY_PATH, super.getContext());
            if (Utilities.checkString(basePath))
                basePath = basePath + getDatabaseDirectoryName() + File.separator + id + File.separator;
            if (Utilities.checkString(basePath))
                Utilities.checkDirectory(basePath, super.getLog());
            this.fsdFile = new File(basePath);
        }

        super.init(false);
    }

    /**
     * Searches database for a given document id, and then returns the content of the specified field.
     *
     * @param   documentId      The id of the document to search for.
     * @param   property        The field name, or the property to search for.
     */
    public String getProperty(String documentId, String property) throws SDXException {
        Document doc = getLuceneDocument(documentId);
        if (doc == null)
            return null;
        else
            return doc.get(property);
    }

    /**
     * Returns a Lucene document from the database.
     *
     * @param   id  The document id.
     * @return  <null> if no document with this id is stored in this database.
     */
    private Document getLuceneDocument(String id) throws SDXException {
        // First build an appropriate query.
        if (!Utilities.checkString(id)) return null;

        TermQuery tq = new TermQuery(new Term(ID_FIELD, id));
        Hits h = search(tq);
        try {

            //TODO? : only *one* hit should be returned, shouldn't it ? -pb

            if (h != null && h.length() > 0)
            // Got a hit, so returns the associated Lucene document.
                return h.doc(0);
            else
            // Returns null if no such document.
                return null;
        } catch (IOException e) {
            String[] args = new String[2];
            args[0] = fsd.toString();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_RETRIEVE_DOCUMENT, args, e);
        }
    }


    /**
     * Returns an entity for a given id.
     *
     * @param id    The entity's id.
     * @return The entity, or <code>null</code> if no entity has this id.
     */
    public DatabaseEntity getEntity(String id) throws SDXException {
        // First build a query for identifying the document by it's id, and search for it
        if (!Utilities.checkString(id)) return null;

        Hits h = search(new TermQuery(new Term(ID_FIELD, id)));

        // The return it if it exists
        if (h.length() > 0) {
            try {
                return getEntity(h.doc(0));
            } catch (IOException e) {
                String[] args = new String[2];
                args[0] = fsd.toString();
                args[1] = e.getMessage();
                throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_RETRIEVE_DOCUMENT, args, e);
            }
        }
        // If no entity has this id, then return null
        else
            return null;
    }

    /**
     * Returns the list of entities within the database.
     */
    public DatabaseEntity[] getEntities() throws SDXException {

        // We must search for the common property to all documents
        Hits h = search(new TermQuery(new Term(ALL_FIELD, ALL_VALUE)));
        int len = h.length();

        // Then build an array of entities by fetching Lucene documents
        DatabaseEntity[] docs = new DatabaseEntity[len];
        try {
            for (int i = 0; i < len; i++) {
                docs[i] = getEntity(h.doc(i));
            }
            return docs;
        } catch (IOException e) {
            String[] args = new String[2];
            args[0] = fsd.toString();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_RETRIEVE_DOCUMENT, args, e);
        }
    }

    /**
     * Builds an entity from a Lucene document.
     *
     * @param   ldoc        The Lucene document.
     */
    protected DatabaseEntity getEntity(Document ldoc) throws SDXException {
        //TODOException?:should we throw an exception in this case?-rbp
        if (ldoc == null) return null;
        // First get the id
        DatabaseEntity de = new DatabaseEntity(ldoc.getField(ID_FIELD).stringValue());
        de.enableLogging(super.getLog());
        Enumeration fields = ldoc.fields();
//        Hashtable processedFields = new Hashtable();
        while (fields.hasMoreElements()) {
            Field f = (Field) fields.nextElement();
            /*i think this is where we were erroneously adding our internal lucene fields to our _context object, but
            i think our changes to lucene may have already fixed this-rbp*/
            de.addProperty(f.name(), f.stringValue());
            /*           if (!processedFields.containsKey(f.name())) {
                String[] values = ldoc.getValues(f.name());
                for (int i = 0; i < values.length; i++) de.addProperty(f.name(), values[i]);
            */
//            if (f.name() != ALL_FIELD | f.name() != ID_FIELD) de.addProperty(f.name(), f.stringValue());
            //           if (processedFields != null) processedFields.put(f.name(), "");

        }
        return de;
    }


    /**
     * Saves an entity within the database.
     *
     * @param entity        The entity to save.
     */
    public void save(DatabaseEntity entity) throws SDXException {

        if (entity != null) {
            // And now we can index it, but first delete an identical document.
            delete(new DatabaseEntity(entity.getId()));

            // We must create a document, and then index it.
            Document ldoc = getLuceneDocument(entity);
            this.write(ldoc);
        }
    }


    protected synchronized void write(Document lDoc) throws SDXException {
        super.write(lDoc);
        super.recycleSearcher();
    }

    /**
     * Returns a Lucene document from a database entity.
     *
     * @param   ent     The database entity to convert.
     */
    private Document getLuceneDocument(DatabaseEntity ent) throws SDXException {

        Document ldoc = new Document();


        ldoc.add(getLuceneField(ID_FIELD, ent.getId()));
        ldoc.add(getLuceneField(ALL_FIELD, ALL_VALUE));

        // Now the properties
        Property[] props = ent.getProperties();
        if (props != null) {
            for (int i = 0; i < props.length; i++) {
                String[] values = props[i].getValues();
                if (values != null) {
                    for (int j = 0; j < values.length; j++) {
                        try {
                            ldoc.add(getLuceneField(props[i].getName(), values[j]));
                        } catch (SDXException sdxE) {
                            LoggingUtils.logWarn(super.getLog(), sdxE.getMessage(), null);
                        }
                    }
                }
            }
        }

        return ldoc;

    }


    /**
     * Returns a Lucene field for indexing a name/value pair.
     *
     * @param name      Name of the field.
     * @param value     Value of the field.
     */
    private Field getLuceneField(String name, String value) throws SDXException {
        //TODO?:not testing the value for an empty string because fred thinks i could be useful?-rbp
        if (!Utilities.checkString(name) || value == null) {
            String[] args = new String[2];
            args[0] = name;
            args[1] = value;
            //not logging here
            throw new SDXException(null, SDXExceptionCode.ERROR_CREATE_LUCENE_FIELD, args, null);
        } else
        	// MAJ Lucene 2.1.0
            //return Field.Keyword(name, value);
        	return new Field(name, value, Field.Store.YES, Field.Index.UN_TOKENIZED);
    }


    /**
     * Returns a property value from an entity in the database.
     *
     * @param   entityId        The needed entity's id.
     * @param   name        The property's name for the desired value.
     *
     * @return The property if is exists, otherwise <code>null</code>. If the property is
     *          defined more than once, the first value is returned.
     */
    public String getPropertyValue(String entityId, String name) throws SDXException {
        DatabaseEntity ent = getEntity(entityId);
        if (ent == null) {
            return null;
        } else
            return ent.getProperty(name);
    }

    /**
     * Returns a repeatable property from an entity in the database.
     *
     * <p>
     * Please note that this method currently doesn't work for Lucene, which still
     * stores (?) and retrieves only the last added repeatable property.TODO:remove this note after we commit our lucene changes
     *
     * @param   entityId       The needed entity's name.
     * @param   propertyName    The needed property's name.
     *
     *  @return     An enumeration of all values for this property, <code>null</code> if this
     *              property is not defined for this entity.
     */
    public String[] getPropertyValues(String entityId, String propertyName) throws SDXException {
        DatabaseEntity ent = getEntity(entityId);
        if (ent == null)
            return null;
        else
            return ent.getPropertyValues(propertyName);
    }

    /**
     * Returns all properties from an entity in the database.
     *
     * @param   entityId    The entity's id.
     * @return  The list of properties, an empty list if no properties are defined or the entity doesn't exist.
     */
    public Property[] getProperties(String entityId) throws SDXException {
        DatabaseEntity ent = getEntity(entityId);
        if (ent == null)
            return new Property[0];
        else
            return ent.getProperties();
    }

    /**
     * Deletes an entity from the database.
     *
     * @param ent       The entity to delete (must have an id).
     * A new DatabaseEntity can be created with the id set to the
     * id of the entity desired for deletion and then this new entity
     * can be passed to this method and the appropriate entity corresponding
     * to that id will be deleted.
     */
    public void delete(DatabaseEntity ent) throws SDXException {
        //TODO?:we may need to optimize the index after the delete, but we will see in the future, as for now the results are ok?-rbp
        // First build a query for the proper document
        if (ent != null && Utilities.checkString(ent.getId())) {
            super.delete(ent.getId());
            super.recycleSearcher();
            super.verifyIndex();
            
        }
    }

    public void delete(DatabaseEntity[] entities) throws SDXException {
        if (entities != null) {
            String[] ids = new String[entities.length];
            for (int i = 0; i < entities.length; i++) {
                DatabaseEntity entity = entities[i];
                if (entity != null) {
                    String id = entity.getId();
                    ids[i] = id;
                }
            }
            delete(ids);
        }

    }

    /**
     * Updates an entity.
     *
     * Deletes the existing entity with the same id
     * and saves the provided entity.
     * @see #delete
     * @see #save
     *
     * @param   ent     The entity to update.
     */
    public void update(DatabaseEntity ent) throws SDXException {
        if (ent != null) {
            delete(ent);
            save(ent);
        }
    }

    /**
     * Returns the number of entities within this database.
     */
    public long size() {
        return super.size();
    }

    /**
     * Empties the _database.
     */
    public void empty() throws SDXException {
        //'true' indicates index should be removed
        super.init(true);
    }

    public String getIndexPath() {
        return super.getIndexPath();
    }

    public synchronized void optimize() throws SDXException {
        super.optimize();
    }


    public boolean entityExists(String id) {
        if (!Utilities.checkString(id)) return false;

        DatabaseEntity dbe = null;
        try {
            dbe = this.getEntity(id);
        } catch (SDXException e) {
            LoggingUtils.logException(super.getLog(), e);
            return false;
        }
        if (dbe == null)
            return false;
        else
            return true;

    }

    public String[] search(Parameters params) throws SDXException {
        return search(params, SEARCH_MODE_AND);
    }

    public String[] search(Parameters params, int mode) throws SDXException {
        if (mode != SEARCH_MODE_OR && mode != SEARCH_MODE_AND && mode != SEARCH_MODE_NOT)
            return new String[0];
        boolean required = false;
        boolean prohibited = false;
        if (mode == SEARCH_MODE_AND)
            required = true;
        else if (mode == SEARCH_MODE_NOT)
            prohibited = true;
        if (params == null) return new String[0];
        String[] paramNames = params.getNames();		//getting the param names
        BooleanQuery bq = Utilities.newBooleanQuery();	//setting the params
        for (int i = 0; i < paramNames.length; i++) {
            try {
            	// MAJ Lucene 2.1.0
                //bq.add(new TermQuery(new Term(paramNames[i], params.getParameter(paramNames[i]))), required, prohibited);
            	if (required)	bq.add(new TermQuery(new Term(paramNames[i], params.getParameter(paramNames[i]))), BooleanClause.Occur.MUST);
            	if (prohibited)	bq.add(new TermQuery(new Term(paramNames[i], params.getParameter(paramNames[i]))), BooleanClause.Occur.MUST_NOT);
            	else			bq.add(new TermQuery(new Term(paramNames[i], params.getParameter(paramNames[i]))), BooleanClause.Occur.SHOULD);
            } catch (ParameterException e) {
                throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_PARAMETERS, null, e);
            }
        }

        Hits hits = super.search(bq);
        if (hits == null)
            return new String[0];

        String[] entities = new String[hits.length()];
        for (int i = 0; i < hits.length(); i++) {
            String entity = null;
            try {
                entity = hits.doc(i).get(ID_FIELD);
            } catch (IOException e) {
                //could not retrieve the document for the int "i" from the hits for attached documents
                String[] args = new String[2];
                args[0] = this.getIndexPath();
                args[1] = e.getMessage();
                throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_RETRIEVE_DOCUMENT, args, e);
            }
            entities[i] = entity;
        }

        return entities;

    }

    public void configure(Configuration configuration) throws ConfigurationException {
        super.configure(configuration);
        //Nothing to configure for the moment-rbp
    }

    public String getDatabaseDirectoryName() {
        return DATABASE_DIR_NAME;
    }

    //TODO: do we really need to do anything below?-rbp
    public DatabaseConnection getConnection() throws SDXException {
        return null;
    }

    public void releaseConnection(DatabaseConnection conn) throws SDXException {
    }

    /**If the entity does not exist the method fails silently
     *
     * @param entityId
     * @param propertyName
     * @param propertyValue
     * @throws SDXException
     */
    public void addProperty(String entityId, String propertyName, String propertyValue) throws SDXException {
        //don't have good params
        if (!Utilities.checkString(entityId) || !Utilities.checkString(propertyName) || !Utilities.checkString(propertyValue)) return;
        //have good params
        DatabaseEntity dbe = this.getEntity(entityId);
        if (dbe != null) {
            dbe.addProperty(propertyName, propertyValue);
            this.update(dbe);
        }

    }

    /**If the entity does not exist the method fails silently
     *
     * @param entityId
     * @param propertyName
     * @param propertyValue
     * @throws SDXException
     */
    public void removeProperty(String entityId, String propertyName, String propertyValue) throws SDXException {
        //don't have good params
        if (!Utilities.checkString(entityId) || !Utilities.checkString(propertyName) || !Utilities.checkString(propertyValue)) return;
        //have good params
        DatabaseEntity dbe = this.getEntity(entityId);
        if (dbe != null) {
            dbe.deleteValue(propertyName, propertyValue);
            this.update(dbe);
        }
    }

    public void removeProperty(String propertyName, String propertyValue) throws SDXException {
        if (!Utilities.checkString(propertyName) || !Utilities.checkString(propertyValue)) return;
        Parameters params = new Parameters();
        params.setParameter(propertyName, propertyValue);
        String[] dbes = this.search(params);
        for (int i = 0; i < dbes.length; i++) {
            DatabaseEntity dbe = getEntity(dbes[i]);
            if (dbe != null) {
                dbe.deleteValue(propertyName, propertyValue);
                this.update(dbe);
            }
        }

    }

    protected String getClassNameSuffix() {
        return Database.CLASS_NAME_SUFFIX;
    }

    public String getWildcardSearchToken() {
        return "*";//lucene wildcard character is asterix
    }

	/* (non-Javadoc)
	 * @see fr.gouv.culture.sdx.utils.AbstractSdxObject#initToSax()
	 */
	protected boolean initToSax() {
		if(!super.initToSax())
			return false;
		else{
			this._xmlizable_objects.put("Database_Directory_Name",this.DATABASE_DIR_NAME);
			return true;
		}
		
	}
	
	/* Save the data objects of the database
	 * @see fr.gouv.culture.sdx.utils.save.Saveable#backup(fr.gouv.culture.sdx.utils.save.SaveParameters)
	 */
	public void backup(SaveParameters save_config) throws SDXException {
		if(save_config != null)
			if(save_config.getAttributeAsBoolean(Saveable.ALL_SAVE_ATTRIB,false))
			{
				save_config.setAttribute(Node.Name.ID,this.getId());
				save_config.setAttribute(Node.Name.TYPE,"LUCENE");
				save_config.savePathInConfig(null);
			}

	}
	
	/* Restore the data objects of the database
	 * @see fr.gouv.culture.sdx.utils.save.Saveable#restore(fr.gouv.culture.sdx.utils.save.SaveParameters)
	 */
	public void restore(SaveParameters save_config) throws SDXException {
		

	}


}
