/*
 * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License.
 * you may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.wso2.carbon.mediation.initializer.utils;

import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.axis2.engine.AxisConfiguration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.config.xml.SynapseImportSerializer;
import org.apache.synapse.libraries.imports.SynapseImport;
import org.wso2.carbon.mediation.initializer.ServiceBusConstants;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;

/**
 * Utility class containing util functions to initialize synapse artifacts
 */
public class SynapseArtifactInitUtils {

    private static final Log log = LogFactory.getLog(SynapseArtifactInitUtils.class);
    private static final String CONNECTOR_XML = "connector.xml";

    private static String APP_UNZIP_DIR;
    private static boolean isAppDirCreated = false;

    static {
        String javaTmpDir = System.getProperty("java.io.tmpdir");
        APP_UNZIP_DIR = javaTmpDir.endsWith(File.separator) ? javaTmpDir + ServiceBusConstants.SYNAPSE_LIB_CONFIGS :
                javaTmpDir + File.separator + ServiceBusConstants.SYNAPSE_LIB_CONFIGS;
    }

    public static String getAppUnzipDir() {
        return APP_UNZIP_DIR;
    }

    /**
     * Function to create synapse imports to enable installed connectors
     *
     * @param axisConfiguration axis configuration
     */
    public static void initializeConnectors (AxisConfiguration axisConfiguration) {
        String synapseLibPath = axisConfiguration.getRepository().getPath() +
                                        File.separator + ServiceBusConstants.SYNAPSE_LIB_CONFIGS;
        File synapseLibDir = new File(synapseLibPath);
        if (synapseLibDir.exists() && synapseLibDir.isDirectory()) {
            File[] connectorList = synapseLibDir.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    return name.endsWith(".zip");
                }
            });

            if (connectorList == null) {
                // No connectors found
                return;
            }

            //Check import related to the connector is available
            String importConfigDirPath = axisConfiguration.getRepository().getPath() +
                    ServiceBusConstants.SYNAPSE_IMPORTS_CONFIG_PATH;
            File importsDir = new File(importConfigDirPath);

            if (!importsDir.exists() && !importsDir.mkdirs()) {
                log.error("Import synapse config directory does not exists and unable to create: " +
                        importsDir.getAbsolutePath());
                // Retrying the same for other connectors is waste
                return;
            }

            for (File connectorZip : connectorList) {
                if (log.isDebugEnabled()) {
                    log.debug("Generating import for connector deployed with package: " + connectorZip.getName());
                }
                String connectorExtractedPath = null;
                try {
                    connectorExtractedPath = extractConnector(connectorZip.getAbsolutePath());
                } catch (IOException e) {
                    log.error("Error while extracting Connector zip : " + connectorZip.getAbsolutePath(), e);
                    continue;
                }
                String packageName = retrievePackageName(connectorExtractedPath);

                // Retrieve connector name
                String connectorName = connectorZip.getName().substring(0, connectorZip.getName().indexOf('-'));
                QName qualifiedName = new QName(packageName, connectorName);
                File importFile = new File(importsDir, qualifiedName.toString() + ".xml");

                if (!importFile.exists()) {
                    // Import file enabling file connector not available in synapse imports directory
                    if (log.isDebugEnabled()) {
                        log.debug("Generating import config to enable connector: " + qualifiedName);
                    }
                    generateImportConfig(qualifiedName, importFile);
                }
            }
        }
    }

    /**
     * Function to create import configuration enabling connector
     *
     * @param qualifiedName
     * @param targetImportFile
     */
    private static void generateImportConfig (QName qualifiedName, File targetImportFile) {
        SynapseImport synImport = new SynapseImport();
        synImport.setLibName(qualifiedName.getLocalPart());
        synImport.setLibPackage(qualifiedName.getNamespaceURI());
        synImport.setStatus(true);
        OMElement impEl = SynapseImportSerializer.serializeImport(synImport);

        if (impEl != null) {
            try (FileWriter fileWriter = new FileWriter(targetImportFile)) {
                fileWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + impEl.toString());
            } catch (IOException e) {
                log.error("Error occurred while writing import file: " + qualifiedName);
            }
        } else {
            log.error("Could not add Synapse Import. Invalid import params for libName : " +
                    qualifiedName.getLocalPart() + " packageName : " + qualifiedName.getNamespaceURI());
        }
    }


    private static String retrievePackageName(String extractedPath) {
        String packageName = null;
        File connectorXml = new File(extractedPath + CONNECTOR_XML);
        if (!connectorXml.exists()) {
            log.error("connector.xml file not found at : " + extractedPath);
        }

        try (InputStream xmlInputStream =  new FileInputStream(connectorXml)) {
            OMElement connectorDef = new StAXOMBuilder(xmlInputStream).getDocumentElement();
            OMAttribute packageAttr = connectorDef.getFirstElement().getAttribute(new QName("package"));
            if (packageAttr != null) {
                packageName = packageAttr.getAttributeValue();
            }
        } catch (XMLStreamException e) {
            log.error("Error while parsing the connector.xml file ", e);
        } catch (FileNotFoundException e) {
            log.error("connector.xml File cannot be loaded from " + extractedPath, e);
        } catch (IOException e) {
            log.error("Error occurred while reading: " + connectorXml.getPath());
        }
        return packageName;
    }



    /**
     * Extract the connector at the provided path to the java temp dir. Return the
     * extracted location
     *
     * @param connectorPath - Absolute path of the Carbon application .car file
     * @return - extracted location
     * @throws org.wso2.carbon.CarbonException - error on extraction
     */
    public static String extractConnector(String connectorPath) throws IOException {
        createTempDirectory();

        String tempConnectorPathFormatted = formatPath(connectorPath);
        String fileName = tempConnectorPathFormatted.substring(tempConnectorPathFormatted.lastIndexOf('/') + 1);
        String dest = getAppUnzipDir() + File.separator + System.currentTimeMillis() + fileName + File.separator;

        createDir(dest);

        extract(connectorPath, dest);
        return dest;
    }

    private static void createTempDirectory(){
        if(isAppDirCreated){
            return;
        }
        createDir(getAppUnzipDir());
        isAppDirCreated = true;
    }

    public static void createDir(String path) {
        File temp = new File(path);
        if (!temp.exists() && !temp.mkdirs()) {
            log.error("Error while creating directory : " + path);
            return;
        }

    }

    /**
     * Format the string paths to match any platform.. windows, linux etc..
     *
     * @param path - input file path
     * @return formatted file path
     */
    public static String formatPath(String path) {
        // removing white spaces
        String pathformatted = path.replaceAll("\\b\\s+\\b", "%20");
        try {
            pathformatted = java.net.URLDecoder.decode(pathformatted, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            log.error("Unsupported Encoding in the path :"+ pathformatted);
        }
        // replacing all "\" with "/"
        return pathformatted.replace('\\', '/');
    }

    private static void extract(String sourcePath, String destPath) throws IOException {
        Enumeration entries;
        ZipFile zipFile;

        zipFile = new ZipFile(sourcePath);
        entries = zipFile.entries();

        String canonicalDestPath = new File(destPath).getCanonicalPath();
        while (entries.hasMoreElements()) {
            ZipEntry entry = (ZipEntry) entries.nextElement();
            String canonicalEntryPath = new File(destPath + entry.getName()).getCanonicalPath();
            if(!canonicalEntryPath.startsWith(canonicalDestPath)){
                throw new IOException("Entry is outside of the target dir: " + entry.getName());
            }
            // if the entry is a directory, create a new dir
            if (!entry.isDirectory() && entry.getName().equalsIgnoreCase(CONNECTOR_XML)) {
                // if the entry is a file, write the file
                copyInputStream(zipFile.getInputStream(entry),
                        new BufferedOutputStream(new FileOutputStream(destPath + entry.getName())));
            }
        }
        zipFile.close();
    }

    private static void copyInputStream(InputStream in, OutputStream out)
            throws IOException {
        byte[] buffer = new byte[40960];
        int len;

        while ((len = in.read(buffer)) >= 0) {
            out.write(buffer, 0, len);
        }

        in.close();
        out.close();
    }

}
