/*
 * Copyright 2022 The Kubernetes Authors
 *
 * 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.
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import Debug from 'debug';
import { isAbsolute as pathIsAbsolute, dirname as pathDirname, join as pathJoin } from 'path';
import { Util, isError } from '@kui-shell/core';
import { loadNotebook } from '@kui-shell/plugin-client-common/notebook';
import indent from '../components/Content/Markdown/indent';
import { hasImports } from '../components/Content/Markdown/KuiFrontmatter';
import { tryFrontmatter } from '../components/Content/Markdown/frontmatter-parser';
const debug = Debug('plugin-client-common/markdown/snippets');
const RE_INCLUDE = /^(\s*){%\s+include "([^"]+)"\s+%}/;
//                 [1]                    [2]
//                  \- [1] leading whitespace
//                                         \- filepath to import
const RE_IMPORT = /^(\s*):import{(.*)}\s*$/;
//                   [1]          [2]
//                    \- [1] leading whitespace
//                                \- filepath to import
const RE_SNIPPET = /^(\s*)--(-*)8<--(-*)\s+"([^"]+)"(\s+"([^"]+)")?\s*$/;
//                    [1]   [2]     [3]      [4]          [6]
//                    \- [1] leading whitespace
//                                           \- [4] snippet file name
//                                                        \- [6] deprecated
function isUrl(a) {
    return /^https?:/.test(a);
}
function dirname(a) {
    if (isUrl(a)) {
        const url = new URL(a);
        url.pathname = pathDirname(url.pathname);
        return url.toString();
    }
    else {
        return pathDirname(a);
    }
}
function join(a, b) {
    if (isUrl(a)) {
        const url = new URL(a);
        url.pathname = pathJoin(url.pathname, b);
        return url.toString();
    }
    else {
        return pathJoin(a, b);
    }
}
export function isAbsolute(uri) {
    return isUrl(uri) || pathIsAbsolute(uri);
}
function toString(data) {
    return (typeof data === 'string' ? data : JSON.stringify(data)).replace(/\n$/, '');
}
/** Rewrite any relative <img> and <a> links to use the given basePath */
function rerouteLinks(basePath, data) {
    return data.replace(/\[([^\]]+)\]\(([^)]+)\)/g, // e.g. [linky](https://linky.com)
    (_, p1, p2) => {
        return `[${p1}](${isAbsolute(p2) ? p2 : join(basePath, p2)})`;
    });
}
/**
 * We want to use remark-directive's `:::` syntax for container
 * directives. It does support nested containers, but the outer
 * containers have to have *more* colons than the inner
 * ones. Ugh. Since we can't predict the maximum nesting depth in
 * advance, this means we need to pick an aribtrary start point, and
 * thus have an unfortunate maximum supported nesting depth.
 *
 * reference:
 * https://github.com/micromark/micromark-extension-directive#syntax
 */
function colonColonColon(nestingDepth) {
    const MAX = 100;
    return new Array(Math.max(3, MAX - nestingDepth)).join(':');
}
/**
 * Simplistic approximation of
 * https://facelessuser.github.io/pymdown-extensions/extensions/snippets/.
 */
export default function inlineSnippets(args, snippetBasePath, includeFrontmatter = true, nestingDepth = 0, inImport = false) {
    const fetchRecursively = (_snippetFileName, srcFilePath, provenance, optionalSnippetBasePathInSnippetLine, nestingDepth = 0, inImport = false) => __awaiter(this, void 0, void 0, function* () {
        const snippetFileName = Util.expandHomeDir(_snippetFileName);
        const getBasePath = (snippetBasePath) => {
            if (!snippetBasePath)
                return '';
            try {
                const basePath = optionalSnippetBasePathInSnippetLine || snippetBasePath;
                return isAbsolute(basePath) ? basePath : srcFilePath ? join(srcFilePath, basePath) : undefined;
            }
            catch (err) {
                debug(err);
                return undefined;
            }
        };
        // Call ourselves recursively, in case a fetched snippet
        // fetches other files. We also may need to reroute relative
        // <img> and <a> links according to the given `basePath`.
        const recurse = (basePath, recursedSnippetFileName, data) => __awaiter(this, void 0, void 0, function* () {
            // Note: intentionally using `snippetBasePath` for the
            // first argument, as this represents the "root" base
            // path, either from the URL of the original filepath (we
            // may be recursing here) or from the command line or from
            // the topmatter of the original document. The second
            // represents the current base path in the recursion.
            const base = isAbsolute(basePath) || !snippetBasePath ? basePath : snippetBasePath;
            return inlineSnippets(args, base, inImport, nestingDepth + 1, inImport)(rerouteLinks(base, data), recursedSnippetFileName, provenance);
        });
        const candidates = optionalSnippetBasePathInSnippetLine
            ? [optionalSnippetBasePathInSnippetLine]
            : [
                Util.cwd(),
                snippetBasePath,
                '../',
                '../snippets',
                '../../snippets',
                '../../../snippets',
                '../../../../snippets',
                '../../',
                '../../../'
            ].filter(Boolean);
        debug('Candidates', snippetFileName, candidates, candidates
            .map(getBasePath)
            .filter(Boolean)
            .map(myBasePath => ({
            myBasePath,
            filepath: join(myBasePath, snippetFileName)
        })));
        const snippetDatas = isUrl(snippetFileName) || isAbsolute(snippetFileName)
            ? [
                yield loadNotebook(snippetFileName, args)
                    .then((data) => __awaiter(this, void 0, void 0, function* () {
                    return ({
                        filepath: snippetFileName,
                        snippetData: yield recurse(snippetBasePath || dirname(snippetFileName), snippetFileName, toString(data))
                    });
                }))
                    .catch(err => {
                    debug('Warning: could not fetch inlined content 1', snippetBasePath, snippetFileName, err);
                    return err;
                })
            ]
            : yield Promise.all(candidates
                .map(getBasePath)
                .filter(Boolean)
                .map(myBasePath => ({
                myBasePath,
                filepath: join(myBasePath, snippetFileName)
            }))
                .filter(_ => _ && _.filepath !== srcFilePath) // avoid cycles
                .map(({ myBasePath, filepath }) => loadNotebook(filepath, args)
                .then((data) => __awaiter(this, void 0, void 0, function* () { return ({ filepath, snippetData: yield recurse(myBasePath, filepath, toString(data)) }); }))
                .catch(err => {
                debug('Warning: could not fetch inlined content 2', myBasePath, snippetFileName, err);
                return err;
            }))).then(_ => _.filter(Boolean));
        const snippetData = snippetDatas.find(_ => _.snippetData !== undefined && !isError(_.snippetData)) ||
            snippetDatas.find(_ => isError(_.snippetData) && !/ENOTDIR/.test(_.snippetData.message)) ||
            snippetDatas[0];
        if (isError(snippetData)) {
            debug('Error: completely failed to fetch inlined content', snippetFileName, snippetDatas);
        }
        else {
            debug('Success in fetch inline content', snippetFileName, snippetDatas);
        }
        return snippetData;
    });
    const oops404 = (snippetFileName, errorMessage = 'Failed to fetch this file') => {
        return `??? bug "Could not fetch snippet ${snippetFileName}"
${indent(errorMessage)}`;
    };
    const processImport = (snippetFileName, srcFilePath, provenance) => __awaiter(this, void 0, void 0, function* () {
        const { filepath, snippetData } = yield fetchRecursively(snippetFileName, srcFilePath, provenance.concat([snippetFileName]), undefined, nestingDepth + 1, true);
        if (snippetData === undefined) {
            return oops404(snippetFileName);
        }
        const { attributes, body, bodyBegin } = tryFrontmatter(snippetData.trim());
        const attributesEnc = bodyBegin === 0 ? '' : encodeURIComponent(JSON.stringify(attributes));
        // remark-directive uses ::: to indicate container directives,
        // i.e. directives that allow one to mark a block of text as
        // "contained" bythe directive, e.g.
        // :::import{key1=value1 key2=value2}
        // ...import content...
        // :::
        const colons = colonColonColon(nestingDepth);
        return `
${colons}import{provenance=${provenance.concat([snippetFileName])} filepath=${filepath} attributes=${attributesEnc}${attributes.title ? ` title="${attributes.title}"` : ''}}
${body}
${colons}
<!-- hack: working around a bug in the directive parser for ${snippetFileName} -->
`;
    });
    return (data, srcFilePath, provenance = []) => __awaiter(this, void 0, void 0, function* () {
        const { body, attributes } = tryFrontmatter(data);
        let importedContent;
        if (hasImports(attributes)) {
            importedContent = Promise.all(attributes.imports.map(_ => processImport(_, srcFilePath, provenance))).then(_ => _.join('\n'));
        }
        const mainContent = yield Promise.all((includeFrontmatter ? data : body).split(/\n/).map((line) => __awaiter(this, void 0, void 0, function* () {
            const matchSnippet = line.match(RE_SNIPPET);
            const match = matchSnippet || line.match(RE_INCLUDE);
            if (!match) {
                const matchImport = line.match(RE_IMPORT);
                if (matchImport) {
                    const indentation = matchImport[1];
                    const snippetFileName = matchImport[2];
                    return indent(yield processImport(snippetFileName, srcFilePath, provenance), indentation);
                }
                else {
                    return line;
                }
            }
            else {
                const indentation = match[1];
                const snippetFileName = matchSnippet ? match[4] : match[2];
                const optionalSnippetBasePathInSnippetLine = matchSnippet ? match[6] : undefined;
                const { snippetData } = yield fetchRecursively(snippetFileName, srcFilePath, provenance, optionalSnippetBasePathInSnippetLine, nestingDepth + 1, inImport);
                if (snippetData === undefined) {
                    return line;
                }
                else if (isError(snippetData)) {
                    return oops404(snippetFileName, snippetData.message);
                }
                else {
                    debug('successfully fetched inlined content', snippetFileName);
                    return indent(toString(snippetData), indentation);
                }
            }
        }))).then(_ => _.join('\n'));
        if (!importedContent || (yield importedContent).length === 0) {
            return mainContent;
        }
        else {
            const { body, bodyBegin } = tryFrontmatter(mainContent);
            const topmatter = !includeFrontmatter || bodyBegin === 0
                ? ''
                : mainContent
                    .split(/\n/)
                    .slice(0, bodyBegin - 1)
                    .join('\n');
            return `${topmatter ? '\n' + topmatter : ''}

<!-- Begin imported content for ${srcFilePath} -->

${yield importedContent}

<!-- End imported content for ${srcFilePath} -->

${body}`;
        }
    });
}
//# sourceMappingURL=snippets.js.map