// 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 * as fs from 'node:fs/promises';
import path from 'node:path';

import Semver from 'semver';

import {BaseCompiler} from '../base-compiler.js';
import {asSafeVer} from '../utils.js';
import {ClangParser} from './argument-parsers.js';

export class SolidityCompiler extends BaseCompiler {
    static get key() {
        return 'solidity';
    }

    override getSharedLibraryPathsAsArguments() {
        return [];
    }

    override getArgumentParserClass() {
        return ClangParser;
    }

    override optionsForFilter() {
        return [
            // We use --combined-json instead of `--asm-json` to have compacted json
            '--combined-json',
            Semver.lt(asSafeVer(this.compiler.semver), '0.8.0', true)
                ? 'asm,ast'
                : 'asm,ast,generated-sources,generated-sources-runtime',
            '-o',
            'contracts',
        ];
    }

    override isCfgCompiler() {
        return false;
    }

    override getOutputFilename(dirPath: string) {
        return path.join(dirPath, 'contracts/combined.json');
    }

    override async processAsm(result) {
        // Handle "error" documents.
        if (!result.asm.includes('\n') && result.asm[0] === '<') {
            return {asm: [{text: result.asm}]};
        }

        // solc gives us a character range for each asm instruction,
        // so open the input file and figure out what line each
        // character is on
        const inputFile = await fs.readFile(result.inputFilename);
        let currentLine = 1;
        const charToLine = inputFile.map(c => {
            const line = currentLine;
            if (c === '\n'.codePointAt(0)) {
                ++currentLine;
            }
            return line;
        });
        const hasOldJSONLayout = Semver.lt(asSafeVer(this.compiler.semver), '0.8.0', true);
        const hasGeneratedSources = Semver.gte(asSafeVer(this.compiler.semver), '0.8.0', true);

        const asm = JSON.parse(result.asm);
        if (!asm.contracts) {
            return {asm: [{text: result.asm}]};
        }
        return {
            asm: (Object.entries(asm.contracts) as [string, any][])
                .filter(([_name, data]) => 'asm' in data) // ignore external contracts
                .sort(([_name1, data1], [_name2, data2]) => data1.asm['.code'][0].begin - data2.asm['.code'][0].begin)
                .map(([name, data]) => {
                    // name is in the format of file:contract
                    // e.g. MyFile.sol:MyContract
                    const [sourceName, contractName] = name.split(':');

                    // to make the asm more readable, we rename the
                    // tags (jumpdests) to show what function they're
                    // part of. here we parse the AST so we know what
                    // range of characters belongs to each function.
                    let contractFunctions;
                    // the layout of this JSON has changed between versions...
                    if (hasOldJSONLayout) {
                        contractFunctions = (
                            asm.sources[sourceName].AST.children.find(node => {
                                return node.name === 'ContractDefinition' && node.attributes.name === contractName;
                            }).children ?? []
                        )
                            .filter(node => {
                                return node.name === 'FunctionDefinition';
                            })
                            .map(node => {
                                const [begin, length] = node.src.split(':').map(x => Number.parseInt(x, 10));

                                let name = node.attributes.isConstructor ? 'constructor' : node.attributes.name;

                                // encode the args into the name so we can
                                // differentiate between overloads
                                if (node.children[0].children.length > 0) {
                                    name +=
                                        '_' +
                                        node.children[0].children
                                            .map(paramNode => {
                                                return paramNode.attributes.type;
                                            })
                                            .join('_');
                                }

                                return {
                                    name: name,
                                    begin: begin,
                                    end: begin + length,
                                    tagCount: 0,
                                };
                            });
                    } else {
                        contractFunctions = asm.sources[sourceName].AST.nodes
                            .find(node => {
                                return node.nodeType === 'ContractDefinition' && node.name === contractName;
                            })
                            .nodes.filter(node => {
                                return node.nodeType === 'FunctionDefinition';
                            })
                            .map(node => {
                                const [begin, length] = node.src.split(':').map(x => Number.parseInt(x, 10));

                                let name = node.kind === 'constructor' ? 'constructor' : node.name;

                                // encode the args into the name so we can
                                // differentiate between overloads
                                if (node.parameters.parameters.length > 0) {
                                    name +=
                                        '_' +
                                        node.parameters.parameters
                                            .map(paramNode => {
                                                return paramNode.typeName.name;
                                            })
                                            .join('_');
                                }

                                return {
                                    name: name,
                                    begin: begin,
                                    end: begin + length,
                                    tagCount: 0,
                                };
                            });
                    }

                    // solc generates some code, for things like detecting
                    // and reverting if a multiplication results in
                    // integer overflow, etc.
                    const processGeneratedSources = generatedSourcesData => {
                        const generatedSources = {};
                        for (const generatedSource of generatedSourcesData) {
                            generatedSources[generatedSource.id] = generatedSource.ast.statements.map(statement => {
                                const [begin, length] = statement.src.split(':').map(x => Number.parseInt(x, 10));
                                return {
                                    name: statement.name,
                                    begin: begin,
                                    end: begin + length,
                                    tagCount: 0,
                                };
                            });
                        }
                        return generatedSources;
                    };

                    // (0.8.x onwards only!)
                    // there are two sets of generated sources, one for the code which deploys
                    // the contract (i.e. the constructor) 'generated-sources', and the code
                    // which is deployed and stored on-chain 'generated-sources-runtime'

                    const generatedSources = hasGeneratedSources
                        ? processGeneratedSources(data['generated-sources'])
                        : {};
                    const generatedSourcesRuntime = hasGeneratedSources
                        ? processGeneratedSources(data['generated-sources-runtime'])
                        : {};

                    const processOpcodes = (opcodes, indent, generatedSources) => {
                        // first iterate the opcodes to find all the tags,
                        // and assign human-readable names to as many of
                        // them as we can
                        const tagNames = {};
                        const processPossibleTagOpcode = (opcode, funcList) => {
                            if (opcode.name === 'tag') {
                                const func = funcList.find(func => {
                                    return opcode.begin >= func.begin && opcode.end <= func.end;
                                });

                                if (func !== undefined) {
                                    // a function can have multiple tags, so append
                                    // a number to each
                                    const tagName = `${func.name}_${func.tagCount}`;

                                    ++func.tagCount;

                                    tagNames[opcode.value] = tagName;
                                    opcode.value = tagName;
                                }
                            }
                        };

                        for (const opcode of opcodes) {
                            // source 0 is the .sol file the user is
                            // editing, everything else is generated
                            // sources (from version 0.8.x onwards).
                            // if source is undefined, then this is
                            // a compiler version which doesn't
                            // provide one (< 0.6.x), but we can
                            // infer 0 in this case.
                            if (opcode.source === 0 || opcode.source === undefined) {
                                opcode.line = charToLine[opcode.begin];

                                processPossibleTagOpcode(opcode, contractFunctions);
                            } else {
                                processPossibleTagOpcode(opcode, generatedSources[opcode.source] || []);
                            }
                        }

                        return opcodes.map(opcode => {
                            const name = `${opcode.name.startsWith('tag') ? indent : `${indent}\t`}${opcode.name}`;

                            let value = opcode.value || '';
                            if (opcode.name === 'PUSH [tag]') {
                                if (tagNames[value] !== undefined) {
                                    value = tagNames[value];
                                }
                            }

                            return {
                                text: `${name} ${value}`,
                                source: {line: opcode.line, file: null},
                            };
                        });
                    };

                    return [
                        {text: `// ${contractName}`},
                        // .code section is the code only run when deploying - the constructor
                        {text: '.code'},
                        processOpcodes(data.asm['.code'], '', generatedSources),
                        {text: ''},
                        // .data section is deployed bytecode - everything else
                        {text: '.data'},
                        (Object.entries(data.asm['.data']) as [string, any][]).map(([id, {'.code': code}]) => {
                            // some .data sections do not contain embedded .code
                            if (code === undefined) return [];
                            return [{text: `\t${id}:`}, processOpcodes(code, '\t', generatedSourcesRuntime)];
                        }),
                    ];
                })
                .flat(Number.POSITIVE_INFINITY),
        };
    }
}
