// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.module_installer.builder;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.BundleUtils;
import org.chromium.base.StrictModeContext;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.components.module_installer.engine.InstallEngine;
import org.chromium.components.module_installer.engine.InstallListener;
import org.chromium.components.module_installer.util.Timer;

/**
 * Represents a feature module. Can be used to install the module, access its interface, etc. See
 * {@link ModuleInterface} for how to conveniently create an instance of the module class for a
 * specific feature module.
 *
 * @param <T> The interface of the module
 */
@JNINamespace("module_installer")
public class Module<T> {
    private final String mName;
    private final Class<T> mInterfaceClass;
    private final String mImplClassName;
    private T mImpl;
    private InstallEngine mInstaller;
    private boolean mIsNativeLoaded;

    /**
     * Instantiates a module.
     *
     * @param name The module's name as used with {@link ModuleInstaller}.
     * @param interfaceClass {@link Class} object of the module interface.
     * @param implClassName fully qualified class name of the implementation of the module's
     *                      interface.
     */
    public Module(String name, Class<T> interfaceClass, String implClassName) {
        mName = name;
        mInterfaceClass = interfaceClass;
        mImplClassName = implClassName;
    }

    @VisibleForTesting
    public InstallEngine getInstallEngine() {
        if (mInstaller == null) {
            try (Timer timer = new Timer()) {
                mInstaller = new ModuleEngine(mImplClassName);
            }
        }
        return mInstaller;
    }

    @VisibleForTesting
    public void setInstallEngine(InstallEngine engine) {
        mInstaller = engine;
    }

    /**
     * Returns true if the module is currently installed and can be accessed.
     */
    public boolean isInstalled() {
        try (Timer timer = new Timer()) {
            return getInstallEngine().isInstalled(mName);
        }
    }

    /**
     * Requests install of the module.
     */
    public void install(InstallListener listener) {
        try (Timer timer = new Timer()) {
            assert !isInstalled();
            getInstallEngine().install(mName, listener);
        }
    }

    /**
     * Requests deferred install of the module.
     */
    public void installDeferred() {
        try (Timer timer = new Timer()) {
            getInstallEngine().installDeferred(mName);
        }
    }

    /**
     * Returns the implementation of the module interface. Must only be called if the module is
     * installed.
     */
    public T getImpl() {
        try (Timer timer = new Timer()) {
            if (mImpl != null) return mImpl;
            assert isInstalled();

            ModuleDescriptor moduleDescriptor = loadModuleDescriptor(mName);
            if (moduleDescriptor.getLoadNativeOnGetImpl()) {
                // Load the module's native code and/or resources if they are present, and the
                // Chrome native library itself has been loaded.
                ensureNativeLoaded();
            }

            mImpl = mInterfaceClass.cast(instantiateReflectively(mImplClassName));
            return mImpl;
        }
    }

    /**
     * Loads native libraries and/or resources if and only if this is not already done, assuming
     * that the module is installed, and enableNativeLoad() has been called.
     */
    public void ensureNativeLoaded() {
        // Can only initialize native once per lifetime of Chrome.
        if (mIsNativeLoaded) return;
        assert LibraryLoader.getInstance().isInitialized();
        ModuleDescriptor moduleDescriptor = loadModuleDescriptor(mName);
        String[] libraries = moduleDescriptor.getLibraries();
        String[] paks = moduleDescriptor.getPaks();
        if (libraries.length > 0 || paks.length > 0) {
            ModuleJni.get().loadNative(mName, libraries, paks);
        }
        mIsNativeLoaded = true;
    }

    /**
     * Loads the {@link ModuleDescriptor} for a module.
     *
     * For bundles, uses reflection to load the descriptor from inside the
     * module. For APKs, returns an empty descriptor since APKs won't have
     * descriptors packaged into them.
     *
     * @param name The module's name.
     * @return The module's {@link ModuleDescriptor}.
     */
    private static ModuleDescriptor loadModuleDescriptor(String name) {
        if (!BundleUtils.isBundle()) {
            return new ModuleDescriptor() {
                @Override
                public String[] getLibraries() {
                    return new String[0];
                }

                @Override
                public String[] getPaks() {
                    return new String[0];
                }

                @Override
                public boolean getLoadNativeOnGetImpl() {
                    return false;
                }
            };
        }

        return (ModuleDescriptor) instantiateReflectively(
                "org.chromium.components.module_installer.builder.ModuleDescriptor_" + name);
    }

    /**
     * Instantiates an object via reflection.
     *
     * Ignores strict mode violations since accessing code in a module may cause its DEX file to be
     * loaded and on some devices that can cause such a violation.
     *
     * @param className The object's class name.
     * @return The object.
     */
    private static Object instantiateReflectively(String className) {
        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
            return Class.forName(className).newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @NativeMethods
    interface Natives {
        void loadNative(String name, String[] libraries, String[] paks);
    }
}
