/*
 * 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.maven.doxia.siterenderer;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.Restriction;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.doxia.Doxia;
import org.apache.maven.doxia.parser.ParseException;
import org.apache.maven.doxia.parser.Parser;
import org.apache.maven.doxia.parser.manager.ParserNotFoundException;
import org.apache.maven.doxia.parser.module.ParserModule;
import org.apache.maven.doxia.parser.module.ParserModuleManager;
import org.apache.maven.doxia.site.SiteModel;
import org.apache.maven.doxia.site.skin.SkinModel;
import org.apache.maven.doxia.site.skin.io.xpp3.SkinXpp3Reader;
import org.apache.maven.doxia.siterenderer.SiteRenderingContext.SiteDirectory;
import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
import org.apache.maven.doxia.util.XmlValidator;
import org.apache.velocity.Template;
import org.apache.velocity.context.Context;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.VelocityException;
import org.apache.velocity.tools.Scope;
import org.apache.velocity.tools.ToolManager;
import org.apache.velocity.tools.config.ConfigurationUtils;
import org.apache.velocity.tools.config.EasyFactoryConfiguration;
import org.apache.velocity.tools.config.FactoryConfiguration;
import org.apache.velocity.tools.generic.AlternatorTool;
import org.apache.velocity.tools.generic.ClassTool;
import org.apache.velocity.tools.generic.ComparisonDateTool;
import org.apache.velocity.tools.generic.ContextTool;
import org.apache.velocity.tools.generic.ConversionTool;
import org.apache.velocity.tools.generic.DisplayTool;
import org.apache.velocity.tools.generic.EscapeTool;
import org.apache.velocity.tools.generic.FieldTool;
import org.apache.velocity.tools.generic.LinkTool;
import org.apache.velocity.tools.generic.LoopTool;
import org.apache.velocity.tools.generic.MathTool;
import org.apache.velocity.tools.generic.NumberTool;
import org.apache.velocity.tools.generic.RenderTool;
import org.apache.velocity.tools.generic.ResourceTool;
import org.apache.velocity.tools.generic.SortTool;
import org.apache.velocity.tools.generic.XmlTool;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.Os;
import org.codehaus.plexus.util.PathTool;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.codehaus.plexus.velocity.VelocityComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>DefaultSiteRenderer class.</p>
 *
 * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 * @since 1.0
 */
@Singleton
@Named
public class DefaultSiteRenderer implements Renderer {
    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSiteRenderer.class);

    // ----------------------------------------------------------------------
    // Requirements
    // ----------------------------------------------------------------------

    @Inject
    private VelocityComponent velocity;

    @Inject
    private ParserModuleManager parserModuleManager;

    @Inject
    private Doxia doxia;

    @Inject
    private PlexusContainer plexus;

    private static final String SKIN_TEMPLATE_LOCATION = "META-INF/maven/site.vm";

    private static final String TOOLS_LOCATION = "META-INF/maven/site-tools.xml";

    private static final String DOXIA_SITE_RENDERER_VERSION = getSiteRendererVersion();

    // ----------------------------------------------------------------------
    // SiteRenderer implementation
    // ----------------------------------------------------------------------

    /** {@inheritDoc} */
    public Map<String, DocumentRenderer> locateDocumentFiles(SiteRenderingContext siteRenderingContext)
            throws IOException, RendererException {
        Map<String, DocumentRenderer> files = new LinkedHashMap<>();
        Map<String, String> moduleExcludes = siteRenderingContext.getModuleExcludes();

        // look in every site directory (in general src/site or target/generated-site)
        for (SiteDirectory siteDirectory : siteRenderingContext.getSiteDirectories()) {
            File siteDirectoryPath = siteDirectory.getPath();
            if (siteDirectoryPath.exists()) {
                Collection<ParserModule> modules = parserModuleManager.getParserModules();
                // use every Doxia parser module
                for (ParserModule module : modules) {
                    File moduleBasedir = new File(siteDirectoryPath, module.getSourceDirectory());

                    String excludes = (moduleExcludes == null) ? null : moduleExcludes.get(module.getParserId());

                    addModuleFiles(
                            siteRenderingContext.getRootDirectory(),
                            moduleBasedir,
                            module,
                            excludes,
                            files,
                            siteDirectory.isEditable());
                }
            }
        }

        return files;
    }

    private List<String> filterExtensionIgnoreCase(List<String> fileNames, String extension) {
        List<String> filtered = new LinkedList<>(fileNames);
        for (Iterator<String> it = filtered.iterator(); it.hasNext(); ) {
            String name = it.next();

            // Take care of extension case
            if (!endsWithIgnoreCase(name, extension)) {
                it.remove();
            }
        }
        return filtered;
    }

    private void addModuleFiles(
            File rootDir,
            File moduleBasedir,
            ParserModule module,
            String excludes,
            Map<String, DocumentRenderer> files,
            boolean editable)
            throws IOException, RendererException {
        if (!moduleBasedir.exists() || ArrayUtils.isEmpty(module.getExtensions())) {
            return;
        }

        String moduleRelativePath =
                PathTool.getRelativeFilePath(rootDir.getAbsolutePath(), moduleBasedir.getAbsolutePath());

        List<String> allFiles = FileUtils.getFileNames(moduleBasedir, "**/*", excludes, false);

        Map<String, String> caseInsensitiveFiles = new HashMap<>();

        for (String extension : module.getExtensions()) {
            String fullExtension = "." + extension;

            List<String> docs = filterExtensionIgnoreCase(allFiles, fullExtension);

            // *.<extension>.vm
            List<String> velocityFiles = filterExtensionIgnoreCase(allFiles, fullExtension + ".vm");

            docs.addAll(velocityFiles);

            for (String doc : docs) {
                DocumentRenderingContext docRenderingContext = new DocumentRenderingContext(
                        moduleBasedir, moduleRelativePath, doc, module.getParserId(), extension, editable);

                // TODO: DOXIA-111: we need a general filter here that knows how to alter the context
                if (endsWithIgnoreCase(doc, ".vm")) {
                    docRenderingContext.setAttribute("velocity", "true");
                }

                String key = docRenderingContext.getOutputName();

                if (files.containsKey(key)) {
                    DocumentRenderer docRenderer = files.get(key);

                    DocumentRenderingContext originalDocRenderingContext = docRenderer.getRenderingContext();

                    File originalDoc = new File(
                            originalDocRenderingContext.getBasedir(), originalDocRenderingContext.getInputName());

                    throw new RendererException("File '" + module.getSourceDirectory() + File.separator + doc
                            + "' clashes with existing '" + originalDoc + "'.");
                }
                // -----------------------------------------------------------------------
                // Handle key without case differences
                // -----------------------------------------------------------------------
                String originalKey = caseInsensitiveFiles.put(key.toLowerCase(Locale.ROOT), key);
                if (originalKey != null) {
                    DocumentRenderingContext originalDocRenderingContext =
                            files.get(originalKey).getRenderingContext();

                    File originalDoc = new File(
                            originalDocRenderingContext.getBasedir(), originalDocRenderingContext.getInputName());

                    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                        throw new RendererException("File '" + module.getSourceDirectory() + File.separator + doc
                                + "' clashes with existing '" + originalDoc + "'.");
                    }

                    if (LOGGER.isWarnEnabled()) {
                        LOGGER.warn("File '" + module.getSourceDirectory() + File.separator + doc
                                + "' could clash with existing '" + originalDoc + "'.");
                    }
                }

                files.put(key, new DoxiaDocumentRenderer(docRenderingContext));
            }
        }
    }

    /** {@inheritDoc} */
    public void render(
            Collection<DocumentRenderer> documents, SiteRenderingContext siteRenderingContext, File outputDirectory)
            throws RendererException, IOException {
        for (DocumentRenderer docRenderer : documents) {
            DocumentRenderingContext docRenderingContext = docRenderer.getRenderingContext();

            File outputFile = new File(outputDirectory, docRenderer.getOutputName());

            File inputFile = new File(docRenderingContext.getBasedir(), docRenderingContext.getInputName());

            boolean modified = !outputFile.exists()
                    || (inputFile.lastModified() > outputFile.lastModified())
                    || (siteRenderingContext.getSiteModel().getLastModified() > outputFile.lastModified());

            if (modified || docRenderer.isOverwrite()) {
                if (!outputFile.getParentFile().exists()) {
                    outputFile.getParentFile().mkdirs();
                }

                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Generating " + outputFile);
                }

                Writer writer = null;
                try {
                    if (!docRenderer.isExternalReport()) {
                        writer = WriterFactory.newWriter(outputFile, siteRenderingContext.getOutputEncoding());
                    }
                    docRenderer.renderDocument(writer, this, siteRenderingContext);
                } finally {
                    IOUtil.close(writer);
                }
            } else {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug(inputFile + " unchanged, not regenerating...");
                }
            }
        }
    }

    /** {@inheritDoc} */
    public void renderDocument(
            Writer writer, DocumentRenderingContext docRenderingContext, SiteRenderingContext siteContext)
            throws RendererException {
        SiteRendererSink sink = new SiteRendererSink(docRenderingContext);

        File doc = new File(docRenderingContext.getBasedir(), docRenderingContext.getInputName());

        Reader reader = null;
        try {
            String resource = doc.getAbsolutePath();

            Parser parser = doxia.getParser(docRenderingContext.getParserId());
            ParserConfigurator configurator = siteContext.getParserConfigurator();
            boolean isConfigured = false;
            if (configurator != null) {
                isConfigured = configurator.configure(docRenderingContext.getParserId(), doc.toPath(), parser);
            }
            if (!isConfigured) {
                // DOXIASITETOOLS-146 don't render comments from source markup
                parser.setEmitComments(false);
                parser.setEmitAnchorsForIndexableEntries(true);
            }

            // TODO: DOXIA-111: the filter used here must be checked generally.
            if (docRenderingContext.getAttribute("velocity") != null) {
                LOGGER.debug("Processing Velocity for " + docRenderingContext.getDoxiaSourcePath());
                try {
                    Context vc = createDocumentVelocityContext(docRenderingContext, siteContext);

                    StringWriter sw = new StringWriter();

                    velocity.getEngine().mergeTemplate(resource, siteContext.getInputEncoding(), vc, sw);

                    String doxiaContent = sw.toString();

                    if (siteContext.getProcessedContentOutput() != null) {
                        // save Velocity processing result, ie the Doxia content that will be parsed after
                        saveVelocityProcessedContent(docRenderingContext, siteContext, doxiaContent);
                    }

                    reader = new StringReader(doxiaContent);
                } catch (VelocityException e) {
                    throw new RendererException(
                            "Error parsing " + docRenderingContext.getDoxiaSourcePath() + " as a Velocity template", e);
                }

                if (parser.getType() == Parser.XML_TYPE && siteContext.isValidate()) {
                    reader = validate(reader, resource);
                }
            } else {
                switch (parser.getType()) {
                    case Parser.XML_TYPE:
                        reader = ReaderFactory.newXmlReader(doc);
                        if (siteContext.isValidate()) {
                            reader = validate(reader, resource);
                        }
                        break;

                    case Parser.TXT_TYPE:
                    case Parser.UNKNOWN_TYPE:
                    default:
                        reader = ReaderFactory.newReader(doc, siteContext.getInputEncoding());
                }
            }

            doxia.parse(reader, docRenderingContext.getParserId(), sink, docRenderingContext.getDoxiaSourcePath());
        } catch (ParserNotFoundException e) {
            throw new RendererException("Error getting a parser for '" + doc + "'", e);
        } catch (ParseException e) {
            StringBuilder errorMsgBuilder = new StringBuilder();
            errorMsgBuilder.append("Error parsing '").append(doc).append("'");
            if (e.getLineNumber() > 0) {
                errorMsgBuilder.append(", line ").append(e.getLineNumber());
            }
            throw new RendererException(errorMsgBuilder.toString(), e);
        } catch (IOException e) {
            throw new RendererException("Error while processing '" + doc + "'", e);
        } finally {
            sink.flush();

            sink.close();

            IOUtil.close(reader);
        }

        mergeDocumentIntoSite(writer, (DocumentContent) sink, siteContext);
    }

    private void saveVelocityProcessedContent(
            DocumentRenderingContext docRenderingContext, SiteRenderingContext siteContext, String doxiaContent)
            throws IOException {
        if (!siteContext.getProcessedContentOutput().exists()) {
            siteContext.getProcessedContentOutput().mkdirs();
        }

        String inputPath = docRenderingContext.getInputName();
        // Remove .vm suffix
        File outputFile =
                new File(siteContext.getProcessedContentOutput(), inputPath.substring(0, inputPath.length() - 3));

        File outputParent = outputFile.getParentFile();
        if (!outputParent.exists()) {
            outputParent.mkdirs();
        }

        FileUtils.fileWrite(outputFile, siteContext.getInputEncoding(), doxiaContent);
    }

    /**
     * Creates a Velocity Context with all generic tools configured wit the site rendering context.
     *
     * @param siteRenderingContext the site rendering context
     * @return a Velocity tools managed context
     */
    protected Context createToolManagedVelocityContext(SiteRenderingContext siteRenderingContext) {
        Locale locale = siteRenderingContext.getLocale();
        String dateFormat = siteRenderingContext.getSiteModel().getPublishDate().getFormat();
        String timeZoneId = siteRenderingContext.getSiteModel().getPublishDate().getTimezone();
        TimeZone timeZone =
                "system".equalsIgnoreCase(timeZoneId) ? TimeZone.getDefault() : TimeZone.getTimeZone(timeZoneId);

        EasyFactoryConfiguration config = new EasyFactoryConfiguration(false);
        config.property("safeMode", Boolean.FALSE);
        config.toolbox(Scope.REQUEST)
                .tool(ContextTool.class)
                .tool(LinkTool.class)
                .tool(LoopTool.class)
                .tool(RenderTool.class);
        config.toolbox(Scope.APPLICATION)
                .property("locale", locale)
                .tool(AlternatorTool.class)
                .tool(ClassTool.class)
                .tool(ComparisonDateTool.class)
                .property("format", dateFormat)
                .property("timezone", timeZone)
                .tool(ConversionTool.class)
                .property("dateFormat", dateFormat)
                .tool(DisplayTool.class)
                .tool(EscapeTool.class)
                .tool(FieldTool.class)
                .tool(MathTool.class)
                .tool(NumberTool.class)
                .tool(ResourceTool.class)
                .property("bundles", new String[] {"site-renderer"})
                .tool(SortTool.class)
                .tool(XmlTool.class);

        FactoryConfiguration customConfig = ConfigurationUtils.findInClasspath(TOOLS_LOCATION);

        if (customConfig != null) {
            config.addConfiguration(customConfig);
        }

        ToolManager manager = new ToolManager(false, false);
        manager.configure(config);

        return manager.createContext();
    }

    /**
     * Create a Velocity Context for a Doxia document, containing every information about rendered document.
     *
     * @param docRenderingContext the document's rendering context
     * @param siteRenderingContext the site rendering context
     * @return a Velocity tools managed context
     */
    protected Context createDocumentVelocityContext(
            DocumentRenderingContext docRenderingContext, SiteRenderingContext siteRenderingContext) {
        Context context = createToolManagedVelocityContext(siteRenderingContext);
        // ----------------------------------------------------------------------
        // Data objects
        // ----------------------------------------------------------------------

        context.put("relativePath", docRenderingContext.getRelativePath());

        String currentFilePath = docRenderingContext.getOutputName();
        context.put("currentFilePath", currentFilePath);
        // TODO Deprecated -- will be removed!
        context.put("currentFileName", currentFilePath);

        String alignedFilePath = PathTool.calculateLink(currentFilePath, docRenderingContext.getRelativePath());
        context.put("alignedFilePath", alignedFilePath);
        // TODO Deprecated -- will be removed!
        context.put("alignedFileName", alignedFilePath);

        context.put("site", siteRenderingContext.getSiteModel());
        // TODO Deprecated -- will be removed!
        context.put("decoration", siteRenderingContext.getSiteModel());

        context.put("locale", siteRenderingContext.getLocale());
        context.put("supportedLocales", Collections.unmodifiableList(siteRenderingContext.getSiteLocales()));

        context.put("publishDate", siteRenderingContext.getPublishDate());

        if (DOXIA_SITE_RENDERER_VERSION != null) {
            context.put("doxiaSiteRendererVersion", DOXIA_SITE_RENDERER_VERSION);
        }

        // Add user properties
        Map<String, ?> templateProperties = siteRenderingContext.getTemplateProperties();

        if (templateProperties != null) {
            for (Map.Entry<String, ?> entry : templateProperties.entrySet()) {
                context.put(entry.getKey(), entry.getValue());
            }
        }

        // ----------------------------------------------------------------------
        // Tools
        // ----------------------------------------------------------------------

        context.put("PathTool", new PathTool());

        context.put("StringUtils", new StringUtils());

        context.put("plexus", plexus);
        return context;
    }

    /**
     * Create a Velocity Context for the site template decorating the document. In addition to all the informations
     * from the document, this context contains data gathered in {@link SiteRendererSink} during document rendering.
     *
     * @param content the document content to be merged into the template
     * @param siteRenderingContext the site rendering context
     * @return a Velocity tools managed context
     */
    protected Context createSiteTemplateVelocityContext(
            DocumentContent content, SiteRenderingContext siteRenderingContext) {
        // first get the context from document
        Context context = createDocumentVelocityContext(content.getRenderingContext(), siteRenderingContext);

        // then add data objects from rendered document

        // Add infos from document
        context.put("authors", content.getAuthors());

        String shortTitle = content.getTitle();
        context.put("shortTitle", shortTitle);

        String projectTitle = null;
        if (StringUtils.isNotEmpty(siteRenderingContext.getSiteModel().getName())) {
            projectTitle = siteRenderingContext.getSiteModel().getName();
        } else if (StringUtils.isNotEmpty(siteRenderingContext.getDefaultTitle())) {
            projectTitle = siteRenderingContext.getDefaultTitle();
        }

        StringBuilder title = new StringBuilder();
        if (StringUtils.isNotEmpty(shortTitle)) {
            title.append(shortTitle);
        }

        if (title.length() > 0 && StringUtils.isNotEmpty(projectTitle)) {
            title.append(" \u2013 "); // Symbol Name: En Dash
        }

        if (StringUtils.isNotEmpty(projectTitle)) {
            title.append(projectTitle);
        }

        context.put("title", title.length() > 0 ? title.toString() : null);

        context.put("headContent", content.getHead());

        context.put("bodyContent", content.getBody());

        // document date (got from Doxia Sink date() API)
        context.put("documentDate", content.getDate());

        // document rendering context, to get eventual inputPath
        context.put("docRenderingContext", content.getRenderingContext());

        return context;
    }

    public void generateDocument(Writer writer, SiteRendererSink sink, SiteRenderingContext siteRenderingContext)
            throws RendererException {
        mergeDocumentIntoSite(writer, sink, siteRenderingContext);
    }

    /** {@inheritDoc} */
    public void mergeDocumentIntoSite(Writer writer, DocumentContent content, SiteRenderingContext siteRenderingContext)
            throws RendererException {
        String templateName = siteRenderingContext.getTemplateName();

        LOGGER.debug("Processing Velocity for template " + templateName + " on "
                + content.getRenderingContext().getDoxiaSourcePath());

        Context context = createSiteTemplateVelocityContext(content, siteRenderingContext);

        ClassLoader old = null;

        if (siteRenderingContext.getTemplateClassLoader() != null) {
            // -------------------------------------------------------------------------
            // If no template classloader was set we'll just use the context classloader
            // -------------------------------------------------------------------------

            old = Thread.currentThread().getContextClassLoader();

            Thread.currentThread().setContextClassLoader(siteRenderingContext.getTemplateClassLoader());
        }

        try {
            Template template;
            Artifact skin = siteRenderingContext.getSkin();

            try {
                SkinModel skinModel = siteRenderingContext.getSkinModel();
                String encoding = (skinModel == null) ? null : skinModel.getEncoding();

                template = (encoding == null)
                        ? velocity.getEngine().getTemplate(templateName)
                        : velocity.getEngine().getTemplate(templateName, encoding);
            } catch (ParseErrorException pee) {
                throw new RendererException(
                        "Velocity parsing error while reading the site template " + "from " + skin.getId() + " skin",
                        pee);
            } catch (ResourceNotFoundException rnfe) {
                throw new RendererException(
                        "Could not find the site template " + "from " + skin.getId() + " skin", rnfe);
            }

            try {
                StringWriter sw = new StringWriter();
                template.merge(context, sw);
                writer.write(sw.toString().replaceAll("\r?\n", SystemUtils.LINE_SEPARATOR));
            } catch (VelocityException ve) {
                throw new RendererException("Velocity error while merging site template.", ve);
            } catch (IOException ioe) {
                throw new RendererException("IO exception while merging site template.", ioe);
            }
        } finally {
            IOUtil.close(writer);

            if (old != null) {
                Thread.currentThread().setContextClassLoader(old);
            }
        }
    }

    private SiteRenderingContext createSiteRenderingContext(
            Map<String, ?> attributes, SiteModel siteModel, String defaultTitle, Locale locale) {
        SiteRenderingContext context = new SiteRenderingContext();

        context.setTemplateProperties(attributes);
        context.setLocale(locale);
        context.setSiteModel(siteModel);
        context.setDefaultTitle(defaultTitle);

        return context;
    }

    /** {@inheritDoc} */
    public SiteRenderingContext createContextForSkin(
            Artifact skin, Map<String, ?> attributes, SiteModel siteModel, String defaultTitle, Locale locale)
            throws IOException, RendererException {
        SiteRenderingContext context = createSiteRenderingContext(attributes, siteModel, defaultTitle, locale);

        context.setSkin(skin);

        ZipFile zipFile = getZipFile(skin.getFile());
        InputStream in = null;

        try {
            if (zipFile.getEntry(SKIN_TEMPLATE_LOCATION) == null) {
                throw new RendererException("Skin does not contain template at " + SKIN_TEMPLATE_LOCATION);
            }
            context.setTemplateName(SKIN_TEMPLATE_LOCATION);
            context.setTemplateClassLoader(
                    new URLClassLoader(new URL[] {skin.getFile().toURI().toURL()}));

            ZipEntry skinDescriptorEntry = zipFile.getEntry(SkinModel.SKIN_DESCRIPTOR_LOCATION);
            if (skinDescriptorEntry != null) {
                in = zipFile.getInputStream(skinDescriptorEntry);

                SkinModel skinModel = new SkinXpp3Reader().read(in);
                context.setSkinModel(skinModel);

                String toolsPrerequisite = skinModel.getPrerequisites() == null
                        ? null
                        : skinModel.getPrerequisites().getDoxiaSitetools();

                Package p = DefaultSiteRenderer.class.getPackage();
                String current = (p == null) ? null : p.getImplementationVersion();

                if (StringUtils.isNotBlank(toolsPrerequisite)
                        && (current != null)
                        && !matchVersion(current, toolsPrerequisite)) {
                    throw new RendererException("Cannot use skin: has " + toolsPrerequisite
                            + " Doxia Sitetools prerequisite, but current is " + current);
                }
            }
        } catch (XmlPullParserException e) {
            throw new RendererException(
                    "Failed to parse " + SkinModel.SKIN_DESCRIPTOR_LOCATION + " skin descriptor from " + skin.getId()
                            + " skin",
                    e);
        } finally {
            IOUtil.close(in);
            closeZipFile(zipFile);
        }

        return context;
    }

    boolean matchVersion(String current, String prerequisite) throws RendererException {
        try {
            ArtifactVersion v = new DefaultArtifactVersion(current);
            VersionRange vr = VersionRange.createFromVersionSpec(prerequisite);

            boolean matched = false;
            ArtifactVersion recommendedVersion = vr.getRecommendedVersion();
            if (recommendedVersion == null) {
                List<Restriction> restrictions = vr.getRestrictions();
                for (Restriction restriction : restrictions) {
                    if (restriction.containsVersion(v)) {
                        matched = true;
                        break;
                    }
                }
            } else {
                // only singular versions ever have a recommendedVersion
                @SuppressWarnings("unchecked")
                int compareTo = recommendedVersion.compareTo(v);
                matched = (compareTo <= 0);
            }

            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Skin doxia-sitetools prerequisite: " + prerequisite + ", current: " + current
                        + ", matched = " + matched);
            }

            return matched;
        } catch (InvalidVersionSpecificationException e) {
            throw new RendererException("Invalid skin doxia-sitetools prerequisite: " + prerequisite, e);
        }
    }

    /** {@inheritDoc} */
    public void copyResources(SiteRenderingContext siteRenderingContext, File outputDirectory) throws IOException {
        ZipFile file = getZipFile(siteRenderingContext.getSkin().getFile());

        try {
            for (Enumeration<? extends ZipEntry> e = file.entries(); e.hasMoreElements(); ) {
                ZipEntry entry = e.nextElement();

                if (!entry.getName().startsWith("META-INF/")) {
                    File destFile = new File(outputDirectory, entry.getName());
                    if (!entry.isDirectory()) {
                        if (destFile.exists()) {
                            // don't override existing content: avoids extra rewrite with same content or extra site
                            // resource
                            continue;
                        }

                        destFile.getParentFile().mkdirs();

                        copyFileFromZip(file, entry, destFile);
                    } else {
                        destFile.mkdirs();
                    }
                }
            }
        } finally {
            closeZipFile(file);
        }

        // Copy extra site resources
        for (SiteDirectory siteDirectory : siteRenderingContext.getSiteDirectories()) {
            File resourcesDirectory = new File(siteDirectory.getPath(), "resources");

            if (resourcesDirectory != null && resourcesDirectory.exists()) {
                copyDirectory(resourcesDirectory, outputDirectory);
            }
        }

        // Check for the existence of /css/site.css
        File siteCssFile = new File(outputDirectory, "/css/site.css");
        if (!siteCssFile.exists()) {
            // Create the subdirectory css if it doesn't exist, DOXIA-151
            File cssDirectory = new File(outputDirectory, "/css/");
            boolean created = cssDirectory.mkdirs();
            if (created && LOGGER.isDebugEnabled()) {
                LOGGER.debug("The directory '" + cssDirectory.getAbsolutePath() + "' did not exist. It was created.");
            }

            // If the file is not there - create an empty file, DOXIA-86
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(
                        "The file '" + siteCssFile.getAbsolutePath() + "' does not exist. Creating an empty file.");
            }
            Writer writer = null;
            try {
                writer = WriterFactory.newWriter(siteCssFile, siteRenderingContext.getOutputEncoding());
                // DOXIA-290...the file should not be 0 bytes.
                writer.write("/* You can override this file with your own styles */");
            } finally {
                IOUtil.close(writer);
            }
        }
    }

    private static void copyFileFromZip(ZipFile file, ZipEntry entry, File destFile) throws IOException {
        FileOutputStream fos = new FileOutputStream(destFile);

        try {
            IOUtil.copy(file.getInputStream(entry), fos);
        } finally {
            IOUtil.close(fos);
        }
    }

    /**
     * Copy the directory
     *
     * @param source      source file to be copied
     * @param destination destination file
     * @throws java.io.IOException if any
     */
    protected void copyDirectory(File source, File destination) throws IOException {
        if (source.exists()) {
            DirectoryScanner scanner = new DirectoryScanner();

            String[] includedResources = {"**/*"};

            scanner.setIncludes(includedResources);

            scanner.addDefaultExcludes();

            scanner.setBasedir(source);

            scanner.scan();

            List<String> includedFiles = Arrays.asList(scanner.getIncludedFiles());

            for (String name : includedFiles) {
                File sourceFile = new File(source, name);

                File destinationFile = new File(destination, name);

                FileUtils.copyFile(sourceFile, destinationFile);
            }
        }
    }

    private Reader validate(Reader source, String resource) throws ParseException, IOException {
        LOGGER.debug("Validating: " + resource);

        try {
            String content = IOUtil.toString(new BufferedReader(source));

            new XmlValidator().validate(content);

            return new StringReader(content);
        } finally {
            IOUtil.close(source);
        }
    }

    // TODO replace with StringUtils.endsWithIgnoreCase() from maven-shared-utils 0.7
    static boolean endsWithIgnoreCase(String str, String searchStr) {
        if (str.length() < searchStr.length()) {
            return false;
        }

        return str.regionMatches(true, str.length() - searchStr.length(), searchStr, 0, searchStr.length());
    }

    private static ZipFile getZipFile(File file) throws IOException {
        if (file == null) {
            throw new IOException("Error opening ZipFile: null");
        }

        try {
            // TODO: plexus-archiver, if it could do the excludes
            return new ZipFile(file);
        } catch (ZipException ex) {
            IOException ioe = new IOException("Error opening ZipFile: " + file.getAbsolutePath());
            ioe.initCause(ex);
            throw ioe;
        }
    }

    private static void closeZipFile(ZipFile zipFile) {
        // TODO: move to plexus utils
        try {
            zipFile.close();
        } catch (IOException e) {
            // ignore
        }
    }

    private static String getSiteRendererVersion() {
        InputStream inputStream = DefaultSiteRenderer.class.getResourceAsStream(
                "/META-INF/" + "maven/org.apache.maven.doxia/doxia-site-renderer/pom.properties");
        if (inputStream == null) {
            LOGGER.debug("pom.properties for doxia-site-renderer not found");
        } else {
            Properties properties = new Properties();
            try (InputStream in = inputStream) {
                properties.load(in);
                return properties.getProperty("version");
            } catch (IOException e) {
                LOGGER.debug("Failed to load pom.properties, so Doxia SiteRenderer version will not be available", e);
            }
        }

        return null;
    }
}
