/*
 * 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.ofbiz.webapp;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

import org.apache.ofbiz.base.component.ComponentConfig;
import org.apache.ofbiz.base.component.ComponentConfig.WebappInfo;
import org.apache.ofbiz.base.util.UtilValidate;

/**
 * Cache for web applications information retrieved from
 * {@linkplain ComponentConfig component configurations}.
 * <p>
 * This improves performance by avoiding to retrieve web applications from
 * component configurations each time.
 * <p>
 * This is a cache which doesn't implement any invalidation mechanism.  Once a
 * web applications is defined, it is <b>memoized</b> because it is not meant
 * to change while OFBiz is running.
 *
 * @see <a href="https://en.wikipedia.org/wiki/Memoization">Memoization</a>
 */
public final class WebAppCache {
    // Synchronized map storing web applications.
    // The LinkedHashMap is used to maintain insertion order (which client code depends on).
    // There is no concurrent implementation of LinkedHashMap, so we are using manual synchronization instead.
    private final LinkedHashMap<String, List<WebappInfo>> serverWebApps;
    // Source for retrieving components configurations.
    private final Supplier<Collection<ComponentConfig>> ccs;

    /**
     * Constructs an empty web application cache.
     * @param supplier the source from which components configurations are retrieved
     */
    WebAppCache(Supplier<Collection<ComponentConfig>> supplier) {
        ccs = supplier;
        serverWebApps = new LinkedHashMap<>();
    }

    /**
     * Retrieves the web applications information that must be visible
     * in the context of the server {@code serverName}.
     * @param serverName the name of the server to match
     * @return the corresponding web applications information
     */
    public List<WebappInfo> getAppBarWebInfos(String serverName) {
        return getAppBarWebInfos(serverName, null);
    }

    /**
     * Retrieves the web applications information that must be visible inside
     * the menu {@code menuName} in the context of the server {@code serverName}.
     * <p>
     * When an empty string or {@code null} is used for {@code menuName},
     * all the web application information corresponding to {@code serverName} are matched.
     * @param serverName the name of server to match
     * @param menuName the name of the menu to match
     * @return the corresponding web applications information
     * @throws NullPointerException when {@code serverName} is {@code null}
     */
    public List<WebappInfo> getAppBarWebInfos(String serverName, String menuName) {
        String serverWebAppsKey = serverName + menuName;
        List<WebappInfo> webInfos = null;
        synchronized (serverWebApps) {
            webInfos = serverWebApps.get(serverWebAppsKey);
        }
        if (webInfos == null) {
            AtomicInteger emptyPosition = new AtomicInteger(999);
            TreeMap<Integer, WebappInfo> tm = ccs.get().stream()
                    .flatMap(cc -> cc.getWebappInfos().stream())
                    .filter(wInfo -> {
                        if (wInfo.getAppBarDisplay()) {
                            return serverName.equals(wInfo.getServer())
                                    && (UtilValidate.isEmpty(menuName) || menuName.equals(wInfo.getMenuName()));
                        } else {
                            return UtilValidate.isEmpty(menuName);
                        }
                    })
                    // Keep only one WebappInfo per title (the last appearing one).
                    .collect(TreeMap::new, (acc, wInfo) -> {
                        String stringKey = UtilValidate.isNotEmpty(wInfo.getPosition()) ? wInfo.getPosition() : wInfo.getTitle();
                        Integer key = null;
                        try {
                            key = Integer.valueOf(stringKey);
                            key = (key != null) && !acc.containsKey(key) ? key : emptyPosition.incrementAndGet();
                        } catch (NumberFormatException e) {
                            key = emptyPosition.incrementAndGet();
                        }
                        acc.put(key, wInfo);
                    },
                            TreeMap::putAll);
            // Create the list of WebappInfos ordered by their title/position.
            webInfos = Collections.unmodifiableList(new ArrayList<>(tm.values()));
            synchronized (serverWebApps) {
                // We are only preventing concurrent modification, we are not guaranteeing a singleton.
                serverWebApps.put(serverWebAppsKey, webInfos);
            }
        }
        return webInfos;
    }

    /**
     * Retrieves the first web application information which mount point correspond to
     * {@code webAppName} in the context of the server {@code serverName}.
     * @param serverName the name of the server to match
     * @param webAppName the name of the web application to match
     * @return the corresponding web application information
     * @throws NullPointerException when {@code serverName} is {@code null}
     */
    public Optional<WebappInfo> getWebappInfo(String serverName, String webAppName) {
        return getAppBarWebInfos(serverName).stream()
                .filter(app -> app.getMountPoint().replaceAll("[/*]", "").equals(webAppName))
                .findFirst();
    }

    // Instance of the cache shared by the loginWorker and Freemarker appbar rendering.
    // TODO: Find a way to share this cache without relying on a global variable.
    private static WebAppCache sharedCache = new WebAppCache(ComponentConfig::getAllComponents);

    /**
     * Provides access to a shared instance of the webapp cache.
     * @return the shared webapp cache.
     */
    public static WebAppCache getShared() {
        return sharedCache;
    }
}
