/**************************************************************************
 OmegaT - Computer Assisted Translation (CAT) tool 
          with fuzzy matching, translation memory, keyword search, 
          glossaries, and translation leveraging into updated projects.

 Copyright (C) 2000-2006 Keith Godfrey and Maxym Mykhalchuk
               2006 Henry Pijffers
               2009 Didier Briel
               2010 Martin Fleurke, Antonio Vilei, Alex Buloichik, Didier Briel
               2012 Thomas Cordonnier
               2013 Aaron Madlon-Kay, Alex Buloichik, Thomas Cordonnier
               2014 Alex Buloichik, Piotr Kulik, Thomas Cordonnier
               2015-2020 Thomas Cordonnier
               Home page: http://www.omegat.org/
               Support center: http://groups.yahoo.com/group/OmegaT/

 This file is part of OmegaT.

 OmegaT 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.

 OmegaT 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.omegat.core.search;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.omegat.core.Core;
import org.omegat.core.data.EntryKey;
import org.omegat.core.data.ExternalTMX;
import org.omegat.core.data.IProject;
import org.omegat.core.data.PrepareTMXEntry;
import org.omegat.core.data.ProjectTMX;
import org.omegat.core.data.SourceTextEntry;
import org.omegat.core.data.TMXEntry;
import org.omegat.core.matching.external.IBrowsableMemory;
import org.omegat.core.matching.external.IExternalMemory;
import org.omegat.core.glossaries.GlossaryEntryStore;
import org.omegat.gui.search.ProjectSearchWindow;
import org.omegat.util.Language;
import org.omegat.util.OStrings;

/**
 * Specific searcher for searches in a project, including tm and glossaries (not used during replacement)
 * 
 * @author Keith Godfrey
 * @author Maxym Mykhalchuk
 * @author Henry Pijffers
 * @author Didier Briel
 * @author Martin Fleurke
 * @author Antonio Vilei
 * @author Alex Buloichik (alex73mail@gmail.com)
 * @author Aaron Madlon-Kay
 * @author Piotr Kulik
 * @author Thomas Cordonnier
 */
public class FullProjectSearcher extends ProjectSearcher {

    // ----------- Added search locations ------------
    
    public static final int SEARCH_SCOPE_TM 				= 	0x04;
    public static final int SEARCH_SCOPE_GLOSSARIES 		= 	0x08;

    /**
     * Create new searcher instance.
     * 
     * @param project
     *            Current project
     */
    public FullProjectSearcher(final ProjectSearchWindow window, final IProject project,
	    boolean removeDup, boolean trueMissing, int numberOfResults, TranslationStateFilter translationStateFilter, int searchLocation, String searchFile,
        TextExpression searchSource, TextExpression searchTarget, TextExpression searchNotes, String lastSearchField, boolean andSearch, boolean andThenSearch,
        TextExpression author, TextExpression translator,long dateAfter, long dateBefore) {
 
        super (window, project, removeDup, trueMissing, numberOfResults, 
            author, translator, dateAfter, dateBefore);
        this.m_translationStateFilter = translationStateFilter;		
        this.m_searchLocation = searchLocation; this.m_andSearch = andSearch;
        this.m_searchFile = searchFile; this.m_lastSearchField = lastSearchField;
		
		if (andThenSearch)
			if ((searchSource != null) && (! searchSource.hasVariables())) { m_searchSource = searchSource.asVariableKeeper(); this.m_searchTarget = searchTarget; this.m_searchNotes = searchNotes; }
			else if ((searchTarget != null) && (! searchTarget.hasVariables())) { m_searchTarget = searchTarget.asVariableKeeper(); this.m_searchSource = searchSource; this.m_searchNotes = searchNotes; }
			else if ((searchNotes != null) && (! searchNotes.hasVariables())) { m_searchNotes = searchNotes.asVariableKeeper(); this.m_searchSource = searchSource; this.m_searchTarget = searchTarget; }
			else { this.m_searchSource = searchSource; this.m_searchTarget = searchTarget; this.m_searchNotes = searchNotes; }	// use normal AND search
		else { this.m_searchSource = searchSource; this.m_searchTarget = searchTarget; this.m_searchNotes = searchNotes; }	// no variables
        
        // Avoid unuseful results :
        if ((searchLocation & SEARCH_SCOPE_ONGOING) > 0)
            m_sourceTranslationStateFilter = TranslationStateFilter.TRANSLATED_ONLY;
        else
            m_sourceTranslationStateFilter = getTranslationStateFilter();
    }

    // /////////////////////////////////////////////////////////
    // thread main loop
    @Override
    protected void doSearch() {
        super.doSearch();	// common part : ongoing search

        // search the TM, if requested
        if ((m_searchLocation & SEARCH_SCOPE_TM) != 0) {
            int count = m_project.getTransMemories().entrySet().size(), progress = 0;
            for (Map.Entry<String, IExternalMemory> tmEn : m_project.getTransMemories().entrySet()) {
                final String fileTM = tmEn.getKey();
                m_window.displayProgress("TM:" + fileTM, progress++, count, m_numFinds);
                try {
                    ISearchable<PrepareTMXEntry> mem = (ISearchable<PrepareTMXEntry>) tmEn.getValue();
                    if (!searchEntries(searchInProvider(mem), fileTM)) return;
                } catch (ClassCastException cce) {
                    // Ignore: we do not search in non-browsable memories
                } catch (Exception otherException) {
                    otherException.printStackTrace();
                }
                checkInterrupted();
            }
            count = m_project.getOtherTargetLanguageTMs().entrySet().size(); progress = 0;
            for (Map.Entry<Language, ProjectTMX> tmEn : m_project.getOtherTargetLanguageTMs().entrySet()) {
                final Language langTM = tmEn.getKey();
                m_window.displayProgress("OTHER_LANG", progress++, count, m_numFinds);
                if (!searchEntriesAlternative(tmEn.getValue().getDefaults(), langTM.getLanguage())) return;
                if (!searchEntriesAlternative(tmEn.getValue().getAlternatives(), langTM.getLanguage())) return;
                checkInterrupted();
            }
            checkInterrupted();
        }

        // search the glossary, if requested
        if ((m_searchLocation & SEARCH_SCOPE_GLOSSARIES) != 0) {
            // Search glossary entries, unless we search for date or author.
            // They are not stored in glossaries, so skip the search in that case.
            if ((m_author != null) || (m_dateAfter < Long.MAX_VALUE) || (m_dateBefore > Long.MIN_VALUE)) return;
			
            String intro = OStrings.getString("SW_GLOSSARY_RESULT");
            List<GlossaryEntryStore> entries = Core.getGlossaryManager().search(""); // parameter not really used
            for (GlossaryEntryStore en : entries) {
                testGlossary(en);
                // stop searching if the max. nr of hits has been reached
                if (m_numFinds >= m_maxResults) {
                    return;
                }
                checkInterrupted();
            }
        }
    }
    
    /**
     * Loops over collection of TMXEntries and checks every entry.
     * If max nr of hits have been reached or serach has been stopped,
     * the function stops and returns false. Else it finishes and returns true;
     * 
     * @param tmEn collection of TMX Entries to check.
     * @param tmxID identifier of the TMX. E.g. the filename or language code
     * @return true when finished and all entries checked,
     *         false when search has stopped before all entries have been checked.
     */
    private boolean searchEntries(Iterable<PrepareTMXEntry> tmEn, final String tmxID) {
        for (PrepareTMXEntry tm : tmEn) {
            // stop searching if the max. nr of hits has been reached
            if (m_numFinds >= m_maxResults) return false;

            //for alternative translations:
            //- it is not feasible to get the sourcetextentry that matches the tm.source, so we cannot get the entryNum and real translation
            //- although the 'trnalsation' is used as 'source', we search it as translation, else we cannot show to which real source it belongs
            testTM (tmxID, tm);

            checkInterrupted();
        }
        return true;
    }

    private boolean searchEntriesAlternative(Iterable<TMXEntry> tmEn, final String tmxID) {
        for (TMXEntry tm : tmEn) {
            // stop searching if the max. nr of hits has been reached
            if (m_numFinds >= m_maxResults) return false;

            //for alternative translations:
            //- it is not feasible to get the sourcetextentry that matches the tm.source, so we cannot get the entryNum and real translation
            //- although the 'trnalsation' is used as 'source', we search it as translation, else we cannot show to which real source it belongs
            testInternalEntry (tmxID, tm);

            checkInterrupted();
        }
        return true;
	}
	
    protected interface EntryBuilder {
        SearchResultEntry buildEntry(List<SearchMatch> src, List<SearchMatch> tra, List<SearchMatch> note);
    }
    
	private void checkMatchesOrdered (TextExpression expr1, String text1, TextExpression expr2, String text2, TextExpression expr3, String text3, int id, EntryBuilder eBuilder) {
		expr1 = expr1.asVariableKeeper();
		List<SearchMatch> matchesA = expr1.searchString(text1); if (matchesA == null) return; 
		List<SearchMatch> matchesB = new java.util.LinkedList<>(), matchesC = new java.util.LinkedList<>();
		if (expr2 != null)
			if (text2 == null) return;
			else {
				List<SearchMatch> matches2, toRemove = new java.util.LinkedList<>();
				for (SearchMatch m: matchesA) {
					matches2 = expr2.rebuildForVariables(((VarMatch) m).groups).searchString(text2); 
					if (matches2 == null) toRemove.add(m); else matchesB.addAll(matches2); 
				}
				matchesA.removeAll (toRemove); toRemove.clear();				
			}
		if (expr3 != null) 
			if (text3 == null) return;
			else {
				List<SearchMatch> matches3, toRemove = new java.util.LinkedList<>();
				for (SearchMatch m: matchesA) {
					matches3 = expr3.rebuildForVariables(((VarMatch) m).groups).searchString(text3); 
					if (matches3 == null) toRemove.add(m); else matchesC.addAll(matches3); 
				}
				matchesA.removeAll (toRemove); toRemove.clear();				
			}
		if (matchesA.size() > 0) 
			switch (id) {
				case 1: addEntry(eBuilder.buildEntry(matchesA, matchesB, matchesC)); break;
				case 2: addEntry(eBuilder.buildEntry(matchesB, matchesA, matchesC)); break;
				case 3: addEntry(eBuilder.buildEntry(matchesB, matchesC, matchesA)); break;
			}
	}
	
	/** 
	 * Check source, target and/or notes against current entry, apply AND/OR operator. If all OK, call constructor and add entry
	 * Contrarily to check filters, can be only called after the matches have been searched
	 **/
	protected void checkMatches(String srcText, String traText, String noteText, EntryBuilder eBuilder) {	
		List<SearchMatch> srcMatches = null, targetMatches = null, noteMatches = null;
		if ((m_searchSource != null) && m_searchSource.isVariableKeeper()) 
			checkMatchesOrdered(m_searchSource, srcText, m_searchTarget, traText, m_searchNotes, noteText, 1, eBuilder);
		else if ((m_searchTarget != null) && m_searchTarget.isVariableKeeper()) 
			checkMatchesOrdered(m_searchTarget, traText, m_searchSource, srcText, m_searchNotes, noteText, 2, eBuilder);
		else if ((m_searchNotes != null) && m_searchNotes.isVariableKeeper()) 
			checkMatchesOrdered(m_searchNotes, noteText, m_searchSource, srcText, m_searchTarget, traText, 3, eBuilder);
		else if (! m_andSearch) {	// we cannot use a short circuit because Search matches are used for display			
			if (m_searchSource != null) srcMatches = m_searchSource.searchString(srcText);
			if ((m_searchTarget != null) && (traText != null)) targetMatches = m_searchTarget.searchString(traText);
			if ((m_searchNotes != null) && (noteText != null)) noteMatches = m_searchNotes.searchString(noteText);
			if ((srcMatches != null) || (targetMatches != null) || (noteMatches != null)) addEntry(eBuilder.buildEntry(srcMatches, targetMatches, noteMatches));
		} else {	// short circuit: if one fails while it was mandatory, stop the search
			if (m_searchSource != null) { srcMatches = m_searchSource.searchString(srcText); if (srcMatches == null) return; }			
			if ((m_searchTarget != null) && (traText != null)) { targetMatches = m_searchTarget.searchString(traText); if (targetMatches == null) return; }
			if ((m_searchNotes != null) && (noteText != null)) { noteMatches = m_searchNotes.searchString(noteText); if (noteMatches == null) return; }
			// If we are still here, almost one of the entries gave a result
			addEntry(eBuilder.buildEntry(srcMatches, targetMatches, noteMatches));		
		}
	}
	
    protected void testOngoing(SourceTextEntry ste) {
        TMXEntry tmxEntry = m_project.getTranslationInfo(ste);
        if (! getTranslationStateFilter().isValidEntry (tmxEntry)) return;
        if (! checkFilters (tmxEntry)) return;
        if ((m_searchFile != null) && (m_searchFile.trim().length() > 0))
            if (! ste.getKey().file.contains(m_searchFile)) return;
        String lastFieldValue = tmxEntry.note; if (m_lastSearchField.contains("PROP")) {
            lastFieldValue += "\n";
            lastFieldValue += "id: " + ste.getKey().id + "\n";
            lastFieldValue += "path: " + ste.getKey().path + "\n";
        }
		checkMatches(ste.getSrcText(), tmxEntry.translation, lastFieldValue, 
			(srcMatches, targetMatches, noteMatches) -> new OngoingSearchResultEntry(ste, tmxEntry, srcMatches, targetMatches, noteMatches));
    }
	
    @Override
	protected boolean testSourceDocument(SourceTextEntry ste) {
        if (! m_sourceTranslationStateFilter.isValidEntry(ste)) return false;
        if ((m_searchFile != null) && (m_searchFile.trim().length() > 0))
            if (! ste.getKey().file.contains(m_searchFile)) return false;
        String lastFieldValue = ste.getComment(); if (m_lastSearchField.contains("PROP")) {
            lastFieldValue += "\n";
            lastFieldValue += "id: " + ste.getKey().id + "\n";
            lastFieldValue += "path: " + ste.getKey().path + "\n";
        }
 		if (super.testSourceDocument(ste)) {
			checkMatches(ste.getSrcText(), ste.getSourceTranslation(), lastFieldValue, 
				(srcMatches, targetMatches, noteMatches) -> new SourceSearchResultEntry(ste, srcMatches, targetMatches, noteMatches));
			return true;
		}
		else return false;
    }	
	
    private void testTM (String fileName, PrepareTMXEntry entry) {
        if (! m_translationStateFilter.isValidEntry (entry)) return;
        if ((m_searchFile != null) && (m_searchFile.trim().length() > 0))
            if (! fileName.contains(m_searchFile)) return;
        if (! checkFilters (entry)) return;
        String lastFieldValue = entry.note; if (m_lastSearchField.contains("PROP")) {
            lastFieldValue += "\n";
            for (org.omegat.util.TMXProp prop: entry.otherProperties) lastFieldValue += prop.getType() + ": " + prop.getValue() + "\n";
        }
		checkMatches(entry.source, entry.translation, lastFieldValue, 
			(srcMatches, targetMatches, noteMatches) -> new ExternalSearchResultEntry(fileName, entry, srcMatches, targetMatches, noteMatches));
    }
	
    @Override protected boolean testInternalEntry (String fileName, TMXEntry entry) {
        if (! m_translationStateFilter.isValidEntry (entry)) return false;
        if (! checkFilters (entry)) return false;
		checkMatches(entry.source, entry.translation, entry.note, 
			(srcMatches, targetMatches, noteMatches) -> new InternalSearchResultEntry(fileName, entry, srcMatches, targetMatches, noteMatches));
	return true;
    }

    private void testGlossary (GlossaryEntryStore entry) {
        if (m_searchFile != null) if (! entry.getOrigin().getLongName().contains(m_searchFile)) return; // since 3.5, it is stored!
        if (! m_trueMissing) { // fields not stored for glossaries
            if (m_author != null) return; if (m_dateAfter < Long.MAX_VALUE) return; if (m_dateBefore > Long.MIN_VALUE) return;
        }
        if (! m_translationStateFilter.isValidEntry (entry)) return;
		checkMatches(entry.getSrcText(), entry.getLocText(), entry.getCommentText(), 
			(srcMatches, targetMatches, noteMatches) -> new GlossarySearchResultEntry(entry, srcMatches, targetMatches, noteMatches));
    }
	
    @Override public TranslationStateFilter getTranslationStateFilter() { return m_translationStateFilter; }	
	@Override public boolean searchOn(int location) { return (location & this.m_searchLocation) > 0; }

    // Used for replacements
    public List<SearchMatch> searchInTranslation(String text) {
        return m_searchTarget == null ? null : m_searchTarget.searchString(text);
    }
    
    public <T> Iterable<T> searchInProvider (ISearchable<T> provider) throws Exception {
        return provider.search(m_maxResults,
            m_searchSource, m_searchTarget, m_searchNotes, m_andSearch,
            m_author, m_dateAfter, m_dateBefore);
    }	
	
    protected final int m_searchLocation;
    protected final boolean m_andSearch;
    protected final String m_lastSearchField;
    protected final TranslationStateFilter m_translationStateFilter, m_sourceTranslationStateFilter;
    protected final TextExpression m_searchSource, m_searchTarget, m_searchNotes;
    private final String m_searchFile;
}
