// Copyright (c) 2022, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

import path from 'node:path';

import {splitArguments} from '../../shared/common-utils.js';
import {CompilationInfo, ExecutionOptions} from '../../types/compilation/compilation.interfaces.js';
import type {
    Fix,
    Link,
    MessageWithLocation,
    ResultLine,
    ResultLineTag,
} from '../../types/resultline/resultline.interfaces.js';
import type {Artifact, ToolInfo, ToolResult} from '../../types/tool.interfaces.js';
import {OptionsHandlerLibrary} from '../options-handler.js';
import * as utils from '../utils.js';
import {ToolEnv} from './base-tool.interface.js';
import {BaseTool} from './base-tool.js';

export class SonarTool extends BaseTool {
    reproducer?: Artifact;
    lang: string;

    static get key() {
        return 'sonar-tool';
    }

    constructor(toolInfo: ToolInfo, env: ToolEnv) {
        super(toolInfo, env);

        this.addOptionsToToolArgs = false;
        this.parseOutput = this.parseOutputAndFixes;
        this.lang = 'cpp';
    }

    makeURL(rule: string, lang: string): string {
        return `https://rules.sonarsource.com/${lang}/RSPEC-${rule.substring(1)}`;
    }

    readTag(tag: any, output: ResultLine[], severity: number, lang: string) {
        const text = `${tag.text} (${lang}:${tag.ruleKey})`;
        const fixes: Fix[] = tag.fixes.map(f => ({
            title: f.message,
            edits: f.edits.map(e => ({
                line: e.startLine,
                column: e.startColumn,
                text: e.text,
                endline: e.endLine,
                endcolumn: e.endColumn,
            })),
        }));
        const flow: MessageWithLocation[] = tag.parts.map(p => ({
            line: p.line,
            column: p.column,
            file: p.filename,
            text: p.text,
            endline: p.endLine,
            endcolumn: p.endColumn,
        }));
        const link: Link = {
            text: 'More...',
            url: this.makeURL(tag.ruleKey, lang),
        };
        const cetag: ResultLineTag = {
            severity,
            text,
            line: tag.line,
            column: tag.column,
            endline: tag.endLine,
            endcolumn: tag.endColumn,
            link,
            fixes,
            flow,
        };
        output.push({
            text: `${(tag.line + ':' + tag.column).padEnd(6)} ${flow.length > 0 ? '\u2795 ' : ''}${text}`,
            tag: cetag,
        });
        if (fixes.length > 0) {
            output.push({text: '\t\u21B3 \u001B[3m\uD83D\uDCA1 A Quick Fix is available for this issue\u001B[0m'});
        }
        if (flow.length > 0) {
            output.push(
                ...flow
                    .filter(f => f.text)
                    .map((f, i) => ({
                        text: `\t\u21B3 ${flow.length > 1 ? ' ' + (i + 1) : ''} ${f.text}`,
                    })),
            );
        }
        output.push({text: `\t\u001B[2m\u21B3 ${this.makeURL(tag.ruleKey, lang)}\u001B[0m`});
    }

    simplifyPathes(lines: string, inputFilepath?: string, pathPrefix?: string): string {
        if (inputFilepath) {
            lines = lines.replaceAll(inputFilepath, '<source>').replaceAll('<stdin>', '<source>');
        }
        if (pathPrefix) {
            lines = lines.replaceAll(pathPrefix, '');
        }
        return lines;
    }

    parseOutputAndFixes(lines: string, inputFilepath?: string, pathPrefix?: string): ResultLine[] {
        if (!lines) return [];
        let output: ResultLine[] = [];
        const lang: string = this.lang;
        try {
            const results = JSON.parse(this.simplifyPathes(lines, inputFilepath, pathPrefix));
            if (results.header) {
                output.push(
                    ...utils.splitLines('\u001B[3m\u001B[32m' + results.header + '\u001B[0m').map(s => ({text: s})),
                );
            }
            if (results.parsingErrors && results.parsingErrors.length > 0) {
                output.push(
                    {text: ''},
                    {
                        text:
                            '\u001B[1m\u26A0\uFE0F ' +
                            'Parsing errors\u001B[0m (these can affect the quality of the analysis):',
                    },
                );
                for (const e of results.parsingErrors) {
                    this.readTag(e, output, 8, lang);
                }
            }
            if (results.issues && results.issues.length > 0) {
                output.push({text: ''}, {text: '\u001B[1m\uD83D\uDC1E Issues:\u001B[0m'});
                for (const e of results.issues) {
                    this.readTag(e, output, 4, lang);
                }
                output.push({text: ''});
            } else if (!results.parsingErrors || results.parsingErrors.length === 0) {
                output.push({text: ''}, {text: '\u001B[1m\u2728 No issue found\u001B[0m'});
            }
            if (results.logs && results.logs.length > 0) {
                output.push(
                    {text: ''},
                    {text: '\u001B[1m\uD83D\uDCDC Logs:\u001B[0m'},
                    ...results.logs.map(l => ({text: `[${l.level}] ${l.message}`})),
                );
            }
            if (results.reproducer) {
                this.reproducer = {
                    content: results.reproducer.content,
                    type: 'application/octet-stream',
                    name: 'sonar-reproducer.zip',
                    title: 'reproducer',
                };
            }
        } catch {
            output = utils.splitLines(lines).map(l => ({text: l}));
        }
        return output;
    }

    buildCompilationCMD(
        compilationInfo: CompilationInfo,
        inputFilePath: string,
        supportedLibraries?: Record<string, OptionsHandlerLibrary>,
    ) {
        const cmd: any[] = [];
        cmd.push(compilationInfo.compiler.exe);

        // Collecting the flags of compilation

        let compileFlags: string[] = splitArguments(compilationInfo.compiler.options);
        const includeflags = super.getIncludeArguments(
            compilationInfo.libraries,
            supportedLibraries || {},
            inputFilePath ? path.dirname(inputFilePath) : undefined,
        );
        compileFlags = compileFlags.concat(includeflags);
        const libOptions = super.getLibraryOptions(compilationInfo.libraries, supportedLibraries || {});
        compileFlags = compileFlags.concat(libOptions);
        const manualCompileFlags = compilationInfo.options.filter(option => option !== inputFilePath);
        compileFlags = compileFlags.concat(manualCompileFlags);
        compileFlags = compileFlags.filter(f => f !== '');

        return cmd.concat(compileFlags);
    }

    override getDefaultExecOptions(): ExecutionOptions {
        const opts = super.getDefaultExecOptions();
        // Allow bigger output for reproducer
        opts.maxOutput = 2 * 1024 * 1024;
        return opts;
    }

    override async runTool(
        compilationInfo: CompilationInfo,
        inputFilePath?: string,
        args?: string[],
        stdin?: string,
        supportedLibraries?: Record<string, OptionsHandlerLibrary>,
    ): Promise<ToolResult> {
        if (inputFilePath == null) {
            return new Promise(resolve => {
                resolve(this.createErrorResponse('Unable to run tool'));
            });
        }
        this.reproducer = undefined;
        this.lang = compilationInfo.compiler.lang === 'c' ? 'c' : 'cpp';
        let sonarArgs: string[] = (args ?? [])
            .filter(a => !a.includes('subprocess'))
            .concat(['--directory', path.dirname(inputFilePath), '--']);
        sonarArgs = sonarArgs.concat(this.buildCompilationCMD(compilationInfo, inputFilePath, supportedLibraries));

        const res: ToolResult = await super.runTool(compilationInfo, inputFilePath, sonarArgs);
        if (this.reproducer) {
            res.artifact = this.reproducer;
        }
        return res;
    }
}
