/*
 * Copyright (C) 2015 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.build.gradle.internal.api;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.BaseConfig;
import com.android.builder.model.ClassField;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import org.codehaus.groovy.runtime.DefaultGroovyMethods;

import java.io.File;
import java.util.Collection;
import java.util.Map;

import groovy.lang.GroovyObject;
import groovy.lang.GroovyObjectSupport;
import groovy.lang.MissingPropertyException;

/**
 * Read-only version of the BaseConfig wrapping another BaseConfig.
 *
 * <p>In the variant API, it is important that the objects returned by the variants are read-only.
 *
 * <p>However, even though the API is defined to use the base interfaces as return type (which all
 * contain only getters), the dynamics of Groovy makes it easy to actually use the setters of the
 * implementation classes.
 *
 * <p>This wrapper ensures that the returned instance is actually just a strict implementation of the
 * base interface and is read-only.
 */
public abstract class ReadOnlyBaseConfig extends GroovyObjectSupport implements BaseConfig {

    @NonNull
    private BaseConfig baseConfig;

    protected ReadOnlyBaseConfig(@NonNull BaseConfig baseConfig) {
        this.baseConfig = baseConfig;
    }

    @NonNull
    @Override
    public String getName() {
        return baseConfig.getName();
    }

    @Nullable
    @Override
    public String getApplicationIdSuffix() {
        return baseConfig.getApplicationIdSuffix();
    }

    @Nullable
    @Override
    public String getVersionNameSuffix() {
        return baseConfig.getVersionNameSuffix();
    }

    @NonNull
    @Override
    public Map<String, ClassField> getBuildConfigFields() {
        // TODO: cache immutable map?
        return ImmutableMap.copyOf(baseConfig.getBuildConfigFields());
    }

    @NonNull
    @Override
    public Map<String, ClassField> getResValues() {
        return ImmutableMap.copyOf(baseConfig.getResValues());
    }

    @NonNull
    @Override
    public Collection<File> getProguardFiles() {
        return ImmutableList.copyOf(baseConfig.getProguardFiles());
    }

    @NonNull
    @Override
    public Collection<File> getConsumerProguardFiles() {
        return ImmutableList.copyOf(baseConfig.getConsumerProguardFiles());
    }

    @NonNull
    @Override
    public Collection<File> getTestProguardFiles() {
        return ImmutableList.copyOf(baseConfig.getTestProguardFiles());
    }

    @NonNull
    @Override
    public Map<String, Object> getManifestPlaceholders() {
        return ImmutableMap.copyOf(baseConfig.getManifestPlaceholders());
    }

    @Nullable
    @Override
    public Boolean getMultiDexEnabled() {
        return baseConfig.getMultiDexEnabled();
    }

    @Nullable
    @Override
    public File getMultiDexKeepFile() {
        return baseConfig.getMultiDexKeepFile();
    }

    @Nullable
    @Override
    public File getMultiDexKeepProguard() {
        return baseConfig.getMultiDexKeepProguard();
    }

    /**
     * Some build scripts add dynamic properties to flavors declaration (and others) and expect to
     * retrieve such properties values through this model. Delegate any property we don't know about
     * to the {@link BaseConfig} groovy object which hopefully will know about the dynamic property.
     *
     * @param name the property name
     * @return the property value if exists or an exception will be thrown.
     */
    @SuppressWarnings("unused") // This is part of the Groovy method dispatch convention.
    public Object propertyMissing(final String name) {
        try {
            return ((GroovyObject) baseConfig).getProperty(name);
        } catch (MissingPropertyException e) {
            // do not leak implementation types, replace the delegate with ourselves in the message
            throw new MissingPropertyException("Could not find " + name + " on " + this);
        }

    }

    /**
     * Do not authorize setting dynamic properties values and provide a meaningful error message.
     */
    @SuppressWarnings("unused") // This is part of the Groovy method dispatch convention.
    public void propertyMissing(String name, Object value) {
        throw new RuntimeException(
                String.format(
                        "Cannot set property %s on read-only %s.",
                        name,
                        baseConfig.getClass().getName()));
    }

    // This is part of the Groovy/Gradle method dispatch convention.
    @SuppressWarnings({"unused"})
    public boolean hasProperty(String name) {
        if (DefaultGroovyMethods.hasProperty(this, name) != null) {
            return true;
        } else {
            GroovyObject groovyObject = (GroovyObject) this.baseConfig;
            // Object _Decorated by Gradle implement hasProperty, so the "usual" Groovy conventions
            // don't apply.
            return (Boolean) groovyObject.invokeMethod("hasProperty", name);
        }
    }
}
