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

 Copyright (C) 2010-2013 Alex Buloichik
               2014 Aaron Madlon-Kay
               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.gui.editor;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.text.BadLocationException;
import javax.swing.text.Highlighter;
import javax.swing.text.Position;

import org.omegat.core.Core;
import org.omegat.filters2.master.PluginUtils;
import org.omegat.gui.editor.mark.CalcMarkersThread;
import org.omegat.gui.editor.mark.EntryMarks;
import org.omegat.gui.editor.mark.IFastMarker;
import org.omegat.gui.editor.mark.IMarker;
import org.omegat.gui.editor.mark.Mark;
import org.omegat.util.Log;
import org.omegat.util.gui.UIThreadsUtil;

/**
 * Class for manage marks and controll all markers.
 * 
 * All markers for inactive segment usually executed in background threads(one
 * thread for one marker class), but markers for active segment executed in UI
 * thread immediately.
 * 
 * @author Alex Buloichik (alex73mail@gmail.com)
 * @author Thomas Cordonnier
 */
public class MarkerController {
    private final EditorController ec;

    /** List of marker's class names. */
    private final String[] markerNames;

    /** Threads for each marker. */
    protected final CalcMarkersThread[] markerThreads;

    private final Highlighter highlighter;

    MarkerController(EditorController ec) {
        this.ec = ec;
        this.highlighter = ec.editor.getHighlighter();

        List<IMarker> ms = new ArrayList<IMarker>();
        // start all markers threads
        for (Class<?> mc : PluginUtils.getMarkerClasses()) {
            try {
                ms.add((IMarker) mc.newInstance());
            } catch (Exception ex) {
                Log.logErrorRB(ex, "PLUGIN_MARKER_INITIALIZE", mc.getName());
            }
        }
        for (IMarker marker : Core.getMarkers()) {
            ms.add(marker);
        }

        markerThreads = new CalcMarkersThread[ms.size()];
        markerNames = new String[ms.size()];
        for (int i = 0; i < ms.size(); i++) {
            IMarker m = ms.get(i);
            markerNames[i] = m.getClass().getName();
            markerThreads[i] = new CalcMarkersThread(this, m, i);
            markerThreads[i].start();
        }
    }

    /**
     * Get marker's index by class name.
     * 
     * @param markerClassName
     *            marker's class name
     * @return marker's index
     */
    int getMarkerIndex(final String markerClassName) {
        for (int i = 0; i < markerNames.length; i++) {
            if (markerNames[i].equals(markerClassName)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Remove all marks for all entries.
     * 
     * @param newEntriesCount
     *            count of newly displayed entries
     */
    void removeAll() {
        UIThreadsUtil.mustBeSwingThread();

        for (CalcMarkersThread th : markerThreads) {
            th.reset();
        }

        synchronized (outputQueue) {
            outputQueue.clear();
        }

        highlighter.removeAllHighlights();
    }

    /**
     * Remove marks for one segment for one marker.
     */
    void remove(SegmentBuilder sb, int makerIndex) {
        UIThreadsUtil.mustBeSwingThread();

        if (sb.marks == null) {
            return;
        }

        List<MarkInfo> me = sb.marks[makerIndex];
        if (me != null) {
            for (MarkInfo info: me) {
                if (info != null && info.highlight != null) {
                    highlighter.removeHighlight(info.highlight);
                }
            }
            sb.marks[makerIndex] = null;
        }
    }

    /**
     * Reprocess all entries for one marker only. Usually used for spell
     * checking or one marker state changes.
     */
    public void reprocess(SegmentBuilder[] entryBuilders, int markerIndex) {
        UIThreadsUtil.mustBeSwingThread();

        if (entryBuilders == null) {
            return;
        }
        
        for (SegmentBuilder sb : entryBuilders) {
            if (!sb.hasBeenCreated()) {
                continue;
            }
            remove(sb, markerIndex);
        }
        markerThreads[markerIndex].add(entryBuilders);
    }

    /**
     * Reprocess one entry immediately, in current thread. Usually used for
     * active entry.
     */
    public void reprocessImmediately(SegmentBuilder entryBuilder, DocumentEvent docEv) {
        UIThreadsUtil.mustBeSwingThread();

        entryBuilder.resetTextAttributes(docEv);
        Document3 doc = ec.editor.getOmDocument();

        List<EntryMarks> evs = new ArrayList<EntryMarks>();
        for (int i = 0; i < markerNames.length; i++) 
            try {
                if (docEv == null) throw new ClassCastException();
                IFastMarker fMark = (IFastMarker) markerThreads[i].marker;
                try {
                    if (entryBuilder.marks[i] == null) entryBuilder.marks[i] = new ArrayList<MarkInfo>();
                    for (MarkInfo rem: fMark.toRemove(docEv, entryBuilder.marks[i])) {
                        entryBuilder.marks[i].remove(rem); 
                        if (rem.highlight != null) highlighter.removeHighlight(rem.highlight);
                    }
                    for (Mark added: fMark.toAdd(docEv)) 
                        // we do not use marksOutput because it may remove all highlights before
                        entryBuilder.marks[i].add (new MarkInfo(added, entryBuilder, doc, highlighter));
                } catch (Throwable ex) {
                    Log.log(ex);
                }
                // entryBuilder.marks[i] = me;
            } catch (ClassCastException cce) {
                remove(entryBuilder, i);
                try {
                    EntryMarks ev = new EntryMarks(entryBuilder, entryBuilder.getDisplayVersion(), i);
                    ev.result = markerThreads[i].marker.getMarksForEntry(ev.ste, ev.sourceText, ev.translationText,
                            ev.isActive);
                    if (ev.result != null) {
                        evs.add(ev);
                    }
                } catch (Throwable ex) {
                    Log.log(ex);
                }
            }
        marksOutput(evs);
    }
    
    /**
     * Reprocess one entry immediately, in current thread. Usually used for
     * active entry.
     */
    public void reprocessImmediately(SegmentBuilder entryBuilder, String newTra) {
        UIThreadsUtil.mustBeSwingThread();

        //entryBuilder.resetTextAttributes(docEv);
        Document3 doc = ec.editor.getOmDocument();

        List<EntryMarks> evs = new ArrayList<EntryMarks>();
        for (int i = 0; i < markerNames.length; i++) 
            try {
                if (newTra == null) throw new ClassCastException();
                IFastMarker fMark = (IFastMarker) markerThreads[i].marker;
                try {
                    if (entryBuilder.marks[i] == null) entryBuilder.marks[i] = new ArrayList<MarkInfo>();
                    for (MarkInfo rem: fMark.toRemove(newTra, entryBuilder.marks[i])) {
                        entryBuilder.marks[i].remove(rem); 
                        if (rem.highlight != null) highlighter.removeHighlight(rem.highlight);
                    }
                    for (Mark added: fMark.toAdd(newTra)) 
                        // we do not use marksOutput because it may remove all highlights before
                        entryBuilder.marks[i].add (new MarkInfo(added, entryBuilder, doc, highlighter));
                } catch (Throwable ex) {
                    Log.log(ex);
                }
                // entryBuilder.marks[i] = me;
            } catch (ClassCastException cce) {
                remove(entryBuilder, i);
                try {
                    EntryMarks ev = new EntryMarks(entryBuilder, entryBuilder.getDisplayVersion(), i);
                    ev.result = markerThreads[i].marker.getMarksForEntry(ev.ste, ev.sourceText, ev.translationText,
                            ev.isActive);
                    if (ev.result != null) {
                        evs.add(ev);
                    }
                } catch (Throwable ex) {
                    Log.log(ex);
                }
            }
        marksOutput(evs);
    }
    

    /**
     * Process all segment for all markers.
     */
    public void process(SegmentBuilder[] entryBuilders) {
        UIThreadsUtil.mustBeSwingThread();

        for (CalcMarkersThread th : markerThreads) {
            th.add(entryBuilders);
        }
    }

    /**
     * Return tooltips texts for specified editor position.
     * 
     * @param entryIndex
     * @param pos
     * @return
     */
    public String getToolTips(int entryIndex, int pos) {
        UIThreadsUtil.mustBeSwingThread();

        if (entryIndex >= ec.m_docSegList.length || entryIndex < 0) {
            return null;
        }
        List<MarkInfo>[] m = ec.m_docSegList[entryIndex].marks;
        if (m == null) {
            return null;
        }
        StringBuilder res = new StringBuilder();
        for (int i = 0; i < m.length; i++) {
            if (m[i] == null) {
                continue;
            }
            for (MarkInfo t : m[i]) {
                if (t != null && t.tooltip != null) {
                    if (t.tooltip.p0.getOffset() <= pos && t.tooltip.p1.getOffset() >= pos) {
                        if (res.length() > 0) {
                            res.append("<br>");
                        }
                        res.append(t.tooltip.text);
                    }
                }
            }
        }
        if (res.length() == 0) {
            return null;
        }
        String r = res.toString();
        r = r.replace("<suggestion>", "<b>");
        r = r.replace("</suggestion>", "</b>");
        return "<html>" + r + "</html>";
    }

    private final Queue<EntryMarks> outputQueue = new LinkedList<EntryMarks>();

    public void queueMarksOutput(EntryMarks ev) {
        synchronized (outputQueue) {
            // output marks
            outputQueue.add(ev);
            outputQueue.notifyAll();
        }
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                List<EntryMarks> evs = new ArrayList<EntryMarks>();
                synchronized (outputQueue) {
                    while (true) {
                        EntryMarks ev = outputQueue.poll();
                        if (ev == null) {
                            break;
                        }
                        evs.add(ev);
                    }
                }
                marksOutput(evs);
            }
        });
    }

    /**
     * Output marks.
     */
    private void marksOutput(List<EntryMarks> evs) {
        UIThreadsUtil.mustBeSwingThread();

        if (evs.isEmpty()) {
            return;
        }
        Document3 doc = ec.editor.getOmDocument();
        doc.trustedChangesInProgress = true;
        try {
            for (int i = 0; i < evs.size(); i++) {
                EntryMarks ev = evs.get(i);
                if (!ev.isSegmentChanged()) {
                    remove(ev.builder, ev.markerIndex);
                    try {
                        if (ev.builder.marks == null) {
                            ev.builder.marks = new List [markerNames.length];
                        }
                        ev.builder.marks[ev.markerIndex] = new ArrayList<MarkInfo>(ev.result.size());
                        for (int j = 0; j < ev.result.size(); j++) {
                            MarkInfo nm = new MarkInfo(ev.result.get(j), ev.builder, doc, highlighter);
                            ev.builder.marks[ev.markerIndex].add(nm);
                        }
                    } catch (BadLocationException ex) {
                    }
                }
            }
        } finally {
            doc.trustedChangesInProgress = false;
        }
    }

    /**
     * Class for store info about displayed mark.
     */
    public static class MarkInfo {
        public final Highlighter.Highlight highlight;
        Tooltip tooltip;

        public MarkInfo(Mark m, SegmentBuilder sb, Document3 doc, Highlighter highlighter) throws BadLocationException {
            if (m.entryPart == Mark.ENTRY_PART.SOURCE && sb.getSourceText() == null) {
                highlight = null; return;
            }

            int sourceStartOffset = sb.getStartSourcePosition();
            int translationStartOffset;
            if (sb.isActive()) {
                translationStartOffset = doc.getTranslationStart();
            } else {
                translationStartOffset = sb.getStartTranslationPosition();
            }
            int startOffset;
            if (m.entryPart == Mark.ENTRY_PART.SOURCE) {
                startOffset = sourceStartOffset;
            } else {
                startOffset = translationStartOffset;
            }
            if (m.painter != null)
                highlight = (Highlighter.Highlight) highlighter.addHighlight(startOffset + m.startOffset, startOffset
                        + m.endOffset, m.painter);
            else
                highlight = null;
            if (m.toolTipText != null) {
                tooltip = new Tooltip(doc, startOffset + m.startOffset, startOffset + m.endOffset, m.toolTipText);
            }
            if (m.attributes != null) {
                doc.setCharacterAttributes(startOffset + m.startOffset, m.endOffset - m.startOffset, m.attributes,
                        false);
            }
        }
    }

    protected static class Tooltip {
        Position p0, p1;
        String text;

        public Tooltip(Document3 doc, int start, int end, String text) throws BadLocationException {
            p0 = doc.createPosition(start);
            p1 = doc.createPosition(end);
            this.text = text;
        }
    }
}
