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

 Copyright (C) 2015-2017 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.regex.Matcher;
import java.util.regex.Pattern;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.omegat.core.Core;
import org.omegat.core.data.IProject;
import org.omegat.core.data.ProjectProperties;
import org.omegat.core.data.SourceTextEntry;
import org.omegat.core.data.TMXEntry;
import org.omegat.core.matching.NearString;
import org.omegat.core.matching.external.IExternalMemory;
import org.omegat.core.matching.FindMatchesSingleton;
import org.omegat.core.events.IStopped;
import org.omegat.gui.exttrans.IMachineTranslation;
import org.omegat.util.Language;
import org.omegat.util.Preferences;
import org.omegat.gui.search.PreTranslateDialog;
import org.omegat.gui.search.SearchVarExpansion;


/**
 * Search in the project, 
 * 
 * @author Thomas Cordonnier
 */
public class PreTranslateSearcher extends ProjectSearcher implements IStopped {
    
    public static final byte TRANSLATE_ENTIRE = 0, TRANSLATE_WHOLE = 1, TRANSLATE_FOUND = 2;
    
    /**
     * Create new searcher instance.
     * 
     * @param project
     *            Current project
     */
    public PreTranslateSearcher(final IProject project, PreTranslateDialog window,  int numberOfResults, boolean asAlternative,
        TextExpression searchSource, TranslationStateFilter filter, IMachineTranslation translator, String prefix,
        TextExpression author, long dateAfter, long dateBefore) {

        super (window, project, !asAlternative, false, numberOfResults, 
            author, null, dateAfter, dateBefore);
        
        this.m_prefix = prefix;
        this.m_searchSource = searchSource; this.m_translationStateFilter = filter;
        this.m_replaceContent = translator;
        this.m_asAlternative = asAlternative;
    }
    
    public boolean isStopped() { return isInterrupted(); }    
    
    // ---- Factories for translators which only exist for pre-translation
    
    public IMachineTranslation buildTranslationMemoriesPreTranslator(final NearString.SORT_KEY criteria, final int minValue, final String memoryPath) {
        return new IMachineTranslation() {
            FindMatchesSingleton matches = new FindMatchesSingleton(m_project.getSourceTokenizer(), new IExternalMemory[0], memoryPath, false, false);

            public String getName() { return "***TranslationMemory***"; }

            public String getTranslation(Language sLang, Language tLang, String text) {
                NearString best = matches.search(m_project, text, true, false, PreTranslateSearcher.this); if (best == null) return null;
                switch (criteria) {
                    case SCORE_STEM: if (best.scoreStem < minValue) return null; break;
                    case SCORE_NO_STEM: if (best.scoreNoStem < minValue) return null; break;
                    case ADJUSTED_SCORE: if (best.adjustedScore < minValue) return null; break;
                    case IMPROVED_SCORE: if (best.scoreImproved < minValue) return null; break;
                }
                String tra = best.translation;
                if (Preferences.isPreference(Preferences.CONVERT_NUMBERS))
                    tra = Core.getMatcher().substituteNumbers(text, best.source, tra);
                String prefix = m_prefix; 
                prefix = prefix.replace("${score}", Integer.toString(best.scoreImproved));
                prefix = prefix.replace("${stemScore}", Integer.toString(best.scoreStem));
                prefix = prefix.replace("${noStemScore}", Integer.toString(best.scoreNoStem));
                prefix = prefix.replace("${adjustedScore}", Integer.toString(best.adjustedScore));					
                prefix = prefix.replace("${improvedScore}", Integer.toString(best.scoreImproved));					
                prefix = prefix.replace("${author}", best.creator);
                return prefix + tra;
            }
        };        
    }
    
    private class ReplaceTextMachineTranslation implements IMachineTranslation {
        private String replacement;
        private boolean m_replaceVars;
        private byte m_replaceMode;
        public final TextExpression.RegexTextExpression findSubpart;
        private SearchVarExpansion varExpansion;
        
        public ReplaceTextMachineTranslation(String replacement, byte replaceMode, boolean replaceVars) {
            this.m_replaceMode = replaceMode; this.m_replaceVars = replaceVars;
            if (m_replaceVars && (replacement.contains("${"))) this.varExpansion = new SearchVarExpansion (Core.getProject().getProjectProperties(), replacement); 
            else this.varExpansion = null;
            this.replacement = replacement; 
            if (m_replaceVars) replacement = replacement.replaceAll("(?<!\\\\)\\$\\{(\\w+)\\}", "!<$1>"); // avoid interpretation by following lines
            // Try to build a searcher for highlighting
            if (! replaceVars) this.findSubpart = new TextExpression.RegexTextExpression(escapeRegexChars(replacement).replace("$", "\\$"), true);
            else if (! replacement.contains("$")) this.findSubpart = new TextExpression.RegexTextExpression(escapeRegexChars(replacement), true);
            else if (m_searchSource instanceof TextExpression.ExactTokenExpression) this.findSubpart = new TextExpression.RegexTextExpression(escapeRegexChars(replacement).replaceAll("(?<!\\\\)\\$\\d+", "(\\\\p{L}+)"), true);
            else if (m_searchSource instanceof TextExpression.RegexTextExpression) {
                String pattern = ((TextExpression.RegexTextExpression) m_searchSource).getPattern().pattern();
                Matcher m = Pattern.compile("\\((.+?)\\)").matcher(pattern);
                List<String> groups = new ArrayList<String>(); while (m.find()) groups.add(m.group(1));
                StringBuffer buf = new StringBuffer(); String copy = escapeRegexChars(replacement);
                while (copy.contains("$")) {
                    buf.append (copy.substring(0, copy.indexOf("$"))); copy = copy.substring(copy.indexOf("$"));
                    m = Pattern.compile("(?<!\\\\)\\$(\\d+)").matcher(copy);
                    if (m.find()) { 
                        int id = Integer.parseInt(m.group(1));
                        if (id == 0) buf.append("(" + pattern + ")");
                        else if (groups.size() > id) buf.append("(").append(groups.get(id - 1)).append(")"); 
                        else buf.append("(\\S+)");
                        copy = copy.substring(m.group(0).length());
                    } else {
                        buf.append ("\\$"); copy = copy.substring(1);
                    }
                }
                buf.append(copy);
                this.findSubpart = new TextExpression.RegexTextExpression(buf.toString(), true);
            }
            else this.findSubpart = new TextExpression.RegexTextExpression(replacement, true);
        }
        
        private final char[] ESCAPE_REGEX = { '*', '+', '[', ']', '(', ')' };
        private String escapeRegexChars (String ori) {            
            for (char ch: ESCAPE_REGEX) ori = ori.replace("" + ch, "\\" + ch);
            return ori;
        }

        public String getName() { return ""; }
                    
        public String getTranslation(Language sLang, Language tLang, String oriText) {
            if (m_replaceMode == TRANSLATE_ENTIRE)                 
                if (! m_replaceVars) return replacement; // trivial case
                else {
                    List<SearchMatch> matches = m_searchSource.searchString(oriText);
                    String repl = replacement; boolean found = false;
                    Matcher replaceMatcher = Pattern.compile("(?<!\\\\)\\$\\^(\\d+)").matcher(repl);
                    while (replaceMatcher.find()) {
                        int varId = Integer.parseInt(replaceMatcher.group(1));
                        repl = replaceMatcher.replaceFirst( oriText.substring(matches.get(varId - 1).getStart(), matches.get(varId - 1).getEnd()) );
                        replaceMatcher.reset(repl); found = true;
                    }
                    if (found) return org.omegat.util.StringUtil.replaceCase(repl, org.omegat.core.Core.getProject().getProjectProperties().getTargetLanguage().getLocale());
                    else try { return ((ReplaceMatch) matches.get(0)).getReplacement(); } catch (Exception e) { return replacement; }
                    
                }
            else {
                List<SearchMatch> matches = m_searchSource.searchString(oriText);
                if (m_replaceMode == TRANSLATE_WHOLE) {
                    List<SearchMatch> englobedList = new ArrayList<> (matches.size());
                    for (SearchMatch m: matches) englobedList.add (m.englobeWord(oriText));
                    matches = englobedList;
                }
                return ReplaceMatch.buildReplacedString(oriText, matches, replacement);
            } 
        }
        
        public void reset (SourceTextEntry ste) {
            if (varExpansion != null) replacement = varExpansion.apply (new SourceSearchResultEntry(ste, null, null, null));
        }
    }
    
    public IMachineTranslation buildReplacerPreTranslator(String replacement, byte replaceMode, boolean replaceVars) {
        return new ReplaceTextMachineTranslation(replacement, replaceMode, replaceVars);
    }
    
    public void setTranslator (IMachineTranslation tra) { this.m_replaceContent = tra; }
    
    // -------------- Searcher extension
    
    protected PreTranslateSearchResultEntry buildOngoingEntry (SourceTextEntry ste, TMXEntry tmxEntry, List<SearchMatch> srcMatches) {
        PreTranslateSearchResultEntry entry = new PreTranslateSearchResultEntry(ste, tmxEntry, srcMatches);        
        try { ((ReplaceTextMachineTranslation) m_replaceContent).reset (ste); } catch (Exception ignored) {}
        try {
            String translation = m_replaceContent.getTranslation (Core.getProject().getProjectProperties().getSourceLanguage(), Core.getProject().getProjectProperties().getTargetLanguage(), ste.getSrcText());
            if (translation == null) return null; // for MT/TM replacers : if no match found, do not even include this entry in the list
            if (m_replaceContent.getName().equals("Source with glossaries")) 
                ((org.omegat.core.machinetranslators.dummy.SourceTranslate) m_replaceContent).rebuildEntryHighlights(entry, translation, m_prefix);
            if (! m_replaceContent.getName().equals("***TranslationMemory***")) translation = m_prefix + translation;
            entry.setTranslationResult(translation);
            if (m_replaceContent instanceof ReplaceTextMachineTranslation)
                entry.setHighlights( ((ReplaceTextMachineTranslation) m_replaceContent).findSubpart.searchString(translation) );
            else {
                if (entry.getHighlights() == null) entry.setHighlights(new ArrayList<SearchMatch>());
                if (m_prefix.contains("$")) for (SearchMatch m: new TextExpression.RegexTextExpression("^\\[(.+?)\\]", true).searchString(translation)) entry.addHighlight(m.getStart(), m.getEnd(), false);
                else if (m_prefix.length() > 0) entry.addHighlight(0, m_prefix.length(), false); 
            }
        } catch (Exception e) {            
            e.printStackTrace();
            entry.setTranslationResult("[ERROR] " + e.getMessage());
            entry.addHighlight(0, 7, true);
        }
        return entry;        
    }
    
    @Override
    protected void testOngoing(SourceTextEntry ste) {
        TMXEntry tmxEntry = m_project.getTranslationInfo(ste.getKey());
        if (! getTranslationStateFilter().isValidEntry (tmxEntry)) return;
        if (! checkFilters (tmxEntry)) return;
        List<SearchMatch> srcMatches = m_searchSource.searchString(ste.getSrcText());
        if (srcMatches != null) { SearchResultEntry entry = buildOngoingEntry(ste, tmxEntry, srcMatches); if (entry != null) addEntry(entry); }
    }
    
    public PreTranslateSearchResultEntry testSingleEntry(SourceTextEntry ste) {
        TMXEntry tmxEntry = m_project.getTranslationInfo(ste.getKey());
        if (! getTranslationStateFilter().isValidEntry (tmxEntry)) return null;
        if (! checkFilters (tmxEntry)) return null;
        List<SearchMatch> srcMatches = m_searchSource.searchString(ste.getSrcText());
        if (srcMatches != null) return buildOngoingEntry(ste, tmxEntry, srcMatches);
        else return null;
    }
    
    
    /** Apply the replacement which is at selected position **/     
    public String replaceOccurence (String oriText, int pos, SourceTextEntry ste) {
        try {
            ProjectProperties prop = m_project.getProjectProperties();
            return m_prefix + m_replaceContent.getTranslation (prop.getSourceLanguage(), prop.getTargetLanguage(), oriText); 
        } catch (Exception e) {
            return oriText;
        }
    }

    @Override public TranslationStateFilter getTranslationStateFilter() { return m_translationStateFilter; }    
    @Override public boolean searchOn(int location) { return (location & SEARCH_SCOPE_ONGOING) > 0; }
    
    public List<SearchMatch> searchInSource(String text) {
        return m_searchSource == null ? null : m_searchSource.searchString(text);
    }
    
    // if iterator exists, use it to avoid parsing again begin of the list
    @Override public Iterator<SourceTextEntry> projectEntries() { 
        if (iter == null) iter = m_project.getAllEntries().iterator(); 
        return iter;
    }
    
    public void startAt (Iterator<SourceTextEntry> iter) { this.iter = iter; }
    
    public boolean translateAsAlternative() { return m_asAlternative; }
    
    private Iterator<SourceTextEntry> iter = null;   	    
    private String m_prefix;
    private IMachineTranslation m_replaceContent;
    protected final TextExpression m_searchSource;
    protected final TranslationStateFilter m_translationStateFilter;
    private boolean m_asAlternative;
}
