"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const path_1 = tslib_1.__importDefault(require("path"));
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const editure_1 = require("editure");
const command_1 = require("@oclif/command");
const base_1 = tslib_1.__importDefault(require("../base"));
const utils_1 = require("../utils");
const logger_1 = tslib_1.__importDefault(require("../utils/logger"));
const collection_1 = require("../utils/collection");
const assets_1 = require("../utils/assets");
const internals_1 = require("../utils/internals");
const constants_1 = require("../constants");
// Internal hints for rendering code lines.
const diffRenderHints = {
    hexo: {
        normal: '',
        add: '[tuture-add]',
        del: '[tuture-del]',
        omit: '\n[tuture-omit]\n',
    },
    plain: {
        normal: '',
        add: '',
        del: '',
        omit: '...',
    },
};
const noteLevels = {
    success: { name: '成功' },
    info: { name: '提示' },
    warning: { name: '注意' },
    danger: { name: '危险' },
};
function concatCodeStr(diffItem) {
    let codeStr = '';
    const DIFF_ADD = [];
    const DIFF_DEL = [];
    diffItem.chunks.map((chunk, chunkIndex) => {
        chunk.changes.map((change, index) => {
            const { content, type } = change;
            if (type === 'add') {
                DIFF_ADD.push(index);
            }
            else if (type === 'del') {
                DIFF_DEL.push(index);
            }
            // handle render code content
            let code = content;
            if (content !== 'normal' && content.length === 1) {
                code = content.replace(/[+-]/, ' ');
            }
            else if (content !== 'normal' && content.length > 1) {
                code = content.slice(1);
            }
            if (chunkIndex === diffItem.chunks.length - 1 &&
                index === chunk.changes.length - 1) {
                codeStr += code;
            }
            else {
                codeStr += `${code}\n`;
            }
            return change;
        });
        return chunk;
    });
    return { codeStr, DIFF_ADD, DIFF_DEL };
}
class Build extends base_1.default {
    // Sanitize explanation string for hexo compatibility
    sanitize(text) {
        if (text && this.userConfig.hexo) {
            return text.replace(/`([^`\n]+)`/g, (_, code) => code.match(/[\{\}]/)
                ? `\`{% raw %}${code}{% endraw %}\``
                : `\`${code}\``);
        }
        return text || '';
    }
    // Template for metadata of hexo posts.
    hexoFrontMatterTmpl(meta) {
        const { name, description, topics, categories, created, cover } = meta;
        const elements = ['---', `title: "${name.replace('"', '')}"`];
        if (description) {
            elements.push(`description: "${this.sanitize(description).replace('"', '')}"`);
        }
        if (topics) {
            const tags = topics
                .map((topic) => `"${this.sanitize(topic)}"`)
                .join(', ');
            elements.push(`tags: [${tags}]`);
        }
        if (categories) {
            const cats = categories
                .map((category) => `"${this.sanitize(category)}"`)
                .join(', ');
            elements.push(`categories: [${cats}]`);
        }
        if (created) {
            elements.push(`date: ${new Date(created).toISOString()}`);
        }
        if (cover) {
            elements.push(`photos:\n  - ${cover}`);
        }
        elements.push('---');
        return elements.join('\n');
    }
    // Template for single line of change.
    changeTmpl(content, type, newFile = false) {
        let prefix = '';
        const mode = this.userConfig.hexo ? 'hexo' : 'plain';
        if (mode === 'plain' && type === 'del') {
            return null;
        }
        if (!newFile) {
            prefix = diffRenderHints[mode][type];
        }
        return prefix + content;
    }
    // Template for code blocks.
    diffBlockTmpl(diff, hiddenLines, link) {
        const filename = path_1.default.basename(diff.to || '');
        const lang = filename ? filename.split('.').slice(-1)[0] : '';
        const mode = this.userConfig.hexo ? 'hexo' : 'plain';
        const { codeStr, DIFF_ADD, DIFF_DEL } = concatCodeStr(diff);
        const code = codeStr
            .split('\n')
            .map((line, index) => {
            if (hiddenLines && hiddenLines.includes(index)) {
                if (hiddenLines.includes(index - 1)) {
                    // If previous line is already hidden, don't show this line.
                    return null;
                }
                const spaces = line.length - line.trimLeft().length;
                return `${' '.repeat(spaces)}// ...`;
            }
            else if (DIFF_ADD.includes(index)) {
                return this.changeTmpl(line, 'add', diff.new);
            }
            else if (DIFF_DEL.includes(index)) {
                return this.changeTmpl(line, 'del', diff.new);
            }
            else {
                return line;
            }
        })
            .filter((line) => line !== null)
            .map((line) => (line && line.match(/^\s+$/) ? '' : line))
            .join('\n');
        const head = [lang];
        if (mode === 'hexo') {
            if (diff.to) {
                head.push(diff.to);
                if (link) {
                    head.push(link);
                    head.push('查看完整代码');
                }
            }
        }
        return `\`\`\`${head.join(' ')}\n${code}\n\`\`\``;
    }
    noteBlockTmpl(content, level) {
        if (this.userConfig.hexo) {
            const title = noteLevels[level]
                ? `**${noteLevels[level].name}**\n\n`
                : '';
            return `{% note ${level} %}\n${title}${content}\n{% endnote %}`;
        }
        return `::: ${level}\n${content}\n:::`;
    }
    getDiffFile(rawDiffs, commit, file) {
        const diffItem = rawDiffs.filter((rawDiff) => utils_1.isCommitEqual(rawDiff.commit, commit))[0];
        if (!diffItem) {
            logger_1.default.log('warning', `Commit ${commit} is not found.`);
            return null;
        }
        return diffItem.diff.filter((diffFile) => diffFile.to === file)[0];
    }
    // Markdown template for the whole tutorial.
    tutorialTmpl(meta, steps, rawDiffs) {
        const { name, description, cover, github } = meta;
        const stepConverter = (node) => {
            return node.children.map((n) => editure_1.toMarkdown(n)).join('\n\n');
        };
        const fileConverter = (node) => {
            return node.display
                ? node.children.map((n) => editure_1.toMarkdown(n)).join('\n\n')
                : '';
        };
        const explainConverter = (node) => {
            return this.sanitize(node.children.map((n) => editure_1.toMarkdown(n)).join('\n\n'));
        };
        const diffBlockConverter = (node) => {
            const { commit, file, hiddenLines } = node;
            const diff = this.getDiffFile(rawDiffs, commit, file);
            const link = github ? `${github}/blob/${commit}/${file}` : undefined;
            return diff ? this.diffBlockTmpl(diff, hiddenLines, link) : '';
        };
        const noteBlockConverter = (node) => {
            const { level = 'default', children } = node;
            const content = children.map((n) => editure_1.toMarkdown(n)).join('\n\n');
            return this.noteBlockTmpl(content, level);
        };
        const customBlockConverters = {
            step: stepConverter,
            file: fileConverter,
            explain: explainConverter,
            ['diff-block']: diffBlockConverter,
            note: noteBlockConverter,
        };
        const elements = steps.map((step) => editure_1.toMarkdown(step, undefined, customBlockConverters));
        // Add cover to the front.
        if (this.userConfig.hexo) {
            elements.unshift(this.hexoFrontMatterTmpl(meta), github ? internals_1.generateUserProfile(github) : '');
        }
        else {
            elements.unshift(cover ? `![](${cover})` : '', this.sanitize(`# ${name}`) || '', this.sanitize(description) || '');
        }
        return elements
            .filter((elem) => elem)
            .join('\n\n')
            .trim()
            .replace(/\n{3,}/g, '\n\n');
    }
    replaceAssetPaths(tutorial, assets) {
        let updated = tutorial;
        // Replace all local paths.
        // If not uploaded, replace it with absolute local path.
        assets.forEach(({ localPath, hostingUri }) => {
            updated = updated.replace(localPath, hostingUri || path_1.default.resolve(localPath));
        });
        return updated;
    }
    generateTutorials(collection, rawDiffs) {
        const { name, articles, description, topics, categories, github, steps, created, cover, } = collection;
        const meta = {
            topics,
            categories,
            github,
            created,
            name,
            description,
            cover,
        };
        const titles = articles.map((split, index) => split.name || `${name} (${index + 1})`);
        // Iterate over each split of tutorial.
        const tutorials = articles.map((article, index) => {
            const articleSteps = steps.filter((step) => step.articleId === article.id);
            // Override outmost metadata with article metadata.
            const articleMeta = Object.assign(Object.assign({}, meta), article);
            // Ensure the order for created timestamp.
            if (created) {
                articleMeta.created = new Date(Date.parse(created.toString()) + index);
            }
            return this.tutorialTmpl(articleMeta, articleSteps, rawDiffs);
        });
        return [tutorials, titles];
    }
    saveTutorials(tutorials, titles, assets) {
        const { buildPath } = this.userConfig;
        if (!this.userConfig.out && !fs_extra_1.default.existsSync(buildPath)) {
            fs_extra_1.default.mkdirSync(buildPath);
        }
        tutorials.forEach((tutorial, index) => {
            // Path to target tutorial.
            const dest = this.userConfig.out || path_1.default.join(buildPath, `${titles[index]}.md`);
            // Replace local asset paths.
            fs_extra_1.default.writeFileSync(dest, this.replaceAssetPaths(tutorial, assets));
            logger_1.default.log('success', `Tutorial has been written to ${chalk_1.default.bold(dest)}.`);
        });
    }
    async run() {
        const { flags } = this.parse(Build);
        this.userConfig = Object.assign(this.userConfig, flags);
        if (!(await utils_1.isInitialized())) {
            logger_1.default.log('error', `Tuture is not initialized. Run ${chalk_1.default.bold('tuture init')} to initialize.`);
            this.exit(1);
        }
        const collection = await collection_1.loadCollection();
        const rawDiffs = JSON.parse(fs_extra_1.default.readFileSync(constants_1.DIFF_PATH).toString());
        const assets = assets_1.loadAssetsTable();
        assets_1.checkAssets(assets);
        if (rawDiffs.length === 0) {
            logger_1.default.log('warning', 'No commits yet. Target tutorial will have empty content.');
        }
        if (flags.hexo && !collection.github) {
            logger_1.default.log('warning', 'No github field provided.');
        }
        const [tutorials, titles] = this.generateTutorials(collection, rawDiffs);
        this.saveTutorials(tutorials, titles, assets);
    }
}
exports.default = Build;
Build.description = 'Build tutorial into a markdown document';
Build.flags = {
    help: command_1.flags.help({ char: 'h' }),
    out: command_1.flags.string({
        char: 'o',
        description: 'name of output file',
    }),
    hexo: command_1.flags.boolean({
        description: 'hexo compatibility mode',
        default: false,
    }),
};
