#!/usr/bin/python3
"""Mypy type checker, runner and prototype compiler.

Type check and program and run it using a Python interpreter, or
compile it to C and run it.

Note that the C back end is early in development and has a very
limited feature set.
"""

import os
import os.path
import shutil
import subprocess
import sys
import tempfile

import typing
from typing import List, Tuple

from mypy import build
from mypy.errors import CompileError


class Options:
    def __init__(self) -> None:
        # Set default options.
        self.target = build.TYPE_CHECK
        self.build_flags = List[str]()
        self.interpreter = 'python'
        self.pyversion = 3


def main() -> None:
    bin_dir = find_bin_directory()
    path, module, args, options = process_options(sys.argv[1:])
    try:
        if options.target == build.TYPE_CHECK:
            type_check_only(path, module, bin_dir, args, options)
        elif options.target == build.C:
            compile_to_c(path, module, bin_dir, args, options)
        else:
            raise RuntimeError('unsupported target %d' % options.target)
    except CompileError as e:
        for m in e.messages:
            sys.stderr.write(m + '\n')
        sys.exit(1)


def find_bin_directory() -> str:
    """Find the directory that contains this script.

    This is used by build to find stubs and other data files.
    """
    script = __file__
    # Follow up to 5 symbolic links (cap to avoid cycles).
    for i in range(5):
        if os.path.islink(script):
            script = readlinkabs(script)
        else:
            break
    return os.path.dirname(script)


def readlinkabs(link: str) -> str:
    """Return an absolute path to symbolic link destination."""
    # Adapted from code by Greg Smith.
    assert os.path.islink(link)
    path = os.readlink(link)
    if os.path.isabs(path):
        return path
    return os.path.join(os.path.dirname(link), path)


def type_check_only(path: str, module: str, bin_dir: str, args: List[str],
                    options: Options) -> None:
    # Type check the program and dependencies and translate to Python.
    build.build(path,
                module=module,
                bin_dir=bin_dir,
                target=build.TYPE_CHECK,
                pyversion=options.pyversion,
                flags=options.build_flags)

    if build.COMPILE_ONLY not in options.build_flags:
        # Run the translated program.
        if module:
            opts = ['-m', module]
        else:
            opts = [path]
        if (options.pyversion == 2 and
                os.path.dirname(typing.__file__).endswith(
                    os.path.join('lib-typing', '3.2')) and
                os.path.isdir('lib-typing')):            
            # We are running Python 2.x test cases and we must force the 2.x
            # typing module to be used.
            os.environ['PYTHONPATH'] = os.path.join('lib-typing', '2.7')
        status = subprocess.call([options.interpreter] + opts + args)
        sys.exit(status)


def compile_to_c(path: str, module: str, bin_dir: str, args: List[str],
                 options: Options) -> None:
    assert not module # Not supported yet
    assert not args   # Not supported yet
    assert options.pyversion == 3
    
    # Compile the program to C (also generate binary by default).
    result = build.build(path, target=build.C, bin_dir=bin_dir,
                         flags=options.build_flags)

    if build.COMPILE_ONLY not in options.build_flags:
        # Run the compiled program.
        # TODO command line arguments
        status = subprocess.call([result.binary_path])
        sys.exit(status)


def process_options(args: List[str]) -> Tuple[str, str, List[str], Options]:
    """Process command line arguments.

    Return (mypy program path (or None),
            module to run as script (or None),
            remaining arguments, passed to mypy program
            parsed flags)
    """
    options = Options()
    if sys.executable:
        options.interpreter = sys.executable
    while args and args[0].startswith('-'):
        if args[0] == '--verbose':
            options.build_flags.append(build.VERBOSE)
            args = args[1:]
        elif args[0] == '--py2' and args[1:]:
            # Generate Python 2 (but this is very incomplete).
            options.pyversion = 2
            options.interpreter = args[1]
            args = args[2:]
        elif args[0] == '-c':
            options.target = build.C
            args = args[1:]
        elif args[0] == '-S':
            options.build_flags.append(build.COMPILE_ONLY)
            args = args[1:]
        elif args[0] == '-m' and args[1:]:
            options.build_flags.append(build.MODULE)
            return None, args[1], args[2:], options
        else:
            usage('Invalid option {}'.format(args[0]))
    
    if not args:
        usage()
    
    return args[0], None, args[1:], options


def usage(msg: str = None) -> None:
    if msg:
        sys.stderr.write('%s\n' % msg)
    sys.stderr.write(
'''Usage: mypy [options] [-m mod | file] [args]

Options:
  -c          compile to native code (EXPERIMENTAL)
  -m mod      run module as a script (terminates option list)
  -S          do not run the program or generate a binary
  --verbose   more verbose messages
  
Environment variables:
  MYPYPATH    additional module search path
  CC          the C compiler (used with -c)
  CFLAGS      command line options to the C compiler (used with -c)
''')
    sys.exit(2)


def fail(msg: str) -> None:
    sys.stderr.write('%s\n' % msg)
    sys.exit(1)


if __name__ == '__main__':
    main()
