#!/usr/bin/env python

# Copyright (c) 2016 ARM Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Generate pins.js for a specified target, using target definitions from the
mbed OS source tree.

It's expecting to be run from the targets/mbedos5 directory.
"""

from __future__ import print_function

from pycparser import parse_file, c_ast, c_generator
from pycparserext.ext_c_parser import GnuCParser

from simpleeval import SimpleEval, DEFAULT_OPERATORS

import ast

import argparse
import json
import sys
import os

# import mbed tools
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'mbed-os'))
from tools.targets import Target


def find_file(root_dir, directories, name):
    """
    Find the first instance of file with name 'name' in the directory tree
    starting with 'root_dir'.

    Filter out directories that are not in directories, or do not start with
    TARGET_.

    Since this looks in (essentially )the same directories as the compiler would
    when compiling mbed OS, we should only find one PinNames.h.
    """

    for root, dirs, files in os.walk(root_dir, topdown=True):
        # modify dirs in place
        dirs[:] = filter(lambda x: x in directories or not x.startswith('TARGET_'), dirs)

        if name in files:
            return os.path.join(root, name)

def enumerate_includes(root_dir, directories):
    """
    Walk through the directory tree, starting at root_dir, and enumerate all
    valid include directories.
    """
    for root, dirs, files in os.walk(root_dir, topdown=True):
        # modify dirs in place
        dirs[:] = filter(lambda x: x in directory_labels 
                                or (    not x.startswith('TARGET_')
                                    and not x.startswith('TOOLCHAIN_')), dirs)
        yield root


class TypeDeclVisitor(c_ast.NodeVisitor):
    def __init__(self, filter_names=[]):
        self.names = filter_names

    def visit(self, node):
        value = None

        if node.__class__.__name__ == "TypeDecl":
            value = self.visit_TypeDecl(node)

        if value is None:
            for name, c in node.children():
                value = value or self.visit(c)

        return value

    def visit_TypeDecl(self, node):
        if node.declname in self.names:
            c_gen = c_generator.CGenerator()
            pins = {}

            operators = DEFAULT_OPERATORS
            operators[ast.BitOr] = lambda a, b: a | b
            operators[ast.LShift] = lambda a, b: a << b
            operators[ast.RShift] = lambda a, b: a << b
            evaluator = SimpleEval(DEFAULT_OPERATORS )

            for pin in node.type.values.enumerators:
                expr = c_gen.visit(pin.value)

                if "(int)" in expr:
                    expr = expr.replace('(int)', '')

                if expr in pins:
                    pins[pin.name] = pins[expr]
                else:
                    pins[pin.name] = evaluator.eval(expr.strip())

            return pins

def enumerate_pins(c_source_file, include_dirs, definitions):
    """
    Enumerate pins specified in PinNames.h, by looking for a PinName enum
    typedef somewhere in the file.
    """
    definitions += ['__attribute(x)__=', '__extension__(x)=', 'register=', '__IO=', 'uint32_t=unsigned int']

    gcc_args = ['-E', '-fmerge-all-constants'] 
    gcc_args += ['-I' + directory for directory in include_dirs] 

    gcc_args += ['-D' + definition for definition in definitions]
    ast = parse_file(c_source_file,
                    use_cpp=True,
                    cpp_path='arm-none-eabi-gcc',
                    cpp_args=gcc_args,
                    parser=GnuCParser())

    # now, walk the AST
    v = TypeDeclVisitor(['PinName'])
    return v.visit(ast)


if __name__ == "__main__":
    if not os.path.exists('./mbed-os'):
        print("Fatal: mbed-os directory does not exist.")
        print("Try running 'make getlibs'")
        sys.exit(1)

    description = """
    Generate pins.js for a specified mbed board, using target definitions from the
    mbed OS source tree.
    """

    parser = argparse.ArgumentParser(description=description)

    parser.add_argument('board', help='mbed board name')
    parser.add_argument('-o',
                        help='Output JavaScript file (default: %(default)s)',
                        default='js/pins.js',
                        type=argparse.FileType('w'))
    parser.add_argument('-c',
                        help='Output C++ file (default: %(default)s)',
                        default='source/pins.cpp',
                        type=argparse.FileType('w'))

    args = parser.parse_args()
    board_name = args.board.upper()

    target = Target(board_name)

    directory_labels = ['TARGET_' + label for label in target.get_labels()] + target.macros

    targets_dir = os.path.join('.', 'mbed-os', 'hal', 'targets')
    hal_dir = os.path.join(targets_dir, 'hal')

    pins_file = find_file(hal_dir, directory_labels, 'PinNames.h')


    includes = enumerate_includes(targets_dir, directory_labels)
    defines = list(directory_labels)

    # enumerate pins from PinNames.h
    pins = enumerate_pins(pins_file, ['./tools'] + list(includes), defines)

    out_file = '\r\n'.join(['var %s = %s;' % pin for pin in pins.iteritems()])
    args.o.write(out_file)

    LICENSE = '''/* Copyright 2016 ARM, Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the \"License\");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an \"AS IS\" BASIS
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * This file is generated by generate_pins.py. Please do not modify.
 */
    '''

    COUNT = '''
unsigned int jsmbed_js_magic_string_count = {};
    '''.format(len(pins))

    LENGTHS = ',\n    '.join(str(len(name)) for name in pins.iterkeys())
    LENGTHS_SOURCE = '''
unsigned int jsmbed_js_magic_string_lengths[] = {
    %s
};
    ''' % LENGTHS

    MAGIC_VALUES = ',\n    '.join(str(value) for value in pins.itervalues())
    MAGIC_SOURCE = '''
unsigned int jsmbed_js_magic_string_values[] = {
    %s
};
    ''' % MAGIC_VALUES

    MAGIC_STRINGS = ',\n    '.join('"' + name + '"' for name in pins.iterkeys())
    MAGIC_STRING_SOURCE = '''
const char * jsmbed_js_magic_strings[] = {
    %s
};
    ''' % MAGIC_STRINGS

    args.c.write(LICENSE + COUNT + LENGTHS_SOURCE + MAGIC_SOURCE + MAGIC_STRING_SOURCE)