/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF 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.apache.synapse.libraries.util;

import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMException;
import org.apache.axiom.om.OMXMLBuilderFactory;
import org.apache.axis2.deployment.DeploymentException;
import org.apache.axis2.deployment.util.Utils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.SynapseException;
import org.apache.synapse.deployers.SynapseArtifactDeploymentException;
import org.apache.synapse.libraries.imports.SynapseImport;
import org.apache.synapse.libraries.model.Library;
import org.apache.synapse.libraries.model.LibraryArtifact;
import org.apache.synapse.libraries.model.SynapseLibrary;

import javax.xml.namespace.QName;
import java.io.*;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class LibDeployerUtils {

    public static final String APP_UNZIP_DIR;

    static {
        String javaTempDir = System.getProperty("java.io.tmpdir");
        APP_UNZIP_DIR = javaTempDir.endsWith(File.separator) ?
                        javaTempDir + LibDeployerConstants.SYNAPSE_LIBS :
                        javaTempDir + File.separator + LibDeployerConstants.SYNAPSE_LIBS;
        createDir(APP_UNZIP_DIR);
    }

    private static final Log log = LogFactory.getLog(LibDeployerUtils.class);



    public static Library createSynapseLibrary(String libPath) {
        createDir(APP_UNZIP_DIR);

        String libFilePath = LibDeployerUtils.formatPath(libPath);
        //extract
        String extractPath = LibDeployerUtils.extractSynapseLib(libFilePath);
        //create synapse lib metadata
        SynapseLibrary synapseLib = LibDeployerUtils.populateDependencies(extractPath +
                                                                          LibDeployerConstants.ARTIFACTS_XML);

        //create a ClassLoader for loading this synapse lib classes/resources
        try {
            ClassLoader libLoader = Utils.getClassLoader(LibDeployerUtils.class.getClassLoader(),
                                                         extractPath, false);
            synapseLib.setLibClassLoader(libLoader);
        } catch (DeploymentException e) {
            throw new SynapseArtifactDeploymentException("Error setting up lib classpath for Synapse" +
                                                         " Library  : " + libFilePath, e);
        }
        //resolve synapse lib artifacts
        LibDeployerUtils.searchAndResolveDependencies(extractPath, synapseLib);
        return synapseLib;
    }

    /**
     * populate Dependencies using main root artifacts.xml.. Schema for artifacts.xml is follwing
     *
     *<artifacts>
         <artifact name="SampleLib" package="synapse.sample" >
                <dependency artifact="templates1" /> +
                <description>sample synapse library</description> ?
         </artifact>
    </artifacts>
     *
     * @param libXmlPath
     * @return
     */
    private static SynapseLibrary populateDependencies(String libXmlPath) {
        File f = new File(libXmlPath);
        if (!f.exists()) {
            throw new SynapseException("artifacts.xml file not found at : " + libXmlPath);
        }
        InputStream xmlInputStream = null;
        try {
            xmlInputStream = new FileInputStream(f);
            OMElement documentElement = OMXMLBuilderFactory.createOMBuilder(xmlInputStream).getDocumentElement();
            if (documentElement == null) {
                throw new SynapseArtifactDeploymentException("Document element for artifacts.xml is " +
                                                             "null. Can't build " +
                                                             "the synapse library configuration");
            }
            Iterator artifactItr = documentElement.getChildrenWithLocalName(LibDeployerConstants.ARTIFACT);
            SynapseLibrary mainSynLibArtifact = null;
            mainSynLibArtifact = createSynapseLibraryWithDeps(((OMElement) artifactItr.next()));
            if (mainSynLibArtifact == null) {
                throw new SynapseArtifactDeploymentException("artifacts.xml is invalid. <artifact> element" +
                                                             " Not Found ");
            }
            return mainSynLibArtifact;
        } catch (FileNotFoundException e) {
            throw new SynapseArtifactDeploymentException("artifacts.xml File cannot be loaded from " + libXmlPath, e);

        } catch (OMException e) {
            throw new SynapseArtifactDeploymentException("Error while parsing the artifacts.xml file ", e);
        } finally {
            if (xmlInputStream != null) {
                try {
                    xmlInputStream.close();
                } catch (IOException e) {
                    log.error("Error while closing input stream.", e);
                }
            }
        }
    }

    /**
     * Builds the Artifact object when an artifact element is given
     *
     * @param artifactEle - artifact OMElement
     * @return created Artifact object
     */
    private static SynapseLibrary createSynapseLibraryWithDeps(OMElement artifactEle) {
        if (artifactEle == null) {
            return null;
        }
        SynapseLibrary synLib = new SynapseLibrary(readAttribute(artifactEle, LibDeployerConstants.NAME),
                                                   readAttribute(artifactEle, LibDeployerConstants.PACKAGE_ATTR));
        synLib.setDescription(readChildText(artifactEle,LibDeployerConstants.DESCRIPTION_ELEMENT));
        // read the dependencies
        Iterator itr = artifactEle.getChildrenWithLocalName(LibDeployerConstants.DEPENDENCY);
        while (itr.hasNext()) {
            OMElement depElement = (OMElement) itr.next();
            // create a synLib for each dependency and add to the root synLib
            LibraryArtifact.Dependency dep = new LibraryArtifact.Dependency(readAttribute(depElement,
                                                                     LibDeployerConstants.ARTIFACT));
            synLib.addDependency(dep);
        }

        return synLib;
    }


    /**
     * Deploys all artifacts under a root artifact..
     *
     * @param rootDirPath - root dir of the extracted artifact
     * @param library     - lib instance
     */
    private static void searchAndResolveDependencies(String rootDirPath,
                                                    SynapseLibrary library) {
        List<LibraryArtifact> libraryArtifacts = new ArrayList<LibraryArtifact>();
        File extractedDir = new File(rootDirPath);
        File[] allFiles = extractedDir.listFiles();
        if (allFiles == null) {
            return;
        }


        // search for all directories under the extracted path
        for (File artifactDirectory : allFiles) {
            if (!artifactDirectory.isDirectory()) {
                continue;
            }

            String directoryPath = formatPath(artifactDirectory.getAbsolutePath());
            String artifactXmlPath = directoryPath + File.separator + LibDeployerConstants.ARTIFACT_XML;

            File f = new File(artifactXmlPath);
            // if the artifact.xml not found, ignore this dir
            if (!f.exists()) {
                continue;
            }

            LibraryArtifact artifact = null;
            InputStream xmlInputStream = null;
            try {
                xmlInputStream = new FileInputStream(f);
                artifact = buildArtifact(library, xmlInputStream, directoryPath);
            } catch (FileNotFoundException e) {
                log.warn("Error while resolving synapse lib dir :"
                                                             + artifactDirectory.getName() +
                                                             " artifacts.xml File cannot be loaded " +
                                                             "from " + artifactXmlPath, e);
            } catch (Exception e) {
                log.warn("Error ocurred while resolving synapse lib dir :"
                                                             + artifactDirectory.getName() +
                                                             " for artifacts.xml path" + artifactXmlPath, e);
            } finally {
                if (xmlInputStream != null) {
                    try {
                        xmlInputStream.close();
                    } catch (IOException e) {
                        log.error("Error while closing input stream.", e);
                    }
                }
            }

            if (artifact == null) {
                log.warn("Could not build lib artifact for path : " + directoryPath + " Synapse Library :" +
                         library.getQName() + ". Continue searching for other lib artifacts");
                continue;

            }
            libraryArtifacts.add(artifact);
        }
        boolean isDepsResolved = library.resolveDependencies(libraryArtifacts);
        if (!isDepsResolved) {
            throw new SynapseArtifactDeploymentException("Error when resolving Dependencies for lib : " + library.toString());
        }
    }

    /**
     * Builds the artifact from the given input steam and adds it as a dependency in the provided
     *  parent Synapse library artifact
     *
     * @param library
     * @param artifactXmlStream - xml input stream of the artifact.xml
     * @param directoryPath
     * @return - Artifact instance if successfull. otherwise null..
     */
    private static LibraryArtifact buildArtifact(SynapseLibrary library, InputStream artifactXmlStream, String directoryPath) {
        LibraryArtifact artifact = null;
        try {
            OMElement artElement = OMXMLBuilderFactory.createOMBuilder(artifactXmlStream).getDocumentElement();

            if (LibDeployerConstants.ARTIFACT.equals(artElement.getLocalName())) {
                artifact = populateLibraryArtifact(artElement, directoryPath, null, library);
            } else {
                log.error("artifact.xml is invalid. Error occurred while resolving Synapse Library : "
                          + library.getQName());
                return null;
            }
        } catch (OMException e) {
            throw new SynapseArtifactDeploymentException("Error parsing artifact.xml for path : " +
                                                         directoryPath ,e);
        }

        if (artifact == null || artifact.getName() == null) {
            log.error("Invalid artifact found in Synapse Library : "
                      + library.getQName() );
            return null;
        }
        return artifact;
    }


    /**
     * Builds the Artifact object when an root artifact element is given . Schema for artifact.xml
     * is as follows
     * <artifact name="templates1" type="synapse/template" >

        <subArtifacts>
            <artifact name="greet_func1" >
                    <file>templ1_ns1.xml</file>
                    <description>sample synapse library artifact Description</description> ?
            </artifact> *
        </subArtifacts> *

        <description>sample synapse library artifact Description</description> ?
    </artifact>
     *
     * @param artifactEle - artifact OMElement
     * @return created Artifact object
     */
    private static LibraryArtifact populateLibraryArtifact(OMElement artifactEle, String artifactPath,
                                                          LibraryArtifact parent , SynapseLibrary library) {
        if (artifactEle == null || artifactPath == null ) {
            return null;
        }

        LibraryArtifact artifact = new LibraryArtifact(readAttribute(artifactEle, LibDeployerConstants.NAME));
        artifact.setParent(parent);
        artifact.setType(readAttribute(artifactEle, LibDeployerConstants.TYPE));
        artifact.setPath(artifactPath);

        artifact.setDescription(readChildText(artifactEle,LibDeployerConstants.DESCRIPTION_ELEMENT));
        //add a description of this artifact(if availalbe) to Synapse Library
        library.addArtifactDescription(artifact);
        // read the subArtifacts
        OMElement subArtifactsElement = artifactEle
                .getFirstChildWithName(new QName(LibDeployerConstants.SUB_ARTIFACTS));
        if (subArtifactsElement != null) {
            Iterator subArtItr = subArtifactsElement.getChildrenWithLocalName(LibDeployerConstants.ARTIFACT);
            while (subArtItr.hasNext()) {
                // as this is also an artifact, use recursion
                LibraryArtifact subArtifact = populateLibraryArtifact((OMElement) subArtItr.next(), artifactPath, artifact, library);
                artifact.addSubArtifact(subArtifact);
            }
        }

        // read and check for files
        Iterator fileItr = artifactEle.getChildrenWithLocalName(LibDeployerConstants.FILE);
        while (fileItr.hasNext()) {
            OMElement fileElement = (OMElement) fileItr.next();
            artifact.setupFile(fileElement.getText());
        }
        return artifact;
    }

    public static void loadLibArtifacts(SynapseImport synImport, Library library) {
        if (synImport.getLibName().equals(library.getQName().getLocalPart()) &&
            synImport.getLibPackage().equals(library.getPackage())) {
            library.loadLibrary();
        }
    }

    public static <T> T getLibArtifact(Map<String, Library> librarySet, String key, Class<T> type) {
        for (Library synapseLibrary : librarySet.values()) {
            try {
                T artifact = (T) synapseLibrary.getArtifact(key);
                if (artifact != null) {
                    return artifact;
                }
            } catch (Exception e) {
                //ignore
            }
        }
        return null;
    }

    public static String getQualifiedName(SynapseImport synImport){
        return new QName(synImport.getLibPackage(),synImport.getLibName()).toString();
    }


    ///////////////////////
    ////////////////// Start Common Utility Methods
    /**
     * Reads an attribute in the given element and returns the value of that attribute
     *
     * @param element - Element to search
     * @param attName - attribute name
     * @return if the attribute found, return value. else null.
     */
    public static String readAttribute(OMElement element, String attName) {
        if (element == null) {
            return null;
        }
        OMAttribute temp = element.getAttribute(new QName(attName));
        if (temp != null) {
            return temp.getAttributeValue();
        }
        return null;
    }

    public static String readChildText(OMElement element, String ln) {
        return readChildText(element, ln, null);
    }

    /**
     * Reads a text node which is in a child element of the given element and returns the text
     * value.
     *
     * @param element - Element to search
     * @param ln      - Child element name
     * @param ns      - Child element namespace
     * @return if the child text element found, return text value. else null.
     */
    public static String readChildText(OMElement element, String ln, String ns) {
        if (element == null) {
            return null;
        }
        OMElement temp = element.getFirstChildWithName(new QName(ns, ln));
        if (temp != null) {
            return temp.getText();
        }
        return null;
    }


    /**
     * Extract the Synapse Library at the provided path to the java temp dir. Return the
     * extracted location
     *
     * @param libPath - Absolute path of the Synapse Lib archive file
     * @return - extracted location
     * @throws SynapseException - error on extraction
     */
    public static String extractSynapseLib(String libPath) throws SynapseException {
        libPath = formatPath(libPath);
        String fileName = libPath.substring(libPath.lastIndexOf('/') + 1);
        String dest = APP_UNZIP_DIR + File.separator + System.currentTimeMillis() +
                      fileName + File.separator;
        createDir(dest);

        try {
            extract(libPath, dest);
        } catch (IOException e) {
            throw new SynapseException("Error while extracting Synapse Library : " + fileName, e);
        }
        return dest;
    }

    /**
     * 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
        path = path.replaceAll("\\b\\s+\\b", "%20");
        // replacing all "\" with "/"
        return path.replace('\\', '/');
    }

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

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

        while (entries.hasMoreElements()) {
            ZipEntry entry = (ZipEntry) entries.nextElement();
            // we don't need to copy the META-INF dir
            if (entry.getName().startsWith("META-INF/")) {
                continue;
            }
            // Skip directories
            if (entry.isDirectory()) {
                continue;
            }
            File dest = new File(destPath, entry.getName());
            dest.getParentFile().mkdirs();
            copyInputStream(zipFile.getInputStream(entry),
                            new BufferedOutputStream(new FileOutputStream(dest)));
        }
        zipFile.close();
    }


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

    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();
    }

    /////////////////// End Of Common Utility Methods


    public static void main(String[] args) {
        new SynapseLibrary(null, null).resolveDependencies(null);
    }
}
