/*
 *  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.causeway.commons.internal.resources;

import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import org.springframework.lang.Nullable;

import org.apache.causeway.commons.internal.base._Bytes;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.context._Context;
import org.apache.causeway.commons.internal.exceptions._Exceptions;

import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.val;

/**
 * <h1>- internal use only -</h1>
 * <p>
 * Utilities for storing and locating resources.
 * </p>
 * <p>
 * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
 * These may be changed or removed without notice!
 * </p>
 * @since 2.0
 */
public final class _Resources {

    // -- URL UTILITY

    /**
     * Returns {@code null} for {@code null}. Throws on invalid input.
     * @implNote XSS detection has potential for false positives
     */
    @SneakyThrows
    public static URL url(final @Nullable String url) {
        if(url == null) {
            return null;
        }
        // simple guard against XSS attacks like javascript:alert(document)
        if(_Strings.condenseWhitespaces(url.toLowerCase(), "").contains("javascript:")) {
            throw new IllegalArgumentException("Not parseable as an URL ('" + url + "').");
        }
        return new URI(url).toURL();
    }

    // -- CLASS PATH RESOURCE LOADING

    /**
     * Returns the resource with path {@code resourceName} relative to {@code contextClass} as an InputStream.
     * @param contextClass
     * @param resourceName
     * @return An input stream for reading the resource, or null if the resource could not be found.
     */
    @SneakyThrows
    public static @Nullable InputStream load(
            final @NonNull Class<?> contextClass,
            final @NonNull String resourceName) {

        val absoluteResourceName = resolveName(contextClass, resourceName);

        return Optional
                .ofNullable(contextClass.getResourceAsStream(absoluteResourceName))
                .orElseGet(()->_Context.getDefaultClassLoader()
                        .getResourceAsStream(absoluteResourceName));
    }

    /**
     * Returns the resource with path {@code resourceName} relative to {@code contextClass} as a String
     * conforming to the given {@code charset}.
     * @param contextClass
     * @param resourceName
     * @param charset
     * @return The resource as a String, or null if the resource could not be found.
     */
    @SneakyThrows
    public static @Nullable String loadAsString(
            final @NonNull Class<?> contextClass,
            final @NonNull String resourceName,
            final @NonNull Charset charset) {

        val inputStream = load(contextClass, resourceName);
        return _Strings.ofBytes(_Bytes.of(inputStream), charset);
    }

    /**
     * Shortcut using Charset UTF-8, see {@link #loadAsString(Class, String, Charset)}
     */
    public static @Nullable String loadAsStringUtf8(
            final @NonNull Class<?> contextClass,
            final @NonNull String resourceName) {
        return loadAsString(contextClass, resourceName, StandardCharsets.UTF_8);
    }

    /**
     * Similar to {@link #loadAsStringUtf8(Class, String)},
     * but throws an {@link IllegalStateException},
     * if the resource was not found or could not be read.
     */
    public static String loadAsStringUtf8ElseFail(
            final @NonNull Class<?> contextClass,
            final @NonNull String resourceName) {
        try {
            var content = loadAsStringUtf8(contextClass, resourceName);
            if(content==null) {
                throw _Exceptions.illegalState("Failed to locate resource '%s' relative to class %s",
                        resourceName, contextClass.getName());
            }
            return content;
        } catch (Exception e) {
            throw _Exceptions.illegalState(e, "Failed to read resource '%s' relative to class %s",
                    resourceName, contextClass.getName());
        }
    }

    /**
     * @param resourceName
     * @return The resource location as an URL, or null if the resource could not be found.
     */
    public static @Nullable URL getResourceUrl(
            final @NonNull Class<?> contextClass,
            final @NonNull String resourceName) {

        final String absoluteResourceName = resolveName(contextClass, resourceName);

        return Optional
                .ofNullable(contextClass.getResource(absoluteResourceName))
                .orElseGet(()->_Context.getDefaultClassLoader()
                        .getResource(absoluteResourceName));
    }

    // -- LOCAL vs EXTERNAL resource path

    private static final Predicate<String> externalResourcePattern =
            Pattern.compile("^\\w+?://.*$").asPredicate();

    /**
     * Returns whether the {@code resourcePath} is intended local and relative
     * to the web-app's context root.
     * @param resourcePath
     */
    public static boolean isLocalResource(final @NonNull String resourcePath) {
        return !externalResourcePattern.test(resourcePath);
    }

    /**
     * To build a path from chunks {@code 'a' + 'b' -> 'a/b'}, also handling cases eg.
     * {@code 'a/' + '/b' -> 'a/b'}
     *
     * @param extendee
     * @param suffix
     */
    public static String combinePath(final @Nullable String extendee, final @Nullable String suffix) {
        return _Strings.combineWithDelimiter(extendee, suffix, "/");
    }

    // -- HELPER

    /*
     *
     * Adapted copy of JDK 8 Class::resolveName
     */
    private static String resolveName(final Class<?> contextClass, String name) {
        if (name == null) {
            return name;
        }
        if (!name.startsWith("/")) {
            Class<?> c = contextClass;
            while (c.isArray()) {
                c = c.getComponentType();
            }
            String baseName = c.getName();
            int index = baseName.lastIndexOf('.');
            if (index != -1) {
                name = baseName.substring(0, index).replace('.', '/')
                        +"/"+name;
            }
        } else {
            name = name.substring(1);
        }
        return name;
    }

}
