#!/usr/bin/env python
# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Runs clang's "modern objective-c" rewriter on chrome code.
Does the same as Xcode's Edit->Convert->To Modern Objective-C Syntax.

Note that this just runs compile commands and doesn't look at build
dependencies, i.e. it doesn't make sure generated headers exist.  It also
requires reclient to be disabled.  Suggested workflow: Build the target you want
to convert locally with reclient to create generated headers, then disable
reclient, re-run gn, and then run this script.

Since Chrome's clang disables the rewriter, to run this you will need to
build ToT clang with `-DCLANG_ENABLE_ARCMT` and (temporarily) add the following
to your Chromium build args:
clang_base_path = /path/to/clang
clang_use_chrome_plugins = false
"""

from __future__ import print_function

import argparse
import glob
import json
import math
import os
import shlex
import subprocess
import sys


def main():
    # As far as I can tell, clang's ObjC rewriter can't do in-place rewriting
    # (the ARC rewriter can).  libclang exposes functions for parsing the remap
    # file, but doing that manually in python seems a lot easier.

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('builddir', help='build directory, e.g. out/gn')
    parser.add_argument(
        'substr',
        default='',
        nargs='?',
        help='source dir part, eg chrome/browser/ui/cocoa',
    )
    args = parser.parse_args()

    rewrite_dir = os.path.abspath(
        os.path.join(args.builddir, 'rewrite_modern_objc')
    )
    try:
        os.mkdir(rewrite_dir)
    except OSError:
        pass

    remap_file = os.path.join(rewrite_dir, 'remap')
    try:
        # Remove remap files from prior runs.
        os.remove(remap_file)
    except OSError:
        pass

    # The basic idea is to call clang's objcmt rewriter for each source file.
    # The rewriter writes a "remap" file containing N times 3 lines:
    # Name of an original source file, the original file's timestamp
    # at rewriting time, and the name of a temp file containing the rewritten
    # contents.
    # The rewriter gets confused if several instances run in parallel.  We could
    # be fancy and have num_cpus rewrite dirs and combine their contents in the
    # end, but for now just run the rewrites serially.

    # First, ask ninja for the compile commands of all .m and .mm files.
    compdb = subprocess.check_output(
        ['ninja', '-C', args.builddir, '-t', 'compdb', 'objc', 'objcxx']
    )

    for cmd in json.loads(compdb):
        objc_file = cmd['file']
        if args.substr not in objc_file:
            continue
        clang_cmd = cmd['command']

        had_error = False
        if 'rewrapper' in clang_cmd:
            print('need builddir with use_remoteexec not set', file=sys.stderr)
            had_error = True
        if 'jumbo' in clang_cmd:
            print('need builddir with use_jumbo_build not set', file=sys.stderr)
            had_error = True
        if 'precompile.h-m' in clang_cmd:
            print(
                'need builddir with enable_precompiled_headers=false',
                file=sys.stderr,
            )
            had_error = True
        if had_error:
            sys.exit(1)

        # Ninja creates the directory containing the build output, but we
        # don't run ninja, so we need to do that ourselves.
        split_cmd = shlex.split(clang_cmd)
        o_index = split_cmd.index('-o')
        assert o_index != -1
        try:
            os.makedirs(os.path.dirname(split_cmd[o_index + 1]))
        except OSError:
            pass

        # Add flags to tell clang to do the rewriting.
        # Passing "-ccc-objcmt-migrate dir" doesn't give us control over each
        # individual setting, so use the Xclang flags.  The individual flags
        # are at
        # http://llvm-cs.pcc.me.uk/tools/clang/include/clang/Driver/Options.td#291
        # Note that -objcmt-migrate-all maps to ObjCMT_MigrateDecls in
        # http://llvm-cs.pcc.me.uk/tools/clang/lib/Frontend/CompilerInvocation.cpp#1479
        # which is not quite all the options:
        # http://llvm-cs.pcc.me.uk/tools/clang/include/clang/Frontend/FrontendOptions.h#248

        flags = ['-Xclang', '-mt-migrate-directory', '-Xclang', rewrite_dir]
        flags += ['-Xclang', '-objcmt-migrate-subscripting']
        flags += ['-Xclang', '-objcmt-migrate-literals']
        # flags += ['-Xclang', '-objcmt-returns-innerpointer-property'] # buggy
        # flags += ['-Xclang', '-objcmt-migrate-property-dot-syntax'] # do not
        # want
        # objcmt-migrate-all is the same as the flags following it here (it does
        # not include the flags listed above it).
        # Probably don't want ns-nonatomic-iosonly (or atomic-property), so we
        # can't use migrate-alll which includes that, and have to manually set
        # the
        # bits of migrate-all we do want.
        # flags += ['-Xclang', '-objcmt-migrate-all']
        # flags += ['-Xclang', '-objcmt-migrate-property']  # not sure if want
        flags += ['-Xclang', '-objcmt-migrate-annotation']
        flags += ['-Xclang', '-objcmt-migrate-instancetype']
        flags += ['-Xclang', '-objcmt-migrate-ns-macros']
        # flags += ['-Xclang', '-objcmt-migrate-protocol-conformance'] # buggy
        # flags += ['-Xclang', '-objcmt-atomic-property']  # not sure if want
        # flags += ['-Xclang', '-objcmt-ns-nonatomic-iosonly']  # not sure if
        # want
        # Want, but needs careful manual review, and doesn't find everything:
        # flags += ['-Xclang', '-objcmt-migrate-designated-init']
        clang_cmd += ' ' + ' '.join(flags)

        print(objc_file)
        subprocess.check_call(clang_cmd, shell=True, cwd=cmd['directory'])

    if not os.path.exists(remap_file):
        print('no changes')
        return

    # Done with rewriting. Now the read the above-described 'remap' file and
    # copy modified files over the originals.
    remap = open(remap_file).readlines()
    for i in range(0, len(remap), 3):
        infile, mtime, outfile = map(str.strip, remap[i : i + 3])
        if args.substr not in infile:
            # Ignore rewritten header files not containing args.substr too.
            continue
        if math.trunc(os.path.getmtime(infile)) != int(mtime):
            print('%s was modified since rewriting; exiting' % infile)
            sys.exit(1)
        os.rename(outfile, infile)  # Copy rewritten file over.

    print('all done. commit, run `git cl format`, commit again, and upload!')


if __name__ == '__main__':
    main()
