import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import isPlainObject from 'lodash/isPlainObject';
import set from 'lodash/set';
import { nanoid } from 'nanoid';
import * as React from 'react';
import { resolveNested } from '../../shared/interactivity/nested';
import { isAnnotatedAction, isDerivedVariable, isLoopVariable, isPyComponent } from '../../types/utils';
/**
 * Check if a value is a variable with a nested array (Variable, DerivedVariable, StreamVariable)
 */
function isVariableWithNested(value) {
    return (value &&
        typeof value === 'object' &&
        '__typename' in value &&
        typeof value.__typename === 'string' &&
        value.__typename.includes('Variable') &&
        'nested' in value &&
        Array.isArray(value.nested));
}
/**
 * Create markers for a given scope and loop instance uid.
 * Used in the terminal case when we find a loop variable. Adds markers for the current action/derived variable/server component we're within.
 */
function createMarkers(scope, loopInstanceUid) {
    const markers = [];
    if (scope.action) {
        markers.push({ type: 'action', path: scope.action });
    }
    if (scope.derivedVariable) {
        markers.push({ type: 'derived_var', path: scope.derivedVariable, loopInstanceUid });
    }
    if (scope.serverComponent) {
        markers.push({ type: 'server_component', path: scope.serverComponent, loopInstanceUid });
    }
    return markers;
}
function updateScope(scope, value, path) {
    const newScope = Object.assign({}, scope);
    if (!scope.action && isAnnotatedAction(value)) {
        newScope.action = path;
    }
    if (!scope.derivedVariable && isDerivedVariable(value)) {
        newScope.derivedVariable = path;
    }
    if (!scope.serverComponent && isPyComponent(value)) {
        newScope.serverComponent = path;
    }
    return newScope;
}
/**
 * Get markers for injection points in a renderer
 *
 * @param renderer renderer to check
 */
export function getInjectionMarkers(renderer) {
    const markers = [];
    function walk(obj, pathSegments, scope = {}) {
        for (const [key, value] of Object.entries(obj)) {
            const currentPath = [...pathSegments, key];
            const dotPath = currentPath.join('.');
            // loop var found (terminal case)
            if (isLoopVariable(value)) {
                markers.push({ path: dotPath, nested: value.nested, type: 'loop_var' }, ...createMarkers(scope, value.uid));
                continue;
            }
            // Check if this is a variable with a nested array containing LoopVariables
            // (Variable, DerivedVariable, StreamVariable - but not LoopVariable itself)
            if (isVariableWithNested(value) && !isLoopVariable(value)) {
                for (let i = 0; i < value.nested.length; i++) {
                    const nestedKey = value.nested[i];
                    if (isLoopVariable(nestedKey)) {
                        markers.push({
                            type: 'nested_loop_var',
                            path: dotPath,
                            nestedIndex: i,
                            loopVarNested: nestedKey.nested,
                        });
                        // Also add derived_var/server_component markers if this variable needs separate caching
                        if (isDerivedVariable(value)) {
                            markers.push({
                                type: 'derived_var',
                                path: dotPath,
                                loopInstanceUid: nestedKey.uid,
                            });
                        }
                        if (isPyComponent(value)) {
                            markers.push({
                                type: 'server_component',
                                path: dotPath,
                                loopInstanceUid: nestedKey.uid,
                            });
                        }
                    }
                }
            }
            if (isPlainObject(value) || Array.isArray(value)) {
                const newScope = updateScope(scope, value, dotPath);
                // recurse into objects/arrays, keeping track of the path
                walk(value, [...pathSegments, key], newScope);
            }
        }
    }
    walk(renderer, []);
    return markers;
}
// Special markers used by the For component
export const MARKER_INDEX = '__index';
export const MARKER_IS_LAST = '__is_last';
export const MARKER_IS_FIRST = '__is_first';
/**
 * Inject a loop variable into a renderer.
 * This will replace any loop variable in the renderer with the loop value.
 *
 * @param renderer renderer to inject into
 * @param markers paths of loop variables in the renderer
 * @param loopValue value to inject
 */
export function applyMarkers({ renderer, markers, loopValue, itemKey, index, itemsLength, }) {
    // early exit if there are no markers
    if (markers.length === 0) {
        return renderer;
    }
    const clonedRenderer = cloneDeep(renderer);
    for (const marker of markers) {
        switch (marker.type) {
            case 'loop_var': {
                let value;
                // special case for index, is_last, and is_first markers
                if (marker.nested.length === 1 && marker.nested[0] === MARKER_INDEX) {
                    value = index;
                }
                else if (marker.nested.length === 1 && marker.nested[0] === MARKER_IS_LAST) {
                    value = index === itemsLength - 1;
                }
                else if (marker.nested.length === 1 && marker.nested[0] === MARKER_IS_FIRST) {
                    value = index === 0;
                }
                else {
                    // replace loop variable with the loop value using the nested path
                    value = resolveNested(loopValue, marker.nested);
                }
                set(clonedRenderer, marker.path, value);
                break;
            }
            case 'nested_loop_var': {
                // Resolve LoopVariable inside a variable's nested array to a string key
                const resolvedKey = resolveNested(loopValue, marker.loopVarNested);
                // Replace the LoopVariable in the nested array with the resolved string
                const variable = get(clonedRenderer, marker.path);
                if (variable && Array.isArray(variable.nested)) {
                    variable.nested[marker.nestedIndex] = String(resolvedKey);
                }
                break;
            }
            case 'action': {
                // regenerate uids for actions for the loading variables as well so they are separate
                set(clonedRenderer, `${marker.path}.loading.uid`, nanoid());
                break;
            }
            case 'derived_var': {
                // inject the loop variable uid into it so we can track it separately on the client
                set(clonedRenderer, `${marker.path}.loop_instance_uid`, `${marker.loopInstanceUid}:${itemKey}`);
                break;
            }
            case 'server_component': {
                // inject the loop variable uid into it so we can track it separately on the client
                set(clonedRenderer, `${marker.path}.loop_instance_uid`, `${marker.loopInstanceUid}:${itemKey}`);
                break;
            }
        }
    }
    return clonedRenderer;
}
/**
 * Check if the renderer component has any loop variables
 * Only checks top-level props of the component
 */
export function hasMarkers(component) {
    for (const [key, value] of Object.entries(component.props)) {
        if (isLoopVariable(value)) {
            return key;
        }
    }
    return null;
}
//# sourceMappingURL=templating.js.map