/*
 * Copyright (c) 2021, Arm Limited. All rights reserved.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

/* eslint-env es6 */

"use strict";

const Handlebars = require("handlebars");
const Q = require("q");
const _ = require("lodash");

const ccConventionalChangelog = require("conventional-changelog-conventionalcommits/conventional-changelog");
const ccParserOpts = require("conventional-changelog-conventionalcommits/parser-opts");
const ccRecommendedBumpOpts = require("conventional-changelog-conventionalcommits/conventional-recommended-bump");
const ccWriterOpts = require("conventional-changelog-conventionalcommits/writer-opts");

const execa = require("execa");

const readFileSync = require("fs").readFileSync;
const resolve = require("path").resolve;

/*
 * Register a Handlebars helper that lets us generate Markdown lists that can support multi-line
 * strings. This is driven by inconsistent formatting of breaking changes, which may be multiple
 * lines long and can terminate the list early unintentionally.
 */
Handlebars.registerHelper("tf-a-mdlist", function (indent, options) {
    const spaces = new Array(indent + 1).join(" ");
    const first = spaces + "- ";
    const nth = spaces + "  ";

    return first + options.fn(this).replace(/\n(?!\s*\n)/gm, `\n${nth}`).trim() + "\n";
});

/*
 * Register a Handlebars helper that concatenates multiple variables. We use this to generate the
 * title for the section partials.
 */
Handlebars.registerHelper("tf-a-concat", function () {
    let argv = Array.prototype.slice.call(arguments, 0);

    argv.pop();

    return argv.join("");
});

function writerOpts(config) {
    /*
     * Flatten the configuration's sections list. This helps us iterate over all of the sections
     * when we don't care about the hierarchy.
     */

    const flattenSections = function (sections) {
        return sections.flatMap(section => {
            const subsections = flattenSections(section.sections || []);

            return [section].concat(subsections);
        })
    };

    const flattenedSections = flattenSections(config.sections);

    /*
     * Register a helper to return a restructured version of the note groups that includes notes
     * categorized by their section.
     */
    Handlebars.registerHelper("tf-a-notes", function (noteGroups, options) {
        const generateTemplateData = function (sections, notes) {
            return (sections || []).flatMap(section => {
                const templateData = {
                    title: section.title,
                    sections: generateTemplateData(section.sections, notes),
                    notes: notes.filter(note => section.scopes?.includes(note.commit.scope)),
                };

                /*
                 * Don't return a section if it contains no notes and no sub-sections.
                 */
                if ((templateData.sections.length == 0) && (templateData.notes.length == 0)) {
                    return [];
                }

                return [templateData];
            });
        };

        return noteGroups.map(noteGroup => {
            return {
                title: noteGroup.title,
                sections: generateTemplateData(config.sections, noteGroup.notes),
                notes: noteGroup.notes.filter(note =>
                    !flattenedSections.some(section => section.scopes?.includes(note.commit.scope))),
            };
        });
    });

    /*
     * Register a helper to return a restructured version of the commit groups that includes commits
     * categorized by their section.
     */
    Handlebars.registerHelper("tf-a-commits", function (commitGroups, options) {
        const generateTemplateData = function (sections, commits) {
            return (sections || []).flatMap(section => {
                const templateData = {
                    title: section.title,
                    sections: generateTemplateData(section.sections, commits),
                    commits: commits.filter(commit => section.scopes?.includes(commit.scope)),
                };

                /*
                 * Don't return a section if it contains no notes and no sub-sections.
                 */
                if ((templateData.sections.length == 0) && (templateData.commits.length == 0)) {
                    return [];
                }

                return [templateData];
            });
        };

        return commitGroups.map(commitGroup => {
            return {
                title: commitGroup.title,
                sections: generateTemplateData(config.sections, commitGroup.commits),
                commits: commitGroup.commits.filter(commit =>
                    !flattenedSections.some(section => section.scopes?.includes(commit.scope))),
            };
        });
    });

    const writerOpts = ccWriterOpts(config)
        .then(writerOpts => {
            const ccWriterOptsTransform = writerOpts.transform;

            /*
             * These configuration properties can't be injected directly into the template because
             * they themselves are templates. Instead, we register them as partials, which allows
             * them to be evaluated as part of the templates they're used in.
             */
            Handlebars.registerPartial("commitUrl", config.commitUrlFormat);
            Handlebars.registerPartial("compareUrl", config.compareUrlFormat);
            Handlebars.registerPartial("issueUrl", config.issueUrlFormat);

            /*
             * Register the partials that allow us to recursively create changelog sections.
             */

            const notePartial = readFileSync(resolve(__dirname, "./templates/note.hbs"), "utf-8");
            const noteSectionPartial = readFileSync(resolve(__dirname, "./templates/note-section.hbs"), "utf-8");
            const commitSectionPartial = readFileSync(resolve(__dirname, "./templates/commit-section.hbs"), "utf-8");

            Handlebars.registerPartial("tf-a-note", notePartial);
            Handlebars.registerPartial("tf-a-note-section", noteSectionPartial);
            Handlebars.registerPartial("tf-a-commit-section", commitSectionPartial);

            /*
             * Override the base templates so that we can generate a changelog that looks at least
             * similar to the pre-Conventional Commits TF-A changelog.
             */
            writerOpts.mainTemplate = readFileSync(resolve(__dirname, "./templates/template.hbs"), "utf-8");
            writerOpts.headerPartial = readFileSync(resolve(__dirname, "./templates/header.hbs"), "utf-8");
            writerOpts.commitPartial = readFileSync(resolve(__dirname, "./templates/commit.hbs"), "utf-8");
            writerOpts.footerPartial = readFileSync(resolve(__dirname, "./templates/footer.hbs"), "utf-8");

            writerOpts.transform = function (commit, context) {
                /*
                 * Fix up commit trailers, which for some reason are not correctly recognized and
                 * end up showing up in the breaking changes.
                 */

                commit.notes.forEach(note => {
                    const trailers = execa.sync("git", ["interpret-trailers", "--parse"], {
                        input: note.text
                    }).stdout;

                    note.text = note.text.replace(trailers, "").trim();
                });

                return ccWriterOptsTransform(commit, context);
            };

            return writerOpts;
        });

    return writerOpts;
}

module.exports = function (parameter) {
    const config = parameter || {};

    return Q.all([
        ccConventionalChangelog(config),
        ccParserOpts(config),
        ccRecommendedBumpOpts(config),
        writerOpts(config)
    ]).spread((
        conventionalChangelog,
        parserOpts,
        recommendedBumpOpts,
        writerOpts
    ) => {
        if (_.isFunction(parameter)) {
            return parameter(null, {
                gitRawCommitsOpts: { noMerges: null },
                conventionalChangelog,
                parserOpts,
                recommendedBumpOpts,
                writerOpts
            });
        } else {
            return {
                conventionalChangelog,
                parserOpts,
                recommendedBumpOpts,
                writerOpts
            };
        }
    });
};