 # ARM Compiler 5 commandline translator
 # Copyright 2013 ARM Limited. All Rights Reserved
 
 # This is an example translator from armcc option to armclang options. A few 
 # options are implemented for reference. You are allowed to modify and extend 
 # this file to add support for new options.
 # You shall not sublicence or redistribute the Software. Please refer to the 
 # EULA of the software for licensing terms.
 # It is recommend to copy the scripts directory out of the installation
 # location before making modifications.
 
 # The script has been tested with the python 2.7.4 installation in
 # <INSTALL_ROOT>/sw/python2.7 directory and may not work correctly with other
 # versions of python.
import argparse, re, sys, subprocess, os, platform

# Set up relative path to ARM Compiler 6
toolname = 'armclang'
if sys.platform == 'win32':
    toolname += '.exe'
#this_script_location = os.path.dirname(sys.argv[0])
#default_tool_location = os.path.join(this_script_location, "..", "..", "..", "bin")
default_tool_location = os.environ["ARM_RVCT_PATH_BIN"]
default_tool = os.path.join(default_tool_location, toolname)

# Note - AC5 stands for ARM Compiler 5 below

class AC5Parser(argparse.ArgumentParser):
    """AC5Parser - A custom parser for the ARM Compiler Commandline translator.
    This wraps the parser from the argparse library with some helper fns, and 
    overrides one call in the private interface to support the lack of 
    multicharacter --via file options.
    """
    def __init__(self, prog):
        # We must set fromfile_prefix_chars to _something_ to make it check for
        # via files at all.
        super(AC5Parser, self).__init__(prog=prog, fromfile_prefix_chars='v', add_help=False)
        # translator only arguments
        self.add_argument('-v','--verbose', action='count', dest='verbosity')
        self.add_argument('-d', '--dry', action='store_true')
        self.add_argument('--ac6', dest='ac6')

    def _read_args_from_files(self, arg_strings):
        """Override of private method for reading from option input files
        """
        # expand arguments referencing files
        new_arg_strings = []
        i = 0
        while i < len(arg_strings):
            arg_string = arg_strings[i]
            # --via option indicates files to expand
            viare = re.compile("^--?via", re.IGNORECASE)
            # for regular arguments, just add them back into the list
            if not arg_string or not viare.match(arg_string):
                new_arg_strings.append(arg_string)
            # replace arguments referencing files with the file content
            else:
                # locate the argument to --via
                arg_filename = ""
                # argument given as --via file
                if len(arg_string) <= 5:
                    if i+1 < len(arg_strings):
                        if not arg_strings[i+1].startswith('-'):
                            i = i+1
                            arg_filename = arg_strings[i]

                # argument given as -via=file
                elif arg_string[4] == "=":
                    arg_filename = arg_string[5:]
                # argument given as --via=file
                elif arg_string[5] == "=":
                    arg_filename = arg_string[6:]

                try:
                    args_file = open(arg_filename)
                    try:
                        file_arg_strings = []
                        for arg_line in args_file.read().splitlines():
                            try:
                                for arg in self.convert_arg_line_to_args(arg_line):
                                    file_arg_strings.append(arg)
                            except IOError as e:
                                e.strerror = "%s: '%s'" % (e.strerror, arg_filename)
                                raise e
                        # recurse for nested vias
                        file_arg_strings = self._read_args_from_files(file_arg_strings)
                        new_arg_strings.extend(file_arg_strings)
                    finally:
                        args_file.close()
                except IOError:
                    err = sys.exc_info()[1]
                    self.error(str(err))

            i = i+1
        # return the modified argument list
        return new_arg_strings

    def convert_arg_line_to_args(self, arg_line):
        """Returns a list of the arguments on the via file line.
           Supports single and double quoting, but not escape sequences
           raises an IOError if the via file line has a trailing quote
        """
        # catch blank line
        if len(arg_line) == 0:
            return []
        # --via files support whole line comments
        if arg_line[0] == ';' or arg_line[0] == '#':
            return []

        # parsing states
        outside_quotes = re.compile(r'^(.*?)(\s|\'|"|$)')
        in_double_quotes = re.compile(r'^(.*?)("|$)')
        in_single_quotes = re.compile(r'^(.*?)(\'|$)')
        # list of args to build up
        arg_strings = []
        # Loop state
        state = outside_quotes # parser state
        arg_sub_line = arg_line # part of the line left to process
        accumulated = "" # Accumulated string from quoted segments
        while True:
            match = state.match(arg_sub_line)
            if match.group(2) == '"':
                if state == outside_quotes:
                    state = in_double_quotes
                else:
                    state = outside_quotes
                accumulated += match.group(1)
            elif match.group(2) == "'":
                if state == outside_quotes:
                    state = in_single_quotes
                else:
                    state = outside_quotes
                accumulated += match.group(1)
            else:
                if state != outside_quotes:
                    raise IOError(1, "Malformed via file")
                arg_strings.append(accumulated + match.group(1))
                accumulated = ""
            arg_sub_line = arg_sub_line[len(match.group(0)):]
            if match.group(2) == "":
                break
        #print "Read from via file:"
        #print arg_strings
        return arg_strings

    def _check_invariant(self, args, kwargs):
        """ Input checking for the below helper functions """
        dest = kwargs['dest'] if 'dest' in kwargs else None
        for arg in args:
            if arg.startswith('--'):
                # setup the dest if not specified so the two map to the same boolean
                if not dest:
                    dest = arg[2:]
                    dest = re.sub('-','_', dest)
                return dest, True
        return dest, False

    def add_negatable_option(self, *args, **kwargs):
        """Add an ARM Compiler 5 option with a --no_... complement that will
        effectively toggle a boolean in the resultant namespace. This accepts 
        all the arguments that the add_argument option accepts.

        It does some primitive input checking (at the moment: guarantee that 
        there is at least one -- option to add a --no_... for) and checks that 
        there is a sensible 'dest' field for the two options to toggle.
        These 'dest' fields default to None, and are set False or True for the
        option and no_option equivalent.

        For example:
            add_negatable_option('-g', '--debug')
        will tell the parser to accept --debug and --no_debug options and set
        the 'debug' field in the namespace to 'True' or 'False' respectively.
        The parser will also accept -g to set 'debug' to 'True' like --debug.
        """
        # First check that this is sensible
        dest, some_long_opts = self._check_invariant(args, kwargs)
        assert dest, "Must give a dest for the negatable options"
        assert some_long_opts, "Can only create negatable options for long options, starting with '--'"
        # remove dest as we are handling it explicitly later on
        kwargs.pop('dest', None)

        # construct --no_ versions for each long option
        noargs = []
        for arg in args:
            if arg.startswith('--'):
                noargs.append('--no_'+arg[2:])
        self.add_argument(*args, action='store_true', default=None, dest=dest, **kwargs)
        self.add_argument(*noargs, action='store_false', default=None, dest=dest, **kwargs)


    def add_complementary_options(self, trueopts, falseopts, **kwargs):
        """Helper function for adding two options that negate eachother, but are
           not --no_... complements.

           It does some primitive input checking (currently that there is a 
           common 'dest' field for the options to toggle.)

           For example:
               add_complementary_options(['--arm'], ['--thumb'])
           will tell the parser to accept --arm and --thumb options and set the 
           'arm' field in the namespace to 'True' for --arm and 'False' for 
           --thumb.
        """
        # First check that this is sensible
        dest, some_long_opts = self._check_invariant(trueopts + falseopts, kwargs)
        assert dest, "Must give a dest for the complementary options"
        # remove dest as we are handling it explicitly later on
        kwargs.pop('dest', None)
        self.add_argument(*trueopts, action='store_true', default=None, dest=dest, **kwargs)
        self.add_argument(*falseopts, action='store_false', default=None, dest=dest, **kwargs)

    def canonicalise_input_args(self, arg_strings):
        """Put input into canonical form to make option matching easier.
           canonical form is --[no_]?long_option or -l, but will accept
           --LONG_OPTION and --long-option.
        """
        new_arg_strings = []
        for arg_string in arg_strings:
            if arg_string.startswith('--'):
                # isolate long option name from --<long option name>[=argument]
                optend = arg_string.find('=')
                if optend == -1:
                    optend = len(arg_string)
                option = arg_string[2:optend]
                # Long options are case insensitive
                option = option.lower()
                option = re.sub('-', '_', option)
                arg_string = '--' + option + arg_string[optend:]
            new_arg_strings.append(arg_string)
        return new_arg_strings

class CaseInsensitiveList(list):
    """Encapsulates a list of option arguments that are case insensitive
    """
    def __init__(self, *args):
        super(CaseInsensitiveList, self).__init__(args)
    def __contains__(self, key):
        for name in self:
            if name.lower() == key.lower():
                return True
        return False

# option arguments
cpuargs = CaseInsensitiveList('cortex-a15', '7-a', '8-A.32', '8-A.32.no_neon', '8-A.64', '8-A.64.no_neon', '8-A.32.crypto', '8-A.64.crypto')
fpuargs = CaseInsensitiveList('VFPv4','SoftVFP','SoftVFP+VFPv4','None','FP-ARMv8')

# set up parser with arguments
parser = AC5Parser(prog="armcc")
parser.add_complementary_options(['--arm'], ['--thumb'], dest='arm_mode')
parser.add_argument('-c', action='store_true')
parser.add_argument('--cpu', action='store', choices=cpuargs)
parser.add_argument('-D', action='append')
parser.add_negatable_option('-g', '--debug')
parser.add_argument('--errors', action='store')
parser.add_argument('--fpu', nargs=1, action='store', choices=fpuargs)
parser.add_argument('--help', action='store_true')
parser.add_argument('-L', action='append')
parser.add_argument('-O0', action='store_const', const='0', dest='optlevel')
parser.add_argument('-O1', action='store_const', const='1', dest='optlevel')
parser.add_argument('-O2', action='store_const', const='2', dest='optlevel')
parser.add_argument('-O3', action='store_const', const='3', dest='optlevel')
parser.add_argument('-Ospace', action='store_const', const='space', dest='opttype')
parser.add_argument('-Otime', action='store_const', const='time', dest='opttype')
parser.add_argument('-o', "--output", action='store')
parser.add_complementary_options(['--signed_chars'], ['--unsigned_chars'])
parser.add_argument('--show_cmdline', action='store_true', default=None)
parser.add_argument('--vsn', action='store_true', default=None)
parser.add_argument('-I', action='append')
parser.add_argument('-J', action='append')
parser.add_argument('--c99', action='store_true')
parser.add_argument('-ffunction_sections', action='store_true', default=None)
parser.add_argument('-Wall', action='store_true', default=None)
parser.add_argument('-Werror', action='store_true', default=None)
parser.add_argument('-Wextra', action='store_true', default=None)
parser.add_argument('-Wno_license_mapping', action='store_true', default=None)

#----
parser.add_argument('--apcs')
parser.add_argument('--diag_style')

parser.add_argument('--reduce_paths', action='store_true')
parser.add_argument('--no_depend_system_headers', action='store_true')
parser.add_argument('--gnu', action='store_true')
parser.add_argument('--enum_is_int', action='store_true')
parser.add_argument('--remarks', action='store_true')
parser.add_argument('--brief_diagnostics', action='store_true')
parser.add_argument('--interface_enums_are_32_bit', action='store_true')
parser.add_argument('-D__ARMCC__', action='store_true')
parser.add_argument('-E', action='store_true')
parser.add_argument('--no_divide', action='store_true')

parser.add_argument('--depend_format')
parser.add_argument('--bss_threshold')
parser.add_argument('--diag_remark')
parser.add_argument('--diag_error')
#---
# parse commandline
argv = parser.canonicalise_input_args(sys.argv[1:])
ac5options, leftovers = parser.parse_known_args(argv)

# handle input files and unrecognised options
inputfiles = []
garbage = []
for leftover in leftovers:
    # anything that doesn't look like an option is an input file
    if leftover.startswith('-'):
        garbage.append(leftover)
    else:
        inputfiles.append(leftover)

# If we have unrecognised options, don't try to translate
if garbage:
    print "This commandline contains options that are not supported in ARM Compiler 6"
    print sys.argv[1:]
    print "please rewrite your commandline to remove these options:"
    print "\n".join(garbage)
    sys.exit(1)

if ac5options.verbosity == 2:
    print vars(ac5options)

# Construct the ARM Compiler 6 commandline
output_command = []
# Find ARM Compiler 6 binary to call:
# first try --ac6 on the commandline,
# then try using default tool location ../bin/armclang
# then fall back to PATH
if ac5options.ac6:
    output_command.append(ac5options.ac6)
elif os.path.isfile(default_tool):
    output_command.append(default_tool)
else:
    output_command.append(toolname)

# Handle each option from parsed namespace, adding the armclang equivalent.
# The order in this list matches the order they were added to the parser.
# Any 'default behaviour' differences are handled separately where possible

if ac5options.arm_mode is not None:
    output_command.append("-marm" if ac5options.arm_mode else "-mthumb")

if ac5options.c is not None:
    if ac5options.c:
        output_command.append('-c')

# Currently --cpu maps to a target option. We have only v8 aarch64 and aarch32 
# and we enable/disable features using a clang -cc1 option.
if ac5options.cpu is not None:
    cpure = re.compile("^8-a.64", re.IGNORECASE)
    noneonre = re.compile(".no_neon", re.IGNORECASE)
    cryptore = re.compile(".crypto", re.IGNORECASE)
    arch ="undef"
    if cpure.match(ac5options.cpu):
        arch = 'aarch64'
    else:
	cpure = re.compile("^7", re.IGNORECASE)
	if cpure.match(ac5options.cpu):
            arch = 'arm'
        else:
            cpure =re.compile("cortex-a1+", re.IGNORECASE)
            if cpure.match(ac5options.cpu):
                arch = 'arm'
            else:
                arch = 'armv8'
	  
    output_command.append('-target')
    output_command.append(arch+'-arm-none-eabi')
  
    output_command.append('-marm')
   
    output_command.append('-mcpu=cortex-a15')
    
    output_command.append('-fshort-wchar')
    #output_command.append('--split_sections')
    
    ## cryptography
    #output_command.append('-Xclang')
    #output_command.append('-target-feature')
    #output_command.append('-Xclang')
    #if cryptore.search(ac5options.cpu):
    #    output_command.append('+crypto')
    #else:
    #    output_command.append('-crypto')
else:
    # ac6 used to have a default target, but not anymore. We keep this script
    # compatible by explicitly passing a target option
    output_command.append('-target')
    output_command.append('aarch64-arm-none-eabi')

if ac5options.Wall is not None:
    if ac5options.Wall:
        output_command.append('-Wall')

if ac5options.Werror is not None:
    if ac5options.Werror:
        output_command.append('-Werror')

if ac5options.Wextra is not None:
    if ac5options.Wextra:
        output_command.append('-Wextra')

if ac5options.Wno_license_mapping is not None:
    if ac5options.Wno_license_mapping:
        output_command.append('-Wno-license-mapping')

if ac5options.ffunction_sections is not None:
    if ac5options.ffunction_sections:
        output_command.append('-ffunction-sections')
        
if ac5options.D is not None:
    for define in ac5options.D:
        output_command.append('-D'+define)

if ac5options.I is not None:
    for include in ac5options.I:
        output_command.append('-I'+include)
        
if ac5options.J is not None:
    for include in ac5options.I:
        output_command.append('-I'+include)
        
if ac5options.debug is not None:
    if ac5options.debug:
        output_command.append('-g')

if ac5options.fpu is not None:
    if ac5options.fpu == ['none']:
        output_command.append('-mfpu=none')

if ac5options.help is not None:
    if ac5options.help:
        output_command.append('--help')

if ac5options.L is not None:
    # Use -Xlinker rather than -Wl, as it is less work
    for lopt in ac5options.L:
        output_command.append('-Xlinker')
        output_command.append(lopt)

# Set optimisation level here, also must handle default behaviour when one 
# is given but not the other, e.g. -O3 has an implicit -Ospace.
# AC5 default is -02 -Ospace.
# just -Otime => -O2 -Otime
# just -O2 => -O2 -Ospace
#             | -Os
# -Ospace     | -Os
# -Otime      | -O2
# -O0         | -O0
# -O0 -Ospace | -O0
# -O0 -Otime  | -O0
# -O1         | -Os
# -O1 -Ospace | -Os
# -O1 -Otime  | -O1
# -O2         | -Os
# -O2 -Ospace | -Os
# -O2 -Otime  | -O2
# -O3         | -Oz
# -O3 -Ospace | -Oz
# -O3 -Otime  | -O3
if ac5options.optlevel is not None or ac5options.opttype is not None:
    optlevel = ac5options.optlevel if ac5options.optlevel else '2'
    opttype = ac5options.opttype if ac5options.opttype else 'space'
    if optlevel == '0':
        output_command.append('-O0')
    elif opttype == 'space':
        if optlevel == '3':
            output_command.append('-Oz')
        else:
            output_command.append('-Os')
    else:
        output_command.append('-O'+optlevel)

if ac5options.output is not None:
    output_command.append('-o')
    output_command.append(ac5options.output)

if ac5options.signed_chars is not None:
    if ac5options.signed_chars:
        output_command.append('-fsigned-chars')
    else:
        output_command.append('-funsigned-chars')

if ac5options.show_cmdline is not None:
    output_command.append('-v')    

if ac5options.vsn is not None:
    output_command.append('--version')

output_command.extend(inputfiles)

# Handle some armcc/clang defaults differences

# The default output name for compile-and-link is different between 
# ARM Compiler 5 and 6 (__image.axf vs a.out)
if ac5options.output is None:
    if not ac5options.c:
        output_command.append('-o')
        output_command.append('__image.axf')
# ARMCC default is -O2 -Ospace, clang is -O0. 
# Use same mapping as for above i.e. -O2 -Ospace => -Os
if ac5options.optlevel is None and ac5options.opttype is None:
    output_command.append('-Os')

if ac5options.verbosity == 1:
    print ' '.join(output_command)

# Dry run - just print out cmdline for testing and exit
if ac5options.dry:
    print '\n'.join(output_command)
    sys.exit(0)
    
# Handling for --errors ARM Compiler 5 option
errs = None
if ac5options.errors:
    try:
        errs = open(ac5options.errors, 'w')
        print "output errors to %s" % ac5options.errors
    except IOError as e:
        print >>sys.stderr, "Can't open %s for output: %s" % (ac5options.errors, e.strerror)
        sys.exit(e.errno)

if errs:
    print "output errors to %s" % ac5options.errors
try:
    print "subprocess.call %s" % output_command 
    return_code = subprocess.call(output_command, stderr=errs)
    print "subprocess.call end"
except OSError as e:
    print >>sys.stderr, "Failed to call ARM Compiler 6 ('%s'): %s" % (output_command[0], e.strerror)
    sys.exit(e.errno)

if ac5options.errors:
    errs.close()

sys.exit(return_code)
