// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { RushConstants } from '../RushConstants';
import { NullOperationRunner } from './NullOperationRunner';
import { convertSlashesForWindows, ShellOperationRunner } from './ShellOperationRunner';
import { OperationStatus } from './OperationStatus';
import { IS_WINDOWS } from '../../utilities/executionUtilities';
export const PLUGIN_NAME = 'ShellOperationRunnerPlugin';
/**
 * Core phased command plugin that provides the functionality for executing an operation via shell command.
 */
export class ShellOperationRunnerPlugin {
    apply(hooks) {
        hooks.createOperations.tap(PLUGIN_NAME, function createShellOperations(operations, context) {
            var _a;
            const { rushConfiguration, isInitial } = context;
            const getCustomParameterValues = getCustomParameterValuesByOperation();
            for (const operation of operations) {
                const { associatedPhase: phase, associatedProject: project } = operation;
                if (!operation.runner) {
                    // This is a shell command. In the future, may consider having a property on the initial operation
                    // to specify a runner type requested in rush-project.json
                    const { parameterValues: customParameterValues, ignoredParameterValues } = getCustomParameterValues(operation);
                    const displayName = getDisplayName(phase, project);
                    const { name: phaseName, shellCommand } = phase;
                    const { scripts } = project.packageJson;
                    // This is the command that will be used to identify the cache entry for this operation
                    const commandForHash = shellCommand !== null && shellCommand !== void 0 ? shellCommand : scripts === null || scripts === void 0 ? void 0 : scripts[phaseName];
                    // For execution of non-initial runs, prefer the `:incremental` script if it exists.
                    // However, the `shellCommand` value still takes precedence per the spec for that feature.
                    const commandToRun = (_a = shellCommand !== null && shellCommand !== void 0 ? shellCommand : (!isInitial ? scripts === null || scripts === void 0 ? void 0 : scripts[`${phaseName}:incremental`] : undefined)) !== null && _a !== void 0 ? _a : scripts === null || scripts === void 0 ? void 0 : scripts[phaseName];
                    operation.runner = initializeShellOperationRunner({
                        phase,
                        project,
                        displayName,
                        commandForHash,
                        commandToRun,
                        customParameterValues,
                        ignoredParameterValues,
                        rushConfiguration
                    });
                }
            }
            return operations;
        });
    }
}
export function initializeShellOperationRunner(options) {
    const { phase, project, commandToRun: rawCommandToRun, displayName, ignoredParameterValues } = options;
    if (typeof rawCommandToRun !== 'string' && phase.missingScriptBehavior === 'error') {
        throw new Error(`The project '${project.packageName}' does not define a '${phase.name}' command in the 'scripts' section of its package.json`);
    }
    if (rawCommandToRun) {
        const { commandForHash: rawCommandForHash, customParameterValues } = options;
        const commandToRun = formatCommand(rawCommandToRun, customParameterValues);
        const commandForHash = rawCommandForHash
            ? formatCommand(rawCommandForHash, customParameterValues)
            : commandToRun;
        return new ShellOperationRunner({
            commandToRun,
            commandForHash,
            displayName,
            phase,
            rushProject: project,
            ignoredParameterValues
        });
    }
    else {
        // Empty build script indicates a no-op, so use a no-op runner
        return new NullOperationRunner({
            name: displayName,
            result: OperationStatus.NoOp,
            silent: phase.missingScriptBehavior === 'silent'
        });
    }
}
/**
 * Helper function to collect all parameter arguments for a phase
 */
function collectPhaseParameterArguments(phase) {
    const customParameterList = [];
    for (const tsCommandLineParameter of phase.associatedParameters) {
        tsCommandLineParameter.appendToArgList(customParameterList);
    }
    return customParameterList;
}
/**
 * Memoizer for custom parameter values by phase
 * @returns A function that returns the custom parameter values for a given phase
 */
export function getCustomParameterValuesByPhase() {
    const customParametersByPhase = new Map();
    function getCustomParameterValuesForPhase(phase) {
        let customParameterList = customParametersByPhase.get(phase);
        if (!customParameterList) {
            customParameterList = collectPhaseParameterArguments(phase);
            customParametersByPhase.set(phase, customParameterList);
        }
        return customParameterList;
    }
    return getCustomParameterValuesForPhase;
}
/**
 * Gets custom parameter values for an operation, filtering out any parameters that should be ignored
 * based on the operation's settings.
 * @returns A function that returns the filtered custom parameter values and ignored parameter values for a given operation
 */
export function getCustomParameterValuesByOperation() {
    const customParametersByPhase = new Map();
    function getCustomParameterValuesForOp(operation) {
        const { associatedPhase: phase, settings } = operation;
        // Check if there are any parameters to ignore
        const parameterNamesToIgnore = settings === null || settings === void 0 ? void 0 : settings.parameterNamesToIgnore;
        if (!parameterNamesToIgnore || parameterNamesToIgnore.length === 0) {
            // No filtering needed - use the cached parameter list for efficiency
            let customParameterList = customParametersByPhase.get(phase);
            if (!customParameterList) {
                customParameterList = collectPhaseParameterArguments(phase);
                customParametersByPhase.set(phase, customParameterList);
            }
            return {
                parameterValues: customParameterList,
                ignoredParameterValues: []
            };
        }
        // Filtering is needed - we must iterate through parameter objects to check longName
        // Note: We cannot use the cached parameter list here because we need access to
        // the parameter objects to get their longName property for filtering
        const ignoreSet = new Set(parameterNamesToIgnore);
        const filteredParameterValues = [];
        const ignoredParameterValues = [];
        for (const tsCommandLineParameter of phase.associatedParameters) {
            const parameterLongName = tsCommandLineParameter.longName;
            tsCommandLineParameter.appendToArgList(ignoreSet.has(parameterLongName) ? ignoredParameterValues : filteredParameterValues);
        }
        return {
            parameterValues: filteredParameterValues,
            ignoredParameterValues
        };
    }
    return getCustomParameterValuesForOp;
}
export function formatCommand(rawCommand, customParameterValues) {
    if (!rawCommand) {
        return '';
    }
    else {
        const fullCommand = `${rawCommand} ${customParameterValues.join(' ')}`;
        return IS_WINDOWS ? convertSlashesForWindows(fullCommand) : fullCommand;
    }
}
export function getDisplayName(phase, project) {
    if (phase.isSynthetic) {
        // Because this is a synthetic phase, just use the project name because there aren't any other phases
        return project.packageName;
    }
    else {
        const phaseNameWithoutPrefix = phase.name.slice(RushConstants.phaseNamePrefix.length);
        return `${project.packageName} (${phaseNameWithoutPrefix})`;
    }
}
//# sourceMappingURL=ShellOperationRunnerPlugin.js.map