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

 Copyright (C) 2000-2006 Keith Godfrey and Maxym Mykhalchuk
               2008 Didier Briel, Alex Buloichik
               2009 Didier Briel
               2012 Didier Briel, Aaron Madlon-Kay
               2013 Aaron Madlon-Kay, Guido Leenders
               2014 Aaron Madlon-Kay, Alex Buloichik
               2015 Aaron Madlon-Kay, Thomas Cordonnier
               2017-2019 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.util;

import java.io.File;
import java.io.IOException;
import java.util.*;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamWriter;

import org.omegat.core.data.ProjectProperties;
import org.omegat.core.segmentation.SRX;
import org.omegat.filters2.TranslationException;
import org.omegat.filters2.master.FilterMaster;
import org.omegat.filters2.master.PluginUtils;

/**
 * Class that reads and saves project definition file.
 * 
 * @author Keith Godfrey
 * @author Maxym Mykhalchuk
 * @author Didier Briel
 * @author Alex Buloichik (alex73mail@gmail.com)
 * @author Aaron Madlon-Kay
 * @author Guido Leenders
 * @author Thomas Cordonnier
 */
public class ProjectFileStorage {

    private static final XMLInputFactory staxInputFactory = XMLInputFactory.newInstance();	

    public static ProjectProperties loadProjectProperties(File projectDir) throws Exception {
        ProjectProperties result = new ProjectProperties(projectDir);

        File inFile = new File(projectDir, OConsts.FILE_PROJECT);

        result.getSourceRootExcludes().clear(); result.getSourceRootExcludes().addAll(Arrays.asList(ProjectProperties.DEFAULT_EXCLUDES));
        
        XMLStreamReader reader = staxInputFactory.createXMLStreamReader(new java.io.FileInputStream(inFile));
        boolean inProj = false; Map<String, String> params = new HashMap<>();
        while (reader.hasNext())
            if (reader.next() == XMLStreamReader.START_ELEMENT)
                if (reader.getName().getLocalPart().equals("project")) {
                    if (!OConsts.PROJ_CUR_VERSION.equals(reader.getAttributeValue(null,"version"))) 
                        throw new TranslationException(StringUtil.format(
                                OStrings.getString("PFR_ERROR_UNSUPPORTED_PROJECT_VERSION"),
                                reader.getAttributeValue(null,"version")));
                    inProj = true;
                }
                else if (inProj) {
                    String name = reader.getName().getLocalPart(); StringBuffer buf = new StringBuffer();
                    if (name.equals("source_dir_excludes")) {
                        result.getSourceRootExcludes().clear();
                    EXCLUDES:
                        while (reader.hasNext()) {
                            int next = reader.next();
                            if ((next == XMLStreamReader.START_ELEMENT) && (reader.getName().getLocalPart().equals("mask")))
                                buf.setLength(0);
                            else if (reader.hasText())
                                buf.append(reader.getText());
                            else if (next == XMLStreamReader.END_ELEMENT) {
                                result.getSourceRootExcludes().add(buf.toString());
                                if (reader.getName().getLocalPart().equals("source_dir_excludes")) break EXCLUDES;
                            }
                        }	
                    } else {
                        while (! ((reader.next() == XMLStreamReader.END_ELEMENT) && (reader.getName().getLocalPart().equals(name))))
                            buf.append(reader.getText());
                        params.put(name, buf.toString());
                    }
                }
                
        // if folder is in default locations, name stored as __DEFAULT__
        String m_root = inFile.getParentFile().getAbsolutePath() + File.separator;

        /*result.setTargetRoot(computeAbsolutePath(m_root, params.get("target_dir"), OConsts.DEFAULT_TARGET));*/
        result.setSourceRoot(computeAbsolutePath(m_root, params.get("source_dir"), OConsts.DEFAULT_SOURCE));
        
        result.setTMRoot(computeAbsolutePath(m_root, params.get("tm_dir"), OConsts.DEFAULT_TM));
        result.setGlossaryRoot(computeAbsolutePath(m_root, params.get("glossary_dir"), OConsts.DEFAULT_GLOSSARY));

        // Compute glossary file location
        String glossaryFile = params.get("glossary_file");
        if (StringUtil.isEmpty(glossaryFile)) {
            glossaryFile = OConsts.DEFAULT_FOLDER_MARKER;
        }
        if (glossaryFile.equalsIgnoreCase(OConsts.DEFAULT_FOLDER_MARKER)) {
            glossaryFile = result.computeDefaultWriteableGlossaryFile();
        } else if (! new File(glossaryFile).isAbsolute()) {
            String absGlossaryRoot = computeAbsolutePath(m_root, result.getGlossaryRoot(), OConsts.DEFAULT_GLOSSARY);
            glossaryFile = new File(absGlossaryRoot, glossaryFile).getPath();
        }
        result.setWriteableGlossary(glossaryFile);

        result.setDictRoot(computeAbsolutePath(m_root, params.get("dictionary_dir"), OConsts.DEFAULT_DICT));

        result.setSourceLanguage(params.get("source_lang"));
        result.setTargetLanguage(params.get("target_lang"));

        result.setSourceTokenizer(loadTokenizer(params.get("source_tok"), result.getSourceLanguage()));
        result.setTargetTokenizer(loadTokenizer(params.get("target_tok"), result.getTargetLanguage()));
        
        result.setSentenceSegmentingEnabled("true".equalsIgnoreCase(params.get("sentence_seg")));
        result.setSupportDefaultTranslations("true".equalsIgnoreCase(params.get("support_default_translations")));
        result.setRemoveTags("true".equalsIgnoreCase(params.get("remove_tags")));
        result.setExternalCommand(params.get("external_command"));

        return result;
    }
    
    private static final XMLOutputFactory staxOutputFactory = XMLOutputFactory.newInstance();	

    /**
     * Saves project file to disk.
     */
    public static void writeProjectFile(ProjectProperties props) throws Exception {
        File outFile = new File(props.getProjectRoot(), OConsts.FILE_PROJECT);
        String m_root = outFile.getParentFile().getAbsolutePath() + File.separator;
        
        try (java.io.FileOutputStream fos = new java.io.FileOutputStream(outFile)) {
            XMLStreamWriter writer = staxOutputFactory.createXMLStreamWriter(fos, "UTF-8");
            writer.writeStartDocument("UTF-8", "1.0");
            writer.writeStartElement("omegat"); 
            writer.writeCharacters("\n    "); writer.writeStartElement("project"); writer.writeAttribute("version", OConsts.PROJ_CUR_VERSION);
            writer.writeCharacters("\n        "); writer.writeStartElement("source_dir"); writer.writeCharacters(computeRelativePath(m_root, props.getSourceRoot(), OConsts.DEFAULT_SOURCE)); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("source_dir_excludes"); 
            for (String mask: props.getSourceRootExcludes()) { writer.writeCharacters("\n            "); writer.writeStartElement("source_dir_excludes"); writer.writeCharacters(mask); writer.writeEndElement(); }
            writer.writeCharacters("\n        "); writer.writeEndElement(); // source_dir_excludes
            //writer.writeStartElement("target_dir"); writer.writeCharacters(computeRelativePath(m_root, props.getTargetRoot(), OConsts.DEFAULT_TARGET)); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("tm_dir"); writer.writeCharacters(computeRelativePath(m_root, props.getTMRoot(), OConsts.DEFAULT_TM)); writer.writeEndElement();
            String glossaryFile = computeRelativePath(props.getGlossaryRoot(), props.getWriteableGlossary(), null); // Rel file name
            String glossaryDir = computeRelativePath(m_root, props.getGlossaryRoot(), OConsts.DEFAULT_GLOSSARY);
            if (glossaryDir.equalsIgnoreCase(OConsts.DEFAULT_FOLDER_MARKER) && props.isDefaultWriteableGlossaryFile()) {
                // Everything equals to default
                glossaryFile = OConsts.DEFAULT_FOLDER_MARKER;
            }
            writer.writeCharacters("\n        "); writer.writeStartElement("glossary_dir"); writer.writeCharacters(glossaryDir); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("glossary_file"); writer.writeCharacters(glossaryFile); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("dictionary_dir"); writer.writeCharacters(computeRelativePath(m_root, props.getDictRoot(), OConsts.DEFAULT_DICT)); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("source_lang"); writer.writeCharacters(props.getSourceLanguage().toString()); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("target_lang"); writer.writeCharacters(props.getTargetLanguage().toString()); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("source_tok"); writer.writeCharacters(props.getSourceTokenizer().getCanonicalName()); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("target_tok"); writer.writeCharacters(props.getTargetTokenizer().getCanonicalName()); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("sentence_seg"); writer.writeCharacters(Boolean.toString(props.isSentenceSegmentingEnabled())); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("remove_tags"); writer.writeCharacters(Boolean.toString(props.isRemoveTags())); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("support_default_translations"); writer.writeCharacters(Boolean.toString(props.isSupportDefaultTranslations())); writer.writeEndElement();
            writer.writeCharacters("\n        "); writer.writeStartElement("external_command"); writer.writeCharacters(props.getExternalCommand()); writer.writeEndElement();			
            writer.writeCharacters("\n    "); writer.writeEndElement(); writer.writeCharacters("\n"); writer.writeEndElement(); // </project></omegat>
            writer.close();
        }

        SRX.saveTo(props.getProjectSRX(), new File(props.getProjectInternal()));
        FilterMaster.saveConfig(props.getProjectFilters(), props.getProjectInternal());
    }

    /**
     * Returns absolute path for any project's folder. Since 1.6.0 supports relative paths (RFE 1111956).
     * 
     * @param relativePath
     *            relative path from project file.
     * @param defaultName
     *            default name for such a project's folder, if relativePath is "__DEFAULT__".
     */
    private static String computeAbsolutePath(String m_root, String relativePath, String defaultName) {
        if (relativePath == null) {
            // Not exist in project file ? Use default.
            return m_root + defaultName + File.separator;
        }
        if (OConsts.DEFAULT_FOLDER_MARKER.equals(relativePath))
            return m_root + defaultName + File.separator;
        else {
            try {
                // check if path starts with a system root
                boolean startsWithRoot = false;
                for (File root : File.listRoots()) {
                    try // Under Windows and Java 1.4, there is an exception if
                    { // using getCanonicalPath on a non-existent drive letter
                      // [1875331] Relative paths not working under
                      // Windows/Java 1.4
                        String platformRelativePath = relativePath.replace('/', File.separatorChar);
                        // If a plaform-dependent form of relativePath is not
                        // used, startWith will always fail under Windows,
                        // because Windows uses C:\, while the path is stored as
                        // C:/ in omegat.project
                        startsWithRoot = platformRelativePath.startsWith(root.getCanonicalPath());
                    } catch (IOException e) {
                        startsWithRoot = false;
                    }
                    if (startsWithRoot)
                        // path starts with a root --> path is already absolute
                        return new File(relativePath).getCanonicalPath() + File.separator;
                }

                // path does not start with a system root --> relative to
                // project root
                return new File(m_root, relativePath).getCanonicalPath() + File.separator;
            } catch (IOException e) {
                return relativePath;
            }
        }
    }

    /**
     * Returns relative path for any project's folder. If absolutePath has default location, returns
     * "__DEFAULT__".
     * 
     * @param absolutePath
     *            absolute path to project folder.
     * @param defaultName
     *            default name for such a project's folder.
     * @since 1.6.0
     */
    private static String computeRelativePath(String m_root, String absolutePath, String defaultName) {
        if (defaultName != null && new File(absolutePath).equals(new File(m_root, defaultName))) {
            return OConsts.DEFAULT_FOLDER_MARKER;
        }

        try {
            // trying to look two folders up
            String res = absolutePath;
            File abs = new File(absolutePath).getCanonicalFile();
            File root = new File(m_root).getCanonicalFile();
            String prefix = "";
            //
            // Try to derive the absolutePath as a relative path
            // from root.
            // First test whether the exact match is possible.
            // Then on each try, one folder is moved up from the root.
            //
            // Currently, maximum MAX_PARENT_DIRECTORIES_ABS2REL levels up.
            // More than these directory levels different seems to be that the paths
            // were not intended to be related.
            //
            for (int i = 0; i < OConsts.MAX_PARENT_DIRECTORIES_ABS2REL; i++) {
                //
                // File separator added to prevent "/MyProject EN-FR/"
                // to be understood as being inside "/MyProject/" [1879571]
                //
                 if ((abs.getPath() + File.separator).startsWith(root.getPath() + File.separator)) {
                     res = prefix + abs.getPath().substring(root.getPath().length());
                     if (res.startsWith(File.separator))
                        res = res.substring(1);
                    break;
                } else {
                    root = root.getParentFile();
                    prefix += File.separator + "..";
                    //
                    // There are no more parent paths.
                    //
                    if (root == null) {
                      break;
                    }
                }
            }
            return res.replace(File.separatorChar, '/');
        } catch (IOException e) {
            return absolutePath.replace(File.separatorChar, '/');
        }
    }
    
    /**
     * Load a tokenizer class from its canonical name.
     * @param className Name of tokenizer class
     * @return Class object of specified tokenizer, or of fallback tokenizer
     * if the specified one could not be loaded for whatever reason.
     */
    private static Class<?> loadTokenizer(String className, Language fallback) {
        if (!StringUtil.isEmpty(className)) {
            try {
                return ProjectFileStorage.class.getClassLoader().loadClass(className);
            } catch (ClassNotFoundException e) {
                Log.log(e.toString());
            }
        }
        return PluginUtils.getTokenizerClassForLanguage(fallback);
    }
}
