/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed 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 androidx.webkit;

import android.content.Context;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresFeature;
import androidx.webkit.internal.ApiHelperForP;
import androidx.webkit.internal.StartupApiFeature;
import androidx.webkit.internal.WebViewFeatureInternal;

import org.chromium.support_lib_boundary.ProcessGlobalConfigConstants;

import java.io.File;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Process Global Configuration for WebView.
 *
 * WebView has some process-global configuration parameters that cannot be changed once WebView has
 * been loaded. This class allows apps to set these parameters.
 * <p>
 * If it is used, the configuration should be set and
 * {@link #apply(androidx.webkit.ProcessGlobalConfig)} should be called prior to
 * loading WebView into the calling process. Most of the methods in
 * {@link android.webkit} and {@link androidx.webkit} packages load WebView, so the
 * configuration should be applied before calling any of these methods.
 * <p>
 * The following code configures the data directory suffix that WebView
 * uses and then applies the configuration. WebView uses this configuration when it is loaded.
 * <pre class="prettyprint">
 * ProcessGlobalConfig config = new ProcessGlobalConfig();
 * config.setDataDirectorySuffix("random_suffix")
 * ProcessGlobalConfig.apply(config);
 * </pre>
 * <p>
 * {@link ProcessGlobalConfig#apply(androidx.webkit.ProcessGlobalConfig)} can only be called once.
 * <p>
 * Only a single thread should access this class at a given time.
 * <p>
 * The configuration should be set up as early as possible during application startup, to ensure
 * that it happens before any other thread can call a method that loads WebView.
 */
public class ProcessGlobalConfig {
    private static final AtomicReference<HashMap<String, Object>> sProcessGlobalConfig =
            new AtomicReference<HashMap<String, Object>>();
    private static final Object sLock = new Object();
    @GuardedBy("sLock")
    private static boolean sApplyCalled = false;
    String mDataDirectorySuffix;
    String mDataDirectoryBasePath;
    String mCacheDirectoryBasePath;

    /**
     * Creates a {@link ProcessGlobalConfig} object.
     */
    public ProcessGlobalConfig() {
    }

    /**
     * Define the directory used to store WebView data for the current process.
     *
     * The provided suffix will be used when constructing data and cache
     * directory paths. If this API is not called, no suffix will be used.
     * Each directory can be used by only one process in the application. If more
     * than one process in an app wishes to use WebView, only one process can use
     * the default directory, and other processes must call this API to define
     * a unique suffix.
     * <p>
     * This means that different processes in the same application cannot directly
     * share WebView-related data, since the data directories must be distinct.
     * Applications that use this API may have to explicitly pass data between
     * processes. For example, login cookies may have to be copied from one
     * process's cookie jar to the other using {@link android.webkit.CookieManager} if both
     * processes' WebViews are intended to be logged in.
     * <p>
     * Most applications should simply ensure that all components of the app
     * that rely on WebView are in the same process, to avoid needing multiple
     * data directories. The {@link android.webkit.WebView#disableWebView} method can be used to
     * ensure that the other processes do not use WebView by accident in this case.
     * <p>
     * This is a compatibility method for
     * {@link android.webkit.WebView#setDataDirectorySuffix(String)}
     *
     * @param context a Context to access application assets This value cannot be null.
     * @param suffix The directory name suffix to be used for the current
     *               process. Must not contain a path separator and should not be empty.
     * @return the ProcessGlobalConfig that has the value set to allow chaining of setters
     * @throws UnsupportedOperationException if underlying WebView does not support the use of
     *                                       the method.
     * @throws IllegalArgumentException if the suffix contains a path separator or is empty.
     */
    @RequiresFeature(name = WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX,
            enforcement =
                    "androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)")
    @NonNull
    public ProcessGlobalConfig setDataDirectorySuffix(@NonNull Context context,
            @NonNull String suffix) {
        final StartupApiFeature.P feature =
                WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
        if (!feature.isSupported(context)) {
            throw WebViewFeatureInternal.getUnsupportedOperationException();
        }
        if (suffix.equals("")) {
            throw new IllegalArgumentException("Suffix cannot be an empty string");
        }
        if (suffix.indexOf(File.separatorChar) >= 0) {
            throw new IllegalArgumentException("Suffix " + suffix
                    + " contains a path separator");
        }
        mDataDirectorySuffix = suffix;
        return this;
    }

    /**
     * Set the base directories that WebView will use for the current process.
     *
     * If this method is not used, WebView uses the default base paths defined by the Android
     * framework.
     * <p>
     * WebView will create and use a subdirectory under each of the base paths supplied to this
     * method.
     * <p>
     * This method can be used in conjunction with {@link #setDataDirectorySuffix(Context, String)}.
     * A different subdirectory is created for each suffix.
     * <p>
     * The base paths must be absolute paths.
     * <p>
     * The data directory must not be under the Android cache directory, as Android may delete
     * cache files when disk space is low and WebView may not function properly if this occurs.
     * Refer to
     * <a href="https://developer.android.com/training/data-storage/app-specific#internal-remove-cache">this</a>
     *  link.
     * <p>
     * If the specified directories already exist then they must be readable and writable by the
     * current process. If they do not already exist, WebView will attempt to create them during
     * initialization, along with any missing parent directories. In such a case, the directory
     * in which WebView creates missing directories must be readable and writable by the
     * current process.
     *
     * @param context a Context to access application assets. This value cannot be null.
     * @param dataDirectoryBasePath the absolute base path for the WebView data directory.
     * @param cacheDirectoryBasePath the absolute base path for the WebView cache directory.
     * @return the ProcessGlobalConfig that has the value set to allow chaining of setters
     * @throws UnsupportedOperationException if underlying WebView does not support the use of
     *                                       the method.
     * @throws IllegalArgumentException if the paths supplied do not have the right permissions
     */
    @SuppressWarnings("StreamFiles")
    @RequiresFeature(name =
            WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS,
            enforcement =
                    "androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)")
    @NonNull
    public ProcessGlobalConfig setDirectoryBasePaths(@NonNull Context context,
            @NonNull File dataDirectoryBasePath, @NonNull File cacheDirectoryBasePath) {
        final StartupApiFeature.NoFramework feature =
                WebViewFeatureInternal.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATH;
        if (!feature.isSupported(context)) {
            throw WebViewFeatureInternal.getUnsupportedOperationException();
        }
        if (!dataDirectoryBasePath.isAbsolute()) {
            throw new IllegalArgumentException("dataDirectoryBasePath must be a non-empty absolute"
                    + " path");
        }
        if (!cacheDirectoryBasePath.isAbsolute()) {
            throw new IllegalArgumentException("cacheDirectoryBasePath must be a non-empty absolute"
                    + " path");
        }
        mDataDirectoryBasePath = dataDirectoryBasePath.getAbsolutePath();
        mCacheDirectoryBasePath = cacheDirectoryBasePath.getAbsolutePath();
        return this;
    }

    /**
     * Applies the configuration to be used by WebView on loading.
     *
     * This method can only be called once.
     * <p>
     * Calling this method will not cause WebView to be loaded and will not block the calling
     * thread.
     *
     * @param config the config to be applied
     * @throws IllegalStateException if WebView has already been initialized
     *                               in the current process or if this method was called before
     */
    public static void apply(@NonNull ProcessGlobalConfig config) {
        // TODO(crbug.com/1355297): We can check if we are storing the config in the place that
        //  WebView is going to look for it, and throw if they are not the same.
        //  For this, we would need to reflect into Android Framework internals to get
        //  ActivityThread.currentApplication().getClassLoader() and see if it is the same as
        //  this.getClass().getClassLoader(). This would add reflection that we might not add a
        //  framework API for. Once we know what framework path we will take for
        //  ProcessGlobalConfig, revisit this.
        synchronized (sLock) {
            if (sApplyCalled) {
                throw new IllegalStateException("ProcessGlobalConfig#apply was "
                        + "called more than once, which is an illegal operation. The configuration "
                        + "settings provided by ProcessGlobalConfig take effect only once, when "
                        + "WebView is first loaded into the current process. Every process should "
                        + "only ever create a single instance of ProcessGlobalConfig and apply it "
                        + "once, before any calls to android.webkit APIs, such as during early app "
                        + "startup."
                );
            }
            sApplyCalled = true;
        }
        HashMap<String, Object> configMap = new HashMap<String, Object>();
        if (webViewCurrentlyLoaded()) {
            throw new IllegalStateException("WebView has already been loaded in the current "
                    + "process, so any attempt to apply the settings in ProcessGlobalConfig will "
                    + "have no effect. ProcessGlobalConfig#apply needs to be called before any "
                    + "calls to android.webkit APIs, such as during early app startup.");
        }
        if (config.mDataDirectorySuffix != null) {
            final StartupApiFeature.P feature =
                    WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
            if (feature.isSupportedByFramework()) {
                ApiHelperForP.setDataDirectorySuffix(config.mDataDirectorySuffix);
            } else {
                configMap.put(ProcessGlobalConfigConstants.DATA_DIRECTORY_SUFFIX,
                        config.mDataDirectorySuffix);
            }
        }
        if (config.mDataDirectoryBasePath != null) {
            configMap.put(ProcessGlobalConfigConstants.DATA_DIRECTORY_BASE_PATH,
                    config.mDataDirectoryBasePath);
        }
        if (config.mCacheDirectoryBasePath != null) {
            configMap.put(ProcessGlobalConfigConstants.CACHE_DIRECTORY_BASE_PATH,
                    config.mCacheDirectoryBasePath);
        }
        if (!sProcessGlobalConfig.compareAndSet(null, configMap)) {
            throw new RuntimeException("Attempting to set ProcessGlobalConfig"
                    + "#sProcessGlobalConfig when it was already set");
        }
    }

    private static boolean webViewCurrentlyLoaded() {
        // TODO(crbug.com/1355297): This is racy but it is the best we can do for now since we can't
        //  access the lock for sProviderInstance in WebView. Evaluate a framework path for
        //  ProcessGlobalConfig.
        try {
            Class<?> webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
            Field providerInstanceField =
                    webViewFactoryClass.getDeclaredField("sProviderInstance");
            providerInstanceField.setAccessible(true);
            return providerInstanceField.get(null) != null;
        } catch (Exception e) {
            // This means WebViewFactory was not found or sProviderInstance was not found within
            // the class. If that is true, WebView doesn't seem to be loaded.
            return false;
        }
    }
}
