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

 Copyright (C) 2012 Alex Buloichik
               2013-2014 Aaron Madlon-Kay, Alex Buloichik
               2016 Thomas Cordonnier
               2017 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.core.data;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.Set;

import org.omegat.core.Core;
import org.omegat.util.FileUtil;
import org.omegat.util.Language;
import org.omegat.util.Log;
import org.omegat.util.OConsts;
import org.omegat.util.Preferences;
import org.omegat.util.StringUtil;
import org.omegat.util.TMXReader2;
import org.omegat.util.TMXWriter2;

/**
 * Class for store data from project_save.tmx.
 * 
 * Orphaned or non-orphaned translation calculated by RealProject.
 * 
 * @author Alex Buloichik (alex73mail@gmail.com)
 * @author Aaron Madlon-Kay
 * @author Thomas Cordonnier
 */
public class ProjectTMX {

    public static final String PROP_FILE = "file";
    public static final String PROP_ID = "id";
    public static final String PROP_PREV = "prev";
    public static final String PROP_NEXT = "next";
    public static final String PROP_PATH = "path";
    public static final String PROP_XICE = "x-ice";
    public static final String PROP_X100PC = "x-100pc";
    public static final String PROP_XAUTO = "x-auto";
    public static final String PROP_REVISOR = "revisor";

    /**
     * Storage for default translations for current project.
     * 
     * It must be used with synchronization around ProjectTMX.
     */
    protected Map<String, TMXEntry> defaults;

    /**
     * Storage for alternative translations for current project.
     * 
     * It must be used with synchronization around ProjectTMX.
     */
    protected Map<EntryKey, TMXEntry> alternatives;
    
    final CheckOrphanedCallback checkOrphanedCallback;

    public ProjectTMX(Language sourceLanguage, Language targetLanguage, boolean isSentenceSegmentingEnabled, File file, CheckOrphanedCallback callback) throws Exception {
        this.checkOrphanedCallback = callback;
        alternatives = new ConcurrentHashMap<EntryKey, TMXEntry>();
        defaults = new ConcurrentHashMap<String, TMXEntry>();

        if (!file.exists()) {
            // file not exist - new project
            return;
        }

        new TMXReader2().readTMX(
                file,
                sourceLanguage,
                targetLanguage,
                isSentenceSegmentingEnabled,
                false,
                true,
                Preferences.isPreference(Preferences.EXT_TMX_USE_SLASH),
                new Loader(sourceLanguage, targetLanguage, isSentenceSegmentingEnabled));
    }

    /**
     * Constructor for TMX delta.
     */
    public ProjectTMX() {
        alternatives = new ConcurrentHashMap<EntryKey, TMXEntry>();
        defaults = new ConcurrentHashMap<String, TMXEntry>();
        checkOrphanedCallback = null;
    }

    /**
     * Check TMX for empty.
     */
    public boolean isEmpty() {
        return defaults.isEmpty() && alternatives.isEmpty();
    }

    /**
     * It saves current translation into file.
     */
    public void save(ProjectProperties props, String translationFile, boolean translationUpdatedByUser)
            throws Exception {
        if (!translationUpdatedByUser) {
            Log.logInfoRB("LOG_DATAENGINE_SAVE_NONEED");
            return;
        }

        File newFile = new File(translationFile + OConsts.NEWFILE_EXTENSION);

        // Save data into '*.new' file
        exportTMX(props, newFile, false, false, true);

        File backup = new File(translationFile + OConsts.BACKUP_EXTENSION);
        File orig = new File(translationFile);
        if (backup.exists()) {
            if (!backup.delete()) {
                throw new IOException("Error delete backup file");
            }
        }

        // Rename existing project file in case a fatal error
        // is encountered during the write procedure - that way
        // everything won't be lost
        if (orig.exists()) {
            FileUtil.rename(orig, backup);
        }

        // Rename new file into TMX file
        FileUtil.rename(newFile, orig);
    }

    public void exportTMX(ProjectProperties props, File outFile, final boolean forceValidTMX,
            final boolean levelTwo, final boolean useOrphaned) throws Exception {
        TMXWriter2 wr = new TMXWriter2(outFile, props.getSourceLanguage(), props.getTargetLanguage(),
                props.isSentenceSegmentingEnabled(), levelTwo, forceValidTMX);
        try {
            Map<String, TMXEntry> tempDefaults = new TreeMap<String, TMXEntry>();
            Map<EntryKey, TMXEntry> tempAlternatives = new TreeMap<EntryKey, TMXEntry>();

            synchronized (this) {
                if (useOrphaned) {
                    // fast call - just copy
                    tempDefaults.putAll(defaults);
                    tempAlternatives.putAll(alternatives);
                } else {
                    // slow call - copy non-orphaned only
                    for(Map.Entry<String, TMXEntry> en:defaults.entrySet()) {
                        if (checkOrphanedCallback.existSourceInProject(en.getKey())) {
                            tempDefaults.put(en.getKey(), en.getValue());
                        }
                    }
                    for(Map.Entry<EntryKey, TMXEntry> en:alternatives.entrySet()) {
                        if (checkOrphanedCallback.existEntryInProject(en.getKey())) {
                            tempAlternatives.put(en.getKey(), en.getValue());
                        }
                    }
                }
            }

            List<String> p=new ArrayList<String>();
            wr.writeComment(" Default translations ");
            for (Map.Entry<String, TMXEntry> en : new TreeMap<String, TMXEntry>(tempDefaults).entrySet()) {
                p.clear();
                if (Preferences.isPreferenceDefault(Preferences.SAVE_AUTO_STATUS, false)) {
                    if (en.getValue().linked == TMXEntry.ExternalLinked.xAUTO) {
                        p.add(PROP_XAUTO);
                        p.add("auto");
                    }
                    if (en.getValue().linked == TMXEntry.ExternalLinked.xSRC) {
                        p.add(PROP_XAUTO);
                        p.add("src");
                    }
                }
                if (en.getValue().revisor != null) { p.add(PROP_REVISOR); p.add (en.getValue().revisor); }				
                wr.writeEntry(en.getKey(), en.getValue().translation, en.getValue(), p);
            }

            wr.writeComment(" Alternative translations ");
            for (Map.Entry<EntryKey, TMXEntry> en : new TreeMap<EntryKey, TMXEntry>(tempAlternatives)
                    .entrySet()) {
                EntryKey k = en.getKey();
                p.clear();
                p.add(PROP_FILE);
                p.add(k.file);
                p.add(PROP_ID);
                p.add(k.id);
                p.add(PROP_PREV);
                p.add(k.prev);
                p.add(PROP_NEXT);
                p.add(k.next);
                p.add(PROP_PATH);
                p.add(k.path);
                if (en.getValue().revisor != null) { p.add(PROP_REVISOR); p.add (en.getValue().revisor); }
                if (Preferences.isPreferenceDefault(Preferences.SAVE_AUTO_STATUS, false)) {
                    if (en.getValue().linked == TMXEntry.ExternalLinked.xAUTO) {
                        p.add(PROP_XAUTO);
                        p.add("auto");
                    }
                    if (en.getValue().linked == TMXEntry.ExternalLinked.xSRC) {
                        p.add(PROP_XAUTO);
                        p.add("src");
                    }					
                    if (en.getValue().linked == TMXEntry.ExternalLinked.xICE) {
                        p.add(PROP_XICE);
                        p.add(k.id);
                    } else if (en.getValue().linked == TMXEntry.ExternalLinked.x100PC) {
                        p.add(PROP_X100PC);
                        p.add(k.id);
                    }
                }
                wr.writeEntry(en.getKey().sourceText, en.getValue().translation, en.getValue(), p);
            }
        } finally {
            wr.close();
        }
    }

    /**
     * Get default translation or null if not exist.
     */
    public TMXEntry getDefaultTranslation(String source) {
        synchronized (this) {
            return defaults.get(source);
        }
    }

    /**
     * Get multiple translation or null if not exist.
     */
    public TMXEntry getMultipleTranslation(EntryKey ek) {
        synchronized (this) {
            return alternatives.get(ek);
        }
    }

    /**
     * Set new translation.
     */
    public void setTranslation(SourceTextEntry ste, TMXEntry te, boolean isDefault) {
        synchronized (this) {
            if (te == null) {
                if (isDefault) {
                    defaults.remove(ste.getKey().sourceText);
                } else {
                    alternatives.remove(ste.getKey());
                }
            } else {
                if (!ste.getSrcText().equals(te.source)) {
                    throw new IllegalArgumentException("Source must be the same as in SourceTextEntry");
                }
                if (isDefault != te.defaultTranslation) {
                    throw new IllegalArgumentException("Default/alternative must be the same");
                }
                if (isDefault) {
                    defaults.put(ste.getKey().sourceText, te);
                } else {
                    alternatives.put(ste.getKey(), te);
                }
            }
        }
    }

    private class Loader implements TMXReader2.LoadCallback {
        private final Language sourceLang;
        private final Language targetLang;
        private final boolean sentenceSegmentingEnabled;

        public Loader(Language sourceLang, Language targetLang,
                boolean sentenceSegmentingEnabled) {
            this.sourceLang = sourceLang;
            this.targetLang = targetLang;
            this.sentenceSegmentingEnabled = sentenceSegmentingEnabled;
        }

        public boolean onEntry(TMXReader2.ParsedTu tu, TMXReader2.ParsedTuv tuvSource,
                TMXReader2.ParsedTuv tuvTarget, boolean isParagraphSegtype) {
            if (tuvSource == null) {
                // source Tuv not found
                return false;
            }
            String creator = null;
            long created = 0;
            String changer = null;
            long changed = 0;
            String translation = null;

            if (tuvTarget != null) {
                creator = StringUtil.nvl(tuvTarget.creationid, tu.creationid);
                created = StringUtil.nvlLong(tuvTarget.creationdate, tu.creationdate);
                changer = StringUtil.nvl(tuvTarget.changeid, tuvTarget.creationid, tu.changeid,
                        tu.creationid);
                changed = StringUtil.nvlLong(tuvTarget.changedate, tuvTarget.creationdate, tu.changedate,
                    tu.creationdate);
                translation = tuvTarget.text;
            }

            List<String> sources = new ArrayList<String>();
            List<String> targets = new ArrayList<String>();
            Core.getSegmenter().segmentEntries(sentenceSegmentingEnabled && isParagraphSegtype, sourceLang,
                    tuvSource.text, targetLang, translation, sources, targets);

            synchronized (this) {
                for (int i = 0; i < sources.size(); i++) {
                    String segmentSource = sources.get(i);
                    String segmentTranslation = targets.get(i);

                    PrepareTMXEntry te = new PrepareTMXEntry();
                    te.source = segmentSource;
                    te.translation = segmentTranslation;
                    te.changer = changer;
                    te.changeDate = changed;
                    te.creator = creator;
                    te.creationDate = created;
                    te.note = tu.note;
                    te.otherProperties = tu.props;

                    EntryKey key = new EntryKey(te.getPropValue(PROP_FILE), te.source,
                            te.getPropValue(PROP_ID), te.getPropValue(PROP_PREV), te.getPropValue(PROP_NEXT),
                            te.getPropValue(PROP_PATH));

                    TMXEntry.ExternalLinked externalLinkedMode = calcExternalLinkedMode(te);

                    boolean defaultTranslation = key.file == null;
                    if (te.otherProperties != null && te.otherProperties.isEmpty()) {
                        te.otherProperties = null;
                    }

                    TMXEntry te1;
                    if (defaultTranslation) {
                        // default translation
                        defaults.put(segmentSource, te1 = new TMXEntry(te, true, externalLinkedMode));
                    } else {
                        // multiple translation
                        alternatives.put(key, te1 = new TMXEntry(te, false, externalLinkedMode));
                    }
                    te1.revisor = te.getPropValue(PROP_REVISOR);
                }
            }
            return true;
        }
    };

    private TMXEntry.ExternalLinked calcExternalLinkedMode(PrepareTMXEntry te) {
        String id = te.getPropValue(PROP_ID);
        TMXEntry.ExternalLinked externalLinked = null;
        if (externalLinked == null && te.hasPropValue(PROP_XICE, id)) {
            externalLinked = TMXEntry.ExternalLinked.xICE;
        }
        if (externalLinked == null && te.hasPropValue(PROP_X100PC, id)) {
            externalLinked = TMXEntry.ExternalLinked.x100PC;
        }
        if (externalLinked == null && te.hasPropValue(PROP_XAUTO, "auto")) {
            externalLinked = TMXEntry.ExternalLinked.xAUTO;
        }
        if (externalLinked == null && te.hasPropValue(PROP_XAUTO, "src")) {
            externalLinked = TMXEntry.ExternalLinked.xSRC;
        }
        return externalLinked;
    }

    /**
     * Returns the collection of TMX entries that have a default translation
     */
    public Collection<TMXEntry> getDefaults() {
        return defaults.values();
    }
    /**
     * Returns the collection of TMX entries that have an alternative translation
     * @return
     */
    public Collection<TMXEntry> getAlternatives() {
        return alternatives.values();
    }

    public interface CheckOrphanedCallback {
        boolean existEntryInProject(EntryKey key);

        boolean existSourceInProject(String src);
        
        void addEntry(SourceTextEntry ste);
    }
    
    static class DefaultCheckOrphanedCallback implements CheckOrphanedCallback {
        // Sets of exist entries for check orphaned
        private Set<String> existSource = new HashSet<String>();
        private Set<EntryKey> existKeys = new HashSet<EntryKey>();

        public boolean existEntryInProject(EntryKey key) { return existKeys.contains(key); }
        public boolean existSourceInProject(String src) { return existSource.contains(src); }        
        public void addEntry(SourceTextEntry ste) {
            existSource.add(ste.getKey().sourceText);
            existKeys.add(ste.getKey());
        }
    }
    
    public void replaceContent(ProjectTMX tmx) {
        synchronized (tmx) {
            for (Map.Entry<String, TMXEntry> me: tmx.defaults.entrySet())
                if (defaults.get(me.getKey()) == null) this.defaults.put(me.getKey(), me.getValue());                            
            for (Map.Entry<EntryKey, TMXEntry> me: tmx.alternatives.entrySet())
                if (alternatives.get(me.getKey()) == null) this.alternatives.put(me.getKey(), me.getValue());
        }
    }

    @SuppressWarnings("unchecked")
    public void iterateByDefaultTranslations(IProject.DefaultTranslationsIterator it) {
        for (Map.Entry<String, TMXEntry> en : defaults.entrySet()) {
            it.iterate(en.getKey(), en.getValue());
        }
    }

    @SuppressWarnings("unchecked")
    public void iterateByMultipleTranslations(IProject.MultipleTranslationsIterator it) {
        for (Map.Entry<EntryKey, TMXEntry> en : alternatives.entrySet()) {
            it.iterate(en.getKey(), en.getValue());
        }
    }
    
    
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("[");
        List<Map.Entry<String, TMXEntry>> d = new ArrayList<Map.Entry<String, TMXEntry>>(defaults.entrySet());
        Collections.sort(d, new Comparator<Map.Entry<String, TMXEntry>>() {
            @Override
            public int compare(Entry<String, TMXEntry> o1, Entry<String, TMXEntry> o2) {
                return o1.getKey().compareTo(o2.getKey());
            }
        });
        for (Map.Entry<String, TMXEntry> e : d) {
            sb.append(e.getKey()).append(": ").append(e.getValue().translation).append(", ");
        }
        List<Map.Entry<EntryKey, TMXEntry>> a = new ArrayList<Map.Entry<EntryKey, TMXEntry>>(alternatives.entrySet());
        Collections.sort(a, new Comparator<Map.Entry<EntryKey, TMXEntry>>() {
            @Override
            public int compare(Entry<EntryKey, TMXEntry> o1, Entry<EntryKey, TMXEntry> o2) {
                return o1.getKey().sourceText.compareTo(o2.getKey().sourceText);
            }
        });
        for (Map.Entry<EntryKey, TMXEntry> e : a) {
            sb.append(e.getKey().sourceText).append(": ").append(e.getValue().translation).append(", ");
        }
        sb.append(']');
        return sb.toString();
    }

}
