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

import java.io.File;
import java.io.IOException;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Searchable;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

import fr.gouv.culture.sdx.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.utils.AbstractSdxObject;
import fr.gouv.culture.sdx.utils.Utilities;
import fr.gouv.culture.sdx.utils.constants.Node;
import fr.gouv.culture.sdx.utils.save.SaveParameters;
import fr.gouv.culture.sdx.utils.save.Saveable;
import fr.gouv.culture.sdx.utils.zip.ZipWrapper;

abstract public class LuceneDataStore extends AbstractSdxObject implements Saveable {

    /** Specify if using the compound file format when indexing */
    protected boolean useCompoundFiles = true;

    /**TODO implement a fields list for keeping track of analyzers on field level, and something similar to meta analyzer,*/
    /** The field name used to store the entity's id. */
    public final static String ID_FIELD = "sdxdocid";
    /** The field used to search for all documents. */
    public final static String ALL_FIELD = "sdxall";
    /**Value for the all field*/
    public final static String ALL_VALUE = "1";
    /**A field name used to store a xml lang value if desired*/
    public final static String XML_LANG_FIELD = "xmllang";
//    MAJ Lucene 2.1.0
//    INFO: In Lucene 2.1.0, filenames of segments files have changed. So, we use static method IndexReader.indexExists() for testing existence of Lucene Index. Moreover, it's a better implementation ;-) [MP]
//    /** The Lucene file to check to see if a database already exists. */
//    protected final static String LUCENE_CHECK_FILE = "segments";
    /**The location of the FSDirectory*/
    protected File fsdFile = null;
    /** The Lucene filesystem directory where the index is stored. */
    protected FSDirectory fsd = null;
    /** The Lucene searcher used for locating entities within the datastore. */
    protected Searcher searcher = null;
    /**The analyzer for the index behind the datastore*/
    protected org.apache.lucene.analysis.Analyzer analyzer = new StandardAnalyzer();


    public LuceneDataStore() {
    }

    protected LuceneDataStore(File dir) {
        this.fsdFile = dir;
    }

    /**Return's the absolute path for the
     *directory in which the database resides
     */
    public String getIndexPath() {
        if (this.fsdFile == null)
            return null;
        else
            return fsdFile.getAbsolutePath();
    }

    /**
     * Say if Lucene uses the compound files
     * @return useCompoundFiles
     */
    public boolean isUsingCompoundFiles(){
    	return useCompoundFiles;
    }

    /**Initializes the LuceneDataStore*/
    protected void init(boolean create) throws SDXException {
        //acquiring a lucene FSDirectory for the class
        getFSDirectory(create);
        //ensuring we have an index to work with
        verifyIndex();
        //acquiring a searcher
        recycleSearcher();
    }

    /**Sets the class field 'fsd' a FSDirectory instance for the fsdFile object*/
    protected void getFSDirectory(boolean create) throws SDXException {
        if (this.fsdFile != null && this.fsd == null) {
            this.fsd = this.getFSDirectory(this.fsdFile, create);
        }
    }

    protected FSDirectory getFSDirectory(File dir, boolean create) throws SDXException {
        FSDirectory l_fsDir = null;
        try {
            l_fsDir = FSDirectory.getDirectory(dir, create);
            return l_fsDir;
        } catch (IOException e) {
            //unable to get a FSDirectory object
            String[] args = new String[2];
            args[0] = getIndexPath();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_ACQUIRE_FSDIRECTORY, args, e);
        }


    }

    /**Ensures we have an index to work with*/
    public void verifyIndex() throws SDXException {
        try {
//        	MAJ Lucene 2.1.0
//          if (!fsd.fileExists(LUCENE_CHECK_FILE)) {
            if (!IndexReader.indexExists(fsd)) {
                IndexWriter w = new IndexWriter(fsd, analyzer, true);
                w.close();
            }
        } catch (IOException e) {
            String[] args = new String[2];
            args[0] = getIndexPath();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_WRITE, args, e);
        }
    }

    /** Recycles the searcher*/
    public synchronized void recycleSearcher() throws SDXException {
        try {
            if (this.searcher != null) {
                //freeing resources
                //TODO?:but what if an external call has already closed the searcher or the searcher is in use?-rbp
                this.searcher.close();
                this.searcher = null;
            }

            //getting the new searcher
            this.searcher = new IndexSearcher(fsd);

        } catch (IOException e) {
            String[] args = new String[1];
            args[0] = getIndexPath();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_SEARCHER_GET, args, e);
        }
    }

    /**Gets an IndexWriter for the current FSDirectory*/
    protected IndexWriter getWriter() throws IOException {
        return getWriter(false);
    }
    protected IndexWriter getWriter(boolean create) throws IOException {
    	return getWriter(this.fsd, create);
    }

    /**Get's an IndexWriter based upon the analyzer class field
     * and the provided Lucene Directory
     *
     * @param directory The lucene directory for which the writer is desired
     * @return  IndexWriter
     * @throws IOException
     */
    protected synchronized IndexWriter getWriter(Directory directory) throws IOException {
        return getWriter(directory, false);
    }
    protected synchronized IndexWriter getWriter(Directory directory, boolean create) throws IOException {
      IndexWriter w = new IndexWriter(directory, this.analyzer, create);
      w.setUseCompoundFile(this.useCompoundFiles);
      return w;
  }

    /**Returns an index reader for the current FSDirectory.*/
    protected IndexReader getReader() throws SDXException {
        try {
            return IndexReader.open(this.fsd);
        } catch (IOException e) {
            String[] args = new String[2];
            args[0] = getIndexPath();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_READ, args, e);
        }
    }

    /**Gets the search class variable*/
    protected Searchable getSearcher() {
        return this.searcher;
    }


    /**Optimizeds the index for the current FSDirectory.*/
    protected synchronized void optimize() throws SDXException {
        IndexWriter w = null;
        try {
            w = getWriter();
            //optimizing
            w.optimize();
        } catch (IOException e) {
            String[] args = new String[1];
            args[0] = getIndexPath();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_OPTIMIZE, args, e);

        } finally {
            if (w != null) {
                try {
                    w.close();
                } catch (IOException e) {
                    String[] args = new String[1];
                    args[0] = getIndexPath();
                    throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_OPTIMIZE, args, e);
                }
            }
            //resetting the searcher
            recycleSearcher();
        }
    }


    /**Writes a document to the index
     *
     * @param lDoc  The lucene document to add
     */
    protected synchronized void write(Document lDoc) throws SDXException {
        if (lDoc != null) {
            //we have a single document to add
            IndexWriter w = null;
            try {
                //initializing an index writer for our main index directory
                w = getWriter();
                //we add the single document
                w.addDocument(lDoc);
                //TODO : implement a kind of locking for the searcher ? -pb
            } catch (IOException e) {
                String[] args = new String[2];
                args[0] = getIndexPath();
                args[1] = e.getMessage();
                throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_WRITE, args, e);
            } finally {
                try {
                    if (w != null) w.close();
                } catch (IOException e) {
                    String[] args = new String[2];
                    args[0] = getIndexPath();
                    args[1] = e.getMessage();
                    throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_WRITE, args, e);
                }
            }
        }

    }

    protected synchronized void writeLuceneData(LuceneData ld) throws SDXException {
        if (ld != null) {
            Document lDoc = ld.getLuceneDocument();
            if (lDoc != null)
                this.write(lDoc);
        }
    }

    /**Deletes a document from the index
     * based upon its id field, marked protected
     * as it is specfic implementation
     *
     * @param docId The id of the document to be deleted
     */
    protected synchronized void delete(String docId) throws SDXException {
        if (Utilities.checkString(docId)) {
            String[] docIds = {docId};
            delete(docIds);
        }
    }

    /**Deletes documents from the index
     * based upon their id field, marked protected
     * as it is specfic implementation
     *
     * @param docIds The ids of the document to be deleted
     */
    protected synchronized void delete(String[] docIds) throws SDXException {
        if (docIds != null) {
            Term[] terms = new Term[docIds.length];
            for (int i = 0; i < docIds.length; i++) {
                String docId = docIds[i];
                if (Utilities.checkString(docId)) {
                    //the below ligne tightly couples us to LuceneDatastore
                    Term term = new Term(ID_FIELD, docId);
                    terms[i] = term;
                }
            }
            delete(terms);
        }
    }

    /**Deletes a document from the index
     * based upon a term field
     *
     * @param term The term of the document to be deleted
     */
    public void delete(Term term) throws SDXException {
        Term[] terms = {term};
        delete(terms);
    }

    /**List of terms to delete using a single index reader
     *
     * @param terms
     * @throws SDXException
     */
    public synchronized void delete(Term[] terms) throws SDXException {
        if (terms != null) {
            IndexReader r = null;
            try {
                //the index reader for deletion
                for (int i = 0; i < terms.length; i++) {
                    Term term = terms[i];
                    if (term != null) {
                        if (r == null) r = getReader();
                        // MAJ Lucene 2.1.0
                        //r.delete(term);	// deleting the document
                        r.deleteDocuments(term);

                    }
                }
            } catch (IOException e) {
                String[] args = new String[2];
                args[0] = getIndexPath();
                args[1] = e.getMessage();
                throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_DELETE, args, e);
            } finally {
                try {
                    if (r != null) r.close();
                } catch (IOException e) {
                    String[] args = new String[3];
                    args[0] = getIndexPath();
                    args[1] = e.getMessage();
                    args[2] = "Unable to close IndexReader.";
                    throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_DELETE, args, e);
                }
            }

        }
    }


    /**Free's the resources associated with this data store
     * USE WITH CARE!
     *
     * @throws IOException  Lucene IOExceptions
     */
    protected void freeResources() throws IOException {
        if (this.searcher != null) {
            this.searcher.close();
            this.searcher = null;
        }

        if (this.fsd != null) {
            this.fsd.close();
            this.fsd = null;
        }

    }

    /**Ensures that resources are freed*/
    protected void finalize() throws Throwable {
        this.freeResources();
        super.finalize();
    }

    /**
     * Searches the database.
     *
     * @param   q   A Lucene query.
     */
    public synchronized Hits search(Query q) throws SDXException {
        return this.search(q, null);
    }

    /**
     * Searches the database.
     *
     * @param   q   A Lucene query.
     */
    public synchronized Hits search(Query q, Filter f) throws SDXException {
        try {
            if (q == null) throw new SDXException(null, SDXExceptionCode.ERROR_LUCENE_QUERY_NULL, null, null);

            //ensuring we have a searcher
            if (this.searcher == null) {
                recycleSearcher();
                if (this.searcher == null) {
                    String[] args = new String[2];
                    args[0] = getIndexPath();
                    throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_SEARCHER_GET, args, null);
                }
            }
            if (f != null)
                return this.searcher.search(q, f);

            else
                return this.searcher.search(q);

        } catch (IOException e) {
            String[] args = new String[2];
            args[0] = getIndexPath();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_SEARCH, args, e);
        }
    }

    /**
     * Returns the number of entities within this database.
     */
    public long size() { //TODO : optimize this : only calculate when not intialized ; track changes in write/delete to modify size counter -pb
        Hits h = null;
        try {
            h = search(new TermQuery(new Term(ALL_FIELD, ALL_VALUE)));

            if (h != null)
                return h.length();
            else
                return 0;
        } catch (SDXException e) {
            return 0;
        }
    }

    protected boolean initToSax(){
    	return true;
    }

	/**Init the LinkedHashMap _xmlizable_volatile_objects with the objects in order to describ them in XML
	 * Some objects need to be refresh each time a toSAX is called*/
	protected void initVolatileObjectsToSax() {
	}



	/** Save Lucene files
	 * @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.TYPE,"LUCENE");
				// add the path to the save_config
				save_config.savePathInConfig(null);
				String id = this.getId();
				if(!id.equals(""))
					save_config.setAttribute(Node.Name.ID,this.getId());
				else
					id ="database";

				String savedir = save_config.getStoreBasePath()+save_config.getAttribute(Saveable.PATH_ATTRIB,"");
				ZipWrapper zw = new ZipWrapper();
				if(fsd != null && fsd.getFile() != null)
					zw.zipDirectory(savedir+ File.separator + id +".zip",fsd.getFile().getAbsolutePath());
			}

	}
	/** Restore Lucene files
	 * @see fr.gouv.culture.sdx.utils.save.Saveable#restore(fr.gouv.culture.sdx.utils.save.SaveParameters)
	 */
	public void restore(SaveParameters save_config) throws SDXException {
		// TODO Auto-generated method stub

	}
}









