"use strict";
/*
 * Copyright © 2020 Atomist, Inc.
 *
 * Licensed 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.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeClassifier = exports.resolveClassifierPath = exports.cacheRemove = exports.cacheRestore = exports.NoOpGoalProjectListenerRegistration = exports.cachePut = exports.CacheOutputGoalDataKey = exports.CacheInputGoalDataKey = void 0;
const configuration_1 = require("@atomist/automation-client/lib/configuration");
const fileGlobs_1 = require("@atomist/automation-client/lib/project/fileGlobs");
const projectUtils_1 = require("@atomist/automation-client/lib/project/util/projectUtils");
const _ = require("lodash");
const sdmGoal_1 = require("../../../api-helper/goal/sdmGoal");
const GoalInvocation_1 = require("../../../api/goal/GoalInvocation");
const commonPushTests_1 = require("../../../api/mapping/support/commonPushTests");
const resolvePlaceholder_1 = require("../../machine/yaml/resolvePlaceholder");
const array_1 = require("../../util/misc/array");
const CompressingGoalCache_1 = require("./CompressingGoalCache");
exports.CacheInputGoalDataKey = "@atomist/sdm/input";
exports.CacheOutputGoalDataKey = "@atomist/sdm/output";
const DefaultGoalCache = new CompressingGoalCache_1.CompressingGoalCache();
/**
 * Goal listener that performs caching after a goal has been run.
 * @param options The options for caching
 * @param classifier Whether only a specific classifier, as defined in the options,
 * needs to be cached. If omitted, all classifiers are cached.
 * @param classifiers Additional classifiers that need to be created.
 */
function cachePut(options, classifier, ...classifiers) {
    const allClassifiers = [];
    if (classifier) {
        allClassifiers.push(classifier, ...(classifiers || []));
    }
    const entries = !!classifier ?
        options.entries.filter(pattern => allClassifiers.includes(pattern.classifier)) :
        options.entries;
    const listenerName = `caching outputs`;
    return {
        name: listenerName,
        listener: async (p, gi) => {
            const { goalEvent } = gi;
            if (!!isCacheEnabled(gi) && !process.env.ATOMIST_ISOLATED_GOAL_INIT) {
                const cloneEntries = _.cloneDeep(entries);
                const goalCache = cacheStore(gi);
                for (const entry of cloneEntries) {
                    const files = [];
                    if (isGlobFilePattern(entry.pattern)) {
                        files.push(...(await getFilePathsThroughPattern(p, entry.pattern.globPattern)));
                    }
                    else if (isDirectoryPattern(entry.pattern)) {
                        files.push(entry.pattern.directory);
                    }
                    if (!_.isEmpty(files)) {
                        const resolvedClassifier = await resolveClassifierPath(entry.classifier, gi);
                        const uri = await goalCache.put(gi, p, files, resolvedClassifier);
                        if (!!resolvedClassifier && !!uri) {
                            entry.classifier = resolvedClassifier;
                            entry.uri = uri;
                        }
                    }
                }
                // Set outputs on the goal data
                const data = sdmGoal_1.goalData(goalEvent);
                const newData = {
                    [exports.CacheOutputGoalDataKey]: [
                        ...(data[exports.CacheOutputGoalDataKey] || []),
                        ...cloneEntries,
                    ],
                };
                goalEvent.data = JSON.stringify(Object.assign(Object.assign({}, (sdmGoal_1.goalData(goalEvent))), newData));
            }
        },
        pushTest: options.pushTest,
        events: [GoalInvocation_1.GoalProjectListenerEvent.after],
    };
}
exports.cachePut = cachePut;
function isGlobFilePattern(toBeDetermined) {
    return toBeDetermined.globPattern !== undefined;
}
function isDirectoryPattern(toBeDetermined) {
    return toBeDetermined.directory !== undefined;
}
async function pushTestSucceeds(pushTest, gi, p) {
    return (pushTest || commonPushTests_1.AnyPush).mapping({
        push: gi.goalEvent.push,
        project: p,
        id: gi.id,
        configuration: gi.configuration,
        addressChannels: gi.addressChannels,
        context: gi.context,
        preferences: gi.preferences,
        credentials: gi.credentials,
        skill: gi.skill,
    });
}
async function invokeCacheMissListeners(optsToUse, p, gi, event) {
    for (const cacheMissFallback of array_1.toArray(optsToUse.onCacheMiss)) {
        const allEvents = [GoalInvocation_1.GoalProjectListenerEvent.before, GoalInvocation_1.GoalProjectListenerEvent.after];
        if ((cacheMissFallback.events || allEvents).filter(e => e === event).length > 0
            && await pushTestSucceeds(cacheMissFallback.pushTest, gi, p)) {
            await cacheMissFallback.listener(p, gi, event);
        }
    }
}
exports.NoOpGoalProjectListenerRegistration = {
    name: "NoOpListener",
    listener: async () => {
    },
    pushTest: commonPushTests_1.AnyPush,
};
/**
 * Goal listener that performs cache restores before a goal has been run.
 * @param options The options for caching
 * @param classifier Whether only a specific classifier, as defined in the options,
 * needs to be restored. If omitted, all classifiers defined in the options are restored.
 * @param classifiers Additional classifiers that need to be restored.
 */
function cacheRestore(options, classifier, ...classifiers) {
    const allClassifiers = [];
    if (classifier) {
        allClassifiers.push(classifier, ...(classifiers || []));
    }
    const optsToUse = Object.assign({ onCacheMiss: exports.NoOpGoalProjectListenerRegistration }, options);
    const classifiersToBeRestored = [];
    if (allClassifiers.length > 0) {
        classifiersToBeRestored.push(...allClassifiers);
    }
    else {
        classifiersToBeRestored.push(...optsToUse.entries.map(entry => entry.classifier));
    }
    const listenerName = `restoring inputs`;
    return {
        name: listenerName,
        listener: async (p, gi, event) => {
            if (!!isCacheEnabled(gi)) {
                const goalCache = cacheStore(gi);
                for (const c of classifiersToBeRestored) {
                    try {
                        const resolvedClassifier = await resolveClassifierPath(c, gi);
                        await goalCache.retrieve(gi, p, resolvedClassifier);
                    }
                    catch (e) {
                        await invokeCacheMissListeners(optsToUse, p, gi, event);
                    }
                }
            }
            else {
                await invokeCacheMissListeners(optsToUse, p, gi, event);
            }
            // Set inputs on the goal data
            const { goalEvent } = gi;
            const data = sdmGoal_1.goalData(goalEvent);
            const newData = {
                [exports.CacheInputGoalDataKey]: [
                    ...(data[exports.CacheInputGoalDataKey] || []),
                    ...classifiersToBeRestored.map(c => ({
                        classifier: c,
                    })),
                ],
            };
            goalEvent.data = JSON.stringify(Object.assign(Object.assign({}, (sdmGoal_1.goalData(goalEvent))), newData));
        },
        pushTest: optsToUse.pushTest,
        events: [GoalInvocation_1.GoalProjectListenerEvent.before],
    };
}
exports.cacheRestore = cacheRestore;
/**
 * Goal listener that cleans up the cache restores after a goal has been run.
 * @param options The options for caching
 * @param classifier Whether only a specific classifier, as defined in the options,
 * needs to be removed. If omitted, all classifiers are removed.
 * @param classifiers Additional classifiers that need to be removed.
 */
function cacheRemove(options, classifier, ...classifiers) {
    const allClassifiers = [];
    if (classifier) {
        allClassifiers.push(...[classifier, ...classifiers]);
    }
    const classifiersToBeRemoved = [];
    if (allClassifiers.length > 0) {
        classifiersToBeRemoved.push(...allClassifiers);
    }
    else {
        classifiersToBeRemoved.push(...options.entries.map(entry => entry.classifier));
    }
    const listenerName = `removing outputs`;
    return {
        name: listenerName,
        listener: async (p, gi) => {
            if (!!isCacheEnabled(gi)) {
                const goalCache = cacheStore(gi);
                for (const c of classifiersToBeRemoved) {
                    const resolvedClassifier = await resolveClassifierPath(c, gi);
                    await goalCache.remove(gi, resolvedClassifier);
                }
            }
        },
        pushTest: options.pushTest,
        events: [GoalInvocation_1.GoalProjectListenerEvent.after],
    };
}
exports.cacheRemove = cacheRemove;
async function getFilePathsThroughPattern(project, globPattern) {
    const oldExcludes = fileGlobs_1.DefaultExcludes;
    fileGlobs_1.DefaultExcludes.splice(0, fileGlobs_1.DefaultExcludes.length); // necessary evil
    try {
        return await projectUtils_1.gatherFromFiles(project, globPattern, async (f) => f.path);
    }
    finally {
        fileGlobs_1.DefaultExcludes.push(...oldExcludes);
    }
}
function isCacheEnabled(gi) {
    return _.get(gi.configuration, "sdm.cache.enabled", false);
}
function cacheStore(gi) {
    return _.get(gi.configuration, "sdm.cache.store", DefaultGoalCache);
}
/**
 * Interpolate information from goal invocation into the classifier.
 */
async function resolveClassifierPath(classifier, gi) {
    if (!classifier) {
        return gi.context.workspaceId;
    }
    const wrapper = { classifier };
    await configuration_1.resolvePlaceholders(wrapper, v => resolvePlaceholder_1.resolvePlaceholder(v, gi.goalEvent, gi, {}));
    return gi.context.workspaceId + "/" + sanitizeClassifier(wrapper.classifier);
}
exports.resolveClassifierPath = resolveClassifierPath;
/**
 * Sanitize classifier for use in path.  Replace any characters
 * which might cause problems on POSIX or MS Windows with "_",
 * including path separators.  Ensure resulting file is not "hidden".
 */
function sanitizeClassifier(classifier) {
    return classifier.replace(/[^-.0-9A-Za-z_+]/g, "_")
        .replace(/^\.+/, ""); // hidden
}
exports.sanitizeClassifier = sanitizeClassifier;
//# sourceMappingURL=goalCaching.js.map