/**************************************************************************
 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
               2013 Aaron Madlon-Kay, Alex Buloichik
               2014-2020 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.util.LinkedList;
import java.util.Arrays;
import java.util.ListIterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import javax.swing.text.AttributeSet;
import javax.swing.text.StyledDocument;
import javax.swing.text.StyleConstants;
import java.awt.Color;
import javax.script.*;

import org.omegat.core.Core;
import org.omegat.util.StringUtil;
import org.omegat.util.Preferences;

/**
 * An entry in the glossary.
 * 
 * @author Keith Godfrey
 * @author Aaron Madlon-Kay
 * @author Alex Buloichik
 * @author Thomas Cordonnier
 */
public class GlossaryEntry implements Comparable<GlossaryEntry> {
    public GlossaryEntry(String src, String[] loc, String[] com, boolean[] fromPriorityGlossary) {
        m_src = StringUtil.normalizeUnicode(src);
        m_loc = loc;
        normalize(m_loc);
        m_com = com;
        normalize(com);
        m_priority = fromPriorityGlossary;
    }

    public GlossaryEntry(String src, String loc, String com, boolean fromPriorityGlossary) {
        this(src, new String[] { loc }, new String[] { com }, new boolean[] { fromPriorityGlossary });
    }

    public String getSrcText() {
        return m_src;
    }

    /**
     * Return the first target-language term string.
     * 
     * Glossary entries can have multiple target strings
     * if they have been combined for display purposes.
     * Access all target strings with {@link GlossaryEntry#getLocTerms(boolean)}.
     * 
     * @return The first target-language term string
     */
    public String getLocText() {
        return m_loc.length > 0 ? m_loc[0] : "";
    }

    /**
     * Return each individual target-language term that
     * corresponds to the source term.
     * 
     * @param uniqueOnly Whether or not to filter duplicates from the list
     * @return All target-language terms
     */
    public String[] getLocTerms(boolean uniqueOnly) {
        if (!uniqueOnly || m_loc.length == 1) return m_loc;
        
        LinkedList<String> list = new LinkedList<String>();
        for (int i = 0; i < m_loc.length; i++) {
            if (i > 0 && m_loc[i].equals(m_loc[i - 1])) continue;
            list.add(m_loc[i]);
        }
        return list.toArray(new String[list.size()]);
    }

    /**
     * Return the first comment string.
     * 
     * Glossary entries can have multiple comment strings
     * if they have been combined for display purposes.
     * Access all comment strings with {@link GlossaryEntry#getComments()}.
     * 
     * @return The first comment string
     */
    public String getCommentText() {        
        return m_com.length > 0 ? m_com[0] : "";
    }

    public String[] getComments() {
        return m_com;
    }

    public boolean getPriority() {
        if (m_priority == null) return false;
        if (m_priority.length == 0) return false;
        for (boolean prio: m_priority) if (prio) return true;
        return false;
    }

    public boolean[] getPriorities() {
        return m_priority;
    }

    private Map<String,LinkedList<String>> getTranslationWithComments() {
        Map<String,LinkedList<String>> res = new TreeMap<String,LinkedList<String>>();
        for (int i = 0; i < m_loc.length; i++) {
            LinkedList<String> item = res.get(m_loc[i]);
            if (item == null) res.put (m_loc[i], item = new LinkedList<String>());
            
            if ((m_com[i] != null) && (m_com[i].length() > 0)) 
                if (m_priority[i]) item.addFirst("\u0007" + m_com[i]);
                else item.add (m_com[i]);
        }
        return res;
    }
    
	public static ScriptEngine engine = null;
	
    public StyledString toStyledString() {
        StyledString result = new StyledString();
		try {
			if (engine == null) {
				engine = new ScriptEngineManager().getEngineByName("Groovy");
				String template = Preferences.getPreferenceDefault(Preferences.GLOSSARY_PANE_TEMPLATE, GlossaryTextArea.DEFAULT_TEMPLATE);
				if (template.startsWith("file:")) {
					template = template.substring(5);
					java.io.File tplFile = new java.io.File (template);
					if (! tplFile.exists()) {
						template = Preferences.getPreferenceDefault(Preferences.SCRIPTS_DIRECTORY, org.omegat.util.StaticUtils.installDir() + "/scripts") + "/layout/glossary/" + template;
						tplFile = org.omegat.gui.scripting.ScriptingWindow.toJavaFile (template);
					}
					if (tplFile.exists()) 
						template = new String(java.nio.file.Files.readAllBytes(tplFile.toPath()));				
					else {
						engine.put("result", result); engine.eval("result.appendError(\"Cannot find " + tplFile.getName().replace("\\", "\\\\") + " ; using default\\n\");");
						template = GlossaryTextArea.DEFAULT_TEMPLATE;
					}
				}
				// Forbid access to Core, but gives access to source and target language, which may enable different algorithm per language
				engine.eval ("package org.omegat.core; public class Core {}"); // forbids access to core from the script
				if (template.contains("sourceLang")) engine.put ("sourceLang", Core.getProject().getProjectProperties().getSourceLanguage().toString()); 
				if (template.contains("targetLang")) engine.put ("targetLang", Core.getProject().getProjectProperties().getTargetLanguage().toString()); 
				engine.eval ("def format(entry,result) {\n" + template + "\n}");
			}
			engine.put("result", result); engine.put("entry", this);
			engine.eval ("format(entry, result)");
		} catch (Exception e) {
			result.appendError (e.getMessage());
		}
        return result;
    }
    
    
    /**
     * If a combined glossary entry contains ',', it needs to be bracketed by
     * quotes, to prevent confusion when entries are combined. However, if the
     * entry contains ';' or '"', it will automatically be bracketed by quotes.
     * 
     * @param entry
     *            A glossary text entry
     * @return A glossary text entry possibly bracketed by quotes
     */
    private static String bracketEntry(String entry) {

        if (entry.contains(",") && !(entry.contains(";") || entry.contains("\"")))
            entry = '"' + entry + '"';
        return entry;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if ( o == null || o.getClass() != this.getClass() ) return false;
        GlossaryEntry otherGlossaryEntry = (GlossaryEntry)o;

        return StringUtil.equalsWithNulls(this.m_src, otherGlossaryEntry.m_src)
                && Arrays.equals(this.m_loc, otherGlossaryEntry.m_loc)
                && Arrays.equals(this.m_com, otherGlossaryEntry.m_com);
    }

    public boolean hasPriorities() {
        for (boolean b: m_priority) if (b) return true;
        return false;
    }

    public boolean hasPriorities(String tra) {
        for (int i = 0; i < m_loc.length; i++) if (m_loc[i].equals(tra) && m_priority[i]) return true;
        return false;
    }
    
    public int compareTo (GlossaryEntry other) {
        if (this.hasPriorities())
            if (! other.hasPriorities()) return -1;
            else return this.m_src.compareTo(other.m_src);
        else
            if (other.hasPriorities()) return +1;
            else return this.m_src.compareTo(other.m_src);
    }
    
    @Override
    public int hashCode() {
        int hash = 98;
        hash = hash * 17 + (m_src == null ? 0 : m_src.hashCode());
        hash = hash * 31 + (m_loc == null ? 0 : Arrays.hashCode(m_loc));
        hash = hash * 13 + (m_com == null ? 0 : Arrays.hashCode(m_com));
        return hash;
    }

    static class StyledString {
        static class Part {
            protected int start,len;

            public Part(int start, int len) {
                this.start = start; this.len = len; 
            }
			
            public void setEnd (int pos) {
                this.len = pos - start;
            }
		}
        static class AttributesPart extends Part {
            private AttributeSet attr;
            
            public AttributesPart(int start, int len, AttributeSet attr) {
                super(start, len); this.attr = attr;
            }
            
            public void apply (StyledDocument doc) {
                doc.setCharacterAttributes(start, len, attr, false);
            }

            public AttributesPart decale (int start) {
                return new AttributesPart (this.start + start, this.len, this.attr);
            }
			
            void insertHtml(StringBuilder sb, AttributesPart[] next) {
                int shift1 = 0, shift2 = 0;
                Color color = StyleConstants.getForeground(attr);
                if (color != Color.black) {
                    String colorString = String.format("%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue());
                    sb.insert(start + len, "</font>");
                    sb.insert(start, "<font color=#" + colorString + ">");
                    shift1 = 20; // "<font color=#xxxxxx>".length()
                    shift2 = 7; // "</font>".length()
                }
                else if (StyleConstants.isBold(attr)) {
                    sb.insert(start + len, "</b>");
                    sb.insert(start, "<b>");
                    shift1 = 3; // "<b>".length()
                    shift2 = 4; // "</b>".length()
                }
                if (next == null) return; 
                for (int i = 0; i < next.length; i++)
                    if (next[i].start >= this.start + this.len) next[i] = next[i].decale(shift1 + shift2);
                    else if (next[i].start >= this.start) next[i] = next[i].decale(shift1);
            }
        }
		static class TooltipPart extends Part {
            private String text;
            
            public TooltipPart(int start, int len, String text) {
                super(start, len); this.text = text;
            }
            
            public TooltipPart shift (int start) {
                return new TooltipPart (this.start + start, this.len, this.text);
            }

			public boolean contains(int pos) { return (pos >= start) && (pos <= start + len); }
			
			public String getText() { return text; }
		}
    
        public StringBuilder text = new StringBuilder();
        public LinkedList<AttributesPart> attrParts = new LinkedList<AttributesPart>();
        public LinkedList<AttributesPart> attrBolds = new LinkedList<AttributesPart>();
        public LinkedList<TooltipPart> tooltips = new LinkedList<TooltipPart>();

        public void markBoldStart() {
            attrBolds.add(new AttributesPart(text.length(), 0, GlossaryTextArea.PRIORITY_ATTRIBUTES));
        }

        public void markBoldEnd() {
            attrBolds.getLast().setEnd (text.length());
        }
		
        public void startTooltip(String tip) {
            tooltips.add(new TooltipPart(text.length(), 0, tip));
        }

        public void endTooltip() {
            tooltips.getLast().setEnd (text.length());
        }		
        
        public StyledString append(StyledString str) {
            int off = text.length();
            text.append(str.text);
            for (AttributesPart attr: str.attrParts) this.attrParts.add (attr.decale(off));
            for (AttributesPart attr: str.attrBolds) this.attrBolds.add (attr.decale(off));	
            for (TooltipPart tt: str.tooltips) this.tooltips.add (tt.shift(off));	
            return this;
        }

        public StyledString append(String str) {
            text.append(str); return this;
        }
		
        public StyledString appendError(String src) {
            attrParts.add (new AttributesPart(text.length(), src.length(), org.omegat.util.gui.Styles.createAttributeSet(java.awt.Color.RED, null, true, null)));
            text.append(src); return this;
        }
     
        public StyledString appendSource(String src) {
            attrParts.add (new AttributesPart(text.length(), src.length(), GlossaryTextArea.SOURCE_ATTRIBUTES));
            text.append(bracketEntry(src)); return this;
        }

        public StyledString appendTarget(String src) {
            attrParts.add (new AttributesPart(text.length(), src.length(), GlossaryTextArea.TARGET_ATTRIBUTES));
            text.append(bracketEntry(src)); return this;
        }

        public StyledString appendTarget(String src, boolean priority) {
            attrParts.add (new AttributesPart(text.length(), src.length(), GlossaryTextArea.TARGET_ATTRIBUTES));
            if (priority) markBoldStart(); text.append(bracketEntry(src)); if (priority) markBoldEnd(); return this;
        }
		
        public StyledString appendComment(String src) {
			if (src.startsWith("\u0007")) return appendComment(src.substring(1), true);
            attrParts.add (new AttributesPart(text.length(), src.length(), GlossaryTextArea.NOTES_ATTRIBUTES));
            text.append(src); return this;
        }

        public StyledString appendComment(String src, boolean priority) {
            attrParts.add (new AttributesPart(text.length(), src.length(), GlossaryTextArea.NOTES_ATTRIBUTES));
            if (priority) markBoldStart(); text.append(src); if (priority) markBoldEnd(); return this;
        }

        public String toHTML() {
            StringBuilder sb = new StringBuilder(text);
            AttributesPart[] attrPartsTmp = attrParts.toArray(new AttributesPart[0]);
            for (ListIterator<AttributesPart> li = attrBolds.listIterator(attrBolds.size()); li.hasPrevious(); ) li.previous().insertHtml(sb, attrPartsTmp);
            for (int i = attrPartsTmp.length - 1; i >= 0; i--) attrPartsTmp[i].insertHtml(sb, null);
            sb.insert(0, "<html><p>").append("</p></html>");
            return sb.toString().replaceAll("\n", "<br>").replaceAll("\t", "&#009;");			
        }
    }
    
    private void normalize(String[] strs) {
        for (int i = 0; i < strs.length; i++) {
            strs[i] = StringUtil.normalizeUnicode(strs[i]);
        }
    }

    private String m_src;
    private String[] m_loc;
    private String[] m_com;
    private boolean[] m_priority;
}
