/*
 * This file is part of Quelea, free projection software for churches.
 * 
 * 
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.quelea.services.lucene;

import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.core.LowerCaseFilterFactory;
import org.apache.lucene.analysis.custom.CustomAnalyzer;
import org.apache.lucene.analysis.miscellaneous.ASCIIFoldingFilterFactory;
import org.apache.lucene.analysis.standard.StandardTokenizerFactory;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.complexPhrase.ComplexPhraseQueryParser;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.MMapDirectory;
import org.quelea.data.bible.BibleChapter;
import org.quelea.services.utils.LoggerUtils;

/**
 * Search index used for indexing the bibles.
 * @author Michael
 */
public class BibleSearchIndex implements SearchIndex<BibleChapter> {

    private static final Logger LOGGER = LoggerUtils.getLogger();
    private Analyzer analyzer;
    private Directory index;
    private Map<Integer, BibleChapter> chapters;

    /**
     * Create a new empty search index.
     */
    public BibleSearchIndex() {
        chapters = new HashMap<>();
        try {
            analyzer = CustomAnalyzer.builder()
                    .withTokenizer(StandardTokenizerFactory.class)
                    .addTokenFilter(LowerCaseFilterFactory.class)
                    .addTokenFilter(ASCIIFoldingFilterFactory.class)
                    .build();
            index = new MMapDirectory(Files.createTempDirectory("quelea-mmap-bible").toAbsolutePath());
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, "Couldn't create song search index");
            throw new RuntimeException("Couldn't create song search index", ex);
        }
    }
    
    @Override
    public int size() {
        return chapters.size();
    }

    /**
     * Add a bible chapter to the index.
     *
     * @param chapter the chapter to add.
     */
    @Override
    public void add(BibleChapter chapter) {
        List<BibleChapter> list = new ArrayList<>();
        list.add(chapter);
        addAll(list);
    }

    /**
     * Add a number of chapters to the index. This is much more efficient than
     * calling add() repeatedly because it just uses one writer rather than
     * opening and closing one for each individual operation.
     *
     * @param bibleList the list of chapters to add.
     */
    @Override
    public void addAll(Collection<? extends BibleChapter> bibleList) {
        try (IndexWriter writer = new IndexWriter(index, new IndexWriterConfig(analyzer))) {
            for(BibleChapter chapter : bibleList) {
                Document doc = new Document();
                doc.add(new TextField("text", chapter.getText(), Field.Store.NO));
                doc.add(new TextField("number", Integer.toString(chapter.getID()), Field.Store.YES));
                writer.addDocument(doc);
                chapters.put(chapter.getID(), chapter);
                LOGGER.log(Level.FINE, "Added bible chapter to index: {0}", chapter.getID());
            }
        }
        catch (IOException ex) {
            LOGGER.log(Level.SEVERE, "Couldn't add value to index", ex);
        }
    }

    /**
     * Remove the given bible chapter from the index.
     *
     * @param chapter the chapter to remove.
     */
    @Override
    public void remove(BibleChapter chapter) {
        try (IndexWriter writer = new IndexWriter(index, new IndexWriterConfig(analyzer))) {
            writer.deleteDocuments(new Term("number", Integer.toString(chapter.getID())));
        }
        catch (IOException ex) {
            LOGGER.log(Level.SEVERE, "Couldn't remove value from index", ex);
        }
    }

    /**
     * Update the given bible chapter in the index.
     *
     * @param chapter the chapter to update.
     */
    @Override
    public void update(BibleChapter chapter) {
        remove(chapter);
        add(chapter);
    }

    /**
     * Search for bible chapters that match the given filter.
     *
     * @param queryString the query string to filter.
     * @param type ignored - may be null.
     * @return a list of all bible chapters that match the given filter.
     */
    @Override
    public BibleChapter[] filter(String queryString, FilterType type) {
        String sanctifyQueryString = SearchIndexUtils.makeLuceneQuery(queryString);
        if(chapters.isEmpty() || sanctifyQueryString.isEmpty()) {
            return chapters.values().toArray(new BibleChapter[chapters.size()]);
        }
        List<BibleChapter> ret;
        try (DirectoryReader dr = DirectoryReader.open(index)) {
            IndexSearcher searcher = new IndexSearcher(dr);
            BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE);
            Query q = new ComplexPhraseQueryParser("text", analyzer).parse(sanctifyQueryString);
            TopScoreDocCollector collector = TopScoreDocCollector.create(10000,10000);
            searcher.search(q, collector);
            ScoreDoc[] hits = collector.topDocs().scoreDocs;
            ret = new ArrayList<>();
            for(int i = 0; i < hits.length; ++i) {
                int docId = hits[i].doc;
                Document d = searcher.doc(docId);
                BibleChapter chapter = chapters.get(Integer.parseInt(d.get("number")));
                ret.add(chapter);
            }
            return ret.toArray(new BibleChapter[ret.size()]);
        }
        catch (ParseException | IOException ex) {
            LOGGER.log(Level.WARNING, "Invalid query string: " + sanctifyQueryString, ex);
            return new BibleChapter[0];
        }
    }
    
    /**
     * Remove everything from this index.
     */
    @Override
    public void clear() {
        SearchIndexUtils.clearIndex(index);
    }
}