/*
 *  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.groovy.contracts;

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

/**
 * Runtime support for a method-level {@link groovy.contracts.Decreases} recursion
 * termination measure. Code generated by
 * {@link org.apache.groovy.contracts.ast.MethodVariantASTTransformation} wraps the
 * method body so that on entry {@link #enter} records the measure (keyed by method
 * signature, per thread) and, when the method is already on the stack — i.e. this
 * is a recursive re-entry — checks that the measure strictly decreased and is
 * non-negative. {@link #exit} restores the enclosing frame's value on return.
 * <p>
 * Because the check is pure entry/exit bookkeeping it needs no static analysis of
 * recursive call sites: any re-entry of the same method (direct, mutual, or via a
 * callback) is compared against its nearest enclosing frame.
 *
 * @since 6.0.0
 * @see groovy.contracts.Decreases
 * @see RecursionVariantViolation
 */
public final class MethodVariantSupport {

    private MethodVariantSupport() {
    }

    /** Sentinel returned by {@link #enter} when checks are disabled, so {@link #exit} is a no-op. */
    private static final Object DISABLED = new Object();

    /** Per-thread stack-of-one-per-method: the measure of the nearest active invocation of each method. */
    private static final ThreadLocal<Map<String, Object>> CURRENT = ThreadLocal.withInitial(HashMap::new);

    /**
     * Record this invocation's measure for {@code key} (unless disabled for
     * {@code className} by {@code -ea}/{@code -da}). If an enclosing invocation of
     * the same method is already active — a recursive re-entry — verify the measure
     * strictly decreased and remained non-negative, raising a
     * {@link RecursionVariantViolation} otherwise. Returns the value {@link #exit}
     * must restore.
     *
     * @param key       the method's signature key
     * @param className the declaring class name (for {@code -ea}/{@code -da} gating)
     * @param measure   the measure value for this invocation
     * @return the enclosing invocation's measure, {@code null} if outermost, or a
     *         disabled sentinel
     */
    public static Object enter(final String key, final String className, final Object measure) {
        if (!VariantSupport.enabled(className)) return DISABLED;
        Map<String, Object> map = CURRENT.get();
        Object enclosing = map.get(key);
        if (enclosing != null) {
            String failure = VariantSupport.describeFailure(enclosing, measure);
            if (failure != null) {
                throw new RecursionVariantViolation("<groovy.contracts.Decreases> recursion measure " + failure);
            }
        }
        map.put(key, measure);
        return enclosing;
    }

    /**
     * Restore the enclosing invocation's measure on return from {@code key} (a no-op
     * when checks were disabled).
     *
     * @param key  the method's signature key
     * @param prev the value returned by the matching {@link #enter} call
     */
    public static void exit(final String key, final Object prev) {
        if (prev == DISABLED) return;
        Map<String, Object> map = CURRENT.get();
        if (prev == null) {
            map.remove(key);
        } else {
            map.put(key, prev);
        }
        // Drop the ThreadLocal when empty so pooled threads do not retain the map (java:S5164).
        if (map.isEmpty()) {
            CURRENT.remove();
        }
    }
}