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

 Copyright (C) 2009 Alex Buloichik, Martin Fleurke
               2010 Alex Buloichik, Didier Briel
               2011 Thomas Cordonnier
               2012 Martin Fleurke, Hans-Peter Jacobs, Thomas Cordonnier
               2015 Aaron Madlon-Kay, Thomas Cordonnier
               2016-2022 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.text.DateFormat;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import javax.swing.event.DocumentEvent;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.Position;
import javax.swing.text.StyleConstants;

import org.omegat.core.Core;
import org.omegat.core.data.ProjectTMX;
import org.omegat.core.data.SourceTextEntry;
import org.omegat.core.data.TMXEntry;
import org.omegat.gui.editor.MarkerController.MarkInfo;
import org.omegat.util.Language;
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.TagUtil;
import org.omegat.util.TagUtil.Tag;
import org.omegat.util.gui.StaticUIUtils;
import org.omegat.util.gui.UIThreadsUtil;
import org.omegat.util.gui.Styles;

/**
 * Class for store information about displayed segment, and for show segment in editor.
 * 
 * RTL and Bidirectional support: see good description at
 * http://www.iamcal.com/understanding-bidirectional-text/. Java support of RTL/bidi depends on supported
 * Unicode version. You can usually find supported Unicode version in the Character class comments.
 *
 * @author Alex Buloichik (alex73mail@gmail.com)
 * @author Didier Briel
 * @author Martin Fleurke
 * @author Hans-Peter Jacobs
 * @author Aaron Madlon-Kay
 * @author Thomas Cordonnier
 */
public class SegmentBuilder {
    /** Attributes for show text. */
    public static final String SEGMENT_MARK_ATTRIBUTE = "SEGMENT_MARK_ATTRIBUTE";
    public static final String SEGMENT_SPELL_CHECK = "SEGMENT_SPELL_CHECK";
    private static final DecimalFormat NUMBER_FORMAT = new DecimalFormat("0000");

    private static final String BIDI_LRE = "\u202a";
    private static final String BIDI_RLE = "\u202b";
    private static final String BIDI_PDF = "\u202c";
    public static final String BIDI_LRM = "\u200e";
    public static final String BIDI_RLM = "\u200f";
    public static final char BIDI_LRM_CHAR = '\u200e';
    public static final char BIDI_RLM_CHAR = '\u200f';

    static AtomicLong globalVersions = new AtomicLong();

    final SourceTextEntry ste;
    final int segmentNumberInProject;

    /**
     * Version of displayed variant of segment. Required for check in delayed
     * thread, like spell checking. Version changed(in Swing thread only) each
     * time when entry drawn, and when user edits it (for active entry).
     */
    private volatile long displayVersion;
    /** Source text of entry with internal bidi chars, or null if not displayed. */
    private String sourceText;
    /** Translation text of entry with internal bidi chars, or null if not displayed. */
    private String translationText;
    /** True if entry is active. */
    private boolean active;
    /** True if translation exist for entry. */
    private boolean transExist;
    /** True if translation changed by another user **/
    private boolean transDiffer;
    /** True if entry has a note attached. */
    private boolean noteExist;
    /** True if translation is default, false - is multiple. */
    private boolean defaultTranslation;

    private final Document3 doc;
    private final EditorController controller;
    private final EditorSettings settings;
    /**
     * Offset of first c.q. last character in active source text
     */
    protected int activeTranslationBeginOffset, activeTranslationEndOffset;

    /** Boundary of full entry display. */
    protected Position beginPosP1, endPosM1;

    /** Source start position - for marks. */
    protected Position posSourceBeg;
    protected int posSourceLength;
    /** Translation start position - for marks. */
    protected Position posTranslationBeg;
    protected int posTranslationLength;

    /** current offset in document to insert new stuff*/
    protected int offset;

    /**
     * Markers for this segment.
     * 
     * Array of displayed marks. 1nd dimension - marker (fixed size), 2nd dimension - marks (can remove/add)
     */
    protected java.util.List<MarkInfo>[] marks;

    /**
     * True if source OR target languages is RTL. In this case, we will insert
     * RTL/LTR embedded direction chars. Otherwise - will not insert, since JDK
     * 1.6 has bug with performance with embedded directions chars.
     */
    protected final boolean hasRTL;

    public SegmentBuilder(final EditorController controller, final Document3 doc, final EditorSettings settings,
            final SourceTextEntry ste, final int segmentNumberInProject, final boolean hasRTL) {
        this.controller = controller;
        this.doc = doc;
        this.settings = settings;
        this.ste = ste;
        this.segmentNumberInProject = segmentNumberInProject;
        this.hasRTL = hasRTL;
    }

    public boolean isDefaultTranslation() {
        return defaultTranslation;
    }

    public void setDefaultTranslation(boolean defaultTranslation) {
        this.defaultTranslation = defaultTranslation;
    }

    /**
     * Create element for one segment.
     *
     * @param doc
     *            document
     * @return OmElementSegment
     */
    public void createSegmentElement(final boolean isActive, TMXEntry trans) {
        createSegmentElement(isActive, doc.getLength(), trans);
    }

    public void prependSegmentElement(final boolean isActive, TMXEntry trans) {
        createSegmentElement(isActive, 0, trans);
    }

    public void createSegmentElement(final boolean isActive, int initialOffset, TMXEntry trans) {
        UIThreadsUtil.mustBeSwingThread();

        displayVersion = globalVersions.incrementAndGet();
        this.active = isActive;

        doc.trustedChangesInProgress = true;
        StaticUIUtils.setCaretUpdateEnabled(controller.editor, false);
        try {
            try {
                if (beginPosP1 != null && endPosM1 != null) {
                    // remove old segment
                    int beginOffset = beginPosP1.getOffset() - 1;
                    int endOffset = endPosM1.getOffset() + 1;
                    doc.remove(beginOffset, endOffset - beginOffset);
                    offset = beginOffset;
                } else {
                    // there is no segment in document yet - need to add
                    offset = initialOffset;
                }

                defaultTranslation = trans.defaultTranslation;
                if (!Core.getProject().getProjectProperties().isSupportDefaultTranslations()) {
                    defaultTranslation = false;
                }
                transExist = trans.isTranslated();
                noteExist = trans.hasNote();
                if (settings.isMarkAutoPopulated() && (trans.linked != null)) {
                    autoInsertAttributes = attrs(true, false, false, false); StyleConstants.setBackground((MutableAttributeSet) autoInsertAttributes, colorForLinked(trans.linked));                    
                }
                if (trans.changer != null)
                    transDiffer =  (! trans.changer.equals (
                        Preferences.getPreferenceDefault(Preferences.TEAM_AUTHOR, System.getProperty("user.name"))
                    ));
                else
                    transDiffer =  (! Preferences.getPreferenceDefault(Preferences.TEAM_AUTHOR, System.getProperty("user.name"))
                        .equals (trans.creator));

                int beginOffset = offset;
                if (isActive) {
                    createActiveSegmentElement(trans);
                } else {
                    createInactiveSegmentElement(trans);
                }
                int endOffset = offset;

                beginPosP1 = doc.createPosition(beginOffset + 1);
                endPosM1 = doc.createPosition(endOffset - 1);
            } catch (BadLocationException ex) {
                throw new RuntimeException(ex);
            }
        } finally {
            doc.trustedChangesInProgress = false;
            StaticUIUtils.setCaretUpdateEnabled(controller.editor, true);
        }
    }

    public boolean hasBeenCreated() {
        return beginPosP1 != null && endPosM1 != null;
    }

    /**
     * Add separator between segments - one empty line.
     */
    public void addSegmentSeparator() {
        addSegmentSeparator(doc.getLength());
    }

    public void prependSegmentSeparator() {
        addSegmentSeparator(0);
    }

    public void addSegmentSeparator(int index) {
        doc.trustedChangesInProgress = true;
        StaticUIUtils.setCaretUpdateEnabled(controller.editor, false);
        try {
            try {
                doc.insertString(index, "\n", null);
            } catch (BadLocationException ex) {
                throw new RuntimeException(ex);
            }
        } finally {
            doc.trustedChangesInProgress = false;
            StaticUIUtils.setCaretUpdateEnabled(controller.editor, true);
        }
    }

    private boolean isInsertSource = false;

    private java.awt.Color colorForLinked(TMXEntry.ExternalLinked linked) {
        switch (linked) {
            case xICE: return Styles.EditorColor.COLOR_MARK_COMES_FROM_TM_XICE.getColor();
            case x100PC: return Styles.EditorColor.COLOR_MARK_COMES_FROM_TM_X100PC.getColor();
            case xAUTO: return Styles.EditorColor.COLOR_MARK_COMES_FROM_TM_XAUTO.getColor();
            case xSRC: return Styles.EditorColor.COLOR_MARK_COMES_FROM_SOURCE_FILE.getColor();
        }
        return null;
    }
    
    /**
     * Create active segment for given entry
     */
    private void createActiveSegmentElement(TMXEntry trans) throws BadLocationException {
        try {
            if (EditorSettings.DISPLAY_MODIFICATION_INFO_ALL.equals(settings.getDisplayModificationInfo())
                    || EditorSettings.DISPLAY_MODIFICATION_INFO_SELECTED.equals(settings
                            .getDisplayModificationInfo())) {
                addModificationInfoPart(trans);
            }

            int prevOffset = offset;
            sourceText = addInactiveSegPart(true, ste.getSrcText());

            Map<Language,ProjectTMX> otherLanguageTMs = Core.getProject().getOtherTargetLanguageTMs();
            for (Map.Entry<Language,ProjectTMX> entry : otherLanguageTMs.entrySet()) {
                TMXEntry altTrans = entry.getValue().getMultipleTranslation(ste.getKey());
                if (altTrans == null) altTrans = entry.getValue().getDefaultTranslation(ste.getSrcText());
                if (altTrans!=null && altTrans.isTranslated()) {
                    Language language = entry.getKey();
                    addOtherLanguagePart(altTrans.translation, language);
                }
            }

            posSourceBeg = doc.createPosition(prevOffset + (hasRTL ? 1 : 0));
            posSourceLength = sourceText.length();

            isInsertSource = false;
            if (trans.isTranslated()) {
                //translation exist
                translationText = trans.translation;
            } else {
                boolean insertSource = !Preferences.isPreference(Preferences.DONT_INSERT_SOURCE_TEXT);
                if (controller.entriesFilter != null && controller.entriesFilter.isSourceAsEmptyTranslation()) {
                    insertSource = true;
                }
                if (insertSource) {
                    // need to insert source text on empty translation
                    String srcText = ste.getSrcText();
                    if (Preferences.isPreference(Preferences.GLOSSARY_REPLACE_ON_INSERT)) {
                        srcText = EditorUtils.replaceGlossaryEntries(srcText);
                    }
                    translationText = srcText;
                    isInsertSource = true;
                    autoInsertAttributes = attrs(true, false, false, false); StyleConstants.setBackground((MutableAttributeSet) autoInsertAttributes, Styles.EditorColor.COLOR_INSERT_SOURCE.getColor());
                } else {
                    // empty text on non-exist translation
                    translationText = "";
                }
            }

            translationText = addActiveSegPart(translationText, trans.linked);
            posTranslationBeg = null;

            doc.activeTranslationBeginM1 = doc.createPosition(activeTranslationBeginOffset - 1);
            doc.activeTranslationEndP1 = doc.createPosition(activeTranslationEndOffset + 1);
        } catch (OutOfMemoryError oome) {
            // Oh shit, we're all out of storage space!
            // Of course we should've cleaned up after ourselves earlier,
            // but since we didn't, do a bit of cleaning up now, otherwise
            // we can't even inform the user about our slacking off.
            doc.remove(0, doc.getLength());

            // Well, that cleared up some, GC to the rescue!
            System.gc();

            // There, that should do it, now inform the user
            long memory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
            Log.logErrorRB("OUT_OF_MEMORY", memory);
            Log.log(oome);
            Core.getMainWindow().showErrorDialogRB("TF_ERROR", "OUT_OF_MEMORY", memory);
            // Just quit, we can't help it anyway
            System.exit(0);

        }
    }

    /**
     * Create method for inactive segment.
     * @param trans TMX entry with translation
     * @throws BadLocationException
     */
    private void createInactiveSegmentElement(TMXEntry trans) throws BadLocationException {
        if (EditorSettings.DISPLAY_MODIFICATION_INFO_ALL.equals(settings.getDisplayModificationInfo())) {
            addModificationInfoPart(trans);
        }

        sourceText = null;
        translationText = null;

        if (settings.isDisplaySegmentSources()) {
            sourceText = ste.getSrcText();
        }

        if (trans.isTranslated()) {
            // translation exist
            translationText = trans.translation;
            if (StringUtil.isEmpty(translationText)) {
                translationText = OStrings.getString("EMPTY_TRANSLATION");
            }
        } else {
            if (sourceText == null) {
                // translation not exist, but source display disabled also -
                // need to display source
                sourceText = ste.getSrcText();
            }
        }

        if (sourceText != null) {
            int prevOffset = offset;
            sourceText = addInactiveSegPart(true, sourceText);
            posSourceBeg = doc.createPosition(prevOffset + (hasRTL ? 1 : 0));
            posSourceLength = sourceText.length();
        } else {
            posSourceBeg = null;
        }

        if (translationText != null) {
            int prevOffset = offset;
            translationText = addInactiveSegPart(false, translationText);
            posTranslationBeg = doc.createPosition(prevOffset + (hasRTL ? 1 : 0));
            posTranslationLength = translationText.length();
        } else {
            posTranslationBeg = null;
        }
    }

    public SourceTextEntry getSourceTextEntry() {
        return ste;
    }

    public long getDisplayVersion() {
        return displayVersion;
    }

    public boolean isActive() {
        return active;
    }

    /** Get source text of entry with internal bidi chars, or null if not displayed. */
    public String getSourceText() {
        return sourceText;
    }

    /** Get translation text of entry with internal bidi chars, or null if not displayed. */
    public String getTranslationText() {
        return translationText;
    }

    public int getStartSourcePosition() {
        if (posSourceBeg != null) {
            return posSourceBeg.getOffset();
        } else {
            return -1;
        }
    }

    public int getStartTranslationPosition() {
        if (posTranslationBeg != null) {
            return posTranslationBeg.getOffset();
        } else {
            return -1;
        }
    }

    /**
     * Get segment's start position.
     *
     * @return start position
     */
    public int getStartPosition() {
        return beginPosP1.getOffset() - 1;
    }

    /**
     * Get segment's end position.
     *
     * @return end position
     */
    public int getEndPosition() {
        return endPosM1.getOffset() + 1;
    }

    /**
     * Set attributes for created paragraphs for better RTL support.
     *
     * @param begin
     *            paragraphs begin
     * @param end
     *            paragraphs end
     * @param isRTL
     *            is text right-to-left?
     */
    private void setAlignment(int begin, int end, boolean isRTL) {
        boolean rtl = false;
        switch (controller.currentOrientation) {
        case ALL_LTR:
            rtl = false;
            break;
        case ALL_RTL:
            rtl = true;
            break;
        case DIFFER:
            rtl = isRTL;
            break;
        }
        doc.setAlignment(begin, end, rtl);
    }

    /**
     * Check if location inside segment.
     */
    public boolean isInsideSegment(int location) {
        return beginPosP1.getOffset() - 1 <= location && location < endPosM1.getOffset() + 1;
    }

    /**
     * Add inactive segment part, without segment begin/end marks.
     *
     * @param isSource is text the source text (true) or translation text (false)
     * @param text
     *            segment part text
     * @throws BadLocationException
     */
    private String addInactiveSegPart(boolean isSource, String text)
            throws BadLocationException {
        int prevOffset = offset;
        boolean rtl = isSource ? controller.sourceLangIsRTL : controller.targetLangIsRTL;
        insertDirectionEmbedding(rtl);
        String result = insertTextWithTags(text, isSource);
        insertDirectionEndEmbedding();
        insert("\n", null);
        setAlignment(prevOffset, offset, rtl);
        return result;
    }

    /**
     * Add inactive segment part, without segment begin/end marks.
     *
     * @param text other language translation text
     * @throws BadLocationException
     */
    private void addOtherLanguagePart(String text, Language language)
            throws BadLocationException {
        int prevOffset = offset;
        boolean rtl = EditorUtils.isRTL(language.getLanguageCode());
        insertDirectionEmbedding(false);
        AttributeSet normal = attrs(true, false, false, false);
        insert(language.getLanguage()+": ", normal);
        insertDirectionEndEmbedding();

        insertDirectionEmbedding(rtl);
        AttributeSet attrs = settings.getOtherLanguageTranslationAttributeSet();
        insert(text, attrs);
        insertDirectionEndEmbedding();
        insert("\n", null);
        setAlignment(prevOffset, offset, rtl);
    }

    /**
     * Adds a string that displays the modification info (author and date). Does
     * nothing if the translation entry is null.
     *
     * @param trans
     *            The translation entry (can be null)
     * @throws BadLocationException
     */
    private void addModificationInfoPart(TMXEntry trans) throws BadLocationException {
        if (!trans.isTranslated())
            return;

        String text;
        if (Preferences.isPreference(Preferences.VIEW_OPTION_TEMPLATE_ACTIVE)) {
             text = ModificationInfoManager.apply(trans);
        } else {
            String author = (trans.changer == null ? OStrings.getString("TF_CUR_SEGMENT_UNKNOWN_AUTHOR")
                    : trans.changer);
            String template;
            if (trans.changeDate != 0) {
                template = OStrings.getString("TF_CUR_SEGMENT_AUTHOR_DATE");
                Date changeDate = new Date(trans.changeDate);
                String changeDateString = DateFormat.getDateInstance().format(changeDate);
                String changeTimeString = DateFormat.getTimeInstance().format(changeDate);
                Object[] args = { author, changeDateString, changeTimeString };
                text = StringUtil.format(template, args);
            } else {
                template = OStrings.getString("TF_CUR_SEGMENT_AUTHOR");
                Object[] args = { author };
                text = StringUtil.format(template, args);
            }
            if (trans.revisor != null)
                text += "\n" + "Last revised by: " + trans.revisor;
        }

        int prevOffset = offset;
        boolean rtl = EditorUtils.localeIsRTL();
        insertDirectionEmbedding(rtl);
        AttributeSet attrs = settings.getModificationInfoAttributeSet();
        insert(text, attrs);
        insertDirectionEndEmbedding();
        insert("\n", null);
        setAlignment(prevOffset, offset, rtl);
    }

    /**
     * Add active (translation) segment part, with segment begin/end marks.
     *
     * @param text
     *            segment part text
     * @throws BadLocationException
     */
    private String addActiveSegPart(String text, TMXEntry.ExternalLinked linked) throws BadLocationException {
        eventsList.clear();
		
        int prevOffset = offset;

        //write translation part
        boolean rtl = controller.targetLangIsRTL;

        AttributeSet attrSegmentMark = settings.getSegmentMarkerAttributeSet();
        if (Preferences.isPreferenceDefault(Preferences.VIEW_EDITOR_ZONE_START_END, true)) {
            //the marker itself is in user language
            insertDirectionEmbedding(EditorUtils.localeIsRTL());		
            insertSegmentMarkText(attrSegmentMark, text, linked);
            if (Preferences.isPreferenceDefault(Preferences.VIEW_EDITOR_ZONE_LINE_BREAKS, true)) insert("\n", null);
        }

        insertDirectionEmbedding(rtl);

        activeTranslationBeginOffset = offset;
        String result = insertTextWithTags(text, false);
        activeTranslationEndOffset = offset;

        insertDirectionEndEmbedding();

        //write segment marker
        //we want the marker AFTER the translated text, so use same direction as target text.
        insertDirectionMarker(rtl);

        if (Preferences.isPreferenceDefault(Preferences.VIEW_EDITOR_ZONE_LINE_BREAKS, true)) insert("\n", null);
        if (Preferences.isPreferenceDefault(Preferences.VIEW_EDITOR_ZONE_START_END, true)) 
            insert(OStrings.getString("TF_CUR_SEGMENT_END"), attrSegmentMark);
        else // no start segment, so we add default segment delimiter and also the info at the end
            insertSegmentMarkText(attrSegmentMark, text, linked);
        insert("\n\n", attrSegmentMark);

        insertDirectionEndEmbedding();

        setAlignment(prevOffset, offset, rtl);
        return result;
    }

    void createInputAttributes(Element element, MutableAttributeSet set) {
        set.addAttributes(attrs(false, false, false, false));
    }

    private void insert(String text, AttributeSet attrs) throws BadLocationException {
        doc.insertString(offset, text, attrs);
        offset += text.length();
    }

    /**
     * Make some changes of segment mark from resource bundle for display
     * correctly in editor.
     *
     * @return changed mark text
     */
    private void insertSegmentMarkText(AttributeSet attrSegmentMark, String text, TMXEntry.ExternalLinked linked) throws BadLocationException {
        String fullMarker = OConsts.segmentMarkerString;

        //replace placeholder with actual segment number
        if (fullMarker.contains("0000")) {
            String replacement = NUMBER_FORMAT.format(segmentNumberInProject);
            if (Preferences.isPreference(Preferences.MARK_NON_UNIQUE_SEGMENTS)
                    && ste.getDuplicate() != SourceTextEntry.DUPLICATE.NONE) {
                replacement = StringUtil.format(OStrings.getString("SW_FILE_AND_NR_OF_MORE"),
                        replacement,
                        ste.getNumberOfDuplicates());
            }
            fullMarker = fullMarker.replace("0000", replacement);
        }
        
        // trim and replace spaces to non-break spaces
        fullMarker = fullMarker.trim().replace(' ', '\u00A0');

        insert(fullMarker, attrSegmentMark);
        if (Preferences.isPreferenceDefault(Preferences.VIEW_EDITOR_ZONE_DISPLAY_ORIGIN, true)) {
            doc.infoStart = offset - 1; offset--;
            if (isInsertSource && text.equals(sourceText))
                insert("\u00A0**\u00A0" + OStrings.getString("TF_SEG_COMESFROM_SOURCE") + "\u00A0**\u00A0", null);
            else if (linked == TMXEntry.ExternalLinked.xSRC)
                insert("\u00A0**\u00A0" + OStrings.getString("TF_SEG_COMESFROM_SOURCE_FILE") + "\u00A0**\u00A0", null);			
            else if (linked != null)
                insert("\u00A0**\u00A0" + OStrings.getString("TF_SEG_COMESFROM_TM_AUTO") + "\u00A0**\u00A0", null);			
            else if ((text != null) && (text.length() > 0))
                insert("\u00A0**\u00A0" + OStrings.getString("TF_SEG_COMESFROM_TRA") + "\u00A0**\u00A0", null);
            else
                insert("\u00A0**\u00A0" + OStrings.getString("TF_SEG_COMESFROM_EMPTY") + "\u00A0**\u00A0", null);
            doc.infoEnd = offset; offset++; 	// restore original value
        }    
    }

    /**
     * Called on the active entry changed. Required for update translation text.
     */
    void onActiveEntryChanged() {
        translationText = doc.extractTranslation();
        displayVersion = globalVersions.incrementAndGet();
    }

    /**
     * Choose segment part attributes based on rules.
     * @param isSource is it a source segment or a target segment
     * @param isPlaceholder is it for a placeholder (OmegaT tag or sprintf-variable etc.) or regular text inside the segment?
     * @param isRemoveText is it text that should be removed in the translation?
     * @param isNBSP is the text a non-breakable space?
     * @return the attributes to format the text
     */
    public AttributeSet attrs(boolean isSource, boolean isPlaceholder, boolean isRemoveText, boolean isNBSP) {
        return settings.getAttributeSet(isSource, isPlaceholder, isRemoveText, ste.getDuplicate(), active, transExist, transDiffer, noteExist, isNBSP);
    }

    /**
     * Inserts the texts and formats the text
     * @param text source or translation text
     * @param isSource true if it is a source text, false if it is a translation
     * @throws BadLocationException
     */
    private String insertTextWithTags(String text, boolean isSource) throws BadLocationException {
        if (!isSource && hasRTL && controller.targetLangIsRTL) {
            text = EditorUtils.addBidiAroundTags(text, ste);
        }
        insert(text, ((!isSource) && (autoInsertAttributes != null)) ? autoInsertAttributes : attrs(isSource, false, false, false));
        return text;
    }

	private static final AttributeSet NO_ATTRIBUTES = new javax.swing.text.SimpleAttributeSet();
	static class Insertion { 
		int pos, length; 
		public Insertion(int pos, int len) { this.pos = pos; this.length = len; }
	}
	private List<Insertion> eventsList = new java.util.ArrayList<>();	
	
	// Set by replaceTextWithColor
	// Since this is only for active segment translation, we reset it to null when we leave
	public AttributeSet autoInsertAttributes = null;
	
    public void resetTextAttributes(DocumentEvent ev) {
        doc.trustedChangesInProgress = true;
        try {
            if (posSourceBeg != null) {
                int sBeg = posSourceBeg.getOffset();
                int sLen = posSourceLength;
                AttributeSet attrs = attrs(true, false, false, false);
                doc.setCharacterAttributes(sBeg, sLen, attrs, true);
            }
            if (active) {
                int tBeg = doc.getTranslationStart(), tLen = posTranslationLength;
                int tEnd = doc.getTranslationEnd();
				if (Preferences.isPreferenceDefault(Preferences.VIEW_EDITOR_ZONE_COLOR_TYPING, false)) 
					if ((ev != null) && (ev.getType() == DocumentEvent.EventType.INSERT) && (ev.getOffset() >= tBeg) && (ev.getOffset() <= tEnd))  {
						boolean over = false;
						for (Insertion ins0: eventsList)
							if (ins0.pos >= ev.getOffset()) ins0.pos += ev.getLength();
							/*else if (ins0.pos + ins0.length <= ev.getOffset()) { // avoid to create too many entries
								over = true;
								ins0.length = Math.max(ins0.pos + ins0.length, ev.getOffset() + ev.getLength()) - ins0.pos;
							}*/
						if (!over) eventsList.add (new Insertion(ev.getOffset(), ev.getLength()));
					}
					else if ((ev != null) && (ev.getType() == DocumentEvent.EventType.REMOVE)) {
						for (Insertion ins0: eventsList)
							if (ins0.pos >= ev.getOffset()) ins0.pos -= ev.getLength();					
					}
                AttributeSet attrs = attrs(false, false, false, false);
				if (autoInsertAttributes != null) attrs = autoInsertAttributes;
                doc.setCharacterAttributes(tBeg, tEnd - tBeg, attrs, true);
                if (Preferences.isPreferenceDefault(Preferences.VIEW_EDITOR_ZONE_COLOR_TYPING, false))
                    for (Insertion ins0: eventsList) doc.setCharacterAttributes(ins0.pos, ins0.length, NO_ATTRIBUTES, true);
            } else {
                if (posTranslationBeg != null) {
                    int tBeg = posTranslationBeg.getOffset();
                    int tLen = posTranslationLength;
                    AttributeSet attrs = attrs(false, false, false, false);
                    doc.setCharacterAttributes(tBeg, tLen, attrs, true);
                }
            }
        } finally {
            doc.trustedChangesInProgress = false;
        }
    }

    /**
     * Writes (if necessary) an RTL or LTR marker. Use it before writing text in some language.
     * @param isRTL is the language that has to be written a right-to-left language?
     * @throws BadLocationException
     */
    private void insertDirectionEmbedding(boolean isRTL) throws BadLocationException {
        if (this.hasRTL) {
            insert(isRTL ? BIDI_RLE : BIDI_LRE, null); // RTL- or LTR- embedding
        }
    }

    /**
     * Writes (if necessary) an end-of-embedding marker. Use it after writing text in some language.
     * @throws BadLocationException
     */
    private void insertDirectionEndEmbedding() throws BadLocationException {
        if (this.hasRTL) {
            insert(BIDI_PDF, null); // end of embedding
        }
    }

    /**
     * Writes (if necessary) an RTL or LTR marker. Use it before writing text in some language.
     * @param isRTL is the language that has to be written a right-to-left language?
     * @throws BadLocationException
     */
    private void insertDirectionMarker(boolean isRTL) throws BadLocationException {
        if (this.hasRTL) {
            insert(isRTL ? BIDI_RLM : BIDI_LRM, null); // RTL- or LTR- marker
        }
    }
}
