/*
 * 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.sling.scripting.jsp;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.sling.api.SlingException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceMetadata;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.commons.classloader.ClassLoaderWriter;
import org.apache.sling.commons.compiler.JavaCompiler;
import org.apache.sling.scripting.jsp.jasper.IOProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The <code>SlingIOProvider</code>
 */
class SlingIOProvider implements IOProvider {

    private static final String WEB_INF_TAGS = "/WEB-INF/tags";

    /** default log */
    private final Logger log = LoggerFactory.getLogger(SlingIOProvider.class);

    private final ThreadLocal<ResourceResolver> requestResourceResolver;

    private final ClassLoaderWriter classLoaderWriter;

    private final JavaCompiler javaCompiler;

    SlingIOProvider(final ClassLoaderWriter classLoaderWriter, final JavaCompiler compiler) {
        this.requestResourceResolver = new ThreadLocal<ResourceResolver>();
        this.classLoaderWriter = classLoaderWriter;
        this.javaCompiler = compiler;
    }

    /**
     * Set the thread context resource resolver.
     */
    ResourceResolver setRequestResourceResolver(final ResourceResolver resolver) {
        final ResourceResolver old = requestResourceResolver.get();
        requestResourceResolver.set(resolver);
        return old;
    }

    /**
     * Reset the thread context resource resolver.
     */
    void resetRequestResourceResolver(final ResourceResolver resolver) {
        requestResourceResolver.set(resolver);
    }

    // ---------- IOProvider interface -----------------------------------------

    /**
     * Returns an InputStream for the file name which is looked up with the
     * ResourceProvider and retrieved from the Resource if the StreamProvider
     * interface is implemented.
     */
    public InputStream getInputStream(final String path)
            throws FileNotFoundException, IOException {
        if ( path.startsWith(":") ) {
            return this.classLoaderWriter.getInputStream(path.substring(1));
        }
        ResourceResolver resolver = requestResourceResolver.get();
        if (resolver != null) {
            try {
                final Resource resource = resolver.getResource(cleanPath(path, true));
                if (resource != null) {
                    final InputStream stream = resource.adaptTo(InputStream.class);
                    if (stream != null) {
                        return stream;
                    }
                }
            } catch (final SlingException se) {
                throw (IOException) new IOException(
                    "Failed to get InputStream for " + path).initCause(se);
            }
        }
        throw new FileNotFoundException("Cannot find " + path);
    }

    /**
     * Returns the value of the last modified meta data field of the resource
     * found at file name or zero if the meta data field is not set. If the
     * resource does not exist or an error occurrs finding the resource, -1 is
     * returned.
     */
    public long lastModified(final String path) {
        if ( path.startsWith(":") ) {
            return this.classLoaderWriter.getLastModified(path.substring(1));
        }
        ResourceResolver resolver = requestResourceResolver.get();
        if (resolver != null) {
            try {
                final Resource resource = resolver.getResource(cleanPath(path, true));
                if (resource != null) {
                    ResourceMetadata meta = resource.getResourceMetadata();
                    long modTime = meta.getModificationTime();
                    return (modTime > 0) ? modTime : 0;
                }
            } catch (final SlingException se) {
                log.error("Cannot get last modification time for " + path, se);
            }
        }
        // fallback to "non-existant" in case of problems
        return -1;
    }

    /**
     * Removes the named item from the repository.
     */
    public boolean delete(final String path) {
        return this.classLoaderWriter.delete(path.substring(1));
    }

    /**
     * Returns an output stream to write to the repository.
     */
    public OutputStream getOutputStream(final String path) {
        return this.classLoaderWriter.getOutputStream(path.substring(1));
    }

    /**
     * Renames a node in the repository.
     */
    public boolean rename(final String oldFileName, final String newFileName) {
        return this.classLoaderWriter.rename(oldFileName.substring(1), newFileName.substring(1));
    }

    /**
     * Creates a folder hierarchy in the repository.
     */
    public boolean mkdirs(final String path) {
        // we just do nothing
        return true;
    }

    /**
     * @see org.apache.sling.scripting.jsp.jasper.IOProvider#getClassLoader()
     */
    public ClassLoader getClassLoader() {
        return this.classLoaderWriter.getClassLoader();
    }

    // ---------- Helper Methods for JspServletContext -------------------------


    URL getURL(final String path) throws MalformedURLException {
        ResourceResolver resolver = requestResourceResolver.get();
        if (resolver != null) {
            try {
                final Resource resource = resolver.getResource(cleanPath(path, true));
                return (resource != null) ? resource.adaptTo(URL.class) : null;
            } catch (final SlingException se) {
                throw (MalformedURLException) new MalformedURLException(
                    "Cannot get URL for " + path).initCause(se);
            }
        }
        return null;
    }

    Set<String> getResourcePaths(final String path) {
        final Set<String> paths = new HashSet<String>();

        ResourceResolver resolver = requestResourceResolver.get();
        if (resolver != null) {
            try {
                final String cleanedPath = cleanPath(path, false);
                final boolean startsWithWebInfTags = cleanedPath.startsWith(WEB_INF_TAGS);

                Resource resource = resolver.getResource(startsWithWebInfTags ? cleanedPath.substring(WEB_INF_TAGS.length()) : cleanedPath);
                if (resource != null) {
                    Iterator<Resource> entries = resolver.listChildren(resource);
                    while (entries.hasNext()) {
                        final String entryPath = entries.next().getPath();
                        if (startsWithWebInfTags) {
                            paths.add(WEB_INF_TAGS + entryPath);
                        } else {
                            paths.add(entryPath);
                        }
                    }
                }
            } catch (final SlingException se) {
                log.warn("getResourcePaths: Cannot list children of " + path,
                    se);
            }
        }

        return paths.isEmpty() ? null : paths;
    }

    // ---------- internal -----------------------------------------------------

    private String cleanPath(String path, final boolean removeWebInfTags) {
        // replace backslash by slash
        path = path.replace('\\', '/');

        // cut off trailing slash
        while (path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }

        if (removeWebInfTags && path.startsWith(WEB_INF_TAGS)) {
            path = path.substring(WEB_INF_TAGS.length());
        }

        return path;
    }

    /**
     * @see org.apache.sling.scripting.jsp.jasper.IOProvider#getJavaCompiler()
     */
    public JavaCompiler getJavaCompiler() {
        return this.javaCompiler;
    }

    /**
     * @see org.apache.sling.scripting.jsp.jasper.IOProvider#getClassLoaderWriter()
     */
    public ClassLoaderWriter getClassLoaderWriter() {
        return this.classLoaderWriter;
    }
}
