/*
 * 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.struts2.util.fs;

import org.apache.struts2.FileManager;
import org.apache.struts2.dispatcher.InternalDestroyable;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.Objects.requireNonNullElseGet;

/**
 * Default implementation of {@link FileManager}
 */
public class DefaultFileManager implements FileManager, InternalDestroyable {

    private static final Logger LOG = LogManager.getLogger(DefaultFileManager.class);

    private static final Pattern JAR_PATTERN = Pattern.compile(
            "^(jar:|wsjar:|zip:|vfsfile:|code-source:)?(file:)?(.*?)(!/|\\.jar/)(.*)");
    private static final int JAR_FILE_PATH = 3;

    protected static final Map<String, Revision> files = Collections.synchronizedMap(new HashMap<>());
    private static final List<URL> lazyMonitoredFilesCache = Collections.synchronizedList(new ArrayList<>());

    protected boolean reloadingConfigs = false;

    public DefaultFileManager() {
    }

    public static void clearCache() {
        files.clear();
        lazyMonitoredFilesCache.clear();
    }

    /**
     * @since 7.2.0
     */
    @Override
    public void destroy() {
        clearCache();
    }

    public void setReloadingConfigs(boolean reloadingConfigs) {
        if (reloadingConfigs && !this.reloadingConfigs) {
            //starting monitoring cached not-monitored files (lazy monitoring on demand because of performance)
            this.reloadingConfigs = true;
            synchronized (lazyMonitoredFilesCache) {
                for (URL fileUrl : lazyMonitoredFilesCache) {
                    monitorFile(fileUrl);
                }
                lazyMonitoredFilesCache.clear();
            }
        }
        this.reloadingConfigs = reloadingConfigs;
    }

    public boolean fileNeedsReloading(URL fileUrl) {
        return fileUrl != null && fileNeedsReloading(fileUrl.toString());
    }

    public boolean fileNeedsReloading(String fileName) {
        Revision revision = files.get(fileName);
        if (revision == null) {
            // no revision yet and we keep the revision history, so
            // return whether the file needs to be loaded for the first time
            return reloadingConfigs;
        }
        return revision.needsReloading();
    }

    public InputStream loadFile(URL fileUrl) {
        if (fileUrl == null) {
            return null;
        }
        InputStream is = openFile(fileUrl);
        monitorFile(fileUrl);
        return is;
    }

    private InputStream openFile(URL fileUrl) {
        try {
            InputStream is = fileUrl.openStream();
            if (is == null) {
                throw new IllegalArgumentException("No file '" + fileUrl + "' found as a resource");
            }
            return is;
        } catch (IOException e) {
            throw new IllegalArgumentException("No file '" + fileUrl + "' found as a resource");
        }
    }

    public void monitorFile(URL fileUrl) {
        String fileName = fileUrl.toString();
        if (!reloadingConfigs) {
            //reserve file for monitoring on demand because of performance
            files.remove(fileName);
            lazyMonitoredFilesCache.add(fileUrl);
            return;
        }
        Revision revision;
        LOG.debug("Creating revision for URL: {}", fileName);
        if (isJarURL(fileUrl)) {
            revision = JarEntryRevision.build(fileUrl, this);
        } else {
            revision = FileRevision.build(fileUrl);
        }
        files.put(fileName, requireNonNullElseGet(revision, () -> Revision.build(fileUrl)));
    }

    /**
     * Check if given URL is matching Jar pattern for different servers
     *
     * @param fileUrl jar file URL
     * @return if given URL is matching Jar pattern for different servers
     */
    protected boolean isJarURL(URL fileUrl) {
        Matcher jarMatcher = JAR_PATTERN.matcher(fileUrl.getPath());
        return jarMatcher.matches();
    }

    public URL normalizeToFileProtocol(URL url) {
        String fileName = url.toExternalForm();
        Matcher jarMatcher = JAR_PATTERN.matcher(fileName);
        try {
            if (jarMatcher.matches()) {
                String path = jarMatcher.group(JAR_FILE_PATH);
                return new URL("file", "", path);
            } else if ("file".equals(url.getProtocol())) {
                return url; // it's already a file
            } else {
                LOG.warn("Could not normalize URL [{}] to file protocol!", url);
                return null;
            }
        } catch (MalformedURLException e) {
            LOG.warn("Error normalizing URL [{}] to file protocol!", url, e);
            return null;
        }
    }

    public boolean support() {
        return false; // allow other implementation to be used first
    }

    public boolean internal() {
        return true;
    }

    public Collection<? extends URL> getAllPhysicalUrls(URL url) throws IOException {
        return List.of(url);
    }

}
