/**************************************************************************
 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
               2007 Didier Briel
               2009-2010 Wildrich Fourie
               2010 Alex Buloichik
               2012 Jean-Christophe Helary, Thomas Cordonnier
               2013 Aaron Madlon-Kay, Alex Buloichik, Thomas Cordonnier
               2015 Yu Tang, Aaron Madlon-Kay, 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.glossary;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.io.File;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

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

import org.omegat.core.Core;
import org.omegat.core.data.ProjectProperties;
import org.omegat.core.data.SourceTextEntry;
import org.omegat.core.data.StringEntry;
import org.omegat.core.glossaries.IWritableGlossary;
import org.omegat.core.search.TextExpression;
import org.omegat.gui.common.EntryInfoThreadPane;
import org.omegat.gui.dialogs.CreateGlossaryEntry;
import org.omegat.gui.editor.EditorUtils;
import org.omegat.gui.main.DockableScrollPane;
import org.omegat.gui.main.MainWindow;
import org.omegat.gui.search.SearchModeBox;
import org.omegat.tokenizer.ITokenizer;
import org.omegat.util.Log;
import org.omegat.util.OConsts;
import org.omegat.util.OStrings;
import org.omegat.util.Preferences;
import org.omegat.util.StringUtil;
import org.omegat.util.gui.DragTargetOverlay;
import org.omegat.util.gui.DragTargetOverlay.FileDropInfo;
import org.omegat.util.gui.JTextPaneLinkifier;
import org.omegat.util.gui.StaticUIUtils;
import org.omegat.util.gui.Styles;
import org.omegat.util.gui.UIThreadsUtil;

/**
 * This is a Glossary pane that displays glossary entries.
 * 
 * @author Keith Godfrey
 * @author Maxym Mykhalchuk
 * @author Didier Briel
 * @author Wildrich Fourie
 * @author Alex Buloichik (alex73mail@gmail.com)
 * @author Jean-Christophe Helary
 * @author Aaron Madlon-Kay
 * @author Thomas Cordonnier
 */
@SuppressWarnings("serial")
public class GlossaryTextArea extends EntryInfoThreadPane<List<GlossaryEntry>> {

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

    public static final AttributeSet NO_ATTRIBUTES = Styles.createAttributeSet(null, null, false, null);
    public static final AttributeSet PRIORITY_ATTRIBUTES = Styles.createAttributeSet(null, null, true, null);

    public static final AttributeSet SOURCE_ATTRIBUTES = Styles.createAttributeSet(Styles.EditorColor.COLOR_GLOSSARY_SOURCE.getColor(), null, null, null);
    public static final AttributeSet TARGET_ATTRIBUTES = Styles.createAttributeSet(Styles.EditorColor.COLOR_GLOSSARY_TARGET.getColor(), null, null, null);
    public static final AttributeSet NOTES_ATTRIBUTES = Styles.createAttributeSet(Styles.EditorColor.COLOR_GLOSSARY_NOTE.getColor(), null, null, null);
    
    public static final String DEFAULT_TEMPLATE = "result.appendSource(entry.srcText); result.append(\": \");\n"
		+ "def map = entry.translationWithComments\n"
		+ "def count = map.size()\n"
		+ "map.each { tra,commentsList -> \n"
		+ "	result.appendTarget(tra, entry.hasPriorities(tra));\n"
		+ "	for (com in commentsList) if ((com != null) && (com.length() > 0)) {\n"
		+ "		result.append(\"\\n -> \"); result.appendComment(com); \n"
		+ "	}\n"
		+ "	if (count-- > 1) result.append(\",\");\n"
		+ "}\n"
		+ "result.append(\"\\n\");\n";
    
    /**
     * Currently processed entry. Used to detect if user moved into new entry. In this case, new find should
     * be started.
     */
    protected StringEntry processedEntry;

    /**
     * Holds the current GlossaryEntries for the TransTips
     */
    protected static List<GlossaryEntry> nowEntries;

    /**
     * popupmenu
     */
    protected JPopupMenu popup;

    private CreateGlossaryEntry createGlossaryEntryDialog;

    /** Creates new form MatchGlossaryPane */
    public GlossaryTextArea(final MainWindow mw) {
        super(true);
		javax.swing.ToolTipManager.sharedInstance().registerComponent(this);

        String title = OStrings.getString("GUI_MATCHWINDOW_SUBWINDOWTITLE_Glossary");
        final DockableScrollPane scrollPane = new DockableScrollPane("GLOSSARY", title, this, true);
        Core.getMainWindow().addDockable(scrollPane);

        setEditable(false);
        StaticUIUtils.makeCaretAlwaysVisible(this);
        setText(EXPLANATION);
        setDragEnabled(true);
        setMinimumSize(new Dimension(100, 50));

        //prepare popup menu
        popup = new JPopupMenu();
        JMenuItem menuItem = new JMenuItem(OStrings.getString("GUI_GLOSSARYWINDOW_addentry"));
        menuItem.addActionListener(e -> Core.getGlossary().showCreateGlossaryEntryDialog());
        popup.add(menuItem);
		menuItem = new JMenuItem(OStrings.getString("GUI_GLOSSARYWINDOW_restrict_caseSensitive"));
		menuItem.addActionListener(e -> {
			String entry = Core.getEditor().getCurrentEntry().getSrcText();
			if (Preferences.isPreferenceDefault(Preferences.GLOSSARY_REMOVE_TAGS, false)) 
				entry = entry.replaceAll("</?\\w+/?>", "");
			// Browse nowEntries but do a case-sensitive search inside
			for (java.util.Iterator<GlossaryEntry> I = nowEntries.iterator(); I.hasNext(); )
				if (! entry.contains (I.next().getSrcText())) I.remove();
			// refresh
			setFoundResult(Core.getEditor().getCurrentEntry(), nowEntries); // modified list
			Core.getEditor().remarkOneMarker("org.omegat.gui.glossary.TransTipsMarker");
		});
		popup.add(menuItem);			
		menuItem = new JMenuItem(OStrings.getString("GUI_GLOSSARYWINDOW_restrict_wholeWords"));
		menuItem.addActionListener(e -> {
			String entry = Core.getEditor().getCurrentEntry().getSrcText();
			if (Preferences.isPreferenceDefault(Preferences.GLOSSARY_REMOVE_TAGS, false)) 
				entry = entry.replaceAll("</?\\w+/?>", "");
			// Browse nowEntries but do a whole words search inside
			int flags = Pattern.UNICODE_CHARACTER_CLASS; 
			if (! Preferences.isPreferenceDefault(Preferences.TRANSTIPS_CASE_SENSITIVE, false)) 
				flags += Pattern.CASE_INSENSITIVE + Pattern.UNICODE_CASE;
			for (java.util.Iterator<GlossaryEntry> I = nowEntries.iterator(); I.hasNext(); )
				if (! Pattern.compile("\\b" + I.next().getSrcText() + "\\b", flags).matcher(entry).find())
					I.remove();
			// refresh
			setFoundResult(Core.getEditor().getCurrentEntry(), nowEntries); // modified list
			Core.getEditor().remarkOneMarker("org.omegat.gui.glossary.TransTipsMarker");
		});
		popup.add(menuItem);			
		menuItem = new JMenuItem(OStrings.getString("GUI_GLOSSARYWINDOW_restrict_subords"));
		menuItem.addActionListener(e -> {
			SearchModeBox dummyBox = new SearchModeBox (0, ITokenizer.StemmingMode.GLOSSARY, null);
			dummyBox.loadPreferences (Preferences.TRANSTIPS + "_");
			java.util.Set<GlossaryEntry> toRemove = new java.util.HashSet<>();
			for (int i = 0; i < nowEntries.size(); i++) {
				TextExpression expr = dummyBox.buildExpression(nowEntries.get(i).getSrcText(), false, true);				
			SUBLOOP:
				for (int j = 0; j < nowEntries.size(); j++) {
					if (i == j) continue SUBLOOP; 
					// remove entry[i] if it is a subset of entry[j]
					if (expr.searchString (nowEntries.get(j).getSrcText()) != null) { toRemove.add(nowEntries.get(i)); break SUBLOOP; }
				}
			}
			nowEntries.removeAll(toRemove);
			// refresh
			setFoundResult(Core.getEditor().getCurrentEntry(), nowEntries); // modified list
			// no need to refresh transtips, they are not affected by this 
		});
		popup.add(menuItem);
        addMouseListener(mouseListener);

        Core.getEditor().registerPopupMenuConstructors(300, new TransTipsPopup());
        
        if (!GraphicsEnvironment.isHeadless()) {
            DragTargetOverlay.apply(this, new FileDropInfo(mw, false) {
                @Override
                public boolean canAcceptDrop() {
                    return Core.getProject().isProjectLoaded();
                }
                @Override
                public String getOverlayMessage() {
                    return OStrings.getString("DND_ADD_GLOSSARY_FILE");
                }
                @Override
                public String getImportDestination() {
                    return Core.getProject().getProjectProperties().getGlossaryRoot();
                }
                @Override
                public boolean acceptFile(File pathname) {
                    String name = pathname.getName().toLowerCase();
                    return name.endsWith(OConsts.EXT_CSV_UTF8) || name.endsWith(OConsts.EXT_TBX)
                            || name.endsWith(OConsts.EXT_TSV_DEF) || name.endsWith(OConsts.EXT_TSV_TXT)
                            || name.endsWith(OConsts.EXT_TSV_UTF8);
                }
                @Override
                public Component getComponentToOverlay() {
                    return scrollPane;
                }
            });
        }
        
        JTextPaneLinkifier.linkify(this);
    }

    @Override
    protected void onProjectOpen() {
        clear();
        Core.getGlossaryManager().start();
    }

    @Override
    protected void onProjectClose() {
        clear();
        setText(EXPLANATION);
        Core.getGlossaryManager().stop();
    }

    @Override
    protected void startSearchThread(SourceTextEntry newEntry) {
		setText(OStrings.getString("SW_SEARCHING")); getStyledDocument().setCharacterAttributes(0, OStrings.getString("SW_SEARCHING").length(), NO_ATTRIBUTES, true);
        new FindGlossaryThread(GlossaryTextArea.this, newEntry, Core.getGlossaryManager()).start();
    }

    /**
     * Refresh content on glossary file changed.
     */
    public void refresh() {
        SourceTextEntry ste = Core.getEditor().getCurrentEntry();
        if (ste != null) {
            startSearchThread(ste);
        }
    }

    /**
     * Sets the list of glossary entries to show in the pane. Each element of the list should be an instance
     * of {@link GlossaryEntry}.
     */
    @Override
    protected void setFoundResult(SourceTextEntry en, List<GlossaryEntry> entries) {
        UIThreadsUtil.mustBeSwingThread();

        clear();

        if (entries == null) {
            return;
        }

        nowEntries = entries;

        // If the TransTips is enabled then underline all the matched glossary
        // entries
        if (Preferences.isPreference(Preferences.TRANSTIPS_SRC) || Preferences.isPreference(Preferences.TRANSTIPS_TRA)) {
            Core.getEditor().remarkOneMarker(TransTipsMarker.class.getName());
        }

        GlossaryEntry.StyledString buf = new GlossaryEntry.StyledString();
        java.util.Collections.sort(entries);
        for (GlossaryEntry entry : entries) {
            GlossaryEntry.StyledString str = entry.toStyledString();
            buf.append(str);
        }
        setText(buf.text.toString());
        StyledDocument doc = getStyledDocument();
        doc.setCharacterAttributes(0, doc.getLength(), NO_ATTRIBUTES, true); // remove old bold settings first
        for (GlossaryEntry.StyledString.AttributesPart part: buf.attrParts) part.apply (doc);
        for (GlossaryEntry.StyledString.AttributesPart part: buf.attrBolds) part.apply (doc);
		this.tooltips = buf.tooltips;
    }

    /** Clears up the pane. */
    @Override
    public void clear() {
        super.clear();
        nowEntries = Collections.emptyList();
    }

    List<GlossaryEntry> getDisplayedEntries() {
        return nowEntries;
    }
	
	private List<GlossaryEntry.StyledString.TooltipPart> tooltips = null;
	
    @Override
    public String getToolTipText(MouseEvent event) {
		if (tooltips == null) return null;
        int pos = viewToModel(event.getPoint());
        for (GlossaryEntry.StyledString.TooltipPart part: tooltips)
            if (part.contains(pos)) return part.getText();
        return null;
	}		

    /**
     * MouseListener for the GlossaryTextArea.
     */
    protected MouseListener mouseListener = new PopupListener(this);

    /**
     * MoueAdapter that knows the GlossaryTextArea. If there is text selected in the Glossary it will be inserted in
     * the Editor upon a right-click. Else a popup is shown to allow to add an entry.
     */
    class PopupListener extends MouseAdapter {

        private GlossaryTextArea glossaryTextArea;

        public PopupListener(GlossaryTextArea gte) {
            super();
            glossaryTextArea = gte;
        }

        @Override
        public void mouseClicked(MouseEvent e) {
            if (e.isPopupTrigger() || e.getButton() == MouseEvent.BUTTON3) {
                String selTxt = glossaryTextArea.getSelectedText();
                if (selTxt == null) {
                    if (Core.getProject().isProjectLoaded()) {
                        popup.show(glossaryTextArea, e.getX(), e.getY());
                    }
                } else {
                    insertTerm(selTxt);
                }
            }
        }
    }

    /**
     * Inserts the given text into the EditorTextArea
     *
     * @param selTxt the text to insert
     */
    private void insertTerm(String selTxt) {
        Core.getEditor().insertText(selTxt);
    }

    public void showCreateGlossaryEntryDialog() {
        Frame parent = Core.getMainWindow().getApplicationFrame();
        showCreateGlossaryEntryDialog(parent);
    }

    public void showCreateGlossaryEntryDialog(final Frame parent) {
        CreateGlossaryEntry d = createGlossaryEntryDialog;
        if (d != null) {
            d.requestFocus();
            return;
        }

        ProjectProperties props = Core.getProject().getProjectProperties();
        final File out = new File(props.getWriteableGlossary());

        final CreateGlossaryEntry dialog = new CreateGlossaryEntry(parent);
        String txt = dialog.getGlossaryFileText().getText();
        txt = MessageFormat.format(txt, out.getAbsolutePath());
        dialog.getGlossaryFileText().setText(txt);
        dialog.setVisible(true);
        dialog.getSourceText().requestFocus();

        dialog.addWindowFocusListener(new WindowFocusListener() {
            @Override
            public void windowLostFocus(WindowEvent e) {
            }

            @Override
            public void windowGainedFocus(WindowEvent e) {
                String sel = null;
                Component component = parent.getMostRecentFocusOwner();
                if (component instanceof JTextComponent) {
                    sel = ((JTextComponent) component).getSelectedText();
                    if (!StringUtil.isEmpty(sel)) {
                        sel = EditorUtils.removeDirectionChars(sel);
                    }
                }
                if (!StringUtil.isEmpty(sel)) {
                    if (StringUtil.isEmpty(dialog.getSourceText().getText())) {
                        setText(dialog.getSourceText(), sel);
                    } else if (StringUtil.isEmpty(dialog.getTargetText().getText())) {
                        setText(dialog.getTargetText(), sel);
                    } else if (StringUtil.isEmpty(dialog.getCommentText().getText())) {
                        setText(dialog.getCommentText(), sel);
                    }
                }
            }
            
            private void setText(JTextComponent comp, String text) {
                comp.setText(text);
                comp.requestFocus();
                comp.selectAll();
            }
        });

        dialog.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosed(WindowEvent e) {
                createGlossaryEntryDialog = null;
                if (dialog.getReturnStatus() == CreateGlossaryEntry.RET_OK) {
                    String src = StringUtil.normalizeUnicode(dialog.getSourceText().getText());
                    String loc = StringUtil.normalizeUnicode(dialog.getTargetText().getText());
                    String com = StringUtil.normalizeUnicode(dialog.getCommentText().getText());
                    if (!StringUtil.isEmpty(src) && !StringUtil.isEmpty(loc)) {
                        try {
                            IWritableGlossary external = (IWritableGlossary) Core.getGlossaryManager().externalGlossaries.get (out.getPath());
                            if (external != null) {
                                external.addEntry(
                                    Core.getProject().getProjectProperties().getSourceLanguage(),
                                    Core.getProject().getProjectProperties().getTargetLanguage(),
                                    new GlossaryEntry(src, loc, com, true)
                                );
                                return;
                            }
                        } catch (Exception ex) {
                            Log.log(ex);
                        }
                        try {
                            GlossaryReaderTSV.append(out, new GlossaryEntry(src, loc, com, true));
                        } catch (Exception ex) {
                            Log.log(ex);
                        }
                    }
                }
            }
        });
        createGlossaryEntryDialog = dialog;
    }
}
