/**************************************************************************
 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.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 javax.xml.bind.DatatypeConverter;

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


/**
 * 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 outputFileName) throws IOException {
        StringBuffer buf = new StringBuffer();
        try (InputStream is = new FileInputStream (xliff.getPath()); BufferedReader reader = new BufferedReader(new InputStreamReader (is, "UTF-8"))) {
            String line = ""; do { line = reader.readLine(); } while (! 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 = DatatypeConverter.parseBase64Binary(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) {
        return getNativeSourceFile(currentFile, 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, 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 {
            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()) {	// 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()) {	// 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), 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() {
		for (IProject.FileInfo info : Core.getProject().getProjectFiles())
			if (info.filePath.equals(Core.getEditor().getCurrentFile()))
				return org.omegat.filters4.xml.xliff.AbstractXliffFilter.class.isAssignableFrom(info.filterClass);
		return false;
	}
	
    /** 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
                + midName, sourceRoot);	// 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, null, 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, null, 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.emptyList(), true), pte, true, null);
			// Undercut: check if one native entry corresponds to several xliff entries
			for (SourceTextEntry ste: p.getAllEntries()) {
				if (p.getTranslationInfo(ste).isTranslated()) continue;
				untranslatedBefore++;
				undercuts += checkSuperSegmentation (p, ste);
			}			
            p.saveProject(false); p.compileProject (oriFile.substring(xProps.getSourceRoot().length())); p.closeProject();
			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());
                        
            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);
        } 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
}
