package org.jboss.windup.rules.apps.xml.operation.xslt;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.lang3.StringUtils;
import org.jboss.forge.furnace.util.ClassLoaders;
import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.config.operation.iteration.AbstractIterationOperation;
import org.jboss.windup.config.parser.xml.when.ParamHandler;
import org.jboss.windup.graph.GraphContext;
import org.jboss.windup.graph.model.LinkModel;
import org.jboss.windup.graph.model.WindupVertexFrame;
import org.jboss.windup.graph.model.resource.FileModel;
import org.jboss.windup.graph.service.GraphService;
import org.jboss.windup.reporting.model.ClassificationModel;
import org.jboss.windup.reporting.service.ClassificationService;
import org.jboss.windup.rules.apps.xml.model.XmlFileModel;
import org.jboss.windup.rules.apps.xml.model.XsltTransformationModel;
import org.jboss.windup.rules.apps.xml.service.XsltTransformationService;
import org.jboss.windup.rules.files.model.FileReferenceModel;
import org.jboss.windup.util.Logging;
import org.jboss.windup.util.exception.WindupException;
import org.ocpsoft.rewrite.config.Rule;
import org.ocpsoft.rewrite.context.EvaluationContext;

/**
 * Graph operation doing the xslt transformation using the .xslt source on the target xml object
 * 
 * @author <a href="mailto:mbriskar@gmail.com">Matej Briskar</a>
 */
public class XSLTTransformation extends AbstractIterationOperation<XmlFileModel> implements XSLTTransformationDescription,
            XSLTTransformationExtension, XSLTTransformationLocation, XSLTTransformationFileSystem, XSLTTransformationOf
{
    private static final Logger LOG = Logging.get(XSLTTransformation.class);

    private ClassLoader contextClassLoader;
    private String description = "";
    private String template;
    private String extension;
    private int effort = 0;

    private Transformer xsltTransformer;
    private Map<String, String> xsltParameters = new HashMap<>();

    private XSLTTransformation(String variable)
    {
        super(variable);
    }

    private XSLTTransformation()
    {
        super();
    }

    /**
     * Set the payload to the fileModel of the given instance even though the variable is not directly of it's type. This is mainly to simplify the
     * creation of the rule, when the FileModel itself is not being iterated but just a model referencing it.
     *
     */
    @Override
    public void perform(GraphRewrite event, EvaluationContext context)
    {
        checkVariableName(event, context);
        WindupVertexFrame payload = resolveVariable(event, getVariableName());
        if (payload instanceof FileReferenceModel)
        {
            FileModel file = ((FileReferenceModel) payload).getFile();
            perform(event, context, (XmlFileModel) file);
        }
        else
        {
            super.perform(event, context);
        }

    }

    public void addXsltParameter(String key, String value)
    {
        xsltParameters.put(key, value);
    }

    /**
     * Create a new transformation for the given ref.
     */
    public static XSLTTransformationOf of(String variable)
    {
        return new XSLTTransformation(variable);
    }

    @Override
    public XSLTTransformationDescription withDescription(String description)
    {
        this.description = description;
        return this;
    }

    @Override
    public XSLTTransformationLocation usingTemplate(String template)
    {
        this.template = template;
        return this;
    }

    @Override
    public XSLTTransformationLocation usingTemplate(String location, ClassLoader loader)
    {
        this.template = location;
        this.contextClassLoader = loader;
        return this;
    }

    @Override
    public XSLTTransformationExtension withExtension(String extension)
    {
        this.extension = extension;
        return this;
    }

    /**
     * Create a new {@link XSLTTransformation} using the given location of the source XSLT file within the current
     * {@link Thread#getContextClassLoader()}.
     */
    public static XSLTTransformationLocation using(String location)
    {
        return using(location, Thread.currentThread().getContextClassLoader());
    }

    /**
     * Create a new {@link XSLTTransformation} using the given location of the source XSLT file path on the file-system.
     */
    public static XSLTTransformationFileSystem usingFilesystem(String location)
    {
        XSLTTransformation tansformation = new XSLTTransformation();
        tansformation.template = location;
        return tansformation;
    }

    /**
     * Create a new {@link XSLTTransformation} using the given location of the source XSLT file within the given {@link ClassLoader}.
     */
    public static XSLTTransformationLocation using(String location, ClassLoader classLoader)
    {
        XSLTTransformation tansformation = new XSLTTransformation();
        /*
         * ClassLoader instance needed to see the file passed in the location
         */
        tansformation.contextClassLoader = classLoader;
        tansformation.template = location;
        return tansformation;
    }

    private InputStream openInputStream(String pathString) throws IOException
    {
        if (this.contextClassLoader != null)
        {
            LOG.fine("Loading Resource " + pathString + " with classloader: " + pathString);
            return contextClassLoader.getResourceAsStream(pathString);
        }
        else
        {
            Path path = Paths.get(pathString);
            if (!Files.isRegularFile(path) && !pathString.equals(this.template))
            {
                // probably a relative file... try to infer it from the original template path
                path = Paths.get(this.template).getParent().resolve(path);
            }

            LOG.fine("Loading File " + path + " with from " + path.toAbsolutePath().normalize().toString());
            return new FileInputStream(path.toFile());
        }
    }

    private void setup()
    {
        try (InputStream resourceAsStream = openInputStream(this.template))
        {
            final Source xsltSource = new StreamSource(resourceAsStream);
            final TransformerFactory tf = TransformerFactory.newInstance();
            tf.setURIResolver(new URIResolver()
            {
                @Override
                public Source resolve(String href, String base) throws TransformerException
                {
                    /*
                     * Fetch local only, for speed reasons.
                     */
                    if (StringUtils.contains(href, "http://"))
                    {
                        LOG.warning("Trying to fetch remote URL for XSLT.  This is not possible; for speed reasons: "
                                    + href + ": " + base);
                        return null;
                    }
                    try
                    {
                        InputStream inputStream = openInputStream(href);
                        return new StreamSource(inputStream);
                    } catch (IOException e)
                    {
                        throw new WindupException("Failed to load template: " + href + " due to: " + e.getMessage(), e);
                    }
                }
            });
            ClassLoaders.executeIn(TransformerFactory.class.getClassLoader(), new Callable<Object>()
            {

                @Override
                public Object call() throws Exception
                {
                    xsltTransformer = tf.newTransformer(xsltSource);
                    return null;
                }

            });
            if (xsltParameters != null)
            {
                for (String key : xsltParameters.keySet())
                {
                    LOG.fine("Setting property: " + key + " -> " + xsltParameters.get(key));
                    xsltTransformer.setParameter(key, xsltParameters.get(key));
                }
            }

            LOG.fine("Created XSLT successfully: " + template);
        }
        catch (TransformerConfigurationException e)
        {
            throw new IllegalStateException("Problem working with xsl file located at " + template
                        + ". Please check if the file really exists.", e);
        }
        catch (Exception e)
        {
            throw new IllegalStateException("Not able to initialize the XSLT transformer.", e);
        }
    }

    @Override
    public void perform(GraphRewrite event, EvaluationContext context, XmlFileModel payload)
    {
        setup();
        GraphContext graphContext = event.getGraphContext();
        GraphService<XsltTransformationModel> transformationService = new GraphService<>(
                    graphContext, XsltTransformationModel.class);
        String fileName = payload.getFileName();

        fileName = StringUtils.replace(fileName, ".", "-");
        fileName = fileName + extension;

        XsltTransformationService xsltTransformationService = new XsltTransformationService(graphContext);
        Path outputPath = xsltTransformationService.getTransformedXSLTPath();

        Path resultPath = outputPath.resolve(fileName);

        Source xmlSource = new DOMSource(payload.asDocument());
        Result xmlResult = new StreamResult(resultPath.toFile());

        try
        {
            xsltTransformer.transform(xmlSource, xmlResult);
            XsltTransformationModel transformation = transformationService.create();
            transformation.setDescription(description);
            transformation.setEffort(effort);
            transformation.setExtension(extension);
            transformation.setSourceLocation(template);
            transformation.setSourceFile(payload);
            transformation.setResult(fileName);

            ClassificationService classificationService = new ClassificationService(graphContext);
            ClassificationModel classificationModel = classificationService.create();
            classificationModel.setClassification("Transformed to: " + description);
            classificationModel.setEffort(effort);
            classificationModel.addFileModel(payload);
            classificationModel.setRuleID(((Rule) context.get(Rule.class)).getId());

            GraphService<LinkModel> linkService = new GraphService<>(graphContext, LinkModel.class);
            LinkModel linkModel = linkService.create();
            linkModel.setDescription(description);
            linkModel.setLink(XsltTransformationService.TRANSFORMEDXML_DIR_NAME + "/" + fileName);
            payload.addLinkToTransformedFile(linkModel);
            // classificationModel.addLink(linkModel);
        }
        catch (TransformerException e)
        {
            LOG.log(Level.SEVERE, "Exception transforming XML.", e);
        }
    }

    /**
     * Set the parameters associated with this {@link XSLTTransformation};
     */
    public XSLTTransformationEffort withParameters(Map<String, String> parameters)
    {
        this.xsltParameters.putAll(parameters);
        return this;
    }

    /**
     * Set the estimated effort associated with this {@link XSLTTransformation};
     */
    public XSLTTransformation withEffort(int effort)
    {
        this.effort = effort;
        return this;
    }

    public void setContextClassLoader(ClassLoader classLoader)
    {
        this.contextClassLoader = classLoader;
    }

    public String getDescription()
    {
        return description;
    }

    public String getTemplate()
    {
        return template;
    }

    public String getExtension()
    {
        return extension;
    }

    public Map<String, String> getXsltParameters()
    {
        return xsltParameters;
    }

}
