import argparse
import copy
import json
import os
import shutil
import subprocess
import sys
import tempfile
import threading

arc_project_prefix = 'a.yandex-team.ru/'
std_lib_prefix = 'contrib/go/_std/src/'
vendor_prefix = 'vendor/'


def compare_versions(version1, version2):
    v1 = tuple(str(int(x)).zfill(8) for x in version1.split('.'))
    v2 = tuple(str(int(x)).zfill(8) for x in version2.split('.'))
    if v1 == v2:
        return 0
    return 1 if v1 < v2 else -1


def get_symlink_or_copyfile():
    os_symlink = getattr(os, 'symlink', None)
    if os_symlink is None:
        os_symlink = shutil.copyfile
    return os_symlink


def copy_args(args):
    return copy.copy(args)


def get_vendor_index(import_path):
    index = import_path.rfind('/' + vendor_prefix)
    if index < 0:
        index = 0 if import_path.startswith(vendor_prefix) else index
    else:
        index = index + 1
    return index


def get_import_path(module_path):
    assert len(module_path) > 0
    import_path = module_path.replace('\\', '/')
    is_std_module = import_path.startswith(std_lib_prefix)
    if is_std_module:
        import_path = import_path[len(std_lib_prefix):]
    elif import_path.startswith(vendor_prefix):
        import_path = import_path[len(vendor_prefix):]
    else:
        import_path = arc_project_prefix + import_path
    assert len(import_path) > 0
    return import_path, is_std_module


def call(cmd, cwd, env=None):
    # print >>sys.stderr, ' '.join(cmd)
    return subprocess.check_output(cmd, stdin=None, stderr=subprocess.STDOUT, cwd=cwd, env=env)


def classify_srcs(srcs, args):
    args.go_srcs = list(filter(lambda x: x.endswith('.go'), srcs))
    args.asm_srcs = list(filter(lambda x: x.endswith('.s'), srcs))
    args.objects = list(filter(lambda x: x.endswith('.o') or x.endswith('.obj'), srcs))
    args.symabis = list(filter(lambda x: x.endswith('.symabis'), srcs))
    args.sysos = list(filter(lambda x: x.endswith('.syso'), srcs))


def get_import_config_info(peers, gen_importmap, import_map={}, module_map={}):
    info = {'importmap': [], 'packagefile': [], 'standard': {}}
    if gen_importmap:
        for key, value in import_map.items():
            info['importmap'].append((key, value))
    for peer in peers:
        peer_import_path, is_std = get_import_path(os.path.dirname(peer))
        if gen_importmap:
            index = get_vendor_index(peer_import_path)
            if index >= 0:
                index += len(vendor_prefix)
                info['importmap'].append((peer_import_path[index:], peer_import_path))
        info['packagefile'].append((peer_import_path, os.path.join(args.build_root, peer)))
        if is_std:
            info['standard'][peer_import_path] = True
    for key, value in module_map.items():
        info['packagefile'].append((key, value))
    return info


def create_import_config(peers, gen_importmap, import_map={}, module_map={}):
    lines = []
    info = get_import_config_info(peers, gen_importmap, import_map, module_map)
    for key in ('importmap', 'packagefile'):
        for item in info[key]:
            lines.append('{} {}={}'.format(key, *item))
    if len(lines) > 0:
        lines.append('')
        content = '\n'.join(lines)
        # print >>sys.stderr, content
        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(content)
            return f.name
    return None


def vet_info_output_name(path):
    return path + '.vet.out'


def vet_report_output_name(path):
    return path + '.vet.txt'


def get_source_path(args):
    return args.test_import_path or args.module_path


def gen_vet_info(args):
    import_path = args.import_path
    info = get_import_config_info(args.peers, True, args.import_map, args.module_map)

    import_map = dict(info['importmap'])
    # FIXME(snermolaev): it seems that adding import map for 'fake' package
    #                    does't make any harm (it needs to be revised later)
    import_map['unsafe'] = 'unsafe'

    for (key, _) in info['packagefile']:
        if key not in import_map:
            import_map[key] = key

    data = {
        'ID': import_path,
        'Compiler': 'gc',
        'Dir': os.path.join(args.arc_source_root, get_source_path(args)),
        'ImportPath': import_path,
        'GoFiles': list(filter(lambda x: x.endswith('.go'), args.go_srcs)),
        'NonGoFiles': [
        ],
        'ImportMap': import_map,
        'PackageFile': dict(info['packagefile']),
        'Standard': dict(info['standard']),
        'PackageVetx': dict((key, vet_info_output_name(value)) for key, value in info['packagefile']),
        'VetxOnly': False,
        'VetxOutput': vet_info_output_name(args.output),
        'SucceedOnTypecheckFailure': True
    }
    # print >>sys.stderr, json.dumps(data, indent=4)
    return data


def create_vet_config(args, info):
    with tempfile.NamedTemporaryFile(delete=False, suffix='.cfg') as f:
        f.write(json.dumps(info))
        return f.name


def dump_vet_report(args, report):
    if report:
        report = report.replace(args.build_root, '$B')
        report = report.replace(args.arc_source_root, '$S')
    with open(args.vet_report_output, 'w') as f:
        f.write(report)


def read_vet_report(args):
    assert args
    report = ''
    if os.path.exists(args.vet_report_output):
        with open(args.vet_report_output, 'r') as f:
            report += f.read()
    return report


def dump_vet_report_for_tests(args, *test_args_list):
    dump_vet_report(args, reduce(lambda x, y: x + read_vet_report(y), filter(None, test_args_list), ''))


def do_vet(args):
    assert args.vet
    info = gen_vet_info(args)
    vet_config = create_vet_config(args, info)
    cmd = [args.go_vet]
    if args.vet_flags:
        cmd.extend(args.vet_flags)
    cmd.append(vet_config)
    p_vet = subprocess.Popen(cmd, stdin=None, stderr=subprocess.PIPE, cwd=args.build_root)
    _, vet_err = p_vet.communicate()
    dump_vet_report(args, vet_err if vet_err else '')


def _do_compile_go(args):
    import_path, is_std_module = args.import_path, args.is_std
    cmd = [args.go_compile, '-o', args.output, '-trimpath', args.build_root, '-p', import_path, '-D', '""']
    cmd += ['-goversion', 'go' + args.goversion]
    if is_std_module:
        cmd.append('-std')
        if import_path == 'runtime' or import_path.startswith('runtime/internal/'):
            cmd.append('-+')
    import_config_name = create_import_config(args.peers, True, args.import_map, args.module_map)
    if import_config_name:
        cmd += ['-importcfg', import_config_name]
    else:
        if import_path == 'unsafe' or len(args.objects) > 0 or args.asmhdr:
            pass
        else:
            cmd.append('-complete')
    if args.asmhdr:
        cmd += ['-asmhdr', args.asmhdr]
    if compare_versions('1.12', args.goversion) >= 0:
        if args.symabis:
            cmd += ['-symabis'] + args.symabis
        if compare_versions('1.13', args.goversion) >= 0:
            pass
        elif import_path in ('runtime', 'runtime/internal/atomic'):
            cmd.append('-allabis')
    compile_workers = '4'
    if args.compile_flags:
        cmd += args.compile_flags
        if '-race' in args.compile_flags:
            compile_workers = '1'
    cmd += ['-pack', '-c={}'.format(compile_workers)]
    cmd += args.go_srcs
    call(cmd, args.build_root)


def do_compile_go(args):
    if args.vet:
        run_vet = threading.Thread(target=do_vet, args=(args,))
        run_vet.start()
    try:
        _do_compile_go(args)
    finally:
        if args.vet:
            run_vet.join()


def do_compile_asm(args):
    assert(len(args.srcs) == 1 and len(args.asm_srcs) == 1)
    cmd = [args.go_asm, '-trimpath', args.build_root]
    cmd += ['-I', args.output_root, '-I', os.path.join(args.pkg_root, 'include')]
    cmd += ['-D', 'GOOS_' + args.targ_os, '-D', 'GOARCH_' + args.targ_arch, '-o', args.output]
    if args.asm_flags:
        cmd += args.asm_flags
    cmd += args.asm_srcs
    call(cmd, args.build_root)


def do_link_lib(args):
    if len(args.asm_srcs) > 0:
        asmargs = copy_args(args)
        asmargs.asmhdr = os.path.join(asmargs.output_root, 'go_asm.h')
        do_compile_go(asmargs)
        for src in asmargs.asm_srcs:
            asmargs.srcs = [src]
            asmargs.asm_srcs = [src]
            asmargs.output = os.path.join(asmargs.output_root, os.path.basename(src) + '.o')
            do_compile_asm(asmargs)
            args.objects.append(asmargs.output)
    else:
        do_compile_go(args)
    if args.objects:
        cmd = [args.go_pack, 'r', args.output] + args.objects + args.sysos
        call(cmd, args.build_root)


def do_link_exe(args):
    assert args.extld is not None
    assert args.non_local_peers is not None
    compile_args = copy_args(args)
    compile_args.output = os.path.join(args.output_root, 'main.a')
    compile_args.import_path = 'main'

    if args.vcs and os.path.isfile(compile_args.vcs):
        build_info = os.path.join('library', 'go', 'core', 'buildinfo')
        if any(map(lambda x: x.startswith(build_info), compile_args.peers)):
            compile_args.go_srcs.append(compile_args.vcs)

    do_link_lib(compile_args)
    cmd = [args.go_link, '-o', args.output]
    import_config_name = create_import_config(args.peers + args.non_local_peers, False, args.import_map, args.module_map)
    if import_config_name:
        cmd += ['-importcfg', import_config_name]
    if args.link_flags:
        cmd += args.link_flags
    cmd += ['-buildmode=exe', '-extld={}'.format(args.extld)]
    extldflags = []
    if args.extldflags is not None:
        extldflags += args.extldflags
    if args.cgo_peers is not None and len(args.cgo_peers) > 0:
        is_group = args.targ_os == 'linux'
        if is_group:
            extldflags.append('-Wl,--start-group')
        extldflags.extend(os.path.join(args.build_root, x) for x in args.cgo_peers)
        if is_group:
            extldflags.append('-Wl,--end-group')
    if len(extldflags) > 0:
        cmd.append('-extldflags=' + ' '.join(extldflags))
    cmd.append(compile_args.output)
    call(cmd, args.build_root)


def gen_cover_info(args):
    lines = []
    lines.extend([
        """
var (
    coverCounters = make(map[string][]uint32)
    coverBlocks = make(map[string][]testing.CoverBlock)
)
        """,
        'func init() {',
    ])
    for var, file in (x.split(':') for x in args.cover_info):
        lines.append('    coverRegisterFile("{file}", _cover0.{var}.Count[:], _cover0.{var}.Pos[:], _cover0.{var}.NumStmt[:])'.format(file=file, var=var))
    lines.extend([
        '}',
        """
func coverRegisterFile(fileName string, counter []uint32, pos []uint32, numStmts []uint16) {
    if 3*len(counter) != len(pos) || len(counter) != len(numStmts) {
        panic("coverage: mismatched sizes")
    }
    if coverCounters[fileName] != nil {
        // Already registered.
        return
    }
    coverCounters[fileName] = counter
    block := make([]testing.CoverBlock, len(counter))
    for i := range counter {
        block[i] = testing.CoverBlock{
            Line0: pos[3*i+0],
            Col0: uint16(pos[3*i+2]),
            Line1: pos[3*i+1],
            Col1: uint16(pos[3*i+2]>>16),
            Stmts: numStmts[i],
        }
    }
    coverBlocks[fileName] = block
}
        """,
    ])
    return lines


def gen_test_main(args, test_lib_args, xtest_lib_args):
    assert args and (test_lib_args or xtest_lib_args)
    test_miner = args.test_miner
    test_module_path = test_lib_args.import_path if test_lib_args else xtest_lib_args.import_path
    is_cover = args.cover_info and len(args.cover_info) > 0

    # Prepare GOPATH
    # $BINDIR
    #    |- __go__
    #        |- src
    #        |- pkg
    #            |- ${TARGET_OS}_${TARGET_ARCH}
    go_path_root = os.path.join(args.output_root, '__go__')
    test_src_dir = os.path.join(go_path_root, 'src')
    target_os_arch = '_'.join([args.targ_os, args.targ_arch])
    test_pkg_dir = os.path.join(go_path_root, 'pkg', target_os_arch, os.path.dirname(test_module_path))
    os.makedirs(test_pkg_dir)

    my_env = os.environ.copy()
    my_env['GOROOT'] = ''
    my_env['GOPATH'] = go_path_root
    my_env['GOARCH'] = args.targ_arch
    my_env['GOOS'] = args.targ_os

    tests = []
    xtests = []
    os_symlink = get_symlink_or_copyfile()

    # Get the list of "internal" tests
    if test_lib_args:
        os.makedirs(os.path.join(test_src_dir, test_module_path))
        os_symlink(test_lib_args.output, os.path.join(test_pkg_dir, os.path.basename(test_module_path) + '.a'))
        cmd = [test_miner, '-benchmarks', '-tests', test_module_path]
        tests = filter(lambda x: len(x) > 0, (call(cmd, test_lib_args.output_root, my_env) or '').strip().split('\n'))
    test_main_found = '#TestMain' in tests

    # Get the list of "external" tests
    if xtest_lib_args:
        xtest_module_path = xtest_lib_args.import_path
        os.makedirs(os.path.join(test_src_dir, xtest_module_path))
        os_symlink(xtest_lib_args.output, os.path.join(test_pkg_dir, os.path.basename(xtest_module_path) + '.a'))
        cmd = [test_miner, '-benchmarks', '-tests', xtest_module_path]
        xtests = filter(lambda x: len(x) > 0, (call(cmd, xtest_lib_args.output_root, my_env) or '').strip().split('\n'))
    xtest_main_found = '#TestMain' in xtests

    test_main_package = None
    if test_main_found and xtest_main_found:
        assert False, 'multiple definition of TestMain'
    elif test_main_found:
        test_main_package = '_test'
    elif xtest_main_found:
        test_main_package = '_xtest'

    shutil.rmtree(go_path_root)

    lines = ['package main', '', 'import (']
    if test_main_package is None:
        lines.append('    "os"')
    lines.extend(['    "testing"', '    "testing/internal/testdeps"'])
    if len(tests) > 0:
        lines.append('    _test "{}"'.format(test_module_path))
    if len(xtests) > 0:
        lines.append('    _xtest "{}"'.format(xtest_module_path))
    if is_cover:
        lines.append('    _cover0 "{}"'.format(test_module_path))
    lines.extend([')', ''])

    for kind in ['Test', 'Benchmark', 'Example']:
        lines.append('var {}s = []testing.Internal{}{{'.format(kind.lower(), kind))
        for test in list(filter(lambda x: x.startswith(kind), tests)):
            lines.append('    {{"{test}", _test.{test}}},'.format(test=test))
        for test in list(filter(lambda x: x.startswith(kind), xtests)):
            lines.append('    {{"{test}", _xtest.{test}}},'.format(test=test))
        lines.extend(['}', ''])

    if is_cover:
        lines.extend(gen_cover_info(args))

    lines.append('func main() {')
    if is_cover:
        lines.extend([
            '    testing.RegisterCover(testing.Cover{',
            '        Mode: "set",',
            '        Counters: coverCounters,',
            '        Blocks: coverBlocks,',
            '        CoveredPackages: "",',
            '    })',
        ])
    lines.extend([
        '    m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, examples)'
        '',
    ])

    if test_main_package:
        lines.append('    {}.TestMain(m)'.format(test_main_package))
    else:
        lines.append('    os.Exit(m.Run())')
    lines.extend(['}', ''])

    content = '\n'.join(lines)
    # print >>sys.stderr, content
    return content


def do_link_test(args):
    assert args.srcs or args.xtest_srcs
    assert args.test_miner is not None

    test_module_path = get_source_path(args)
    test_import_path, _ = get_import_path(test_module_path)

    test_lib_args = None
    xtest_lib_args = None

    if args.srcs:
        test_lib_args = copy_args(args)
        test_lib_args.output = os.path.join(args.output_root, 'test.a')
        test_lib_args.vet_report_output = vet_report_output_name(test_lib_args.output)
        test_lib_args.module_path = test_module_path
        test_lib_args.import_path = test_import_path
        do_link_lib(test_lib_args)

    if args.xtest_srcs:
        xtest_lib_args = copy_args(args)
        xtest_lib_args.srcs = xtest_lib_args.xtest_srcs
        classify_srcs(xtest_lib_args.srcs, xtest_lib_args)
        xtest_lib_args.output = os.path.join(args.output_root, 'xtest.a')
        xtest_lib_args.vet_report_output = vet_report_output_name(xtest_lib_args.output)
        xtest_lib_args.module_path = test_module_path + '_test'
        xtest_lib_args.import_path = test_import_path + '_test'
        if test_lib_args:
            xtest_lib_args.module_map[test_import_path] = test_lib_args.output
        do_link_lib(xtest_lib_args)

    test_main_content = gen_test_main(args, test_lib_args, xtest_lib_args)
    test_main_name = os.path.join(args.output_root, '_test_main.go')
    with open(test_main_name, "w") as f:
        f.write(test_main_content)
    test_args = copy_args(args)
    test_args.srcs = [test_main_name]
    if test_args.test_import_path is None:
        # it seems that we can do it unconditionally, but this kind
        # of mangling doesn't really looks good to me and we leave it
        # for pure GO_TEST module
        test_args.module_path = test_args.module_path + '___test_main__'
        test_args.import_path = test_args.import_path + '___test_main__'
    classify_srcs(test_args.srcs, test_args)
    if test_lib_args:
        test_args.module_map[test_lib_args.import_path] = test_lib_args.output
    if xtest_lib_args:
        test_args.module_map[xtest_lib_args.import_path] = xtest_lib_args.output

    if args.vet:
        dump_vet_report_for_tests(test_args, test_lib_args, xtest_lib_args)
    test_args.vet = False

    do_link_exe(test_args)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(prefix_chars='+')
    parser.add_argument('++mode', choices=['lib', 'exe', 'test'], required=True)
    parser.add_argument('++srcs', nargs='*', required=True)
    parser.add_argument('++cgo-srcs', nargs='*')
    parser.add_argument('++test_srcs', nargs='*')
    parser.add_argument('++xtest_srcs', nargs='*')
    parser.add_argument('++cover_info', nargs='*')
    parser.add_argument('++output', nargs='?', default=None)
    parser.add_argument('++build-root', required=True)
    parser.add_argument('++output-root', required=True)
    parser.add_argument('++tools-root', required=True)
    parser.add_argument('++host-os', choices=['linux', 'darwin', 'windows'], required=True)
    parser.add_argument('++host-arch', choices=['amd64'], required=True)
    parser.add_argument('++targ-os', choices=['linux', 'darwin', 'windows'], required=True)
    parser.add_argument('++targ-arch', choices=['amd64', 'x86'], required=True)
    parser.add_argument('++peers', nargs='*')
    parser.add_argument('++non-local-peers', nargs='*')
    parser.add_argument('++cgo-peers', nargs='*')
    parser.add_argument('++asmhdr', nargs='?', default=None)
    parser.add_argument('++test-import-path', nargs='?')
    parser.add_argument('++test-miner', nargs='?')
    parser.add_argument('++arc-project-prefix', nargs='?', default=arc_project_prefix)
    parser.add_argument('++std-lib-prefix', nargs='?', default=std_lib_prefix)
    parser.add_argument('++extld', nargs='?', default=None)
    parser.add_argument('++extldflags', nargs='+', default=None)
    parser.add_argument('++goversion', required=True)
    parser.add_argument('++asm-flags', nargs='*')
    parser.add_argument('++compile-flags', nargs='*')
    parser.add_argument('++link-flags', nargs='*')
    parser.add_argument('++vcs', nargs='?', default=None)
    parser.add_argument('++vet', nargs='?', const=True, default=False)
    parser.add_argument('++vet-flags', nargs='*', default=None)
    parser.add_argument('++arc-source-root')
    args = parser.parse_args()

    # Temporary work around for noauto
    if args.cgo_srcs and len(args.cgo_srcs) > 0:
        cgo_srcs_set = set(args.cgo_srcs)
        args.srcs = list(filter(lambda x: x not in cgo_srcs_set, args.srcs))

    args.pkg_root = os.path.join(str(args.tools_root), 'pkg')
    args.tool_root = os.path.join(args.pkg_root, 'tool', '{}_{}'.format(args.host_os, args.host_arch))
    args.go_compile = os.path.join(args.tool_root, 'compile')
    args.go_cgo = os.path.join(args.tool_root, 'cgo')
    args.go_link = os.path.join(args.tool_root, 'link')
    args.go_asm = os.path.join(args.tool_root, 'asm')
    args.go_pack = os.path.join(args.tool_root, 'pack')
    args.go_vet = os.path.join(args.tool_root, 'vet') if args.vet is True else args.vet
    args.output = os.path.normpath(args.output)
    args.vet_report_output = vet_report_output_name(args.output)
    args.build_root = os.path.normpath(args.build_root) + os.path.sep
    args.output_root = os.path.normpath(args.output_root)
    args.import_map = {}
    args.module_map = {}

    assert args.mode == 'test' or args.test_srcs is None and args.xtest_srcs is None
    # add lexical oreder by basename for go sources
    args.srcs.sort(key=lambda x: os.path.basename(x))
    if args.test_srcs:
        args.srcs += sorted(args.test_srcs, key=lambda x: os.path.basename(x))
        del args.test_srcs
    if args.xtest_srcs:
        args.xtest_srcs.sort(key=lambda x: os.path.basename(x))

    arc_project_prefix = args.arc_project_prefix
    std_lib_prefix = args.std_lib_prefix

    # compute root relative module dir path
    assert args.output is None or args.output_root == os.path.dirname(args.output)
    assert args.output_root.startswith(args.build_root)
    args.module_path = args.output_root[len(args.build_root):]
    assert len(args.module_path) > 0
    args.import_path, args.is_std = get_import_path(args.module_path)

    classify_srcs(args.srcs, args)

    assert args.asmhdr is None or args.word == 'go'

    try:
        os.unlink(args.output)
    except OSError:
        pass

    # We are going to support only 'lib', 'exe' and 'cgo' build modes currently
    # and as a result we are going to generate only one build node per module
    # (or program)
    dispatch = {
        'lib': do_link_lib,
        'exe': do_link_exe,
        'test': do_link_test
    }

    exit_code = 1
    try:
        dispatch[args.mode](args)
        exit_code = 0
    except KeyError:
        print >>sys.stderr, 'Unknown build mode [{}]...'.format(args.mode)
    except subprocess.CalledProcessError as e:
        print >>sys.stderr, '{} returned non-zero exit code {}. stop.'.format(' '.join(e.cmd), e.returncode)
        print >>sys.stderr, e.output
        exit_code = e.returncode
    except Exception as e:
       print >>sys.stderr, "Unhandled exception [{}]...".format(str(e))
    sys.exit(exit_code)
