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

import groovy.lang.IntRange;
import org.apache.groovy.lang.annotation.GroovyABI;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static org.codehaus.groovy.runtime.DefaultGroovyMethodsSupport.createSimilarList;

/**
 * Runtime helpers used by the GEP-20 multi-assignment destructuring bytecode.
 *
 * <p>These are called from generated bytecode only. User code should not rely on them.</p>
 *
 * @since 6.0.0
 */
@GroovyABI(since = "6.0.0")
public final class MultipleAssignmentSupport {

    private MultipleAssignmentSupport() {}

    private static final Class<?>[] INT_RANGE_PARAM = { IntRange.class };

    /**
     * Resolve the value for a tail rest binding ({@code *t}) at declarator index
     * {@code fromIndex}. The GEP-20 dispatch model has three paths:
     *
     * <ul>
     *   <li><b>Path A</b> — {@code Stream<T>}: wrap the already-advanced iterator
     *       in a fresh sequential Stream and chain {@code onClose} to the
     *       original. The new Stream reports no spliterator characteristics
     *       (preserving them would require reading the original spliterator,
     *       which is mutually exclusive with the iterator path used for head
     *       extraction).</li>
     *   <li><b>Path B</b> — receiver with a resolvable {@code getAt(IntRange)}.
     *       Implemented as fast-path {@code instanceof} branches for
     *       {@code List}, {@code CharSequence}, and Java arrays (size-aware so
     *       out-of-bounds {@code fromIndex} returns the canonical empty value
     *       without calling user code), with an MOP-dispatched fallback for
     *       any other Path B receiver — {@link java.util.BitSet} (returns
     *       {@code BitSet}), user custom classes, etc.</li>
     *   <li><b>Path C</b> — anything else iterable: return the already-advanced
     *       iterator.</li>
     * </ul>
     *
     * <p>The actual {@code instanceof} checks are ordered for performance
     * (List/CharSequence/array fast paths first, then Stream, then MOP
     * fallback, then iterator) but the conceptual dispatch order is A, then
     * B, then C.</p>
     *
     * <p>Primitive streams ({@code IntStream}, {@code LongStream},
     * {@code DoubleStream}) are not subtypes of {@code Stream<T>} and therefore
     * fall through to Path B (if they expose {@code getAt(IntRange)}) or
     * Path C; their elements are boxed by the underlying iterator.</p>
     *
     * @param rhs       the original RHS value
     * @param fromIndex the declarator position of the rest binding
     * @param seq       the iterator that has already yielded the preceding fixed-slot values
     */
    public static Object tailRest(final Object rhs, final int fromIndex, final Iterator<?> seq) {
        if (rhs instanceof List) {
            if (fromIndex >= ((List<?>) rhs).size()) return createSimilarList((List<?>) rhs, 0);
            IntRange range = new IntRange(true, fromIndex, -1);
            return InvokerHelper.invokeMethod(rhs, "getAt", range);
        }
        if (rhs instanceof CharSequence) {
            if (fromIndex >= ((CharSequence) rhs).length()) return "";
            IntRange range = new IntRange(true, fromIndex, -1);
            return InvokerHelper.invokeMethod(rhs, "getAt", range);
        }
        if (rhs != null && rhs.getClass().isArray()) {
            // Match the non-empty branch: ArrayGroovyMethods.getAt(T[], IntRange) returns a
            // mutable List<T>, so the empty case must also be a mutable List<T> — not an empty
            // T[] — for typed rest binders like `List<T> *t` to round-trip consistently, and
            // not Collections.emptyList() so that downstream mutation of an empty rest binding
            // behaves the same as when it captured one or more elements.
            if (fromIndex >= Array.getLength(rhs)) return new ArrayList<>();
            IntRange range = new IntRange(true, fromIndex, -1);
            return InvokerHelper.invokeMethod(rhs, "getAt", range);
        }
        if (rhs instanceof Stream) {
            Spliterator<?> spliterator = Spliterators.spliteratorUnknownSize(seq, 0);
            Stream<?> tail = StreamSupport.stream(spliterator, false);
            return tail.onClose(((Stream<?>) rhs)::close);
        }
        // MOP fallback: any other receiver with getAt(IntRange) — BitSet, user classes, etc.
        // Picked deliberately AFTER the Stream branch so a Stream RHS never materialises via
        // StreamGroovyMethods.getAt(Stream, IntRange).
        if (rhs != null && InvokerHelper.getMetaClass(rhs).pickMethod("getAt", INT_RANGE_PARAM) != null) {
            IntRange range = new IntRange(true, fromIndex, -1);
            return InvokerHelper.invokeMethod(rhs, "getAt", range);
        }
        return seq;
    }

    /**
     * Resolve the value for a head or middle rest binding. Requires a sized, indexable RHS
     * (List, CharSequence, or array — or any other receiver with a non-reversing
     * {@code getAt(IntRange)}); for other RHS types this routes through the MOP so that:
     *
     * <ul>
     *   <li>Truly non-indexable sources (iterators, sets, custom Iterables without a
     *       {@code getAt(IntRange)} extension) fail fast with
     *       {@code MissingMethodException} — preventing silent materialisation of
     *       unbounded sources.</li>
     *   <li>{@code Stream} RHS resolves {@code StreamGroovyMethods.getAt(Stream, IntRange)},
     *       which rejects the destructuring slice's reverse range with
     *       {@code IllegalArgumentException("reverse range")} — also fail-fast, also
     *       no materialisation. (Head/middle rest from {@code Stream} is rejected at
     *       compile time under {@code @CompileStatic}; the IAE only surfaces in
     *       dynamic mode.)</li>
     * </ul>
     *
     * <p>When the RHS is shorter than the sum of fixed slots around the rest, the computed range
     * is inverted. DGM's {@code getAt(IntRange)} would reverse an inverted range; for destructuring
     * we want an empty slice instead, so this helper short-circuits that case for the sized fast
     * paths (List/CharSequence/array). Other indexable receivers are responsible for their own
     * bounds handling.</p>
     *
     * @param rhs       the original RHS value
     * @param fromIndex positive index where the slice begins
     * @param toIndex   negative index where the slice ends (inclusive)
     */
    public static Object nonTailRestSlice(final Object rhs, final int fromIndex, final int toIndex) {
        int size = -1;
        if (rhs instanceof List) size = ((List<?>) rhs).size();
        else if (rhs instanceof CharSequence) size = ((CharSequence) rhs).length();
        else if (rhs != null && rhs.getClass().isArray()) size = Array.getLength(rhs);

        if (size >= 0) {
            int effectiveTo = toIndex < 0 ? size + toIndex : toIndex;
            if (fromIndex > effectiveTo || fromIndex >= size || effectiveTo < 0) {
                // Empty-slice returns a mutable list: the non-empty path delegates to
                // getAt(IntRange) which returns a fresh mutable List<T> (for both List
                // and array RHS), so the empty path must agree — both for the array
                // List<T>-vs-T[] alignment and for downstream mutation consistency.
                if (rhs instanceof CharSequence) return "";
                if (rhs instanceof List) return createSimilarList((List<?>) rhs, 0);
                return new ArrayList<>();
            }
        }

        IntRange range = new IntRange(true, fromIndex, toIndex);
        return InvokerHelper.invokeMethod(rhs, "getAt", range);
    }
}
