/**************************************************************************
 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 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.gui.glossary.GlossaryEntry;
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_ORPHANS 	= 	0x02;
    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, int numberOfResults, TranslationStateFilter translationStateFilter, int searchLocation, String searchFile,
        TextExpression searchSource, TextExpression searchTarget, TextExpression searchNotes, boolean andSearch, boolean andThenSearch,
        TextExpression author, TextExpression translator,long dateAfter, long dateBefore) {
 
        super (window, project, removeDup, numberOfResults, translationStateFilter, searchLocation,
            searchSource, searchTarget, searchNotes, andSearch,
            author, translator, dateAfter, dateBefore);
        this.m_translationStateFilter = translationStateFilter;		
        this.m_searchLocation = searchLocation; this.m_andSearch = andSearch;        
        this.m_searchFile = searchFile;
        
        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 Memory, if requested
        if ((m_searchLocation & (SEARCH_SCOPE_ORPHANS | SEARCH_SCOPE_ONGOING)) != 0) {
            final String file = OStrings.getString("CT_ORPHAN_STRINGS");
			m_window.displayProgress("ORPHAN", 1, 2, m_numFinds);

            // search in orphaned
            m_project.iterateByDefaultTranslations((String source, TMXEntry en) -> {
                    // stop searching if the max. nr of hits has been reached
                    if (m_numFinds >= m_maxResults) return;
                    
                    checkInterrupted();
                    if (m_project.isOrphaned(source))
                        testInternalEntry(file, en);
                });

			m_window.displayProgress("ORPHAN", 2, 2, m_numFinds);
            m_project.iterateByMultipleTranslations((EntryKey source, TMXEntry en) -> {
                    // stop searching if the max. nr of hits has been reached
                    if (m_numFinds >= m_maxResults) return;
                    
                    checkInterrupted();
                    if (m_project.isOrphaned(source))
                        testInternalEntry(file, en);
                });
        }

        // 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", 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<GlossaryEntry> entries = Core.getGlossaryManager().search(""); // parameter not really used
            for (GlossaryEntry 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 SearchResultEntry 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 null; 
		List<SearchMatch> matchesB = new java.util.LinkedList<>(), matchesC = new java.util.LinkedList<>();
		if (expr2 != null)
			if (text2 == null) return null;
			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 null;
			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: return eBuilder.buildEntry(matchesA, matchesB, matchesC);
				case 2: return eBuilder.buildEntry(matchesB, matchesA, matchesC);
				case 3: return eBuilder.buildEntry(matchesB, matchesC, matchesA);
			}
        return null;
	}
	
	/** 
	 * 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 SearchResultEntry checkMatchesFields(String srcText, String traText, String noteText, EntryBuilder eBuilder) {	
		List<SearchMatch> srcMatches = null, targetMatches = null, noteMatches = null;
		if ((m_searchSource != null) && m_searchSource.isVariableKeeper()) 
			return checkMatchesOrdered(m_searchSource, srcText, m_searchTarget, traText, m_searchNotes, noteText, 1, eBuilder);
		else if ((m_searchTarget != null) && m_searchTarget.isVariableKeeper()) 
			return checkMatchesOrdered(m_searchTarget, traText, m_searchSource, srcText, m_searchNotes, noteText, 2, eBuilder);
		else if ((m_searchNotes != null) && m_searchNotes.isVariableKeeper()) 
			return 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)) return eBuilder.buildEntry(srcMatches, targetMatches, noteMatches);
			else return null;
		} 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 null; }			
			if ((m_searchTarget != null) && (traText != null)) { targetMatches = m_searchTarget.searchString(traText); if (targetMatches == null) return null; }
			if ((m_searchNotes != null) && (noteText != null)) { noteMatches = m_searchNotes.searchString(noteText); if (noteMatches == null) return null; }
			// If we are still here, almost one of the entries gave a result
			return eBuilder.buildEntry(srcMatches, targetMatches, noteMatches);
		}
	}
	
    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;

        SearchResultEntry res = checkMatchesFields(entry.source, entry.translation, entry.note, 
			(srcMatches, targetMatches, noteMatches) -> new ExternalSearchResultEntry(fileName, entry, srcMatches, targetMatches, noteMatches)); 
        if (res != null) addEntry(res);         
    }
	
    private void testInternalEntry (String fileName, TMXEntry entry) {
        if (! m_translationStateFilter.isValidEntry (entry)) return;
        if (! checkFilters (entry)) return;

        SearchResultEntry res = checkMatchesFields(entry.source, entry.translation, entry.note, 
			(srcMatches, targetMatches, noteMatches) -> new InternalSearchResultEntry(fileName, entry, srcMatches, targetMatches, noteMatches)); 
        if (res != null) addEntry(res); 
    }
    
    @Override
    protected void testSource(SourceTextEntry ste) {
        if ((m_searchFile != null) && (m_searchFile.trim().length() > 0))
            if (! ste.getKey().file.contains(m_searchFile)) return;
        super.testSource(ste);
    }
	
    @Override
    public OngoingSearchResultEntry testOngoing(SourceTextEntry ste, boolean add) {
        TMXEntry tmxEntry = m_project.getTranslationInfo(ste);
        if (! getTranslationStateFilter().isValidEntry (tmxEntry)) return null;
        if (! checkFilters (tmxEntry)) return null;
        if ((m_searchFile != null) && (m_searchFile.trim().length() > 0))
            if (! ste.getKey().file.contains(m_searchFile)) return null;
        OngoingSearchResultEntry res = (OngoingSearchResultEntry) checkMatchesFields(ste.getSrcText(), tmxEntry.translation, tmxEntry.note, 
			(srcMatches, targetMatches, noteMatches) -> new OngoingSearchResultEntry(ste, tmxEntry, srcMatches, targetMatches, noteMatches)); 
        if (add) if (res != null) addEntry(res); return res;
    }

    private void testGlossary (GlossaryEntry entry) {
        if (! m_translationStateFilter.isValidEntry (entry)) return;

        SearchResultEntry res = checkMatchesFields(entry.getSrcText(), entry.getLocText(), entry.getCommentText(), 
			(srcMatches, targetMatches, noteMatches) -> new GlossarySearchResultEntry(entry, srcMatches, targetMatches, noteMatches)); 
        if (res != null) addEntry(res); 
    }
    
    private final String m_searchFile;
}
