#!/usr/bin/env python3
# Usage: find_type.py FILENAME START_LINE START_COL END_LINE END_COL MYPY_AND_ARGS
# Prints out the type of the expression in the given location if the mypy run
# succeeds cleanly.  Otherwise, prints out the errors encountered.
# Note: this only works on expressions, and not assignment targets.
# Note: MYPY_AND_ARGS is should be the remainder of argv, not a single
# spaces-included argument.
# NOTE: Line numbers are 1-based; column numbers are 0-based.
#
#
# Example vim usage:
# function RevealType()
#   " Set this to the command you use to run mypy on your project.  Include the mypy invocation.
#   let mypycmd = 'python3 -m mypy mypy --incremental'
#   let [startline, startcol] = getpos("'<")[1:2]
#   let [endline, endcol] = getpos("'>")[1:2]
#   " Convert to 0-based column offsets
#   let startcol = startcol - 1
#   " Change this line to point to the find_type.py script.
#   execute '!python3 /path/to/mypy/misc/find_type.py % ' . startline . ' ' . startcol . ' ' . endline . ' ' . endcol . ' ' . mypycmd
# endfunction
# vnoremap <Leader>t :call RevealType()<CR>
#
# For an Emacs example, see misc/macs.el.

from __future__ import annotations

import os.path
import re
import subprocess
import sys
import tempfile

REVEAL_TYPE_START = "reveal_type("
REVEAL_TYPE_END = ")"


def update_line(line: str, s: str, pos: int) -> str:
    return line[:pos] + s + line[pos:]


def run_mypy(mypy_and_args: list[str], filename: str, tmp_name: str) -> str:
    proc = subprocess.run(
        mypy_and_args + ["--shadow-file", filename, tmp_name], stdout=subprocess.PIPE
    )
    assert isinstance(
        proc.stdout, bytes
    )  # Guaranteed to be true because we called run with universal_newlines=False
    return proc.stdout.decode(encoding="utf-8")


def get_revealed_type(line: str, relevant_file: str, relevant_line: int) -> str | None:
    m = re.match(r'(.+?):(\d+): note: Revealed type is "(.*)"$', line)
    if m and int(m.group(2)) == relevant_line and os.path.samefile(relevant_file, m.group(1)):
        return m.group(3)
    else:
        return None


def process_output(output: str, filename: str, start_line: int) -> tuple[str | None, bool]:
    error_found = False
    for line in output.splitlines():
        t = get_revealed_type(line, filename, start_line)
        if t:
            return t, error_found
        elif "error:" in line:
            error_found = True
    return None, True  # finding no reveal_type is an error


def main() -> None:
    filename, start_line_str, start_col_str, end_line_str, end_col_str, *mypy_and_args = sys.argv[
        1:
    ]
    start_line = int(start_line_str)
    start_col = int(start_col_str)
    end_line = int(end_line_str)
    end_col = int(end_col_str)
    with open(filename) as f:
        lines = f.readlines()
        lines[end_line - 1] = update_line(
            lines[end_line - 1], REVEAL_TYPE_END, end_col
        )  # insert after end_col
        lines[start_line - 1] = update_line(lines[start_line - 1], REVEAL_TYPE_START, start_col)
        with tempfile.NamedTemporaryFile(mode="w", prefix="mypy") as tmp_f:
            tmp_f.writelines(lines)
            tmp_f.flush()

            output = run_mypy(mypy_and_args, filename, tmp_f.name)
            revealed_type, error = process_output(output, filename, start_line)
            if revealed_type:
                print(revealed_type)
            if error:
                print(output)
            exit(int(error))


if __name__ == "__main__":
    main()
