/*
 * Copyright (C) 2014 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 com.android.layoutlib.bridge.intensive;

import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.Result;
import com.android.ide.common.rendering.api.SessionParams;
import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.ide.common.resources.FrameworkResources;
import com.android.ide.common.resources.ResourceItem;
import com.android.ide.common.resources.ResourceRepository;
import com.android.ide.common.resources.ResourceResolver;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.io.FolderWrapper;
import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.android.BridgeContext;
import com.android.layoutlib.bridge.android.RenderParamsFlags;
import com.android.layoutlib.bridge.impl.DelegateManager;
import com.android.layoutlib.bridge.impl.RenderAction;
import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator;
import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback;
import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser;
import com.android.resources.Density;
import com.android.resources.Navigation;
import com.android.resources.ResourceType;
import com.android.utils.ILogger;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.DisplayMetrics;

import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import com.google.android.collect.Lists;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * This is a set of tests that loads all the framework resources and a project checked in this
 * test's resources. The main dependencies
 * are:
 * 1. Fonts directory.
 * 2. Framework Resources.
 * 3. App resources.
 * 4. build.prop file
 *
 * These are configured by two variables set in the system properties.
 *
 * 1. platform.dir: This is the directory for the current platform in the built SDK
 *     (.../sdk/platforms/android-<version>).
 *
 *     The fonts are platform.dir/data/fonts.
 *     The Framework resources are platform.dir/data/res.
 *     build.prop is at platform.dir/build.prop.
 *
 * 2. test_res.dir: This is the directory for the resources of the test. If not specified, this
 *     falls back to getClass().getProtectionDomain().getCodeSource().getLocation()
 *
 *     The app resources are at: test_res.dir/testApp/MyApplication/app/src/main/res
 */
public class Main {

    private static final String PLATFORM_DIR_PROPERTY = "platform.dir";
    private static final String RESOURCE_DIR_PROPERTY = "test_res.dir";

    private static final String PLATFORM_DIR;
    private static final String TEST_RES_DIR;
    /** Location of the app to test inside {@link #TEST_RES_DIR}*/
    private static final String APP_TEST_DIR = "/testApp/MyApplication";
    /** Location of the app's res dir inside {@link #TEST_RES_DIR}*/
    private static final String APP_TEST_RES = APP_TEST_DIR + "/src/main/res";

    private static LayoutLog sLayoutLibLog;
    private static FrameworkResources sFrameworkRepo;
    private static ResourceRepository sProjectResources;
    private static  ILogger sLogger;
    private static Bridge sBridge;

    /** List of log messages generated by a render call. It can be used to find specific errors */
    private static ArrayList<String> sRenderMessages = Lists.newArrayList();

    @Rule
    public static TestWatcher sRenderMessageWatcher = new TestWatcher() {
        @Override
        protected void succeeded(Description description) {
            // We only check error messages if the rest of the test case was successful.
            if (!sRenderMessages.isEmpty()) {
                fail(description.getMethodName() + " render error message: " + sRenderMessages.get
                        (0));
            }
        }
    };

    static {
        // Test that System Properties are properly set.
        PLATFORM_DIR = getPlatformDir();
        if (PLATFORM_DIR == null) {
            fail(String.format("System Property %1$s not properly set. The value is %2$s",
                    PLATFORM_DIR_PROPERTY, System.getProperty(PLATFORM_DIR_PROPERTY)));
        }

        TEST_RES_DIR = getTestResDir();
        if (TEST_RES_DIR == null) {
            fail(String.format("System property %1$s.dir not properly set. The value is %2$s",
                    RESOURCE_DIR_PROPERTY, System.getProperty(RESOURCE_DIR_PROPERTY)));
        }
    }

    private static String getPlatformDir() {
        String platformDir = System.getProperty(PLATFORM_DIR_PROPERTY);
        if (platformDir != null && !platformDir.isEmpty() && new File(platformDir).isDirectory()) {
            return platformDir;
        }
        // System Property not set. Try to find the directory in the build directory.
        String androidHostOut = System.getenv("ANDROID_HOST_OUT");
        if (androidHostOut != null) {
            platformDir = getPlatformDirFromHostOut(new File(androidHostOut));
            if (platformDir != null) {
                return platformDir;
            }
        }
        String workingDirString = System.getProperty("user.dir");
        File workingDir = new File(workingDirString);
        // Test if workingDir is android checkout root.
        platformDir = getPlatformDirFromRoot(workingDir);
        if (platformDir != null) {
            return platformDir;
        }

        // Test if workingDir is platform/frameworks/base/tools/layoutlib/bridge.
        File currentDir = workingDir;
        if (currentDir.getName().equalsIgnoreCase("bridge")) {
            currentDir = currentDir.getParentFile();
        }
        // Test if currentDir is  platform/frameworks/base/tools/layoutlib. That is, root should be
        // workingDir/../../../../  (4 levels up)
        for (int i = 0; i < 4; i++) {
            if (currentDir != null) {
                currentDir = currentDir.getParentFile();
            }
        }
        return currentDir == null ? null : getPlatformDirFromRoot(currentDir);
    }

    private static String getPlatformDirFromRoot(File root) {
        if (!root.isDirectory()) {
            return null;
        }
        File out = new File(root, "out");
        if (!out.isDirectory()) {
            return null;
        }
        File host = new File(out, "host");
        if (!host.isDirectory()) {
            return null;
        }
        File[] hosts = host.listFiles(path -> path.isDirectory() &&
                (path.getName().startsWith("linux-") || path.getName().startsWith("darwin-")));
        for (File hostOut : hosts) {
            String platformDir = getPlatformDirFromHostOut(hostOut);
            if (platformDir != null) {
                return platformDir;
            }
        }
        return null;
    }

    private static String getPlatformDirFromHostOut(File out) {
        if (!out.isDirectory()) {
            return null;
        }
        File sdkDir = new File(out, "sdk");
        if (!sdkDir.isDirectory()) {
            return null;
        }
        File[] sdkDirs = sdkDir.listFiles(path -> {
            // We need to search for $TARGET_PRODUCT (usually, sdk_phone_armv7)
            return path.isDirectory() && path.getName().startsWith("sdk");
        });
        for (File dir : sdkDirs) {
            String platformDir = getPlatformDirFromHostOutSdkSdk(dir);
            if (platformDir != null) {
                return platformDir;
            }
        }
        return null;
    }

    private static String getPlatformDirFromHostOutSdkSdk(File sdkDir) {
        File[] possibleSdks = sdkDir.listFiles(
                path -> path.isDirectory() && path.getName().contains("android-sdk"));
        for (File possibleSdk : possibleSdks) {
            File platformsDir = new File(possibleSdk, "platforms");
            File[] platforms = platformsDir.listFiles(
                    path -> path.isDirectory() && path.getName().startsWith("android-"));
            if (platforms == null || platforms.length == 0) {
                continue;
            }
            Arrays.sort(platforms, (o1, o2) -> {
                final int MAX_VALUE = 1000;
                String suffix1 = o1.getName().substring("android-".length());
                String suffix2 = o2.getName().substring("android-".length());
                int suff1, suff2;
                try {
                    suff1 = Integer.parseInt(suffix1);
                } catch (NumberFormatException e) {
                    suff1 = MAX_VALUE;
                }
                try {
                    suff2 = Integer.parseInt(suffix2);
                } catch (NumberFormatException e) {
                    suff2 = MAX_VALUE;
                }
                if (suff1 != MAX_VALUE || suff2 != MAX_VALUE) {
                    return suff2 - suff1;
                }
                return suffix2.compareTo(suffix1);
            });
            return platforms[0].getAbsolutePath();
        }
        return null;
    }

    private static String getTestResDir() {
        String resourceDir = System.getProperty(RESOURCE_DIR_PROPERTY);
        if (resourceDir != null && !resourceDir.isEmpty() && new File(resourceDir).isDirectory()) {
            return resourceDir;
        }
        // TEST_RES_DIR not explicitly set. Fallback to the class's source location.
        try {
            URL location = Main.class.getProtectionDomain().getCodeSource().getLocation();
            return new File(location.getPath()).exists() ? location.getPath() : null;
        } catch (NullPointerException e) {
            // Prevent a lot of null checks by just catching the exception.
            return null;
        }
    }

    /**
     * Initialize the bridge and the resource maps.
     */
    @BeforeClass
    public static void setUp() {
        File data_dir = new File(PLATFORM_DIR, "data");
        File res = new File(data_dir, "res");
        sFrameworkRepo = new FrameworkResources(new FolderWrapper(res));
        sFrameworkRepo.loadResources();
        sFrameworkRepo.loadPublicResources(getLogger());

        sProjectResources =
                new ResourceRepository(new FolderWrapper(TEST_RES_DIR + APP_TEST_RES), false) {
            @NonNull
            @Override
            protected ResourceItem createResourceItem(@NonNull String name) {
                return new ResourceItem(name);
            }
        };
        sProjectResources.loadResources();

        File fontLocation = new File(data_dir, "fonts");
        File buildProp = new File(PLATFORM_DIR, "build.prop");
        File attrs = new File(res, "values" + File.separator + "attrs.xml");
        sBridge = new Bridge();
        sBridge.init(ConfigGenerator.loadProperties(buildProp), fontLocation,
                ConfigGenerator.getEnumMap(attrs), getLayoutLog());
    }

    @Before
    public void beforeTestCase() {
        sRenderMessages.clear();
    }

    /** Test activity.xml */
    @Test
    public void testActivity() throws ClassNotFoundException {
        renderAndVerify("activity.xml", "activity.png");
    }

    /** Test allwidgets.xml */
    @Test
    public void testAllWidgets() throws ClassNotFoundException {
        renderAndVerify("allwidgets.xml", "allwidgets.png");

        // We expect fidelity warnings for Path.isConvex. Fail for anything else.
        sRenderMessages.removeIf(message -> message.equals("Path.isConvex is not supported."));
    }

    @Test
    public void testArrayCheck() throws ClassNotFoundException {
        renderAndVerify("array_check.xml", "array_check.png");
    }

    @Test
    public void testAllWidgetsTablet() throws ClassNotFoundException {
        renderAndVerify("allwidgets.xml", "allwidgets_tab.png", ConfigGenerator.NEXUS_7_2012);

        // We expect fidelity warnings for Path.isConvex. Fail for anything else.
        sRenderMessages.removeIf(message -> message.equals("Path.isConvex is not supported."));
    }

    private static void gc() {
        // See RuntimeUtil#gc in jlibs (http://jlibs.in/)
        Object obj = new Object();
        WeakReference ref = new WeakReference<Object>(obj);
        obj = null;
        while(ref.get() != null) {
            System.gc();
        }
    }

    @AfterClass
    public static void tearDown() {
        sLayoutLibLog = null;
        sFrameworkRepo = null;
        sProjectResources = null;
        sLogger = null;
        sBridge = null;

        gc();

        System.out.println("Objects still linked from the DelegateManager:");
        DelegateManager.dump(System.out);
    }

    /** Test expand_layout.xml */
    @Test
    public void testExpand() throws ClassNotFoundException {
        // Create the layout pull parser.
        LayoutPullParser parser = createLayoutPullParser("expand_vert_layout.xml");
        // Create LayoutLibCallback.
        LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger());
        layoutLibCallback.initResources();

        ConfigGenerator customConfigGenerator = new ConfigGenerator()
                .setScreenWidth(300)
                .setScreenHeight(20)
                .setDensity(Density.XHIGH)
                .setNavigation(Navigation.NONAV);

        SessionParams params = getSessionParams(parser, customConfigGenerator,
                layoutLibCallback, "Theme.Material.Light.NoActionBar.Fullscreen", false,
                RenderingMode.V_SCROLL, 22);

        renderAndVerify(params, "expand_vert_layout.png");

        customConfigGenerator = new ConfigGenerator()
                .setScreenWidth(20)
                .setScreenHeight(300)
                .setDensity(Density.XHIGH)
                .setNavigation(Navigation.NONAV);
        parser = createLayoutPullParser("expand_horz_layout.xml");
        params = getSessionParams(parser, customConfigGenerator,
                layoutLibCallback, "Theme.Material.Light.NoActionBar.Fullscreen", false,
                RenderingMode.H_SCROLL, 22);

        renderAndVerify(params, "expand_horz_layout.png");
    }

    /** Test indeterminate_progressbar.xml */
    @Test
    public void testVectorAnimation() throws ClassNotFoundException {
        // Create the layout pull parser.
        LayoutPullParser parser = createLayoutPullParser("indeterminate_progressbar.xml");
        // Create LayoutLibCallback.
        LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger());
        layoutLibCallback.initResources();

        SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
                layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
                RenderingMode.V_SCROLL, 22);

        renderAndVerify(params, "animated_vector.png", TimeUnit.SECONDS.toNanos(2));

        parser = createLayoutPullParser("indeterminate_progressbar.xml");
        params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
                layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
                RenderingMode.V_SCROLL, 22);
        renderAndVerify(params, "animated_vector_1.png", TimeUnit.SECONDS.toNanos(3));
    }

    /**
     * Test a vector drawable that uses trimStart and trimEnd. It also tests all the primitives
     * for vector drawables (lines, moves and cubic and quadratic curves).
     */
    @Test
    public void testVectorDrawable() throws ClassNotFoundException {
        // Create the layout pull parser.
        LayoutPullParser parser = createLayoutPullParser("vector_drawable.xml");
        // Create LayoutLibCallback.
        LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger());
        layoutLibCallback.initResources();

        SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
                layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
                RenderingMode.V_SCROLL, 22);

        renderAndVerify(params, "vector_drawable.png", TimeUnit.SECONDS.toNanos(2));
    }

    /** Test activity.xml */
    @Test
    public void testScrolling() throws ClassNotFoundException {
        // Create the layout pull parser.
        LayoutPullParser parser = createLayoutPullParser("scrolled.xml");
        // Create LayoutLibCallback.
        LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger());
        layoutLibCallback.initResources();

        SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
                layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
                RenderingMode.V_SCROLL, 22);
        params.setForceNoDecor();
        params.setExtendedViewInfoMode(true);

        RenderResult result = renderAndVerify(params, "scrolled.png");
        assertNotNull(result);
        assertTrue(result.getResult().isSuccess());

        ViewInfo rootLayout = result.getRootViews().get(0);
        // Check the first box in the main LinearLayout
        assertEquals(-90, rootLayout.getChildren().get(0).getTop());
        assertEquals(-30, rootLayout.getChildren().get(0).getLeft());
        assertEquals(90, rootLayout.getChildren().get(0).getBottom());
        assertEquals(150, rootLayout.getChildren().get(0).getRight());

        // Check the first box within the nested LinearLayout
        assertEquals(-450, rootLayout.getChildren().get(5).getChildren().get(0).getTop());
        assertEquals(90, rootLayout.getChildren().get(5).getChildren().get(0).getLeft());
        assertEquals(-270, rootLayout.getChildren().get(5).getChildren().get(0).getBottom());
        assertEquals(690, rootLayout.getChildren().get(5).getChildren().get(0).getRight());
    }

    @Test
    public void testGetResourceNameVariants() throws Exception {
        // Setup
        SessionParams params = createSessionParams("", ConfigGenerator.NEXUS_4);
        AssetManager assetManager = AssetManager.getSystem();
        DisplayMetrics metrics = new DisplayMetrics();
        Configuration configuration = RenderAction.getConfiguration(params);
        Resources resources = new Resources(assetManager, metrics, configuration);
        resources.mLayoutlibCallback = params.getLayoutlibCallback();
        resources.mContext =
                new BridgeContext(params.getProjectKey(), metrics, params.getResources(),
                        params.getAssets(), params.getLayoutlibCallback(), configuration,
                        params.getTargetSdkVersion(), params.isRtlSupported());
        // Test
        assertEquals("android:style/ButtonBar",
                resources.getResourceName(android.R.style.ButtonBar));
        assertEquals("android", resources.getResourcePackageName(android.R.style.ButtonBar));
        assertEquals("ButtonBar", resources.getResourceEntryName(android.R.style.ButtonBar));
        assertEquals("style", resources.getResourceTypeName(android.R.style.ButtonBar));
        int id = resources.mLayoutlibCallback.getResourceId(ResourceType.STRING, "app_name");
        assertEquals("com.android.layoutlib.test.myapplication:string/app_name",
                resources.getResourceName(id));
        assertEquals("com.android.layoutlib.test.myapplication",
                resources.getResourcePackageName(id));
        assertEquals("string", resources.getResourceTypeName(id));
        assertEquals("app_name", resources.getResourceEntryName(id));
    }

    @NonNull
    private LayoutPullParser createLayoutPullParser(String layoutPath) {
        return new LayoutPullParser(APP_TEST_RES + "/layout/" + layoutPath);
    }

    /**
     * Create a new rendering session and test that rendering the given layout doesn't throw any
     * exceptions and matches the provided image.
     * <p>
     * If frameTimeNanos is >= 0 a frame will be executed during the rendering. The time indicates
     * how far in the future is.
     */
    @Nullable
    private RenderResult renderAndVerify(SessionParams params, String goldenFileName, long frameTimeNanos)
            throws ClassNotFoundException {
        // TODO: Set up action bar handler properly to test menu rendering.
        // Create session params.
        RenderSession session = sBridge.createSession(params);

        if (frameTimeNanos != -1) {
            session.setElapsedFrameTimeNanos(frameTimeNanos);
        }

        if (!session.getResult().isSuccess()) {
            getLogger().error(session.getResult().getException(),
                    session.getResult().getErrorMessage());
        }
        // Render the session with a timeout of 50s.
        Result renderResult = session.render(50000);
        if (!renderResult.isSuccess()) {
            getLogger().error(session.getResult().getException(),
                    session.getResult().getErrorMessage());
        }
        try {
            String goldenImagePath = APP_TEST_DIR + "/golden/" + goldenFileName;
            ImageUtils.requireSimilar(goldenImagePath, session.getImage());

            return RenderResult.getFromSession(session);
        } catch (IOException e) {
            getLogger().error(e, e.getMessage());
        } finally {
            session.dispose();
        }

        return null;
    }

    /**
     * Create a new rendering session and test that rendering the given layout doesn't throw any
     * exceptions and matches the provided image.
     */
    @Nullable
    private RenderResult renderAndVerify(SessionParams params, String goldenFileName)
            throws ClassNotFoundException {
        return renderAndVerify(params, goldenFileName, -1);
    }

    /**
     * Create a new rendering session and test that rendering the given layout on nexus 5
     * doesn't throw any exceptions and matches the provided image.
     */
    @Nullable
    private RenderResult renderAndVerify(String layoutFileName, String goldenFileName)
            throws ClassNotFoundException {
        return renderAndVerify(layoutFileName, goldenFileName, ConfigGenerator.NEXUS_5);
    }

    /**
     * Create a new rendering session and test that rendering the given layout on given device
     * doesn't throw any exceptions and matches the provided image.
     */
    @Nullable
    private RenderResult renderAndVerify(String layoutFileName, String goldenFileName,
            ConfigGenerator deviceConfig)
            throws ClassNotFoundException {
        SessionParams params = createSessionParams(layoutFileName, deviceConfig);
        return renderAndVerify(params, goldenFileName);
    }

    private SessionParams createSessionParams(String layoutFileName, ConfigGenerator deviceConfig)
            throws ClassNotFoundException {
        // Create the layout pull parser.
        LayoutPullParser parser = createLayoutPullParser(layoutFileName);
        // Create LayoutLibCallback.
        LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger());
        layoutLibCallback.initResources();
        // TODO: Set up action bar handler properly to test menu rendering.
        // Create session params.
        return getSessionParams(parser, deviceConfig,
                layoutLibCallback, "AppTheme", true, RenderingMode.NORMAL, 22);
    }

    /**
     * Uses Theme.Material and Target sdk version as 22.
     */
    private SessionParams getSessionParams(LayoutPullParser layoutParser,
            ConfigGenerator configGenerator, LayoutLibTestCallback layoutLibCallback,
            String themeName, boolean isProjectTheme, RenderingMode renderingMode, int targetSdk) {
        FolderConfiguration config = configGenerator.getFolderConfig();
        ResourceResolver resourceResolver =
                ResourceResolver.create(sProjectResources.getConfiguredResources(config),
                        sFrameworkRepo.getConfiguredResources(config),
                        themeName, isProjectTheme);

        SessionParams sessionParams = new SessionParams(
                layoutParser,
                renderingMode,
                null /*used for caching*/,
                configGenerator.getHardwareConfig(),
                resourceResolver,
                layoutLibCallback,
                0,
                targetSdk,
                getLayoutLog());
        sessionParams.setFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true);
        return sessionParams;
    }

    private static LayoutLog getLayoutLog() {
        if (sLayoutLibLog == null) {
            sLayoutLibLog = new LayoutLog() {
                @Override
                public void warning(String tag, String message, Object data) {
                    System.out.println("Warning " + tag + ": " + message);
                    failWithMsg(message);
                }

                @Override
                public void fidelityWarning(@Nullable String tag, String message,
                        Throwable throwable, Object data) {

                    System.out.println("FidelityWarning " + tag + ": " + message);
                    if (throwable != null) {
                        throwable.printStackTrace();
                    }
                    failWithMsg(message == null ? "" : message);
                }

                @Override
                public void error(String tag, String message, Object data) {
                    System.out.println("Error " + tag + ": " + message);
                    failWithMsg(message);
                }

                @Override
                public void error(String tag, String message, Throwable throwable, Object data) {
                    System.out.println("Error " + tag + ": " + message);
                    if (throwable != null) {
                        throwable.printStackTrace();
                    }
                    failWithMsg(message);
                }
            };
        }
        return sLayoutLibLog;
    }

    private static ILogger getLogger() {
        if (sLogger == null) {
            sLogger = new ILogger() {
                @Override
                public void error(Throwable t, @Nullable String msgFormat, Object... args) {
                    if (t != null) {
                        t.printStackTrace();
                    }
                    failWithMsg(msgFormat == null ? "" : msgFormat, args);
                }

                @Override
                public void warning(@NonNull String msgFormat, Object... args) {
                    failWithMsg(msgFormat, args);
                }

                @Override
                public void info(@NonNull String msgFormat, Object... args) {
                    // pass.
                }

                @Override
                public void verbose(@NonNull String msgFormat, Object... args) {
                    // pass.
                }
            };
        }
        return sLogger;
    }

    private static void failWithMsg(@NonNull String msgFormat, Object... args) {
        sRenderMessages.add(args == null ? msgFormat : String.format(msgFormat, args));
    }
}
