#!/usr/bin/env python
# encoding: utf-8

from __future__ import print_function
from waflib import Logs, Options, Utils
from waflib.Build import BuildContext
from waflib.Configure import conf
from waflib.TaskGen import before_method, feature
import os.path, os
from collections import OrderedDict

SOURCE_EXTS = [
    '*.S',
    '*.c',
    '*.cpp',
]

UTILITY_SOURCE_EXTS = [ 'utility/' + glob for glob in SOURCE_EXTS ]

COMMON_VEHICLE_DEPENDENT_LIBRARIES = [
    'AP_AccelCal',
    'AP_ADC',
    'AP_AHRS',
    'AP_Airspeed',
    'AP_Baro',
    'AP_BattMonitor',
    'AP_BoardConfig',
    'AP_Buffer',
    'AP_Common',
    'AP_Compass',
    'AP_Declination',
    'AP_GPS',
    'AP_HAL',
    'AP_HAL_Empty',
    'AP_InertialSensor',
    'AP_Math',
    'AP_Mission',
    'AP_NavEKF',
    'AP_NavEKF2',
    'AP_Notify',
    'AP_OpticalFlow',
    'AP_Param',
    'AP_Rally',
    'AP_RangeFinder',
    'AP_Scheduler',
    'AP_SerialManager',
    'AP_Terrain',
    'AP_Vehicle',
    'DataFlash',
    'Filter',
    'GCS_MAVLink',
    'RC_Channel',
    'StorageManager',
    'AP_Tuning',
    'AP_RPM',
    'AP_RSSI',
    'AP_Mount',
]

def _get_legacy_defines(sketch_name):
    return [
        'APM_BUILD_DIRECTORY=APM_BUILD_' + sketch_name,
        'SKETCH="' + sketch_name + '"',
        'SKETCHNAME="' + sketch_name + '"',
    ]

IGNORED_AP_LIBRARIES = [
    'doc',
    'GCS_Console',
]

@conf
def ap_get_all_libraries(bld):
    libraries = []
    for lib_node in bld.srcnode.ant_glob('libraries/*', dir=True, src=False):
        name = lib_node.name
        if name in IGNORED_AP_LIBRARIES:
            continue
        if name.startswith('AP_HAL'):
            continue
        if name == 'SITL':
            continue
        libraries.append(name)
    libraries.extend(['AP_HAL', 'AP_HAL_Empty'])
    return libraries

@conf
def ap_common_vehicle_libraries(bld):
    return COMMON_VEHICLE_DEPENDENT_LIBRARIES

_grouped_programs = {}

@conf
def ap_program(bld,
               program_groups='bin',
               program_dir=None,
               use_legacy_defines=True,
               program_name=None,
               **kw):
    if 'target' in kw:
        bld.fatal('Do not pass target for program')
    if 'defines' not in kw:
        kw['defines'] = []
    if 'source' not in kw:
        kw['source'] = bld.path.ant_glob(SOURCE_EXTS)

    if not program_name:
        program_name = bld.path.name

    if use_legacy_defines:
        kw['defines'].extend(_get_legacy_defines(bld.path.name))

    kw['cxxflags'] = kw.get('cxxflags', []) + ['-include', 'ap_config.h']
    kw['features'] = kw.get('features', []) + bld.env.AP_PROGRAM_FEATURES

    program_groups = Utils.to_list(program_groups)

    if not program_dir:
        program_dir = program_groups[0]

    name = os.path.join(program_dir, program_name)

    tg_constructor = bld.program
    if bld.env.AP_PROGRAM_AS_STLIB:
        tg_constructor = bld.stlib
    else:
        if bld.env.STATIC_LINKING:
            kw['features'].append('static_linking')


    tg = tg_constructor(
        target='#%s' % name,
        name=name,
        program_name=program_name,
        program_dir=program_dir,
        **kw
    )

    for group in program_groups:
        _grouped_programs.setdefault(group, []).append(tg)

@conf
def ap_example(bld, **kw):
    kw['program_groups'] = 'examples'
    ap_program(bld, use_legacy_defines=False, **kw)

# NOTE: Code in libraries/ is compiled multiple times. So ensure each
# compilation is independent by providing different index for each.
# The need for this should disappear when libraries change to be
# independent of vehicle type.
LAST_IDX = 0

def _get_next_idx():
    global LAST_IDX
    LAST_IDX += 1
    return LAST_IDX

def unique_list(items):
    '''remove duplicate elements from a list while maintaining ordering'''
    return list(OrderedDict.fromkeys(items))

@conf
def ap_stlib(bld, **kw):
    if 'name' not in kw:
        bld.fatal('Missing name for ap_stlib')
    if 'vehicle' not in kw:
        bld.fatal('Missing vehicle for ap_stlib')
    if 'libraries' not in kw:
        bld.fatal('Missing libraries for ap_stlib')

    sources = []
    libraries = unique_list(kw['libraries'] + bld.env.AP_LIBRARIES)

    for lib_name in libraries:
        lib_node = bld.srcnode.find_dir('libraries/' + lib_name)
        if lib_node is None:
            bld.fatal('Could not find library ' + lib_name)
        lib_sources = lib_node.ant_glob(SOURCE_EXTS + UTILITY_SOURCE_EXTS)
        sources.extend(lib_sources)

    kw['cxxflags'] = kw.get('cxxflags', []) + ['-include', 'ap_config.h']
    kw['features'] = kw.get('features', []) + bld.env.AP_STLIB_FEATURES
    kw['source'] = sources
    kw['target'] = kw['name']
    kw['defines'] = _get_legacy_defines(kw['vehicle'])
    kw['idx'] = _get_next_idx()

    bld.stlib(**kw)

_created_program_dirs = set()
@feature('cxxstlib', 'cxxprogram')
@before_method('process_rule')
def ap_create_program_dir(self):
    if not hasattr(self, 'program_dir'):
        return
    if self.program_dir in _created_program_dirs:
        return
    self.bld.bldnode.make_node(self.program_dir).mkdir()
    _created_program_dirs.add(self.program_dir)

@feature('cxxstlib')
@before_method('process_rule')
def ap_stlib_target(self):
    if self.target.startswith('#'):
        self.target = self.target[1:]
    self.target = '#%s' % os.path.join('lib', self.target)

@conf
def ap_find_tests(bld, use=[]):
    if not bld.env.HAS_GTEST:
        return

    features = []
    if bld.cmd == 'check':
        features.append('test')

    use = Utils.to_list(use)
    use.append('GTEST')

    includes = [bld.srcnode.abspath() + '/tests/']

    for f in bld.path.ant_glob(incl='*.cpp'):
        ap_program(
            bld,
            features=features,
            includes=includes,
            source=[f],
            use=use,
            program_name=f.change_ext('').name,
            program_groups='tests',
            use_legacy_defines=False,
            cxxflags=['-Wno-undef'],
        )

_versions = []

@conf
def ap_version_append_str(ctx, k, v):
    ctx.env['AP_VERSION_ITEMS'] += [(k, '"{}"'.format(os.environ.get(k, v)))]

@conf
def write_version_header(ctx, tgt):
    with open(tgt, 'w') as f:
        print('#pragma once\n', file=f)

        for k, v in ctx.env['AP_VERSION_ITEMS']:
            print('#define {} {}'.format(k, v), file=f)

@conf
def ap_find_benchmarks(bld, use=[]):
    if not bld.env.HAS_GBENCHMARK:
        return

    includes = [bld.srcnode.abspath() + '/benchmarks/']

    for f in bld.path.ant_glob(incl='*.cpp'):
        ap_program(
            bld,
            features=['gbenchmark'],
            includes=includes,
            source=[f],
            use=use,
            program_name=f.change_ext('').name,
            program_groups='benchmarks',
            use_legacy_defines=False,
        )

def test_summary(bld):
    from io import BytesIO
    import sys

    if not hasattr(bld, 'utest_results'):
        Logs.info('check: no test run')
        return

    fails = []

    for filename, exit_code, out, err in bld.utest_results:
        Logs.pprint('GREEN' if exit_code == 0 else 'YELLOW',
                    '    %s' % filename,
                    'returned %d' % exit_code)

        if exit_code != 0:
            fails.append(filename)
        elif not bld.options.check_verbose:
            continue

        if len(out):
            buf = BytesIO(out)
            for line in buf:
                print("    OUT: %s" % line.decode(), end='', file=sys.stderr)
            print()

        if len(err):
            buf = BytesIO(err)
            for line in buf:
                print("    ERR: %s" % line.decode(), end='', file=sys.stderr)
            print()

    if not fails:
        Logs.info('check: All %u tests passed!' % len(bld.utest_results))
        return

    Logs.error('check: %u of %u tests failed' %
               (len(fails), len(bld.utest_results)))

    for filename in fails:
        Logs.error('    %s' % filename)

    bld.fatal('check: some tests failed')

_build_commands = {}

def _process_build_command(bld):
    if bld.cmd not in _build_commands:
        return

    params = _build_commands[bld.cmd]

    targets = params['targets']
    if targets:
        if bld.targets:
            bld.targets += ',' + targets
        else:
            bld.targets = targets

    program_group_list = Utils.to_list(params['program_group_list'])
    bld.options.program_group.extend(program_group_list)

def build_command(name,
                   targets=None,
                   program_group_list=[],
                   doc='build shortcut'):
    _build_commands[name] = dict(
        targets=targets,
        program_group_list=program_group_list,
    )

    class context_class(BuildContext):
        cmd = name
    context_class.__doc__ = doc

def _select_programs_from_group(bld):
    groups = bld.options.program_group
    if not groups:
        if bld.targets:
            groups = []
        else:
            groups = ['bin']

    if 'all' in groups:
        groups = _grouped_programs.keys()

    for group in groups:
        if group not in _grouped_programs:
            bld.fatal('Group %s not found' % group)

        tg = _grouped_programs[group][0]
        if bld.targets:
            bld.targets += ',' + tg.name
        else:
            bld.targets = tg.name

        for tg in _grouped_programs[group][1:]:
            bld.targets += ',' + tg.name

def options(opt):
    g = opt.ap_groups['build']

    g.add_option('--program-group',
        action='append',
        default=[],
        help='''
Select all programs that go in <PROGRAM_GROUP>/ for the build. Example: `waf
--program-group examples` builds all examples. The special group "all" selects
all programs.
''')

    g.add_option('--upload',
        action='store_true',
        help='''
Upload applicable targets to a connected device. Not all platforms may support
this. Example: `waf copter --upload` means "build arducopter and upload it to
my board".
''')

    g = opt.ap_groups['check']

    g.add_option('--check-verbose',
        action='store_true',
        help='Output all test programs.')


def build(bld):
    global LAST_IDX
    # FIXME: This is done to prevent same task generators being created with
    # different idx when build() is called multiple times (e.g. waf bin tests).
    # Ideally, task generators should be created just once.
    LAST_IDX = 0
    bld.add_pre_fun(_process_build_command)
    bld.add_pre_fun(_select_programs_from_group)
