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

 Copyright (C) 2017-2021 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.filters4.xml.xliff;

import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.UUID;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.XMLEvent;

import org.omegat.core.Core;
import org.omegat.core.data.EntryKey;
import org.omegat.core.data.IProject;
import org.omegat.core.data.ITMXEntry;
import org.omegat.core.data.TMXEntry;
import org.omegat.core.data.PrepareTMXEntry;
import org.omegat.core.data.SourceTextEntry.SourceTranslationInfo;
import org.omegat.core.data.SourceTextEntry.SourceTranslationEntry;
import org.omegat.filters2.Instance;
import org.omegat.util.Preferences;

/**
 * Filter for support SDL-Xliff, based on XLIFF-1
 *
 * @creator Thomas Cordonnier
 */
public class SdlXliff extends Xliff1Filter {
    
    // ---------------------------- IFilter API ----------------------------
    
    @Override
    public String getFileFormatName() {
        return "SDL-Xliff";
    }

    @Override
    public Instance[] getDefaultInstances() {
        return new Instance[]  { new Instance("*.sdlxliff") };
    }
    
    @Override
    public boolean isFileSupported(java.io.File inFile, Map<String, String> config, org.omegat.filters2.FilterContext context) {
        try {
            StartElement el = findEvent(inFile, java.util.regex.Pattern.compile(".*/.*:xliff"));
            if (el == null) return false;
            namespace = el.getName().getNamespaceURI();
            if (el.getAttributeByName(new QName("http://sdl.com/FileTypes/SdlXliff/1.0", "version")) != null) return true;                
            return super.isFileSupported(inFile, config, context);
        } catch (Exception npe) {
            return false; // version attribute is mandatory
        }
    }
    
    // ----------------------------- specific part ----------------------
    
    private String currentMid = null;
    private Map<String, StringBuffer> sdlComments = new HashMap<>();
    private StringBuffer commentBuf = null;
    private Map<UUID, String> omegatNotes = new HashMap<>();
    private Map<String, UUID> defaultNoteLocations = new HashMap<>();
    private Map<EntryKey, UUID> altNoteLocations = new HashMap<>();
    private Map<String, StringBuffer> tagDefs = new java.util.WeakHashMap<>();
    private String currentTagId = null;    
    private Map<String, StringBuffer> segProps = new HashMap<>();
	private String currentProp = null;
	private java.util.Set<String> midSet = new java.util.HashSet<String>();
	private boolean has_seg_defs = false;
    
    @Override	// also starts on cmt-defs or tag-defs, else like in standard XLIFF
    protected void checkCurrentCursorPosition(javax.xml.stream.XMLStreamReader reader, boolean doWrite) {
        if (reader.getEventType() == StartElement.START_ELEMENT) {
            String name = reader.getLocalName();
            if (name.equals("cmt-defs")) this.isEventMode = true;
            // Tags: use reader because we only need a prefix and this list can be very long
            if (name.equals("tag")) { this.currentTagId = reader.getAttributeValue(null,"id"); tagDefs.put(this.currentTagId, new StringBuffer()); }
            if (this.currentTagId != null) {
                StringBuffer buf = tagDefs.get(this.currentTagId);
                buf.append("<").append(name);
                for (int i = 0, len = reader.getAttributeCount(); i < len; i++) 
                    buf.append(" ").append(reader.getAttributeLocalName(i)).append("=").append(reader.getAttributeValue(i));                    
                buf.append(">");
            }
        }
        else if (reader.getEventType() == Characters.CHARACTERS) {            
            if (this.currentTagId != null) tagDefs.get(this.currentTagId).append(reader.getText());
        }
        else if (reader.getEventType() == EndElement.END_ELEMENT) {
            if (reader.getLocalName().equals("tag")) this.currentTagId = null;
            if (this.currentTagId != null) tagDefs.get(this.currentTagId).append("</").append(reader.getLocalName()).append(">");
        }
        super.checkCurrentCursorPosition(reader, doWrite);
    }	
    
    @Override
    protected boolean processStartElement (StartElement startElement, XMLStreamWriter writer) throws  XMLStreamException {
        if (startElement.getName().getLocalPart().equals("cmt-def")) {
            sdlComments.put (startElement.getAttributeByName(new QName("id")).getValue(), commentBuf = new StringBuffer());
            return true;
        }
        if (startElement.getName().getLocalPart().equals("mrk"))
			if (startElement.getAttributeByName(new QName("mtype")).getValue().equals("seg"))
				midSet.add(currentMid = startElement.getAttributeByName(new QName("mid")).getValue());
            else if (startElement.getAttributeByName(new QName("mtype")).getValue().equals("x-sdl-comment")) {
                String id = startElement.getAttributeByName(new QName("http://sdl.com/FileTypes/SdlXliff/1.0", "cid")).getValue();
                this.addNoteFromSource (currentMid, sdlComments.get(id).toString());
            }
        if (startElement.getName().equals("tag-defs")) this.isEventMode = false; // avoid to produce very big redundant list of tags
        if (startElement.getName().equals(new QName("http://sdl.com/FileTypes/SdlXliff/1.0", "seg"))) {
			segProps.clear(); currentProp = null; has_seg_defs = true; // start a new set of properties
            Attribute mid = startElement.getAttributeByName(new QName("id"));
		    if (mid != null) currentMid = mid.getValue();
			if (writer != null) {
                fromEventToWriter (eFactory.createStartElement(startElement.getName(), null, startElement.getNamespaces()), writer);
                for (java.util.Iterator I = startElement.getAttributes(); I.hasNext(); ) {
                    Attribute A = (Attribute) I.next(); 
                    if (! A.getName().getLocalPart().equals("conf"))
                        writer.writeAttribute(A.getName().getPrefix(), A.getName().getNamespaceURI(), A.getName().getLocalPart(), A.getValue());
                    else 
                        if (mid == null) writer.writeAttribute("conf", A.getValue());
                        else {
                            String value = "Translated";
                            if ((Core.getMainWindow() != null) && (Core.getMainWindow().getMainMenu().getRevisionMenuItem().isSelected()))  {
                                ITMXEntry entry = this.currentEntryTranslation(mid.getValue());
                                if (entry.getPropValue("revisor") != null) value = "ApprovedTranslation";
                            }
                            writer.writeAttribute("conf", value);
                        }
                }
                return false; // we already added current element
            }
		}
        if (startElement.getName().equals(new QName("http://sdl.com/FileTypes/SdlXliff/1.0", "value"))) 
            segProps.put (currentProp = startElement.getAttributeByName(new QName("key")).getValue(), commentBuf = new StringBuffer());
        if (startElement.getName().getLocalPart().equals("trans-unit")) has_seg_defs = false;
        return super.processStartElement(startElement, writer);
    } 

    @Override
    protected boolean processEndElement (EndElement endElement, XMLStreamWriter writer) throws  XMLStreamException {
        if (endElement.getName().equals(new QName("http://sdl.com/FileTypes/SdlXliff/1.0", "value"))) { 
			commentBuf = null; currentProp = null; 
		}
        if (endElement.getName().getLocalPart().equals("seg")) {
			ITMXEntry entry = currentEntryTranslation(currentMid);
			midSet.remove(currentMid); currentMid = null;
			if ((writer != null) && (entry != null)) {
				if (segProps.get("last_modified_by") == null) { // null, not empty = no such value in the file
					writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "value"); 
					writer.writeAttribute("key", "last_modified_by");
					writer.writeCharacters(entry.getChanger() != null ? entry.getChanger() : Preferences.getPreferenceDefault(Preferences.TEAM_AUTHOR, System.getProperty("user.name")));
					writer.writeEndElement(); // sdl:value
				}
				if (segProps.get("modified_on") == null) {// null, not empty = no such value in the file
					writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "value"); 
					writer.writeAttribute("key","modified_on");
					writer.writeCharacters(TRADOS_DATE_FORMAT.format(entry.getChangeDate() != 0 ? new java.util.Date(entry.getChangeDate()) : new java.util.Date()));
					writer.writeEndElement(); // sdl:value
				}
			}
		}
        if (endElement.getName().getLocalPart().equals("trans-unit")) {
			if (writer != null) {
				if ((midSet.size() > 0) && (! has_seg_defs)) writer.writeStartElement("sdl", "http://sdl.com/FileTypes/SdlXliff/1.0", "seg-defs");
				for (String mid0: midSet) {	// those which were not generated by previous lines
					writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "seg"); 
					writer.writeAttribute("id", mid0); 
					ITMXEntry entry = currentEntryTranslation(currentMid);
					if (entry != null) {
						String value = "Translated";
						if ((Core.getMainWindow() != null) && (Core.getMainWindow().getMainMenu().getRevisionMenuItem().isSelected()))  {
							if (entry.getPropValue("revisor") != null) value = "ApprovedTranslation";
						}
						writer.writeAttribute("conf", value);
						writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "value"); 
						writer.writeAttribute("key", "last_modified_by");
						writer.writeCharacters(entry.getChanger() != null ? entry.getChanger() : Preferences.getPreferenceDefault(Preferences.TEAM_AUTHOR, System.getProperty("user.name")));
						writer.writeEndElement(); // sdl:value

						writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "value"); 
						writer.writeAttribute("key", "modified_on");
						writer.writeCharacters(TRADOS_DATE_FORMAT.format(entry.getChangeDate() != 0 ? new java.util.Date(entry.getChangeDate()) : new java.util.Date()));
						writer.writeEndElement(); // sdl:value
					}
					writer.writeStartElement("sdl", "http://sdl.com/FileTypes/SdlXliff/1.0", "seg");
				}
				if ((midSet.size() > 0) && (! has_seg_defs)) writer.writeEndElement(); // sdl:seg-defs"
			}
			midSet.clear();
		}
        if (endElement.getName().getLocalPart().equals("seg-value")) commentBuf = null;
        if (endElement.getName().getLocalPart().equals("tag")) currentBuffer = null;
        if (endElement.getName().getLocalPart().equals("cmt-def")) commentBuf = null;
        if (endElement.getName().getLocalPart().equals("cmt-defs")) {
            this.isEventMode = false;
            if (writer != null) {
                IProject proj = Core.getProject();
                proj.iterateByDefaultTranslations((String source, TMXEntry trans) -> {
                    if (! trans.hasNote()) return;
                    
                    UUID id = UUID.randomUUID(); omegatNotes.put(id, trans.note); defaultNoteLocations.put(source, id); 
                    createSdlNote (id, trans, writer);
                });
                proj.iterateByMultipleTranslations((EntryKey key, TMXEntry trans) -> {
                    if (! trans.hasNote()) return;
                    
                    UUID id = UUID.randomUUID(); omegatNotes.put(id, trans.note); altNoteLocations.put (key, id);
                    createSdlNote (id, trans, writer);
                });
            }
        }
        if (endElement.getName().getLocalPart().equals("tag-defs")) {
            this.isEventMode = false;
            if (writer != null)
                for (String type: new String[] { "italic", "bold", "underline", "superscript","subscript"}) 
					if (tagDefs.get("omegat-" + type) == null) { // does not yet exist in the source file
						writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "tag"); 
						writer.writeAttribute("id", "omegat-" + type);
						writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "bpt"); 
						writer.writeAttribute("name", "cf");
						writer.writeAttribute("word-end", "false");
						writer.writeAttribute("can-hide", "true");
						writer.writeCharacters("<cf " + type + "=\"true\">");
						writer.writeEndElement(/*bpt*/);
						writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "ept"); 
						writer.writeAttribute("name", "cf");
						writer.writeAttribute("word-end", "false");
						writer.writeAttribute("can-hide", "true");
						writer.writeCharacters("</cf>");
						writer.writeEndElement(/*ept*/);
						writer.writeEndElement(/*tag*/);
					}
        }
        return super.processEndElement (endElement, writer);
    }
    
    // Do not generate tag for comment inside source
    protected boolean isUntaggedTag (StartElement stEl) {
        return (stEl.getName().equals(new QName("urn:oasis:names:tc:xliff:document:1.2", "mrk"))
            && (stEl.getAttributeByName(new QName("mtype")).getValue().equals("x-sdl-comment") || stEl.getAttributeByName(new QName("mtype")).getValue().equals("x-sdl-added")))
            || super.isUntaggedTag(stEl);
    }    
    
    // Track change 'DELETED' should not appear at all in the 
    protected boolean isDeletedTag (StartElement stEl) {
        return (stEl.getName().equals(new QName("urn:oasis:names:tc:xliff:document:1.2", "mrk"))
            && stEl.getAttributeByName(new QName("mtype")).getValue().equals("x-sdl-deleted"))
            || super.isUntaggedTag(stEl);
    }
    
    @Override
    protected char findPrefix (StartElement stEl) {
        if (stEl.getName().getLocalPart().equals("g")) 
            try {
                String txt = tagDefs.get(stEl.getAttributeByName(new QName("id")).getValue()).toString();
                if (txt.contains("italic=True") && ! (txt.contains("bold=True") || txt.contains("strong=True"))) return 'i';
                if (! txt.contains("italic=True") && (txt.contains("bold=True") || txt.contains("strong=True"))) return 'b';
                if (txt.contains("<bpt name=italic") || txt.contains("<ept name=italic")) return 'i';
                if (txt.contains("<bpt name=bold") || txt.contains("<ept name=bold")) return 'b';
                if (txt.contains("<bpt name=strong") || txt.contains("<ept name=strong")) return 'b';                
                if (txt.contains("size")) return 's';
                if (txt.contains("color")) return 'c';
                if (txt.contains("footnote")) return 'n';
                if (txt.contains("<cf")) return 'f';
            }
            catch (Exception e) {}
        // default
        return super.findPrefix (stEl);
    }    
    
	private static final java.util.regex.Pattern OUT_PTN = java.util.regex.Pattern.compile("(\\w) id=\"([\\d\\w\\-]+)\"");
	private static final java.util.regex.Pattern IN_PTN1 = java.util.regex.Pattern.compile("<cf (italic|bold|strong|size)=True");
	private static final java.util.regex.Pattern IN_PTN2 = java.util.regex.Pattern.compile("name=(italic|bold|strong|size)");
	
	
	@Override
	protected String buildProtectedPartDetails(List<XMLEvent> saved) {
		String base = super.buildProtectedPartDetails(saved);
		java.util.regex.Matcher matcher = OUT_PTN.matcher(base);
		if (matcher.find()) {
			if (! tagDefs.containsKey(matcher.group(2))) return base;
			String text = tagDefs.get(matcher.group(2)).toString();
			java.util.regex.Matcher inMatcher = IN_PTN1.matcher(text);
			if (inMatcher.find()) return inMatcher.group(1);
			inMatcher = IN_PTN2.matcher(text);
			if (inMatcher.find()) return inMatcher.group(1);
			return base + ">\n" + text;
		}
		return base;
	}
	
	@Override
	public boolean isUsingStandardNote() { return false; }
	
    private static void createSdlNote(UUID id, TMXEntry trans, XMLStreamWriter writer) {
        try {
            writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "cmt-def"); 
            writer.writeAttribute("id", id.toString());
            writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "Comments"); 
            writer.writeStartElement("http://sdl.com/FileTypes/SdlXliff/1.0", "Comment"); 
            writer.writeCharacters (trans.note);
            writer.writeEndElement(/*Comment*/); writer.writeEndElement(/*Comments*/);  writer.writeEndElement(/*cmt-def*/); 
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected boolean processCharacters (Characters event, XMLStreamWriter writer) throws XMLStreamException { 
        if (commentBuf != null) { 
			commentBuf.append(event.toString());
			if (currentMid != null) {
				ITMXEntry entry = currentEntryTranslation(currentMid);
				if ((writer != null) && (entry != null)) {
					if ("last_modified_by".equals(currentProp)) {
						writer.writeCharacters(entry.getChanger() != null ? entry.getChanger() : Preferences.getPreferenceDefault(Preferences.TEAM_AUTHOR, System.getProperty("user.name")));	
						return false;
					}
					if ("modified_on".equals(currentProp)) {
						writer.writeCharacters(TRADOS_DATE_FORMAT.format(entry.getChangeDate() != 0 ? new java.util.Date(entry.getChangeDate()) : new java.util.Date()));
						return false;
					}
				}
			}
		}			
        return super.processCharacters(event, writer);
    }    

    // This method is called only during translation generation: so we can use it to add notes!
    @Override
    protected List<XMLEvent> restoreTags (String unitId, String path, String src, String tra) {
        List<XMLEvent> res = super.restoreTags(unitId, path, src, tra);
        EntryKey key = new EntryKey("", src, unitId, null,null,path); UUID addNote = null;
        if (altNoteLocations.get(key) != null) addNote = altNoteLocations.get(key);        
        else if (defaultNoteLocations.get(src) != null) addNote = defaultNoteLocations.get(src);
        if ((addNote != null) && (omegatNotes.get(addNote) != null)) {
            List<Attribute> attr = new java.util.LinkedList<Attribute>();
            attr.add (eFactory.createAttribute("sdl", "http://sdl.com/FileTypes/SdlXliff/1.0", "cid", addNote.toString()));
            attr.add (eFactory.createAttribute(new QName("mtype"), "x-sdl-comment"));
            res.add (0, eFactory.createStartElement(new QName("urn:oasis:names:tc:xliff:document:1.2", "mrk"), attr.iterator(), null));            
            res.add (eFactory.createEndElement(new QName("urn:oasis:names:tc:xliff:document:1.2", "mrk"), null));             
        }
        return res;
    }
    
    /** Creates a tag for given pseudo-tag. As usual, SDL does not use the standard way **/
    @Override
    public StartElement createAddTagElement (char type, int idx) {
        List<Attribute> attrs = java.util.Collections.emptyList();
        switch (type) {
            case '\u2460': attrs = java.util.Collections.singletonList(eFactory.createAttribute(new QName("id"), "omegat-italic")); break;
            case '\u2461': attrs = java.util.Collections.singletonList(eFactory.createAttribute(new QName("id"), "omegat-bold")); break;
            case '\u2462': attrs = java.util.Collections.singletonList(eFactory.createAttribute(new QName("id"), "omegat-underline")); break;
            case '\u2463': attrs = java.util.Collections.singletonList(eFactory.createAttribute(new QName("id"), "omegat-superscript")); break;
            case '\u2464': attrs = java.util.Collections.singletonList(eFactory.createAttribute(new QName("id"), "omegat-subscript")); break;
        }
        return eFactory.createStartElement(new QName("urn:oasis:names:tc:xliff:document:1.2", "g"), attrs.iterator(), null);
    }    

    /** Remove entries with only tags **/
    @Override
    protected boolean isToIgnore (String src, String tra) {
        if (tra == null) return false;
        while (src.startsWith("<")) src = src.substring(Math.max(1, src.indexOf(">") + 1));
        while (tra.startsWith("<")) tra = tra.substring(Math.max(1, tra.indexOf(">") + 1));
        return (src.length() == 0) && (tra.length() == 0);
    }
    
	private final java.text.SimpleDateFormat TRADOS_DATE_FORMAT = new java.text.SimpleDateFormat("M/d/y H:m:s");
	
	@Override
	protected SourceTranslationInfo buildTranlsationInfo (String tra, String note) {
		long creationDate = 0L, changeDate = 0L;
		StringBuffer buf = segProps.get("created_by");
		String creator = buf == null ? null : buf.toString();
		try { creationDate = TRADOS_DATE_FORMAT.parse (segProps.get("created_on").toString()).getTime(); } catch (Exception e) {}
		buf = segProps.get("last_modified_by");
		String changer = buf == null ? null : buf.toString();
		try { changeDate = TRADOS_DATE_FORMAT.parse (segProps.get("modified_on").toString()).getTime(); } catch (Exception e) {}
		if (creator == null) creator = changer;
		if (changer != null)
			return new SourceTranslationEntry (tra, note, false, creator, creationDate, changer, changeDate); // XLIFF does not implement fuzzy
		else
			return super.buildTranlsationInfo(tra, note);
	}
	
	@Override protected boolean isStandardTranslationState() { return false; } // because SDLXLIFF does not have attributes in target	
}
