import glob, re, os.path, textwrap
from colorama import Fore, Back, Style

def target_prefix(dir):
    if dir.startswith("net"):
        return "net"
    elif dir.startswith("serial"):
        return "serial"
    elif dir.startswith("util"):
        return "util"
    elif dir.startswith("runtime"):
        return "runtime"
    elif dir.startswith("zk"):
        return "zk"
    elif dir.startswith("raft"):
        return "raft"
    elif dir.startswith("sdk/src/crypto"):
        return "sdk::crypto"
    elif dir.startswith("sdk"):
        return "sdk"
    elif dir.startswith("contract/dao"):
        return "dao"
    elif dir.startswith("contract/money"):
        return "money"
    elif dir.startswith("rpc"):
        return "rpc"
    elif dir.startswith("system"):
        return "system"
    elif dir.startswith("dht"):
        return "dht"
    elif dir.startswith("consensus"):
        return "consensus"
    elif dir.startswith("zkas"):
        return "zkas"
    elif dir.startswith("blockchain"):
        return "blockchain"
    elif dir.startswith("wallet"):
        return "wallet"
    else:
        assert not dir or dir == "tx"
        return ""

def target_suffix(prefix, base):
    if prefix in ("dao", "money"):
        # Just shorten the target to simply "dao" or "money"
        # We don't need the fine grained details
        return ""
    elif base in ("mod.rs", "lib.rs"):
        # Just use the module name as the target with these files
        return ""
    # Otherwise just use the filename as the target suffix
    return base.removesuffix(".rs")

def log_target(fname):
    dir, base = os.path.dirname(fname), os.path.basename(fname)
    prefix = target_prefix(dir)
    suffix = target_suffix(prefix, base)
    # you don't need :: when the suffix is empty
    if not suffix and not prefix:
        return ""
    if not suffix:
        return prefix
    if not prefix:
        return suffix
    return f"{prefix}::{suffix}"

def replace(fname, contents):
    target = log_target(fname)

    # You can debug like this:
    #if target != "net::protocol_seed":
    #    return None

    print(f"Replacing {target}" + " "*(40 - len(target)) + f"[{fname}]")

    result = []
    lines = contents.split("\n")
    i = 0
    while i < len(lines):
        line = lines[i]
        # Line number
        ln = lambda: i + 1

        # only used for debug output
        old_text = None
        new_text = None
        # This is used as a debug goto
        is_modified = False

        log_level = None
        if "trace!(" in line:
            log_level = "trace"
        elif "debug!(" in line:
            log_level = "debug"
        elif "info!(" in line:
            log_level = "info"
        elif "warn!(" in line:
            log_level = "warn"
        elif "error!(" in line:
            log_level = "error"

        if log_level is not None:
            # Walk backwards to find the function name
            # Range is (i, 0]
            function_name = None
            for j in range(i - 1, -1, -1):
                past_line = lines[j]
                if (match := re.search(
                    r"fn ([\w]+)(<[\w: \+']+>)?\(",
                    past_line
                )):
                    function_name = match.group(1)
                    break
            assert function_name is not None

            local_target = target
            if target in (
                "consensus::protocol_proposal",
                "consensus::protocol_sync",
                "consensus::protocol_sync_consensus",
                "consensus::protocol_tx",
                "runtime::db",
                "net::hosts",
                "net::protocol_address",
                "net::protocol_ping",
                "net::protocol_seed",
                "net::protocol_version",
                "net::p2p",
                "net::message_subscriber",
                "net::channel",
            ):
                local_target += f"::{function_name}()"

            # No target exists for this file at all. Just ignore these
            # We would normally delete any target set for these files
            # but so far we have none of them, so just ignore them.
            if not target:
                print(
                    "    "
                    + Back.RED + "Skip [no target]:" + Style.RESET_ALL
                    + f" {line}"
                )
            # Single lines with a target that's a constant or string
            elif re.search(rf'{log_level}!\(target: ([\w]+|"[\w:\-\(\)]+"),', line):
                old_text = f"{ln()}: {line}"

                line = re.sub(
                    r'target: ([\w]+|"[\w:\-\(\)]+"),',
                    f'target: "{local_target}",',
                    line
                )

                is_modified = True
                new_text = f"{ln()}: {line}"
            # Normal single lines with no target set
            elif f'{log_level}!("' in line:
                old_text = f"{ln()}: {line}"

                #print(f"    No target: {line}")
                line = line.replace(f'{log_level}!(',
                                    f'{log_level}!(target: "{local_target}", ')

                is_modified = True
                new_text = f"{ln()}: {line}"
            # Multiline logs
            # We read the next line and check if there's a target set or not
            else:
                assert re.search(rf"{log_level}!\($", line)

                old_text = f"{ln()}: {line}"
                new_text = f"{ln()}: {line}"

                result.append(line)
                i += 1
                assert i < len(lines)
                line = lines[i]

                old_text += f"\n{ln()}: {line}"

                # Constants or target strings set
                if re.search(r'target: ([\w]+|"[\w:\-\(\)]+"),', line):
                    line = re.sub(
                        r'target: ([\w]+|"[\w:\-\(\)]+"),',
                        f'target: "{local_target}",',
                        line
                    )

                    new_text += f"\n{ln()}: {line}"
                # Multi-line logs with no target set
                # Insert an extra line with the target
                else:
                    assert re.search('^"', line)

                    leading_space = lambda line: len(line) - len(line.lstrip())

                    added_line = (" "*leading_space(line)
                                  + f'target: "{local_target}",')
                    result.append(added_line)

                    new_text += f"\n{ln()}: {added_line}\n{ln() + 1}: {line}"

                is_modified = True

        if is_modified:
            assert old_text is not None and new_text is not None
            print(
                Fore.RED
                + textwrap.indent(old_text, "    < ")
                + Style.RESET_ALL
            )
            print(
                Fore.GREEN
                + textwrap.indent(new_text, "    > ")
                + Style.RESET_ALL
            )
            print()

        result.append(line)
        i += 1
    return "\n".join(result)

def main():
    for fname in glob.glob("**/*.rs", root_dir="src/", recursive=True):
        with open(f"src/{fname}", "r") as f:
            contents = f.read()

        if (contents := replace(fname, contents)) is None:
            # Skip this file
            continue

        # Uncomment this to apply the changes
        with open(f"src/{fname}", "w") as f:
            f.write(contents)

if __name__ == "__main__":
    main()

