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

 Copyright (C) 2014 Thomas Cordonnier
               2015 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.matching.external;

import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Collections;
import java.util.Properties;
import javax.swing.JOptionPane;

import org.omegat.core.Core;
import org.omegat.core.CoreEvents;
import org.omegat.core.events.IEntryEventListener;
import org.omegat.core.data.EntryKey;
import org.omegat.core.data.TMXEntry;
import org.omegat.core.data.PrepareTMXEntry;
import org.omegat.core.data.SourceTextEntry;
import org.omegat.core.data.ProjectTMX;
import org.omegat.core.data.IProject;
import org.omegat.filters2.master.PluginUtils;
import org.omegat.util.Language;
import org.omegat.util.Log;
import org.omegat.util.OStrings;
import org.omegat.util.StringUtil;

/**
 * Defines an writeable translation memory that can override project_save.tmx
 * There should be only zero or one such memory in the project : project_save.properties
 * 
 * @author Thomas CORDONNIER
 */
public class ProjectMemory extends ProjectTMX implements IEntryEventListener {

    /**
     * In order to work as ProjectMemory, an writeable external memory needs also to store the current entry source.
     * However you are free to store it how you prefer, provided that you can distinguish it from other properties
     **/
    public static class ContextTMXEntry extends PrepareTMXEntry {
        public static final long serialVersionUID = 1L;
    
        /** Default constructor: enables field by field setting, as most clients do **/
        public ContextTMXEntry() {
        }
        
        /** Constructor for deletion entry : translation and note must be null **/
        public ContextTMXEntry(SourceTextEntry sourceEntry, boolean isDefault) {
            this.isDefault = isDefault;
            this.entryNum = sourceEntry.entryNum();
            this.linked = null;
            this.source = sourceEntry.getSrcText();
            this.translation = null; // needed for deletion!            
            this.note = null; // needed for deletion!            
        }
        
        /** Constructor for insertion/update : TMXEntry must NOT be null **/
        public ContextTMXEntry(TMXEntry tmx, SourceTextEntry sourceEntry) {
            super(tmx);
            this.isDefault = tmx.defaultTranslation;
            this.entryNum = sourceEntry.entryNum();
            this.linked = tmx.linked;
        }
    
        public int entryNum = -1;
        public boolean isDefault;
        public TMXEntry.ExternalLinked linked;
    }


    /**
     * Methods of this interface will be called by ProjectMemory but are implemented by tiers tools
     **/
    public interface IExternalProjectMemory {
        /**
         * Fetches last modifications from the database
         * @param timeStamp		
         *		a <i>server-side</i> timestamp : 
         *			the client should take care about an eventual time-unsynchronization
         *			because the server does not know all its clients
         **/
        Iterable<ContextTMXEntry> findChanges (long timeStamp) throws Exception;

        /**
         * Register current translation to a server. 
         * The implementation <i>must</i> use the context from the entry (default or entry key),
         * which means not only to store the context, 
         * but to ensure that findChanges will not return two entries with same context.
         */
        void registerTranslation(ContextTMXEntry entry) throws Exception;
        
        /**
         * Removes from your server the entry with the following source and context (default or entry key).
         */
        void removeTranslation(ContextTMXEntry entry) throws Exception;        
    }
    
    /** This extension also enables to use the provider as a translation memory, which makes useless to keep orphans **/
    public interface IConvertibleExternalProjectMemory extends IExternalProjectMemory {
        /** 
         * Creates a translation memory provider for orphan segments.
         * Note : the provider <i>should</i> implement a way to remove non-orphan segments: else, they will be merged with the contents of project memory during search.
         **/
        public IExternalMemory asTranslationMemory() throws Exception;
    }
    

    private IExternalProjectMemory memory;
    private IProject project;
    private long timeStamp = 0;
    // force to have less than 1 query per given period (default, 1 second)
    // this does not mean that there will be 1 query per period, but less: 
    // if the user activates several entries during a second, only the first one will generate a refresh
    private long timeStamp_interval = 1000;
    // whenever to keep in local memory segments which are orphan locally, coming from the server
    // orphans do not appear in the editor, they are only kept for search;
    // a good reason to avoid them is if you know that the server contains a lot more segments than the local project
    // if not set by the properties, it is true only if during first call we retrieve less than 2 times the project size.
    private Boolean keepOrphans = null;
    
    public static final long TIMESTAMP_REFRESH = -1L;
    
    public ProjectMemory(
        Language sourceLanguage, Language targetLanguage, boolean isSentenceSegmentingEnabled, File saveTmxFile, CheckOrphanedCallback callback,
        IProject project, File propertiesFile
    ) throws Exception {
        super(sourceLanguage, targetLanguage, isSentenceSegmentingEnabled, saveTmxFile, callback);
        this.project = project;
        Properties config = new Properties();
        FileInputStream fis = new FileInputStream (propertiesFile); config.load (fis); fis.close();
        Class<?> memoryClass = PluginUtils.getPluginsClassLoader().loadClass(config.getProperty("class"));
        Constructor<?> cons = (Constructor<?>) memoryClass.getConstructor (Properties.class);
        this.memory = (IExternalProjectMemory) cons.newInstance (config);
        try {
            timeStamp_interval = Long.parseLong(config.getProperty("timestamp.interval")); 
            Log.log ("Created project memory with timestamp interval " + timeStamp_interval + " milliseconds");
        } catch (Exception e) { 
            /* not declared? not an error */
            Log.log ("Created project memory with default timestamp interval (1 second)");
        }
        if (config.getProperty("project.reuse-save_tmx") != null) 
            try { timeStamp = new File (project.getProjectProperties().getProjectInternal() + "/project_save.tmx").lastModified(); } catch (Exception estamp) {}
        if (config.getProperty("distant.keep-orphans") != null) 
            keepOrphans = "true".equals (config.getProperty("distant.keep-orphans"));
        else if (memory instanceof IConvertibleExternalProjectMemory)
            if (project == Core.getProject())	// project-save.tmx
                project.getTransMemories().put(OStrings.getString("CT_DISTANT_ORPHAN_STRINGS"), ((IConvertibleExternalProjectMemory) memory).asTranslationMemory());
            else	// other language
                project.getTransMemories().put(OStrings.getString("CT_DISTANT_ORPHAN_STRINGS") + " (" + targetLanguage + ")", ((IConvertibleExternalProjectMemory) memory).asTranslationMemory());
        CoreEvents.registerEntryEventListener(this);
    }
    
    public void onNewFile(String activeFileName) {}
    
    public void onEntryActivated(SourceTextEntry newEntry) {
        try {
            Set<Integer> changes = refresh();
            if (changes.size() > 0) {
                Core.getEditor().commitAndDeactivate();
                Core.getEditor().refreshEntries (changes);
                Core.getEditor().activateEntry(); 
            }
        } catch (Exception e) {
            e.printStackTrace();
            JOptionPane.showMessageDialog(null, "Could not synchronize with server:\n" + e.getClass().getName() + " : " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
        }
    }
    
    private synchronized Set<Integer> refresh() throws Exception {
        if (timeStamp == TIMESTAMP_REFRESH) return Collections.emptySet(); // do not call the server during refresh
        else if (System.currentTimeMillis() - timeStamp < timeStamp_interval) return Collections.emptySet(); // do not call the server when user is too fast

        this.timeStamp = TIMESTAMP_REFRESH; // flag to say that we are doing refresh
        final List<SourceTextEntry> sourceEntries = Core.getProject().getAllEntries();
        final Map<EntryKey, ContextTMXEntry> orphans = (keepOrphans == null) ? new HashMap<EntryKey, ContextTMXEntry>() : null;
        final Set<Integer> changedSegments = new HashSet<Integer>();
        for (ContextTMXEntry tmx0: memory.findChanges (this.timeStamp))
            if (tmx0.entryNum > 0) {	// the entry has been identified inside the project 
                project.setTranslation (sourceEntries.get(tmx0.entryNum - 1), tmx0, tmx0.isDefault, tmx0.linked);
                changedSegments.add (tmx0.entryNum);
                for (SourceTextEntry dup: Core.getProject().getAllEntries().get(tmx0.entryNum - 1).getDuplicates()) changedSegments.add (dup.entryNum());
            } else // orphan entry 
                if ((keepOrphans == null) || (keepOrphans == true)) {	
                    EntryKey eKey = new EntryKey("unknown", tmx0.source, "unknown" + tmx0.hashCode(),"unknown","unknown","unknown");
                    if (tmx0.otherProperties != null) // try to read entry key from properties
                        eKey = new EntryKey (StringUtil.nvl(tmx0.getPropValue(PROP_FILE), "unknown"), tmx0.source,  
                            StringUtil.nvl(tmx0.getPropValue(PROP_ID), "unknown" + tmx0.hashCode()),
                            StringUtil.nvl(tmx0.getPropValue(PROP_PREV), "unknown"), StringUtil.nvl(tmx0.getPropValue(PROP_NEXT), "unknown"),
                            StringUtil.nvl(tmx0.getPropValue(PROP_PATH), "unknown"));
                    if (keepOrphans == null) {	// create a temporary list 
                        if (tmx0.isDefault) 
                            orphans.put (new EntryKey(null, tmx0.source, null,null,null,null), tmx0);
                        else 
                            orphans.put (eKey, tmx0);
                        if (orphans.size() > sourceEntries.size() * 2) keepOrphans = false;	// now a real boolean, not null
                    }
                    else if (keepOrphans == true)	// external orphan segments are saved in project-save
                        project.setTranslation (new SourceTextEntry(eKey, -1, null, Collections.EMPTY_LIST, true), tmx0, tmx0.isDefault, tmx0.linked);
                }
        if (keepOrphans == null) { // first call, but now we have the size of the map.
            for (Map.Entry<EntryKey,ContextTMXEntry> me: orphans.entrySet()) 
                project.setTranslation (new SourceTextEntry(me.getKey(), -1, null, Collections.EMPTY_LIST, true), me.getValue(), me.getValue().isDefault, me.getValue().linked);
            keepOrphans = true;
        }
        this.timeStamp = System.currentTimeMillis();
        return changedSegments;
    }

    /**
     * Set new translation.
     * 
     * Save the translation in ProjectTmx hashmaps, then in the database.
     */
    @Override
    public void setTranslation(SourceTextEntry ste, TMXEntry te, boolean isDefault) {
        super.setTranslation(ste, te, isDefault);
        if (this.timeStamp == TIMESTAMP_REFRESH) return; // do not update database if we are refreshing...
        
        try {            
            if (te == null) memory.removeTranslation (new ContextTMXEntry (ste, isDefault));
            else memory.registerTranslation (new ContextTMXEntry (te, ste));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
}
