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

 Copyright (C) 2009-2010 Alex Buloichik
               2011 Martin Fleurke
               2012 Jean-Christophe Helary, Thomas Cordonnier
               2015 Aaron Madlon-Kay
               2016-2018 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.gui.exttrans;

import java.awt.Color;
import java.awt.Dimension;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.omegat.core.Core;
import org.omegat.core.data.ProjectProperties;
import org.omegat.core.data.SourceTextEntry;
import org.omegat.core.data.TMXEntry;
import org.omegat.core.machinetranslators.CachedTranslate;
import org.omegat.filters2.master.PluginUtils;
import org.omegat.gui.common.EntryInfoSearchThread;
import org.omegat.gui.common.EntryInfoThreadPane;
import org.omegat.gui.main.DockableScrollPane;
import org.omegat.util.Language;
import org.omegat.util.Log;
import org.omegat.util.OStrings;
import org.omegat.util.Preferences;
import org.omegat.util.StringUtil;
import org.omegat.util.gui.StaticUIUtils;
import org.omegat.util.gui.Styles;
import org.omegat.util.gui.UIThreadsUtil;
import org.omegat.util.Preferences;

import javax.swing.JPopupMenu;
import javax.swing.JMenuItem;
import javax.swing.text.AttributeSet;
import javax.swing.text.StyledDocument;

/**
 * Pane for display machine translations.
 * 
 * @author Alex Buloichik (alex73mail@gmail.com)
 * @author Martin Fleurke
 * @author Jean-Christophe Helary
 * @author Aaron Madlon-Kay
 * @author Thomas Cordonnier
 */
@SuppressWarnings("serial")
public class MachineTranslateTextArea extends EntryInfoThreadPane<MachineTranslationInfo> {

    private static final AttributeSet NO_ATTRIBUTES = Styles.createAttributeSet(null, null, false, null);
    private static final AttributeSet RESULT_OK = Styles.createAttributeSet(Styles.EditorColor.COLOR_EXTTRANS_OK.getColor(), null, false, null);
    private static final AttributeSet RESULT_ERROR = Styles.createAttributeSet(Styles.EditorColor.COLOR_EXTTRANS_FAIL.getColor(), null, false, null);
    private static final AttributeSet DURATION_ATTRIBUTES = Styles.createAttributeSet(Styles.EditorColor.COLOR_EXTTRANS_INFO.getColor(), null, false, null);

    private static final String EXPLANATION = OStrings.getString("GUI_MACHINETRANSLATESWINDOW_explanation");

    public final IMachineTranslation[] translators;

    protected Map<IMachineTranslation,MachineTranslationInfo> displayed = new java.util.TreeMap<>((a, b) -> a.getName().compareTo(b.getName()));
    private SourceTextEntry currentEntry;

    public MachineTranslateTextArea() {
        super(true);

        setEditable(false);
        StaticUIUtils.makeCaretAlwaysVisible(this);

        this.setText(EXPLANATION);
        setDragEnabled(true);
        setMinimumSize(new Dimension(100, 50));

        String title = OStrings.getString("GUI_MATCHWINDOW_SUBWINDOWTITLE_MachineTranslate");
        Core.getMainWindow().addDockable(new DockableScrollPane("MACHINE_TRANSLATE", title, this, true));

        List<IMachineTranslation> tr = new ArrayList<IMachineTranslation>();
        for (Class<?> mtc : PluginUtils.getMachineTranslationClasses()) {
            try {
                tr.add((IMachineTranslation) mtc.newInstance());
            } catch (Exception ex) {
                Log.log(ex);
            }
        }
        translators = tr.toArray(new IMachineTranslation[tr.size()]);
        
		// Add popup menu for insertion
		addMouseListener(new java.awt.event.MouseAdapter() {
			@Override
			public void mouseClicked(java.awt.event.MouseEvent e) {
				if (displayed.isEmpty()) return;
				
				int mousepos = MachineTranslateTextArea.this.viewToModel(e.getPoint());
				MachineTranslationInfo selected = null;
				for (MachineTranslationInfo info: displayed.values())
					if (mousepos > info.position) selected = info;				
				if (selected == null) return;
				if (selected.isException()) return;
				
				// set up the menu
				if (e.isPopupTrigger() || e.getButton() == java.awt.event.MouseEvent.BUTTON3) {
					final MachineTranslationInfo selectedCopy = selected;
					JPopupMenu popup = new JPopupMenu();
					JMenuItem item1 = popup.add(MessageFormat.format(OStrings.getString("MT_INSERT"), selected.origin.getName()));
					item1.addActionListener(ev -> Core.getEditor().insertText(selectedCopy.getDisplayedResult()));
					JMenuItem item2 = popup.add(MessageFormat.format(OStrings.getString("MT_REPLACE"), selected.origin.getName()));
					item2.addActionListener(ev -> {
                        Core.getEditor().replaceEditTextWithColor(selectedCopy.getDisplayedResult(), 
                            Styles.EditorColor.COLOR_MARK_COMES_FROM_MACHINE_TRANSLATION.getColor(), true);
                        Core.getEditor().insertInfo(" " + OStrings.getString("TF_SEG_COMESFROM_MT_PANE") + " (" + selectedCopy.origin.getName() + ")");
                    });
					
					popup.show(MachineTranslateTextArea.this, e.getPoint().x, e.getPoint().y);
				}
			}
			
		});
        
    }

    /** Cycle getDisplayedTranslation **/
    private java.util.Iterator<MachineTranslationInfo> cycle;
    
    public MachineTranslationInfo getDisplayedTranslation() {
         if ((cycle == null) || (! cycle.hasNext())) {
             cycle = displayed.values().iterator();
             
             String pref = Preferences.getPreference(Preferences.MT_PREFERRED);
             if (pref != null) {
                 for (Map.Entry<IMachineTranslation,MachineTranslationInfo> entry: displayed.entrySet()) 
                     if (entry.getKey().getClass().getName().equals(pref)) 
                         if ((entry.getValue() != null) && (! entry.getValue().isException())) return entry.getValue();
             }
         }
         
        // Default (if no preference, or if preferred engine did not answer correctly): use cycle
        try {
             MachineTranslationInfo next = cycle.next();
             if (next.isException())
                 if (cycle.hasNext()) return getDisplayedTranslation(); else return null;
             else
                 return next;
            
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    protected void onProjectClose() {
        UIThreadsUtil.mustBeSwingThread();
        this.setText(EXPLANATION);
    }

    public void forceLoad() {
        startSearchThread(currentlyProcessedEntry, true);
    }
    
    private List<FindThread> threads;
	private boolean doInsert_inEditor = Preferences.isPreference(Preferences.MT_INSERT);
	
    
    @Override
    protected void startSearchThread(SourceTextEntry newEntry) {
        startSearchThread(newEntry, false);
    }

    private void startSearchThread(SourceTextEntry newEntry, boolean force) {
        UIThreadsUtil.mustBeSwingThread();

        clear();
        currentEntry = newEntry;
        if (threads != null) {
            for (FindThread t: threads) t.cancel();
            threads.clear();
        }
        else threads = new ArrayList (translators.length);
        
        for (IMachineTranslation mt : translators) 
            try {
                CachedTranslate cmt = (CachedTranslate) mt;				
                if (cmt.isEnabled()) { FindThread newThread = new FindThreadWithCache(cmt, newEntry, force); newThread.start(); threads.add (newThread); }
            } catch (ClassCastException cce) {
                FindThread newThread = new FindThreadNoCache(mt, newEntry); newThread.start(); threads.add (newThread);
            }
		doInsert_inEditor = Preferences.isPreference(Preferences.MT_INSERT);
    }

    @Override
    protected void setFoundResult(final SourceTextEntry se, final MachineTranslationInfo data) {
        UIThreadsUtil.mustBeSwingThread();
        
        if (se != currentEntry) { // Change entry
            setText ("");
            displayed.clear();
            currentEntry = se;
        }

        if (data != null) {
            if (data.result != null)
                displayed.put (data.origin, data);
            else if (data.origin != null)
                displayed.remove (data.origin);
            StyledDocument doc = getStyledDocument(); this.setText("");
            for (Map.Entry<IMachineTranslation,MachineTranslationInfo> entry: displayed.entrySet()) 
                try {
                    MachineTranslationInfo info = entry.getValue();
                    info.position = doc.getLength();
                    if (displayed.size() > 1) {
                        doc.insertString (info.position, info.getDisplayedResult() + "\n",
                            info.isException() ? RESULT_ERROR : RESULT_OK);
                        doc.insertString (doc.getLength(), "<" + entry.getKey().getName() + ">", NO_ATTRIBUTES);
                        if (Preferences.isPreferenceDefault(Preferences.MT_SHOW_DURATION, false))
                            doc.insertString (doc.getLength(), 
                                " " + StringUtil.format(OStrings.getString("TF_OPTIONSMENU_MACHINETRANSLATE_SHOW_DURATION_SECONDS"), 
                                     java.text.NumberFormat.getInstance().format( ((double) info.duration) / 1000.0 )
                                ), 
                                DURATION_ATTRIBUTES);
						doc.insertString (doc.getLength(), "\n\n", NO_ATTRIBUTES);
                    } else {
                        if (info.origin instanceof org.omegat.core.machinetranslators.LocalTranslate) // no color because no exception possible
                            doc.insertString (info.position, info.getDisplayedResult(), NO_ATTRIBUTES);
                        else
                            doc.insertString (info.position, info.getDisplayedResult(), info.isException() ? RESULT_ERROR : RESULT_OK);						
                    }
                } catch (javax.swing.text.BadLocationException bde) {
                    bde.printStackTrace();
                    // Insert the text with no color
                    StringBuffer buf = new StringBuffer (this.getText());
                    buf.append (entry.getValue().getDisplayedResult()).append("\n");
                    if (displayed.size() > 1)
                        buf.append("<").append(entry.getKey().getName()).append(">\n\n");
                }
            if (Preferences.isPreference(Preferences.MT_INSERT)) {
                TMXEntry te = Core.getProject().getTranslationInfo(currentEntry);
                if (!te.isTranslated()) {
                    Thread.yield();
                    if (! Core.getMatcher().isInsertMatch()) {
						if (data.isException()) return;
						if ((data.origin != null) && (data.origin.getClass().getName().equals(Preferences.getPreference(Preferences.MT_PREFERRED)))) {
							Core.getEditor().replaceEditTextWithColor(data.result.toString(), Styles.EditorColor.COLOR_MARK_COMES_FROM_MACHINE_TRANSLATION.getColor(), false);
							Core.getEditor().insertInfo(" " + OStrings.getString("TF_SEG_COMESFROM_MT_PANE") + " (" + data.origin.getName() + ")");
							doInsert_inEditor = false;
						} else if (doInsert_inEditor) {
							Core.getEditor().replaceEditTextWithColor(data.result.toString(), Styles.EditorColor.COLOR_MARK_COMES_FROM_MACHINE_TRANSLATION.getColor(), false);
							Core.getEditor().insertInfo(" " + OStrings.getString("TF_SEG_COMESFROM_MT_PANE") + " (" + data.origin.getName() + ")");
							doInsert_inEditor = false;							
						}
                    }
                }
            }
        }
    }
    
    @Override
    public void clear() {
        super.clear();
        displayed.clear();
    }

    protected abstract class FindThread extends EntryInfoSearchThread<MachineTranslationInfo> {
        protected final String srcText;
        protected boolean cancelled = false;
        protected long start = System.currentTimeMillis();

        protected FindThread(final SourceTextEntry newEntry) {
            super(MachineTranslateTextArea.this, newEntry);
            srcText = newEntry.getSrcText();
        }
        
        public void cancel() {
            this.cancelled = true;
        }

        @Override
        protected MachineTranslationInfo search() throws Exception {
            ProjectProperties pp = Core.getProject().getProjectProperties();
            if (pp == null) return null;
            			
            Language sourceLang = pp.getSourceLanguage(); if (sourceLang == null) return null;
            Language targetLang = pp.getTargetLanguage(); if (targetLang == null) return null;

            return getTranslationInfo(sourceLang, targetLang);
        }
        
        protected abstract MachineTranslationInfo getTranslationInfo(Language sourceLang, Language targetLang);
    }
    
    protected final class FindThreadWithCache extends FindThread {
        private final CachedTranslate translator;
        private final boolean force;
    
        public FindThreadWithCache(final CachedTranslate translator, final SourceTextEntry newEntry, boolean force) {
            super(newEntry);
            this.translator = translator;
            this.force = force;
        }
    
        @Override
        protected MachineTranslationInfo getTranslationInfo(Language sourceLang, Language targetLang) {
            try {
                String result = getTranslation(sourceLang, targetLang);
                return result == null ? null : new MachineTranslationInfo(translator, this.start, result);			
            } catch (Exception e) {
                return new MachineTranslationInfo(translator, this.start, e);		
            }
        }	
    
        private String getTranslation(Language sourceLang, Language targetLang) throws Exception {
            if (cancelled) return null; // We are already in another segment
            if (!force) {
                if (!Preferences.isPreferenceDefault(Preferences.MT_AUTO_FETCH, true)) {
                    return translator.getCachedTranslation(sourceLang, targetLang, srcText);
                }
                if (Preferences.isPreference(Preferences.MT_ONLY_UNTRANSLATED)) {
                    TMXEntry entry = Core.getProject().getTranslationInfo(currentlyProcessedEntry);
                    if (entry.isTranslated()) {
                        return translator.getCachedTranslation(sourceLang, targetLang, srcText);
                    }
                }
            }
            return translator.getTranslation(sourceLang, targetLang, srcText);
        }
    }

    protected final class FindThreadNoCache extends FindThread {
        private final IMachineTranslation translator;

        public FindThreadNoCache(final IMachineTranslation translator, final SourceTextEntry newEntry) {
            super(newEntry);
            this.translator = translator;
        }
        
        protected MachineTranslationInfo getTranslationInfo(Language sourceLang, Language targetLang) {
            if (cancelled) return null; // We are already in another segment
            try {
                String result = translator.getTranslation(sourceLang, targetLang, srcText);			
                return result == null ? null : new MachineTranslationInfo(translator, this.start, result);			
            } catch (Exception e) {
                return new MachineTranslationInfo(translator, this.start, e);		
            }
        }
    }
}
