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

 Copyright (C) 2012-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.filehooks;

import java.io.*;
import java.util.zip.*;
import java.util.regex.*;

import java.util.Base64;
import java.util.List;
import java.util.LinkedList;

import java.nio.file.StandardCopyOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.omegat.core.Core;
import org.omegat.core.data.IProject;
import org.omegat.core.data.RealProject;
import org.omegat.core.data.SourceTextEntry;
import org.omegat.core.data.ProjectProperties;
import org.omegat.util.Preferences;
import org.omegat.core.segmentation.Segmenter;
import org.omegat.core.threads.LongProcessInterruptedException;
import org.omegat.core.data.EntryKey;
import org.omegat.core.data.SourceTextEntry;
import org.omegat.core.data.PrepareTMXEntry;
import org.omegat.core.data.TMXEntry;


/**
 * Hook used by XLIFF files: can get native file, and call cross-compilation
 * 
 * @author Thomas CORDONNIER
 */
public class XliffFileHook implements IFileHook {
    public static final String ORIGINAL_NATIVE_DIR = "original";		// For Okapi
    public static final String SOURCE_NATIVE_DIR = "source-native", COMPILED_NATIVE_DIR = "target-native";		// For Trados, asked by client 

    // Info declared inside the sdlxliff file : Studio wants to have exactly the same!
    protected static class FileInfo {
        File original;
        String srcLang, traLang;
        
        public boolean exists() { return original.exists(); }
    }
    
    public static final Pattern ORI_FIND_PATTERN = Pattern.compile("original\\s*=\\s*\\\"(.+?)\\\"");
    public static final Pattern SRC_LANG_PATTERN = Pattern.compile("source-language\\s*=\\s*\\\"(.+?)\\\"");
    public static final Pattern TRA_LANG_PATTERN = Pattern.compile("target-language\\s*=\\s*\\\"(.+?)\\\"");
    
    public static FileInfo readOriginalFileInfo(File sdlxliff) throws IOException {
        try (InputStream is = new java.io.FileInputStream (sdlxliff); BufferedReader reader = new BufferedReader(new InputStreamReader (is))) {
			String line = ""; do { line = reader.readLine(); } while (! line.contains("<file"));
            FileInfo res = new FileInfo();
            Matcher m = ORI_FIND_PATTERN.matcher(line); if (m.find()) res.original = new File(m.group(1));
            m = SRC_LANG_PATTERN.matcher(line); if (m.find()) res.srcLang = m.group(1);
            m = TRA_LANG_PATTERN.matcher(line); if (m.find()) res.traLang = m.group(1);
            org.omegat.util.Log.log ("readOriginalFileName In file "+ sdlxliff + ": <file original='" + res.original + "'>");
            return res;
		}
	}	
    
    public static File readOriginalFileName (String sdlxliff) throws IOException { return readOriginalFileInfo(new File(sdlxliff)).original; }
	
    public static void extractBase64 (File xliff, String subFileName, String outputFileName) throws IOException {
        StringBuffer buf = new StringBuffer(); boolean inFile = false;
        try (InputStream is = new FileInputStream (xliff.getPath()); BufferedReader reader = new BufferedReader(new InputStreamReader (is, "UTF-8"))) {
            String line = ""; do { 
                line = reader.readLine(); 
                Matcher m = ORI_FIND_PATTERN.matcher(line); if (m.find()) {
                    if (subFileName == null) inFile = true; else inFile = subFileName.equals(m.group(1));
                }
            } while (! (inFile && line.contains("<internal-file")));
            line = line.substring(line.indexOf("<internal-file")); line = line.substring(line.indexOf('>') + 1); buf.append(line);
            do { line = reader.readLine(); buf.append(line); } while (! line.contains("</internal-file>"));
            buf.setLength(buf.indexOf("<")); 
        }
		base64ToFile (buf, outputFileName);
	}
	
	protected static void base64ToFile(StringBuffer buf, String outputFileName) throws IOException {
        try (OutputStream os = new FileOutputStream (outputFileName)) { 
            byte[] base64Zip = Base64.getDecoder().decode(buf.toString());
            ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream (base64Zip));
            ZipEntry ZE = zis.getNextEntry();
            int len; byte[] buffer = new byte[1024]; while ((len = zis.read(buffer)) > 0) os.write(buffer, 0, len);
        }
    }
    
    @Override
    public String getNativeSourceFile (String currentFile, SourceTextEntry entry) {
        return getNativeSourceFile(currentFile, entry, Core.getProject().getProjectProperties().getSourceRoot());
    }
		
	// Can be called by other XLIFF hooks in case the file is not in source root
	public String getNativeSourceFile (String currentFile, SourceTextEntry entry, String baseDir) {
        currentFile = currentFile.replace("/", File.separator);
        final String paramFileName = currentFile;
        currentFile = currentFile.substring(0, currentFile.lastIndexOf('.'));
        File handler = new File (currentFile);
        if (handler.exists()) 
            try { return handler.getCanonicalPath(); } catch (Exception e) { return currentFile; }
        // else 
        try {
            // 1. Try in ORIGINAL_NATIVE_DIR
            currentFile = currentFile.substring(baseDir.length());
            handler = new File (baseDir).getParentFile();
            handler = new File (handler, ORIGINAL_NATIVE_DIR);
            currentFile = handler.getCanonicalPath() + File.separator + currentFile;
            handler = new File (currentFile); if (handler.exists()) return currentFile;
            // 2. Try to read from SOURCE_NATIVE
            handler = new File (handler.getParentFile().getParentFile(), SOURCE_NATIVE_DIR);
            currentFile = currentFile.substring(Core.getProject().getProjectProperties().getProjectRoot().length() + ORIGINAL_NATIVE_DIR.length() + 1);
            currentFile = handler.getCanonicalPath() + File.separator + currentFile;
            handler = new File (currentFile); if (handler.exists()) return currentFile;
            // 3. Use the entry key
            try { // works only with files created with filter4 from 3.4-TEST or 3.5-DEV or later
                currentFile = entry.getKey().path; currentFile = currentFile.substring(0, currentFile.indexOf("##"));
                System.err.println("For entry " + entry.entryNum() + " key = " + currentFile);
                handler = new File (currentFile); if (handler.exists()) return currentFile;
                handler = new File (Core.getProject().getProjectProperties().getProjectRoot()); handler = new File (handler, SOURCE_NATIVE_DIR);                
                String key = currentFile;
                currentFile = currentFile.replace("\\", File.separator); currentFile = currentFile.substring(currentFile.lastIndexOf(File.separator) + File.separator.length());
                currentFile = handler.getCanonicalPath() + File.separator + currentFile;
                extractBase64(new File(paramFileName), key, currentFile); return currentFile;                
            } catch (Exception e) {
                org.omegat.util.Log.log(e); 
            }
            // 4. try to read file declared inside the <file> markup
            handler = readOriginalFileInfo(new File (baseDir // +  java.io.File.separator
                +  paramFileName.substring(baseDir.length()))).original;
            if (handler.exists()) return handler.getCanonicalPath();
            else {
                String ext = handler.getName(); ext = ext.substring(ext.lastIndexOf('.'));
                handler = new File (Core.getProject().getProjectProperties().getProjectRoot()); handler = new File (handler, SOURCE_NATIVE_DIR);
                currentFile = paramFileName.substring(baseDir.length());
                if (currentFile.contains(File.separator)) handler = new File (handler, currentFile).getParentFile(); 
                handler.mkdirs();
                currentFile = handler.getCanonicalPath() + File.separator + paramFileName.substring(paramFileName.lastIndexOf(File.separator) + 1);
                currentFile = currentFile.substring(0, currentFile.lastIndexOf('.'));
                if (! currentFile.substring(currentFile.lastIndexOf(File.separator) + 1).contains(".")) currentFile += ext;
                extractBase64(new File(paramFileName), null, currentFile);
                return currentFile;
            }
        } catch (Exception e) {
            org.omegat.util.Log.log(e); 
        }
        return currentFile;
    }
    
    protected Exception lastCompileException = null;
    public Exception lastCompileException() { return lastCompileException; } 
    
    protected String nativeTarget = null;
    @Override public String getNativeTargetFile (String currentFile) { return nativeTarget; }
    
	@Override
	public boolean supportsPseudoTags() {
		return org.omegat.filters4.xml.xliff.AbstractXliffFilter.class.isAssignableFrom(IFileHook.getFilterClass(Core.getEditor().getCurrentFile(), Core.getProject()));
	}
	
    /** Cross-compilation: creates an OmegaT project with source = native file and compile it with current memory **/
    @Override 
    public void postCompile(File destDir, String midName) {
		postCompile (destDir, midName, Core.getProject().getProjectProperties().getSourceRoot());
	}
	
	public void postCompile(File destDir, String midName, String sourceRoot) {
        lastCompileException = null;
        ProjectProperties oriProps = Core.getProject().getProjectProperties();
        try {
            String oriFile = getNativeSourceFile(sourceRoot // +  java.io.File.separator
                + Core.getEditor().getCurrentFile(), Core.getEditor().getCurrentEntry());	// find the file, eventually extract it
            final File fori = new File(oriFile); if (! fori.exists()) {
                lastCompileException = new java.io.FileNotFoundException ("Original file (" + oriFile + ") not found");
                return;
            }
            Core.getMainWindow().showProgressMessage("Try to call cross-compilation");
            File root = new File (oriProps.getProjectRoot() + File.separator + "X-compile"); root.mkdirs();
            ProjectProperties xProps = new ProjectProperties(root);
            xProps.setSourceRoot(fori.getParent() + File.separator);
            xProps.setGlossaryRoot(oriProps.getGlossaryRoot());
            xProps.setWriteableGlossary(oriProps.getWriteableGlossary());
            xProps.setTMRoot(oriProps.getTMRoot());
            xProps.setDictRoot(oriProps.getDictRoot());
            xProps.setSourceLanguage(oriProps.getSourceLanguage());
            xProps.setTargetLanguage(oriProps.getTargetLanguage());
            xProps.setSourceTokenizer(oriProps.getSourceTokenizer());
            xProps.setTargetTokenizer(oriProps.getTargetTokenizer());
            RealProject p = new RealProject(xProps); p.createProject(); p.closeProject();
            Files.copy(
                Paths.get(oriProps.getProjectRoot() + "/omegat/project_save.tmx"), 
                new File(root, "/omegat/project_save.tmx").toPath(), 
                StandardCopyOption.REPLACE_EXISTING); 
            p = new RealProject(xProps); p.loadProject(true); 
            int untranslatedBefore = 0, undercuts = 0, overcuts = 0;
            // Overcut: check if one xliff entry corresponds to several native entries
            final Segmenter segmenter = Core.getSegmenter();
            final List<StringBuilder> spaces = new LinkedList<StringBuilder>();
            final List<PrepareTMXEntry> defaultEntries = new LinkedList<>();
            p.iterateByDefaultTranslations((String source, TMXEntry trans) -> {
                List<String> segSrc = segmenter.segment (oriProps.getSourceLanguage(), source, spaces, null);
                if (segSrc.size() < 2) return; // next loop
                List<String> segTra = segmenter.segment (oriProps.getTargetLanguage(), trans.translation, spaces, null);
                if (segSrc.size() == segTra.size()) 					
                    for (int i = 0; i < segSrc.size(); i++) {
                        PrepareTMXEntry pte = new PrepareTMXEntry();
                        pte.source = segSrc.get(i); pte.translation = segTra.get(i);
                        defaultEntries.add(pte);
                    }
            });
            final java.util.Map<EntryKey,PrepareTMXEntry[]> altEntries = new java.util.HashMap<>();
            p.iterateByMultipleTranslations((EntryKey source, TMXEntry trans) -> {
                List<String> segSrc = segmenter.segment (oriProps.getSourceLanguage(), trans.source, spaces, null);
                if (segSrc.size() < 2) return; // next loop
                List<String> segTra = segmenter.segment (oriProps.getTargetLanguage(), trans.translation, spaces, null);
                if (segSrc.size() == segTra.size()) {					
                    PrepareTMXEntry[] tab = new PrepareTMXEntry[segSrc.size()]; altEntries.put (source, tab);
                    for (int i = 0; i < segSrc.size(); i++) {
                        PrepareTMXEntry pte = new PrepareTMXEntry();
                        pte.source = segSrc.get(i); pte.translation = segTra.get(i);
                        tab[i] = pte;
                    }
                }
            });
            for (PrepareTMXEntry pte: defaultEntries) // after build, to avoid iterator conflicts
                p.setTranslation (new SourceTextEntry(new EntryKey(null, pte.source, null, null, null, null), -1, null, java.util.Collections.EMPTY_LIST, true), pte, true, null);
            for (java.util.Map.Entry<EntryKey,PrepareTMXEntry[]> me: altEntries.entrySet()) 
                if (me.getKey().prev != null) 
                    for (int i = 0; i < me.getValue().length; i++) {
                        EntryKey eKey;
                        if (i == 0) eKey = new EntryKey(null, me.getValue()[i].source, null, me.getKey().prev, me.getValue()[i+1].source,null);
                        else if (i == me.getValue().length - 1) eKey = new EntryKey(null, me.getValue()[i].source, null, me.getValue()[i-1].source,me.getKey().next,null);
                        else eKey = new EntryKey(null, me.getValue()[i].source, null, me.getValue()[i-1].source, me.getValue()[i+1].source,null);
                        p.setTranslation (new SourceTextEntry(eKey, -1, null, java.util.Collections.EMPTY_LIST, true), me.getValue()[i], false, null);
                    }
                else  // set as default entry, we cannot create several entry keys
                    for (PrepareTMXEntry pte: me.getValue())
                        p.setTranslation (new SourceTextEntry(new EntryKey(null, pte.source, null, null, null, null), -1, null, java.util.Collections.EMPTY_LIST, true), pte, true, null);
            // Undercut: check if one native entry corresponds to several xliff entries
            for (SourceTextEntry ste: p.getAllEntries()) {
                if (p.getTranslationInfo(ste.getKey()).isTranslated()) continue;
                untranslatedBefore++;
                undercuts += checkSuperSegmentation (p, ste);
                
                if (ste.getDuplicate() == SourceTextEntry.DUPLICATE.NONE) { // Check multiple translations, in case only key differs
                    final IProject p2 = p;
                    p.iterateByMultipleTranslations((EntryKey source, TMXEntry trans) -> {
                        if (! source.sourceText.equals(ste.getSrcText())) return;
                        p2.setTranslation(ste, new PrepareTMXEntry(trans), true, null);
                    });
                }
            }	
            p.saveProject(false); p.compileProject (oriFile.substring(xProps.getSourceRoot().length())); p.closeProject();
                        
            nativeTarget = xProps.getTargetRoot() // +  java.io.File.separator
                + p.getTargetPathForSourceFile(fori.getName());
            File nativeDest = new File(oriProps.getProjectRoot(), COMPILED_NATIVE_DIR); nativeDest.mkdirs();
            nativeDest = new File(nativeDest, p.getTargetPathForSourceFile(fori.getName()));
            Files.copy(Paths.get(nativeTarget), nativeDest.toPath(), StandardCopyOption.REPLACE_EXISTING);
            if (Preferences.isPreferenceDefault(Preferences.TRADOS_RENAME_METHOD, false)) {
                String name = nativeDest.getName();
                name = name.substring(0, name.lastIndexOf('.')) + "-xCompile" + name.substring(name.lastIndexOf('.'));
                nativeDest.renameTo(new File(nativeDest.getParentFile(), name));
            }
            nativeTarget = nativeDest.getCanonicalPath();
            if (! Preferences.isPreferenceDefault(Preferences.TRADOS_KEEP_INTERMEDIATE, false)) org.omegat.util.StaticUtils.deltree (root);
            System.err.println("Before undercut : " + untranslatedBefore + "/" + p.getAllEntries().size());
            System.err.println("After undercut : " + (untranslatedBefore + undercuts) + "/" + p.getAllEntries().size());
            System.err.println("After overcut : " + (untranslatedBefore + undercuts + defaultEntries.size() + altEntries.size()) + "/" + p.getAllEntries().size());
        } catch (Exception e) {
            lastCompileException = e; e.printStackTrace();
        }
    }
    
    // Check that the current entry corresponds to two or more entries in the xliff
    private static int checkSuperSegmentation(RealProject p, SourceTextEntry ste) {
        StringBuffer sourceText = new StringBuffer(ste.getSrcText()); StringBuffer tra = new StringBuffer();
        final List<PrepareTMXEntry> addedEntries = new LinkedList<>();
        while (sourceText.length() > 0) {
            try {
                p.iterateByDefaultTranslations((String source, TMXEntry trans) -> {
                    if (sourceText.toString().equals(source)) {
                        PrepareTMXEntry pte = new PrepareTMXEntry();
                        pte.source = ste.getSrcText(); pte.translation = tra.toString();
                        p.setTranslation (ste, pte, true, null); addedEntries.add(pte);
                        sourceText.setLength(0); throw new LongProcessInterruptedException();
                    }
                    if (sourceText.toString().startsWith(source)) {
                        tra.append(trans.translation); sourceText.delete(0, source.length());
                        while (sourceText.toString().startsWith(" ")) { tra.append(" "); sourceText.delete(0,1); }
                        throw new LongProcessInterruptedException();
                    }
                });
                // We are in the end of the loop
                if (tra.length() > 0) {
                    PrepareTMXEntry pte = new PrepareTMXEntry();
                    pte.source = ste.getSrcText(); pte.translation = tra.toString() + sourceText;
                    p.setTranslation (ste, pte, true, null);
                }
                return addedEntries.size();
            } catch (LongProcessInterruptedException stop) {
                // continue while loop
            } catch (Exception other) {
                return addedEntries.size();
            }
        } 
        return addedEntries.size();
    }
    
    @Override public boolean supportsOpenInStudio(String file) { return file.endsWith(".sdlxliff"); } // Not for .xlf
}
