/*
 * Copyright 2021 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.car.app.managers;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;

import java.util.HashMap;
import java.util.Map;

/**
 * A map of {@link Manager}s created lazily by invoking the corresponding {@link ManagerFactory}.
 *
 */
@RestrictTo(LIBRARY_GROUP)
public class ManagerCache {
    private final Map<Class<?>, Manager> mValues = new HashMap<>();
    private final Map<Class<?>, RuntimeException> mExceptions = new HashMap<>();
    private final Map<Class<?>, ManagerFactory<? extends Manager>> mFactories = new HashMap<>();
    private final Map<String, Class<?>> mClassByName = new HashMap<>();
    private final Map<Class<?>, String> mNameByClass = new HashMap<>();

    /**
     * Adds a {@link ManagerFactory} to this container.
     *
     * @param clazz   {@link Manager} class this factory is able to produce
     * @param name    name of the given {@link Manager}, or {@code null} if this {@link Manager} is
     *                for internal library use only
     * @param factory a factory able to produce an instance of this type of manager
     * @param <T>     the type of {@link Manager} being added to the container
     */
    public <T extends Manager> void addFactory(@NonNull Class<T> clazz,
            @Nullable String name, @NonNull ManagerFactory<T> factory) {
        mFactories.put(clazz, factory);
        if (name != null) {
            mClassByName.put(name, clazz);
            mNameByClass.put(clazz, name);
        }
    }

    /**
     * Returns the name of a {@link Manager} identified by its class.
     *
     * @throws IllegalArgumentException if the name is unknown
     */
    @NonNull
    public String getName(@NonNull Class<?> clazz) {
        String name = mNameByClass.get(clazz);
        if (name == null) {
            throw new IllegalArgumentException("The class does not correspond to a car service");
        }
        return name;
    }

    /**
     * Returns an instance of a {@link Manager} identified by its name.
     *
     * @throws IllegalArgumentException if the name is unknown
     * @throws IllegalStateException    if the given manager can not be instantiated
     */
    @NonNull
    public Object getOrCreate(@NonNull String name) throws IllegalArgumentException {
        Class<?> clazz = mClassByName.get(name);
        if (clazz == null) {
            throw new IllegalArgumentException(
                    "The name '" + name + "' does not correspond to a car service");
        }
        return getOrCreate(clazz);
    }

    /**
     * Returns an instance of a {@link Manager} identified by its class
     *
     * @throws IllegalArgumentException if the class is unknown
     * @throws IllegalStateException    if the given manager can not be instantiated
     */
    @SuppressWarnings("unchecked")
    @NonNull
    public <T> T getOrCreate(@NonNull Class<T> clazz) {
        RuntimeException exception = mExceptions.get(clazz);
        if (exception != null) {
            throw exception;
        }
        Manager value = mValues.get(clazz);
        if (value != null) {
            return (T) value;
        }

        ManagerFactory<? extends Manager> factory = mFactories.get(clazz);
        if (factory == null) {
            throw new IllegalArgumentException("The class '" + clazz + "' does not correspond to a "
                    + "car service");
        }
        try {
            value = factory.create();
            mValues.put(clazz, value);
            return (T) value;
        } catch (RuntimeException newException) {
            mExceptions.put(clazz, newException);
            throw newException;
        }
    }
}
