"""This module contains functionality used to easily analyse Truffle
projects."""
import json
import logging
import os
import re
import sys
import warnings
from pathlib import PurePath

from ethereum.utils import sha3

from mythril.analysis.report import Report
from mythril.analysis.security import fire_lasers
from mythril.analysis.symbolic import SymExecWrapper
from mythril.ethereum import util
from mythril.ethereum.evmcontract import EVMContract
from mythril.laser.ethereum.util import get_instruction_index
from mythril.solidity.soliditycontract import SourceMapping

log = logging.getLogger(__name__)


def format_Warning(message, category, filename, lineno, line=""):
    return "{}: {}\n\n".format(str(filename), str(message))


warnings.formatwarning = format_Warning


def analyze_truffle_project(sigs, args):
    """

    :param sigs:
    :param args:
    """
    warnings.warn(
        "The option --truffle is being deprecated, Please use the truffle-security plugin, https://github.com/ConsenSys/truffle-security",
        FutureWarning,
    )

    project_root = os.getcwd()

    build_dir = os.path.join(project_root, "build", "contracts")

    files = os.listdir(build_dir)

    for filename in files:

        if re.match(r".*\.json$", filename) and filename != "Migrations.json":

            with open(os.path.join(build_dir, filename)) as cf:
                contractdata = json.load(cf)

            try:
                name = contractdata["contractName"]
                bytecode = contractdata["deployedBytecode"]
                filename = PurePath(contractdata["sourcePath"]).name
            except KeyError:
                print(
                    "Unable to parse contract data. Please use Truffle 4 to compile your project."
                )
                sys.exit()
            if len(bytecode) < 4:
                continue
            get_sigs_from_truffle(sigs, contractdata)

            ethcontract = EVMContract(bytecode, name=name)

            address = util.get_indexed_address(0)
            sym = SymExecWrapper(
                ethcontract,
                address,
                args.strategy,
                max_depth=args.max_depth,
                create_timeout=args.create_timeout,
                execution_timeout=args.execution_timeout,
                transaction_count=args.transaction_count,
                modules=args.modules or [],
                compulsory_statespace=False,
            )
            issues = fire_lasers(sym)

            if not len(issues):
                if args.outform == "text" or args.outform == "markdown":
                    print("# Analysis result for " + name + "\n\nNo issues found.")
                else:
                    result = {
                        "contract": name,
                        "result": {"success": True, "error": None, "issues": []},
                    }
                    print(json.dumps(result))
            else:

                report = Report()
                # augment with source code

                deployed_disassembly = ethcontract.disassembly
                constructor_disassembly = ethcontract.creation_disassembly

                source = contractdata["source"]

                deployed_source_map = contractdata["deployedSourceMap"].split(";")
                source_map = contractdata["sourceMap"].split(";")

                deployed_mappings = get_mappings(source, deployed_source_map)
                constructor_mappings = get_mappings(source, source_map)

                for issue in issues:
                    if issue.function == "constructor":
                        mappings = constructor_mappings
                        disassembly = constructor_disassembly
                    else:
                        mappings = deployed_mappings
                        disassembly = deployed_disassembly

                    index = get_instruction_index(
                        disassembly.instruction_list, issue.address
                    )

                    if index:
                        try:
                            offset = mappings[index].offset
                            length = mappings[index].length

                            issue.filename = filename
                            issue.code = source.encode("utf-8")[
                                offset : offset + length
                            ].decode("utf-8")
                            issue.lineno = mappings[index].lineno
                        except IndexError:
                            log.debug("No code mapping at index %d", index)

                    report.append_issue(issue)

                if args.outform == "json":

                    result = {
                        "contract": name,
                        "result": {
                            "success": True,
                            "error": None,
                            "issues": list(map(lambda x: x.as_dict, issues)),
                        },
                    }
                    print(json.dumps(result))

                else:
                    if args.outform == "text":
                        print(
                            "# Analysis result for " + name + ":\n\n" + report.as_text()
                        )
                    elif args.outform == "markdown":
                        print(report.as_markdown())


def get_sigs_from_truffle(sigs, contract_data):
    """

    :param sigs:
    :param contract_data:
    """
    abis = contract_data["abi"]
    for abi in abis:
        if abi["type"] != "function":
            continue
        function_name = abi["name"]
        list_of_args = ",".join([input["type"] for input in abi["inputs"]])
        signature = function_name + "(" + list_of_args + ")"
        sigs.add("0x" + sha3(signature)[:4].hex(), signature)


def get_mappings(source, deployed_source_map):
    """

    :param source:
    :param deployed_source_map:
    :return:
    """
    mappings = []
    prev_item = ""
    for item in deployed_source_map:
        if item == "":
            item = prev_item

        mapping = item.split(":")

        if len(mapping) > 0 and len(mapping[0]) > 0:
            offset = int(mapping[0])

        if len(mapping) > 1 and len(mapping[1]) > 0:
            length = int(mapping[1])

        if len(mapping) > 2 and len(mapping[2]) > 0:
            idx = int(mapping[2])

        if idx == -1:
            lineno = None
        else:
            lineno = source.encode("utf-8")[0:offset].count("\n".encode("utf-8")) + 1
        prev_item = item

        mappings.append(SourceMapping(idx, offset, length, lineno, item))

    return mappings
