/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.flink.runtime.metrics.scope;

import org.apache.flink.metrics.CharacterFilter;
import org.apache.flink.util.CollectionUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import static org.apache.flink.util.Preconditions.checkNotNull;

/**
 * This class represents the format after which the "scope" (or namespace) of the various component
 * metric groups is built. Component metric groups are for example "TaskManager", "Task", or
 * "Operator".
 *
 * <p>User defined scope formats allow users to include or exclude certain identifiers from the
 * scope. The scope for metrics belonging to the "Task" group could for example include the task
 * attempt number (more fine grained identification), or exclude it (continuity of the namespace
 * across failure and recovery).
 */
public abstract class ScopeFormat {

    // ------------------------------------------------------------------------
    //  Scope Format Special Characters
    // ------------------------------------------------------------------------

    /**
     * If the scope format starts with this character, then the parent components scope format will
     * be used as a prefix.
     *
     * <p>For example, if the TaskManager's job format is {@code "*.<job_name>"}, and the
     * TaskManager format is {@code "<host>"}, then the job's metrics will have {@code
     * "<host>.<job_name>"} as their scope.
     */
    public static final String SCOPE_INHERIT_PARENT = "*";

    public static final String SCOPE_SEPARATOR = ".";

    private static final String SCOPE_VARIABLE_PREFIX = "<";
    private static final String SCOPE_VARIABLE_SUFFIX = ">";

    // ------------------------------------------------------------------------
    //  Scope Variables
    // ------------------------------------------------------------------------

    public static final String SCOPE_HOST = asVariable("host");

    // ----- Task Manager ----

    public static final String SCOPE_TASKMANAGER_ID = asVariable("tm_id");

    // ----- Job -----

    public static final String SCOPE_JOB_ID = asVariable("job_id");
    public static final String SCOPE_JOB_NAME = asVariable("job_name");

    // ----- Task ----

    public static final String SCOPE_TASK_VERTEX_ID = asVariable("task_id");
    public static final String SCOPE_TASK_NAME = asVariable("task_name");
    public static final String SCOPE_TASK_ATTEMPT_ID = asVariable("task_attempt_id");
    public static final String SCOPE_TASK_ATTEMPT_NUM = asVariable("task_attempt_num");
    public static final String SCOPE_TASK_SUBTASK_INDEX = asVariable("subtask_index");

    // ----- Operator ----

    public static final String SCOPE_OPERATOR_ID = asVariable("operator_id");
    public static final String SCOPE_OPERATOR_NAME = asVariable("operator_name");

    // ------------------------------------------------------------------------
    //  Scope Format Base
    // ------------------------------------------------------------------------

    /** The scope format. */
    private final String format;

    /** The format, split into components. */
    private final String[] template;

    private final int[] templatePos;

    private final int[] valuePos;

    // ------------------------------------------------------------------------

    protected ScopeFormat(String format, ScopeFormat parent, String[] variables) {
        checkNotNull(format, "format is null");

        final String[] rawComponents = format.split("\\" + SCOPE_SEPARATOR);

        // compute the template array
        final boolean parentAsPrefix =
                rawComponents.length > 0 && rawComponents[0].equals(SCOPE_INHERIT_PARENT);
        if (parentAsPrefix) {
            if (parent == null) {
                throw new IllegalArgumentException(
                        "Component scope format requires parent prefix (starts with '"
                                + SCOPE_INHERIT_PARENT
                                + "'), but this component has no parent (is root component).");
            }

            this.format = format.length() > 2 ? format.substring(2) : "<empty>";

            String[] parentTemplate = parent.template;
            int parentLen = parentTemplate.length;

            this.template = new String[parentLen + rawComponents.length - 1];
            System.arraycopy(parentTemplate, 0, this.template, 0, parentLen);
            System.arraycopy(rawComponents, 1, this.template, parentLen, rawComponents.length - 1);
        } else {
            this.format = format.isEmpty() ? "<empty>" : format;
            this.template = rawComponents;
        }

        // --- compute the replacement matrix ---
        // a bit of clumsy Java collections code ;-)

        HashMap<String, Integer> varToValuePos = arrayToMap(variables);
        List<Integer> templatePos = new ArrayList<>();
        List<Integer> valuePos = new ArrayList<>();

        for (int i = 0; i < template.length; i++) {
            final String component = template[i];

            // check if that is a variable
            if (component != null
                    && component.length() >= 3
                    && component.charAt(0) == '<'
                    && component.charAt(component.length() - 1) == '>') {

                // this is a variable
                Integer replacementPos = varToValuePos.get(component);
                if (replacementPos != null) {
                    templatePos.add(i);
                    valuePos.add(replacementPos);
                }
            }
        }

        this.templatePos = integerListToArray(templatePos);
        this.valuePos = integerListToArray(valuePos);
    }

    // ------------------------------------------------------------------------

    public String format() {
        return format;
    }

    protected final String[] copyTemplate() {
        String[] copy = new String[template.length];
        System.arraycopy(template, 0, copy, 0, template.length);
        return copy;
    }

    protected final String[] bindVariables(String[] template, String[] values) {
        final int len = templatePos.length;
        for (int i = 0; i < len; i++) {
            template[templatePos[i]] = values[valuePos[i]];
        }
        return template;
    }

    // ------------------------------------------------------------------------

    @Override
    public String toString() {
        return "ScopeFormat '" + format + '\'';
    }

    // ------------------------------------------------------------------------
    //  Utilities
    // ------------------------------------------------------------------------

    /**
     * Formats the given string to resemble a scope variable.
     *
     * @param scope The string to format
     * @return The formatted string
     */
    public static String asVariable(String scope) {
        return SCOPE_VARIABLE_PREFIX + scope + SCOPE_VARIABLE_SUFFIX;
    }

    /**
     * Concatenates the given component names separated by the delimiter character. Additionally the
     * character filter is applied to all component names.
     *
     * @param filter Character filter to be applied to the component names
     * @param delimiter Delimiter to separate component names
     * @param components Array of component names
     * @return The concatenated component name
     */
    public static String concat(CharacterFilter filter, Character delimiter, String... components) {
        StringBuilder sb = new StringBuilder();
        sb.append(filter.filterCharacters(components[0]));
        for (int x = 1; x < components.length; x++) {
            sb.append(delimiter);
            sb.append(filter.filterCharacters(components[x]));
        }
        return sb.toString();
    }

    protected static String valueOrNull(Object value) {
        return (value == null || (value instanceof String && ((String) value).isEmpty()))
                ? "null"
                : value.toString();
    }

    protected static HashMap<String, Integer> arrayToMap(String[] array) {
        HashMap<String, Integer> map = CollectionUtil.newHashMapWithExpectedSize(array.length);
        for (int i = 0; i < array.length; i++) {
            map.put(array[i], i);
        }
        return map;
    }

    private static int[] integerListToArray(List<Integer> list) {
        int[] array = new int[list.size()];
        int pos = 0;
        for (Integer i : list) {
            array[pos++] = i;
        }
        return array;
    }
}
