/*
 * Copyright (C) 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 com.android.build.config;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * Compares the make-based configuration as reported by dumpconfig.mk
 * with what was computed from the new tool.
 */
public class OutputChecker {
    // Differences that we know about, either know issues to be fixed or intentional.
    private static final RegexSet IGNORED_VARIABLES = new RegexSet(
            // TODO: Rewrite the enforce packages exist logic into this tool.
            "PRODUCT_ENFORCE_PACKAGES_EXIST",
            "PRODUCT_ENFORCE_PACKAGES_EXIST_ALLOW_LIST",
            "PRODUCTS\\..*\\.PRODUCT_ENFORCE_PACKAGES_EXIST",
            "PRODUCTS\\..*\\.PRODUCT_ENFORCE_PACKAGES_EXIST_ALLOW_LIST",

            // This is generated by this tool, but comes later in the make build system.
            "INTERNAL_PRODUCT",

            // This can be set temporarily by product_config.mk
            ".KATI_ALLOW_RULES"
            );

    private final FlatConfig mConfig;
    private final TreeMap<String, Variable> mVariables;

    /**
     * Represents the before and after state of a variable.
     */
    public static class Variable {
        public final String name;
        public final VarType type;
        public final Str original;
        public final Value updated;

        public Variable(String name, VarType type, Str original) {
            this(name, type, original, null);
        }

        public Variable(String name, VarType type, Str original, Value updated) {
            this.name = name;
            this.type = type;
            this.original = original;
            this.updated = updated;
        }

        /**
         * Return copy of this Variable with the updated field also set.
         */
        public Variable addUpdated(Value updated) {
            return new Variable(name, type, original, updated);
        }

        /**
         * Return whether normalizedOriginal and normalizedUpdate are equal.
         */
        public boolean isSame() {
            final Str normalizedOriginal = Value.normalize(original);
            final Str normalizedUpdated = Value.normalize(updated);
            if (normalizedOriginal == null && normalizedUpdated == null) {
                return true;
            } else if (normalizedOriginal != null) {
                return normalizedOriginal.equals(normalizedUpdated);
            } else {
                return false;
            }
        }
    }

    /**
     * Construct OutputChecker with the config it will check.
     */
    public OutputChecker(FlatConfig config) {
        mConfig = config;
        mVariables = getVariables(config);
    }

    /**
     * Add a WARNING_DIFFERENT_FROM_KATI for each of the variables which have changed.
     */
    public void reportErrors(Errors errors) {
        for (Variable var: getDifferences()) {
            if (IGNORED_VARIABLES.matches(var.name)) {
                continue;
            }
            errors.WARNING_DIFFERENT_FROM_KATI.add("product_config processing differs from"
                    + " kati processing for " + var.type + " variable " + var.name + ".\n"
                    + "original: "
                    + Value.oneLinePerWord(var.original, "<null>") + "\n"
                    + "updated: "
                    + Value.oneLinePerWord(var.updated, "<null>"));
        }
    }

    /**
     * Get the Variables that are different between the normalized form of the original
     * and updated.  If one is null and the other is not, even if one is an empty string,
     * the values are considered different.
     */
    public List<Variable> getDifferences() {
        final ArrayList<Variable> result = new ArrayList();
        for (Variable var: mVariables.values()) {
            if (!var.isSame()) {
                result.add(var);
            }
        }
        return result;
    }

    /**
     * Get all of the variables for this config.
     *
     * VisibleForTesting
     */
    static TreeMap<String, Variable> getVariables(FlatConfig config) {
        final TreeMap<String, Variable> result = new TreeMap();

        // Add the original values to mAll
        for (Map.Entry<String, Str> entry: getModifiedVars(config.getInitialVariables(),
                    config.getFinalVariables()).entrySet()) {
            final String name = entry.getKey();
            result.put(name, new Variable(name, config.getVarType(name), entry.getValue()));
        }

        // Add the updated values to mAll
        for (Map.Entry<String, Value> entry: config.getValues().entrySet()) {
            final String name = entry.getKey();
            final Value value = entry.getValue();
            Variable var = result.get(name);
            if (var == null) {
                result.put(name, new Variable(name, config.getVarType(name), null, value));
            } else {
                result.put(name, var.addUpdated(value));
            }
        }

        return result;
    }

    /**
     * Get the entries that are different in the two maps.
     */
    public static Map<String, Str> getModifiedVars(Map<String, Str> before,
            Map<String, Str> after) {
        final HashMap<String, Str> result = new HashMap();

        // Entries that were added or changed.
        for (Map.Entry<String, Str> afterEntry: after.entrySet()) {
            final String varName = afterEntry.getKey();
            final Str afterValue = afterEntry.getValue();
            final Str beforeValue = before.get(varName);
            if (beforeValue == null || !beforeValue.equals(afterValue)) {
                result.put(varName, afterValue);
            }
        }

        // removed Entries that were removed, we just treat them as empty string
        for (Map.Entry<String, Str> beforeEntry: before.entrySet()) {
            final String varName = beforeEntry.getKey();
            if (!after.containsKey(varName)) {
                result.put(varName, new Str(""));
            }
        }

        return result;
    }
}
