import json
import os
import subprocess
import sys
import pexpect
import zipfile
import datetime
import time

import waflib
from waflib import Node, Logs
from waflib.Build import BuildContext


waf_dir = sys.path[0]
sys.path.append(os.path.join(waf_dir, 'tools'))
sys.path.append(os.path.join(waf_dir, 'tools/log_hashing'))
sys.path.append(os.path.join(waf_dir, 'sdk/tools/'))
sys.path.append(os.path.join(waf_dir, 'waftools'))

import waftools.asm
import waftools.gitinfo
import waftools.ldscript
import waftools.openocd
import waftools.xcode_pebble

LOGHASH_OUT_PATH = 'src/fw/loghash_dict.json'

def truncate(msg):
    if msg is None:
        return msg

    # Don't truncate exceptions thrown by waf itself
    if "Traceback " in msg:
        return msg

    truncate_length = 600
    if len(msg) > truncate_length:
        msg = msg[:truncate_length-4] + '...\n' + waflib.Logs.colors.NORMAL
    return msg


def get_comma_separated_args(option, opt, value, parser):
    setattr(parser.values, option.dest, value.split(','))


old_display = waflib.Task.TaskBase.display
def wrap_display(self):
    return truncate(old_display(self))
waflib.Task.TaskBase.display = wrap_display

old_format_error = waflib.Task.TaskBase.format_error
def wrap_format_error(self):
    return truncate(old_format_error(self))
waflib.Task.TaskBase.format_error = wrap_format_error


def run_arm_gdb(ctx, elf_node, cmd_str="", target_server_port=3333):
    from tools.gdb_driver import find_gdb_path
    arm_none_eabi_path = find_gdb_path()
    if arm_none_eabi_path is None:
        ctx.fatal("pebble-gdb not found!")
    os.system('{} {} {} --ex="target remote :{}"'.format(
                arm_none_eabi_path, elf_node.path_from(ctx.path),
                cmd_str, target_server_port)
              )


def options(opt):
    opt.load('pebble_arm_gcc', tooldir='waftools')
    opt.load('show_configure', tooldir='waftools')
    opt.recurse('applib-targets')
    opt.recurse('tests')
    opt.recurse('src/bluetooth-fw')
    opt.recurse('src/fw')
    opt.recurse('src/idl')
    opt.recurse('sdk')
    opt.add_option('--board', action='store',
                   choices=[ 'bb2',
                             'ev2_4',
                             'v1_5',
                             'v2_0',
                             'snowy_bb2',  # alias for snowy_dvt, but with #define IS_BIGBOARD
                             'snowy_evt2',
                             'snowy_dvt',
                             'snowy_s3',
                             'spalding_bb2',  # snowy_bb2 with s4 display
                             'spalding_evt',
                             'spalding',
                             'silk_evt',
                             'silk_bb',
                             'silk',
                             'silk_bb2',
                             'cutts_bb',
                             'robert_bb',
                             'robert_bb2',
                             'robert_evt',
                             'robert_es'],
                   help='Which board we are targeting '
                        'bb2, snowy_dvt, spalding, silk...')
    opt.add_option('--jtag', action='store', default=None, dest='jtag',  # default is bb2 (below)
                   choices=waftools.openocd.JTAG_OPTIONS.keys(),
                   help='Which JTAG programmer we are using '
                        '(bb2 (default), olimex, ev2, etc)')
    opt.add_option('--internal_sdk_build', action='store_true',
                   help='Build the internal version of the SDK')
    opt.add_option('--future_ux', action='store_true',
                   help='Build future UX features and APIs. Implies --internal_sdk_build.')
    opt.add_option('--nosleep', action='store_true',
                   help='Disable sleep and stop mode (to use JTAG+GDB)')
    opt.add_option('--nostop', action='store_true',
                   help='Disable stop mode (to use JTAG+GDB)')
    opt.add_option('--lowpowerdebug', action='store_true',
                   help='Lowpowerdebug can be toggled from the CLI but is off by default. This just turns it on by default')
    opt.add_option('--nowatch', action='store_true',
                   help='Disable the watchface idle timeout')
    opt.add_option('--nowatchdog', action='store_true',
                   help='Disable automatic reboots when watchdog fires')
    opt.add_option('--test_apps', action='store_true',
                   help='Enables test apps (off by default)')
    opt.add_option('--test_apps_list', action='callback', type='string', callback=get_comma_separated_args,
                   help='Specify AppInstallId\'s of the test apps to be compiled with the firmware')
    opt.add_option('--performance_tests', action='store_true',
                   help='Enables instrumentation + apps for performance testing (off by default)')
    opt.add_option('--verbose_logs', action='store_true',
                   help='Enables verbose logs (off by default)')
    opt.add_option('--ui_debug', action='store_true',
                   help='Enable window dump & layer nudge CLI cmd (off by default)')
    opt.add_option('--qemu', action='store_true',
                   help='Build an image for qemu instead of a real board.')
    opt.add_option('--nojs', action='store_true', help='Removes js support from the current build.')
    opt.add_option('--sdkshell', action='store_true',
                   help='Use the sdk shell instead of the normal shell')
    opt.add_option('--nolog', action='store_true',
                   help='Disable PBL_LOG macros to save space')
    opt.add_option('--nohash', action='store_true',
                   help='Disable log hashing and make the logs human readable')

    opt.add_option('--lang',
                   action='store',
                   default='en_US',
                   help='Which language to package (isocode)')

    opt.add_option('--compile_commands', action='store_true', help='Create a clang compile_commands.json')
    opt.add_option('--file', action='store', help='Specify a file to use with the flash_fw command')
    opt.add_option('--tty',
        help='Selects a tty to use for serial imaging. Must be specified for all image commands')
    opt.add_option('--baudrate', action='store', type=int, help='Optional: specifies the baudrate to run the targetted uart at')
    opt.add_option('--onlysdk', action='store_true', help="only build the sdk")
    opt.add_option('--qemu_host', default='localhost:12345',
        help='host:port for the emulator console connection')
    opt.add_option('--force-fit-tintin', action='store_true',
                   help='Force fit for Tintin')
    opt.add_option('--no-link', action='store_true',
                   help='Do not link the final firmware binary. This is used for static analysis')
    opt.add_option('--noprompt', action='store_true',
                   help='Disable the serial console to save space')
    opt.add_option('--build_test_apps', action='store_true',
                   help='Turns on building of test apps')
    opt.add_option('--bb_large_spi', action='store_true',
                   help='Sets a flag to use all 8MB of BigBoard flash')
    opt.add_option('--profiler', action='store_true', help='Enable the profiler.')
    opt.add_option('--profile_interrupts', action='store_true',
                   help='Enable profiling of all interrupts.')
    opt.add_option('--voice_debug', action='store_true',
                   help='Enable all voice logging.')
    opt.add_option('--voice_codec_tests', action='store_true',
                   help='Enable voice codec tests. Enables the profiler')
    opt.add_option('--battery_debug', action='store_true',
                   help='Set the PMIC\'s max charging voltage to 4.3V.')
    opt.add_option('--no_sandbox', action='store_true',
                   help='Disable the MPU for 3rd party apps.')
    opt.add_option('--malloc_instrumentation', action='store_true',
                   help='Enables malloc instrumentation')
    opt.add_option('--infinite_backlight', action='store_true',
                   help='Makes the backlight never time-out.')
    opt.add_option('--mfg', action='store_true', help='Enable specific MFG-only options in the PRF build')
    opt.add_option('--no-pulse-everywhere',
                   action='store_true',
                   help='Disables PULSE everywhere, uses legacy logs and prompt')
    opt.add_option('--bootloader-test', action='store', default='none',
                   choices=['none', 'stage1', 'stage2'],
                   help='Build bootloader test (stage1 or stage2). Implies --mfg.')
    opt.add_option('--reboot_on_bt_crash', action='store_true', help='Forces a BT '
                   'chip crash to immediately force a system reboot instead of just cycling airplane mode. '
                   'This makes it easier for us to actually get crash info')


def handle_configure_options(conf):
    if conf.options.noprompt:
        conf.env.append_value('DEFINES', 'DISABLE_PROMPT')
        conf.env.DISABLE_PROMPT = True

    if conf.options.beta or conf.options.release:
        conf.env.append_value('DEFINES', 'RELEASE')

    if conf.options.malloc_instrumentation:
        conf.env.append_value('DEFINES', 'MALLOC_INSTRUMENTATION')
        print "Enabling malloc instrumentation"

    if conf.options.qemu:
        conf.env.append_value('DEFINES', 'TARGET_QEMU')

    if conf.options.test_apps_list:
        conf.options.test_apps = True
        conf.env.test_apps_list = conf.options.test_apps_list
        print "Enabling test apps: " + str(conf.options.test_apps_list)

    if conf.options.build_test_apps or conf.options.test_apps:
        conf.env.BUILD_TEST_APPS = True

    if conf.options.performance_tests:
        conf.env.PERFORMANCE_TESTS = True

    if conf.options.voice_debug:
        conf.env.VOICE_DEBUG = True

    if conf.options.voice_codec_tests:
        conf.env.VOICE_CODEC_TESTS = True
        conf.env.append_value('DEFINES', 'VOICE_CODEC_TESTS')
        conf.options.profiler = True

    if conf.env.MICRO_FAMILY == 'STM32F4':
        if conf.options.lowpowerdebug and not conf.options.nosleep:
            Logs.warn('On snowy --lowpowerdebug can only be used with --nosleep. Forcing --nosleep on!\n'
                      'See PBL-10174.')
            conf.env.append_value('DEFINES', 'PBL_NOSLEEP')

    if 'bb' in conf.options.board:
        conf.env.append_value('DEFINES', 'IS_BIGBOARD')

    if conf.options.nosleep:
        conf.env.append_value('DEFINES', 'PBL_NOSLEEP')
        print "Sleep/stop mode disabled"

    if conf.options.nostop:
        conf.env.append_value('DEFINES', 'PBL_NOSTOP')
        print "Stop mode disabled"

    if conf.options.lowpowerdebug:
        conf.env.append_value('DEFINES', 'LOW_POWER_DEBUG')
        print "Sleep and Stop mode debugging enabled"

    if conf.options.nowatch:
        conf.env.append_value('DEFINES', 'NO_WATCH_TIMEOUT')
        print "Watch watchdog disabled"

    if conf.options.nowatchdog:
        conf.env.append_value('DEFINES', 'NO_WATCHDOG')
        conf.env.NO_WATCHDOG = True
        print "Watchdog reboot disabled"

    if conf.options.reboot_on_bt_crash:
        conf.env.append_value('DEFINES', 'REBOOT_ON_BT_CRASH=1')
        print "BT now crash will trigger an MCU reboot"

    if conf.options.test_apps:
        conf.env.append_value('DEFINES', 'ENABLE_TEST_APPS')
        print "Im in ur firmware, bloatin ur binz! (Test apps enabled)"

    if conf.options.performance_tests:
        conf.env.append_value('DEFINES', 'PERFORMANCE_TESTS')
        conf.options.profiler = True
        print "Instrumentation and apps for performance measurement enabled (enables profiler)"

    if conf.options.verbose_logs:
        conf.env.append_value('DEFINES', 'VERBOSE_LOGGING')
        print "Verbose logging enabled"

    if conf.options.ui_debug:
        conf.env.append_value('DEFINES', 'UI_DEBUG')

    if conf.options.no_sandbox or conf.options.qemu:
        print "Sandbox disabled"
    else:
        conf.env.append_value('DEFINES', 'APP_SANDBOX')

    if conf.options.bb_large_spi:
        conf.env.append_value('DEFINES', 'LARGE_SPI_FLASH')
        print "Enabling 8MB BigBoard flash"

    if not conf.options.nolog:
        conf.env.append_value('DEFINES', 'PBL_LOG_ENABLED')
        if not conf.options.nohash:
            conf.env.append_value('DEFINES', 'PBL_LOGS_HASHED')

    if conf.options.profile_interrupts:
        conf.env.append_value('DEFINES', 'PROFILE_INTERRUPTS')
        if not conf.options.profiler:
            # Can't profile interrupts without the profiler enabled
            print "Enabling profiler"
            conf.options.profiler = True

    if conf.options.profiler:
        conf.env.append_value('DEFINES', 'PROFILER')
        if not conf.options.nostop:
            print "Enable --nostop for accurate profiling."
            conf.env.append_value('DEFINES', 'PBL_NOSTOP')

    if conf.options.voice_debug:
        conf.env.append_value('DEFINES', 'VOICE_DEBUG')

    if conf.options.battery_debug:
        conf.env.append_value('DEFINES', 'BATTERY_DEBUG')
        print "Enabling higher battery charge voltage."

    if conf.options.future_ux and not conf.is_tintin():
        print "Future UX features enabled."
        conf.env.FUTURE_UX = True

    conf.env.INTERNAL_SDK_BUILD = bool(conf.options.internal_sdk_build)
    if conf.env.INTERNAL_SDK_BUILD:
        print "Internal SDK enabled"

    if conf.options.force_fit_tintin:
        conf.env.append_value('DEFINES', 'TINTIN_FORCE_FIT')
        print "Functionality is secondary to usability"

    if (conf.is_snowy_compatible() and not conf.options.no_lto) or conf.options.lto:
        conf.options.lto = True
        print "Turning on LTO."

    if conf.options.no_link:
        conf.env.NO_LINK = True
        print "Not linking firmware"

    if conf.options.infinite_backlight and 'bb' in conf.options.board:
        conf.env.append_value('DEFINES', 'INFINITE_BACKLIGHT')
        print "Enabling infinite backlight."

    if conf.options.bootloader_test in ['stage1', 'stage2']:
        print "Forcing MFG on for bootloader test build."
        conf.options.mfg = True

    if conf.options.bootloader_test == 'stage1':
        conf.env.append_value('DEFINES', 'BOOTLOADER_TEST_STAGE1=1')
        conf.env.append_value('DEFINES', 'BOOTLOADER_TEST_STAGE2=0')
    elif conf.options.bootloader_test == 'stage2':
        conf.env.append_value('DEFINES', 'BOOTLOADER_TEST_STAGE1=0')
        conf.env.append_value('DEFINES', 'BOOTLOADER_TEST_STAGE2=1')
    else:
        conf.env.append_value('DEFINES', 'BOOTLOADER_TEST_STAGE1=0')
        conf.env.append_value('DEFINES', 'BOOTLOADER_TEST_STAGE2=0')

    if not conf.options.mfg and not conf.options.no_pulse_everywhere:
        conf.env.append_value('DEFINES', 'PULSE_EVERYWHERE=1')

def _create_cm0_env(conf):
    prev_env = conf.env
    prev_variant = conf.variant

    # Create a new Cortex M0 environment that's used to build for the DA14681:
    conf.setenv('cortex-m0')

    # Copy the defines fron the stock env into our m0 env
    conf.env.append_unique('DEFINES', prev_env.DEFINES)

    Logs.pprint('CYAN', 'Configuring ARM cortex-m0 environment')

    conf.env.append_unique('DEFINES', 'ARCH_NO_NATIVE_LONG_DIVIDE')

    CPU_FLAGS = ['-mcpu=cortex-m0', '-mthumb']
    OPT_FLAGS = [
        '-fvar-tracking-assignments',  # Track variable locations better
        '-fmessage-length=0', '-fsigned-char',
        '-fbuiltin',
        '-fno-builtin-itoa',
        '-ffreestanding',
        '-Os',
    ]
    if not conf.options.no_debug:
        OPT_FLAGS += [
            '-g3',
            '-gdwarf-4',  # More detailed debug info
        ]

    C_FLAGS = ['-std=c11', '-ffunction-sections',
               '-Wall', '-Wextra', '-Werror', '-Wpointer-arith',
               '-Wno-unused-parameter', '-Wno-missing-field-initializers',
               '-Wno-error=unused-parameter',
               '-Wno-packed-bitfield-compat']

    conf.find_program('arm-none-eabi-gcc', var='CC', mandatory=True)
    conf.env.AS = conf.env.CC
    for tool in ['ar', 'objcopy']:
        conf.find_program('arm-none-eabi-' + tool, var=tool.upper(),
                          mandatory=True)

    conf.env.append_unique('CFLAGS', CPU_FLAGS + OPT_FLAGS + C_FLAGS)

    ASFLAGS = ['-x', 'assembler-with-cpp']
    conf.env.append_unique('ASFLAGS', ASFLAGS + CPU_FLAGS + OPT_FLAGS)

    conf.env.append_unique('LINKFLAGS',
                           ['-Wl,--cref',
                            '-Wl,--gc-sections',
                            '-nostdlib',
                            ] + CPU_FLAGS + OPT_FLAGS)

    conf.load('gcc gas objcopy ldscript')
    conf.load('file_name_c_define')

    conf.variant = prev_variant
    conf.env = prev_env


def configure(conf):
    if not conf.options.board:
        conf.fatal('No board selected! '
                   'You must pass a --board argument when configuring.')

    # Has to be 'waftools.gettext' as unadorned 'gettext' will find the gettext
    # module in the standard library.
    conf.load('waftools.gettext')

    conf.recurse('platform')

    conf.env.QEMU = conf.options.qemu
    conf.env.NOJS = conf.options.nojs

    # The BT controller is the only thing different between robert_es and robert_evt, so just
    # retend robert_es is robert_evt. We'll be removing robert_es fairly soon anyways.
    bt_board = None
    if conf.options.board == 'robert_es':
        bt_board = 'robert_es'
        conf.options.board = 'robert_evt'

    if conf.options.jtag:
        conf.env.JTAG = conf.options.jtag
    elif conf.options.board in ('snowy_bb2', 'spalding_bb2'):
        conf.env.JTAG = 'jtag_ftdi'
    elif conf.options.board in ('cutts_bb', 'robert_bb', 'robert_bb2', 'robert_evt',
                                'silk_evt', 'silk_bb', 'silk_bb2', 'silk'):
        conf.env.JTAG = 'swd_ftdi'
    else:
        # default to bb2
        conf.env.JTAG = 'bb2'

    # Cutts and Robert access flash through the ITCM bus (except in QEMU)
    if (conf.is_cutts() or conf.is_robert()) and not conf.env.QEMU:
        conf.env.FLASH_ITCM = True
    else:
        conf.env.FLASH_ITCM = False

    # Set platform used for building the SDK
    if conf.is_tintin():
        conf.env.PLATFORM_NAME = 'aplite'
        conf.env.MIN_SDK_VERSION = 2
    elif conf.is_spalding():
        conf.env.PLATFORM_NAME = 'chalk'
        conf.env.MIN_SDK_VERSION = 3
    elif conf.is_snowy_compatible():
        conf.env.PLATFORM_NAME = 'basalt'
        conf.env.MIN_SDK_VERSION = 2
    elif conf.is_silk():
        conf.env.PLATFORM_NAME = 'diorite'
        conf.env.MIN_SDK_VERSION = 2
    elif conf.is_cutts() or conf.is_robert():
        conf.env.PLATFORM_NAME = 'emery'
        conf.env.MIN_SDK_VERSION = 3
    else:
        conf.fatal('No platform specified for {}!'.format(conf.options.board))

    # Save this for later
    conf.env.BOARD = conf.options.board

    if conf.is_tintin():
        conf.env.MICRO_FAMILY = 'STM32F2'
    elif conf.is_snowy_compatible() or conf.is_silk():
        conf.env.MICRO_FAMILY = 'STM32F4'
    elif conf.is_cutts() or conf.is_robert():
        conf.env.MICRO_FAMILY = 'STM32F7'
    else:
        conf.fatal('No micro family specified for {}!'.format(conf.options.board))

    if conf.options.mfg:
        # Note that for the most part PRF and MFG firmwares are the same, so for MFG PRF builds
        # both MANUFACTURING_FW and RECOVERY_FW will be defined.
        conf.env.IS_MFG = True
        conf.env.append_value('DEFINES', ['MANUFACTURING_FW'])

    conf.find_program('node nodejs', var='NODE',
                      errmsg="Unable to locate the Node command. "
                             "Please check your Node installation and try again.")

    conf.recurse('src/idl')
    conf.recurse('src/fw')
    conf.recurse('sdk')

    conf.recurse('bin/boot')
    waftools.openocd.write_cfg(conf)

    # Save a baseline environment that we'll use for unit tests
    # Detach so operations against conf.env don't affect unit_test_env
    unit_test_env = conf.env.derive()
    unit_test_env.detach()

    # Save a baseline environment that we'll use for ARM environments
    base_env = conf.env

    handle_configure_options(conf)


    # robert_es is the exact same as robert_evt, except for the BT chip, so gets converted to
    # robert_evt above, but we need to handle it as robert_es here.
    if bt_board is None:
        bt_board = conf.get_board()
    # Select BT controller based on configuration:
    if conf.env.QEMU:
        conf.env.bt_controller = 'qemu'
        conf.env.append_value('DEFINES', ['BT_CONTROLLER_QEMU'])
    elif conf.is_tintin() or conf.is_snowy() or conf.is_spalding():
        conf.env.bt_controller = 'cc2564x'
        conf.env.append_value('DEFINES', ['BT_CONTROLLER_CC2564X'])
    elif bt_board in ('silk_bb2', 'silk', 'robert_bb2', 'robert_evt'):
        conf.env.bt_controller = 'da14681-01'
        conf.env.append_value('DEFINES', ['BT_CONTROLLER_DA14681'])
    else:
        conf.env.bt_controller = 'da14681-00'
        conf.env.append_value('DEFINES', ['BT_CONTROLLER_DA14681'])

    _create_cm0_env(conf)

    conf.recurse('src/bluetooth-fw')

    Logs.pprint('CYAN', 'Configuring arm_firmware environment')
    conf.setenv('', base_env)
    conf.load('pebble_arm_gcc', tooldir='waftools')

    conf.setenv('arm_prf_mode', env=conf.env)
    conf.env.append_value('DEFINES', ['RECOVERY_FW'])

    Logs.pprint('CYAN', 'Configuring unit test environment')
    conf.setenv('local', unit_test_env)

    if sys.platform.startswith('linux'):
        libclang_path = subprocess.check_output(['llvm-config', '--libdir']).strip()
        conf.env.append_value('INCLUDES', [os.path.join(libclang_path, 'clang/3.2/include/'),])

    # The waf clang tool likes to use llvm-ar as it's ar tool, but that doesn't work on our build
    # servers. Fall back to boring old ar. This will populate the 'AR' env variable so future
    # searches for what value to put into env['AR'] will find this one.
    conf.find_program('ar')

    conf.load('clang')
    conf.load('pebble_test', tooldir='waftools')

    conf.env.CLAR_DIR = conf.path.make_node('tools/clar/').abspath()
    conf.env.CFLAGS = [ '-std=c11',

                        '-Wall',
                        '-Werror',
                        '-Wno-error=unused-variable',
                        '-Wno-error=unused-function',
                        '-Wno-error=missing-braces',

                        '-g3',
                        '-gdwarf-4',
                        '-O0',
                        '-fdata-sections',
                        '-ffunction-sections' ]

    conf.env.append_value('DEFINES', 'CLAR_FIXTURE_PATH="' +
                                     conf.path.make_node('tests/fixtures/').abspath() + '"')

    conf.env.append_value('DEFINES', 'PBL_LOG_ENABLED')

    if conf.options.compile_commands:
        conf.load('clang_compilation_database', tooldir='waftools')

        if not os.path.lexists('compile_commands.json'):
            filename = 'compile_commands.json'
            source = conf.path.get_bld().make_node(filename)
            os.symlink(source.path_from(conf.path), filename)

    prev_env = conf.env
    Logs.pprint('CYAN', 'Configuring 32 bit host environment')
    # Copy 'local' to serve as the basis for '32bit':
    env_32bit = conf.env.derive().detach()
    env_32bit.append_value('CFLAGS', '-m32')
    env_32bit.append_value('LINKFLAGS', '-m32')
    env_32bit.LINK_CC = 'gcc'
    conf.all_envs['32bit'] = env_32bit
    conf.set_env(prev_env)

    # Note: this will modify the 'local' conf when targeting emscripten:
    conf.recurse('applib-targets')

    Logs.pprint('CYAN', 'Configuring stored apps environment')
    conf.setenv('stored_apps', base_env)
    conf.recurse('stored_apps')

    # Confirm that requirements-*.txt and requirements-osx-brew.txt have been satisfied.
    import tool_check
    tool_check.tool_check()

    # Warn user not to use Cutts BB build with a Robert screen
    if conf.options.board == 'cutts_bb':
        Logs.warn('NOTE: Do not use this build with a C2/Robert display '
                  '(6V6 rail will damage the display)')


def _run_remote_suite(ctx, suite):
    # PEBBLESDK_TEST_ROOT must be defined in order to initiate integration tests
    try:
        pebblesdk_test_root = os.environ['PEBBLESDK_TEST_ROOT']
    except KeyError:
        waflib.Logs.pprint('RED', 'Error: environment variable $PEBBLESDK_TEST_ROOT must be defined')
        return

    # Check if firmware has been built
    # Assume we're looking for a "normal" PBZ, as recovery PBZs aren't supported by integration tests
    fw_bin_path = ctx.get_tintin_fw_node().abspath()
    fw_bin_exists = os.path.isfile(fw_bin_path)

    if not fw_bin_exists:
        waflib.Logs.pprint('RED', ('Error: BIN not found at expected location {}, '
                                   'have you run `waf build` yet?'.format(fw_bin_path)))
        return

    # Check if firmware has been bundled
    version_string, version_ts, _ = _get_version_info(ctx)
    fw_type = 'qemu' if ctx.env.QEMU else 'normal'
    fw_pbz_path = ctx.get_pbz_node(fw_type, ctx.env.BOARD, version_string).abspath()
    fw_pbz_exists = os.path.isfile(fw_pbz_path)

    if not fw_pbz_exists:
        waflib.Logs.pprint('CYAN', ('Warning: PBZ not found at expected location {}, '
                                    'running `waf bundle`...').format(fw_pbz_path))

    bundle(ctx)

    # Run power tests using remote_runner.py
    remote_runner_path = os.path.join(pebblesdk_test_root, 'remote_runner.py')
    if not os.path.isfile(remote_runner_path):
        waflib.Logs.pprint('RED', ('Error: remote_runner.py not found in {}. '
                                   'Are you sure that PEBBLESDK_TEST_ROOT is defined correctly?'
                                   .format(pebblesdk_test_root)))
        return
    subprocess.call([remote_runner_path, '--pbz', fw_pbz_path, '[%s]' % suite])


class power_test(BuildContext):
    cmd = 'power_test'

    def execute_build(ctx):
        _run_remote_suite(ctx, 'power')


class integration_test(BuildContext):
    cmd = 'integration_test'

    def execute_build(ctx):
        _run_remote_suite(ctx, 'tintin_3x')


def stop_build_timer(ctx):
    t = datetime.datetime.utcnow() - ctx.pbl_build_start_time
    node = ctx.path.get_bld().make_node('build_time')
    with open(node.abspath(), 'w') as fout:
        fout.write(str(int(round(t.total_seconds()))))


def build(bld):
    bld.DYNAMIC_RESOURCES = []
    bld.LOGHASH_DICTS = []

    # Start this timer here to include the time to generate tasks.
    bld.pbl_build_start_time = datetime.datetime.utcnow()
    bld.add_post_fun(stop_build_timer)

    if bld.variant in ('test', 'test_rocky_emx', 'applib'):
        bld.set_env(bld.all_envs['local'])

    bld.load('file_name_c_define', tooldir='waftools')

    bld.recurse('platform')
    bld.recurse('src/idl')

    if bld.cmd == 'install':
        raise Exception("install isn't a supported command. Did you mean flash?")

    if bld.variant == 'pdc2png':
        bld.recurse('src/libutil')
        bld.recurse('tools')
        return

    if bld.variant == 'tools':
        bld.recurse('tools')
        return

    if bld.variant in ('', 'applib', 'prf'):
        # Dependency for SDK
        bld.recurse('src/fw/vendor/jerryscript')

    if bld.variant == '':
        # sdk generation
        bld.recurse('sdk')

    if bld.variant == 'applib':
        bld.recurse('resources')
        bld.recurse('src/libutil')
        bld.recurse('src/fw')
        bld.recurse('src/fw/vendor/nanopb')
        bld.recurse('src/include')
        bld.recurse('applib-targets')
        return

    if bld.options.onlysdk:
        # stop here, sdk generation is done
        return

    # Do not enable stationary mode in PRF or release firmware
    if (bld.variant != 'prf' and not bld.env.QEMU and bld.env.NORMAL_SHELL != 'sdk'):
        bld.env.append_value('DEFINES', 'STATIONARY_MODE')

    if bld.variant == 'prf':
        bld.set_env(bld.all_envs['arm_prf_mode'])
    elif bld.variant == 'test':
        if bld.env.APPLIB_TARGET == 'emscripten':
            bld.fatal('Did you mean ./waf test_rocky_emx ?')
        bld.recurse('src/include')
        bld.recurse('src/fw/vendor/jerryscript')
        bld.recurse('src/fw/vendor/nanopb')
        bld.recurse('src/libbtutil')
        bld.recurse('src/libos')
        bld.recurse('src/libutil')
        bld.recurse('tools')
        bld.recurse('tests')
        return
    elif bld.variant == 'test_rocky_emx':
        if bld.env.APPLIB_TARGET != 'emscripten':
            bld.fatal('Make sure to ./waf configure with --target=emscripten')
        bld.recurse('src/libutil')
        bld.recurse('src/libos')
        bld.recurse('src/fw/vendor/jerryscript')
        bld.recurse('src/fw/vendor/nanopb')
        bld.recurse('applib-targets')
        bld.recurse('tools')
        bld.recurse('tests')
        return

    if bld.variant == '':
        bld.recurse('stored_apps')

    bld.recurse('src/include')
    bld.recurse('src/libbtutil')
    bld.recurse('src/bluetooth-fw')
    bld.recurse('src/libc')
    bld.recurse('src/libos')
    bld.recurse('src/libutil')
    bld.recurse('src/fw')

    bld.recurse('tools/qemu_spi_cooker')

    # Generate resources. Leave this until the end so we collect all the env['DYNAMIC_RESOURCES']
    # values that the other build steps added.
    bld.recurse('resources')

    # if we're not linking the firmware don't run these
    if not bld.env.NO_LINK:
        bld.add_post_fun(size_fw)
        bld.add_post_fun(size_resources)
        if 'PBL_LOGS_HASHED' in bld.env.DEFINES:
            bld.add_post_fun(merge_loghash_dicts)


class build_prf(BuildContext):
    """executes the recovery firmware build"""
    cmd = 'build_prf'
    variant = 'prf'


class build_applib(BuildContext):
    cmd = 'build_applib'
    variant = 'applib'


def merge_loghash_dicts(bld):
    loghash_dict = bld.path.get_bld().make_node(LOGHASH_OUT_PATH)

    import log_hashing.newlogging
    log_hashing.newlogging.merge_loghash_dict_json_files(loghash_dict, bld.LOGHASH_DICTS)


class SizeFirmware(BuildContext):
    cmd = 'size_fw'
    fun = 'size_fw'

def size_fw(ctx):
    """prints size information of the firmware"""

    fw_elf = ctx.get_tintin_fw_node().change_ext('.elf')
    if fw_elf is None:
        ctx.fatal('No fw ELF found for size')

    fw_bin = ctx.get_tintin_fw_node()
    if fw_bin is None:
        ctx.fatal('No fw BIN found for size')

    import binutils
    text, data, bss = binutils.size(fw_elf.abspath())
    total = text + data
    output = ('{:>7}    {:>7}    {:>7}    {:>7}    {:>7} filename\n'
              '{:7}    {:7}    {:7}    {:7}    {:7x} tintin_fw.elf'.
              format('text', 'data', 'bss', 'dec', 'hex', text, data, bss, total, total))
    Logs.pprint('YELLOW', '\n' + output)

    try:
        space_left = _check_firmware_image_size(ctx, fw_bin.path_from(ctx.path))
    except FirmwareTooLargeException as e:
        if ctx.env.MICRO_FAMILY == 'STM32F2' and ctx.env.QEMU:
            # Let us off with a warning for now
            Logs.warn(str(e))
        else:
            ctx.fatal(str(e))
    else:
        Logs.pprint('CYAN', 'FW: ' + space_left)


class SizeResources(BuildContext):
    cmd = 'size_resources'
    fun = 'size_resources'


def size_resources(ctx):
    """prints size information of resources"""

    if ctx.variant == 'prf':
        return

    pbpack_path = ctx.path.get_bld().find_node('system_resources.pbpack')
    if pbpack_path is None:
        ctx.fatal('No resource pbpack found')

    if ctx.env.MICRO_FAMILY == 'STM32F4':
        max_size = 512 * 1024
    elif ctx.env.MICRO_FAMILY == 'STM32F7':
        max_size = 1024 * 1024
    else:
        max_size = 256 * 1024

    pbpack_actual_size = os.path.getsize(pbpack_path.path_from(ctx.path))
    bytes_free = max_size - pbpack_actual_size

    from waflib import Logs
    Logs.pprint('CYAN', 'Resources: %d/%d (%d free)\n' % (pbpack_actual_size, max_size, bytes_free))

    if pbpack_actual_size > max_size:
        ctx.fatal('Resources are too large for target board %d > %d'
                  % (pbpack_actual_size, max_size))


def size(ctx):
    from waflib import Options
    Options.commands = ['size_fw', 'size_resources'] + Options.commands


class size_prf(BuildContext):
    """checks the size of PRF"""
    cmd = 'size_prf'
    variant = 'prf'


class test(BuildContext):
    """builds and runs the tests"""
    cmd = 'test'
    variant = 'test'


class test_rocky_emx(BuildContext):
    """builds and runs the tests"""
    cmd = 'test_rocky_emx'
    variant = 'test_rocky_emx'


def docs(ctx):
    """builds the documentation out to build/doxygen"""
    ctx.exec_command('doxygen Doxyfile', stdout=None, stderr=None)


class DocsSdk(BuildContext):
    """builds the sdk documentation out to build/sdk/<platformname>/doxygen_sdk"""
    cmd = 'docs_sdk'
    fun = 'docs_sdk'


def docs_sdk(ctx):
    pebble_sdk = ctx.path.get_bld().make_node('sdk')
    supported_platforms = pebble_sdk.listdir()

    for platform in supported_platforms:
        doxyfile = pebble_sdk.find_node(platform).find_node('Doxyfile-SDK.auto')
        if doxyfile:
            ctx.exec_command('doxygen {}'.format(doxyfile.path_from(ctx.path)),
                             stdout=None, stderr=None)


def docs_all(ctx):
    """builds the documentation with all dependency graphs out to build/doxygen"""
    ctx.exec_command('doxygen Doxyfile-all-graphs', stdout=None, stderr=None)

# Bundle commands
#################################################


def _get_version_info(ctx):
    # FIXME: it's probably a better idea to lift board + version info from the .bin file... this can get out of sync!
    git_revision = waftools.gitinfo.get_git_revision(ctx)
    if git_revision['TAG'] != '?':
        version_string = git_revision['TAG']
        version_ts = int(git_revision['TIMESTAMP'])
        version_commit = git_revision['COMMIT']
    else:
        version_string = 'dev'
        version_ts = 0
        version_commit = ''
    return version_string, version_ts, version_commit


def _make_bundle(ctx, fw_bin_path, fw_type='normal', board=None, resource_path=None, write=True):
    import mkbundle

    if board is None:
        board = ctx.env.BOARD

    b = mkbundle.PebbleBundle()

    version_string, version_ts, version_commit = _get_version_info(ctx)
    out_file = ctx.get_pbz_node(fw_type, ctx.env.BOARD, version_string).path_from(ctx.path)

    try:
        _check_firmware_image_size(ctx, fw_bin_path)
        b.add_firmware(fw_bin_path, fw_type, version_ts, version_commit, board, version_string)
    except FirmwareTooLargeException as e:
        ctx.fatal(str(e))
    except mkbundle.MissingFileException as e:
        ctx.fatal('Error: Missing file ' + e.filename + ', have you run ./waf build yet?')

    if resource_path is not None:
        b.add_resources(resource_path, version_ts)
    if 'RELEASE' not in ctx.env.DEFINES and 'PBL_LOGS_HASHED' in ctx.env.DEFINES:
        loghash_dict = ctx.path.get_bld().make_node(LOGHASH_OUT_PATH).abspath()
        b.add_loghash(loghash_dict)

    # Add a LICENSE.txt file
    b.add_license('LICENSE.txt')

    # make sure ctx.capability is available
    ctx.recurse('platform', mandatory=False)

    if ctx.capability('HAS_JAVASCRIPT'):
        js_tooling = ctx.path.get_bld().find_node('src/fw/vendor/jerryscript/js_tooling/js_tooling.js')
        if js_tooling is not None:
            b.add_jstooling(js_tooling.path_from(ctx.path), ctx.capability('JAVASCRIPT_BYTECODE_VERSION'))

    if fw_type == 'normal':
        layouts_node = ctx.path.get_bld().find_node('resources/layouts.json.auto')
        if layouts_node is not None:
            b.add_layouts(layouts_node.path_from(ctx.path))

    if write:
        b.write(out_file)
        waflib.Logs.pprint('CYAN', 'Writing bundle to: %s' % out_file)

    return b


class BundleCommand(BuildContext):
    cmd = 'bundle'
    fun = 'bundle'


def bundle(ctx):
    """bundles a firmware"""

    if ctx.env.QEMU:
        bundle_qemu(ctx)
    else:
        _make_bundle(ctx, ctx.get_tintin_fw_node().path_from(ctx.path),
                     resource_path=ctx.get_pbpack_node().path_from(ctx.path))
        _generate_release_notes(ctx)


def _generate_release_notes(ctx):
    def _write_tag_to_release_notes(task):
        output = task.outputs[0].abspath()
        tag = task.generator.version_tag
        with open(output, 'w') as f:
            f.write(tag)
        task.dep_vars = tag

    git_revision = waftools.gitinfo.get_git_revision(ctx)
    version = "{}.{}".format(git_revision['MAJOR_VERSION'], git_revision['MINOR_VERSION'])
    version_hotfix = "{}.{}".format(version, git_revision['PATCH_VERSION'])
    summary_node = ctx.path.find_node('release-notes').find_node('summary-{}.txt'.format(version_hotfix))
    if summary_node is None:
        summary_node = ctx.path.find_node('release-notes').find_node('summary-{}.txt'.format(version))
    if summary_node is not None:
        ctx(rule='cp ${SRC} ${TGT}',
            source=summary_node,
            target=ctx.path.get_bld().make_node('release-notes.txt'))
    else:
        ctx(rule=_write_tag_to_release_notes,
            version_tag=git_revision['TAG'],
            target=ctx.path.get_bld().make_node('release-notes.txt'))


class bundle_prf(BuildContext):
    """bundles a recovery firmware"""
    cmd = 'bundle_prf'
    variant = 'prf'

    def execute_build(ctx):
        _make_bundle(ctx, ctx.get_tintin_fw_node().path_from(ctx.path), fw_type='recovery')


def _bundle_resourceless_fw(ctx, fw_path, fw_type):
    # We need to create a dummy pbpack and bundle it in. Some FW images don't use
    # resources, but the firmware will refuse to upgrade to a firmware if a resource
    # file isn't sent over, regardless of it's validity.
    import tempfile

    # We need to actually write some content in here or else the phone app won't think the
    # resources are valid, and put_bytes will refuse to update to any firmware image if it doesn't
    # come with a corresponding resource pack. No one will ever read these though so who cares
    # what the content is.
    with tempfile.NamedTemporaryFile(delete=False) as dummy_pbpack:
        dummy_pbpack.write('DUMMY')
        pbpack_path = dummy_pbpack.name

    try:
        _make_bundle(ctx, fw_path, fw_type=fw_type, resource_path=pbpack_path)
    finally:
        os.remove(pbpack_path)


class bundle_recovery(BuildContext):
    """bundles a recovery firmware as normal firmware"""
    cmd = 'bundle_recovery'
    variant = 'prf'

    def execute_build(ctx):
        _bundle_resourceless_fw(ctx, ctx.get_tintin_fw_node().path_from(ctx.path),
                                fw_type='recovery')


class BundleQEMUCommand(BuildContext):
    cmd = 'bundle_qemu'
    fun = 'bundle_qemu'


def bundle_qemu(ctx):
    """bundle QEMU images together into a "fake" PBZ"""

    qemu_image_micro(ctx)
    qemu_image_spi(ctx)

    b = _make_bundle(ctx, ctx.get_tintin_fw_node().path_from(ctx.path),
                     resource_path=ctx.get_pbpack_node().path_from(ctx.path),
                     write=False, board='qemu_{}'.format(ctx.env.BOARD))

    version_string, _, _ = _get_version_info(ctx)
    qemu_pbz = ctx.get_pbz_node('qemu', ctx.env.BOARD, version_string)
    out_file = qemu_pbz.path_from(ctx.path)

    with zipfile.ZipFile(out_file, 'w', compression=zipfile.ZIP_DEFLATED) as pbz_file:
        pbz_file.writestr('manifest.json', json.dumps(b.bundle_manifest))

        files = [ctx.get_tintin_fw_node(),
                 ctx.get_pbpack_node(),
                 'qemu_micro_flash.bin',
                 'qemu_spi_flash.bin']
        if 'PBL_LOGS_HASHED' in ctx.env.DEFINES:
            files.append(LOGHASH_OUT_PATH)

        for fitem in files:
            if isinstance(fitem, Node.Node):
                fnode = fitem
            else:
                fnode = ctx.path.get_bld().make_node(fitem)
            img_path = fnode.path_from(ctx.path)
            pbz_file.write(img_path, os.path.basename(img_path))

    waflib.Logs.pprint('CYAN', 'Writing bundle to: %s' % out_file)

class QemuImageMicroCommand(BuildContext):
    cmd = 'qemu_image_micro'
    fun = 'qemu_image_micro'


class QemuImageMicroPrfCommand(BuildContext):
    cmd = 'qemu_image_prf_micro'
    fun = 'qemu_image_prf_micro'


class QemuImageSpiCommand(BuildContext):
    cmd = 'qemu_image_spi'
    fun = 'qemu_image_spi'


class MfgImageSpiCommand(BuildContext):
    cmd = 'mfg_image_spi'
    fun = 'mfg_image_spi'


def qemu_image_micro(ctx):
    fw_hex = ctx.get_tintin_fw_node().change_ext('.hex')
    _create_qemu_image_micro(ctx, fw_hex.path_from(ctx.path))


def qemu_image_prf_micro(ctx):
    fw_hex = ctx.get_tintin_fw_node_prf().change_ext('.hex')
    _create_qemu_image_micro(ctx, fw_hex.path_from(ctx.path))


def _create_qemu_image_micro(ctx, path_to_firmware_hex):
    """creates the micro-flash image for qemu"""
    from intelhex import IntelHex

    if not ctx.env.BOOTLOADER_HEX:
        ctx.fatal('Board "{}" does not have a bootloader binary available'
                  .format(ctx.env.BOARD))

    micro_flash_node = ctx.path.get_bld().make_node('qemu_micro_flash.bin')
    micro_flash_path = micro_flash_node.path_from(ctx.path)
    waflib.Logs.pprint('CYAN', 'Writing micro flash image to {}'.format(micro_flash_path))

    img = IntelHex(ctx.env.BOOTLOADER_HEX)
    img.merge(IntelHex(path_to_firmware_hex), overlap='replace')

    # Write firwmare image and pad up to next 512 byte multiple. This is because QEMU
    # assumes all block devices are multiples of 512 byte sectors
    img.padding = 0xff
    flash_end = ((img.maxaddr() + 511) // 512) * 512
    img.tobinfile(micro_flash_path, start=0x08000000, end=flash_end-1)


def _create_spi_flash_image(ctx, name):
    spi_flash_node = ctx.path.get_bld().make_node(name)
    spi_flash_path = spi_flash_node.path_from(ctx.path)
    waflib.Logs.pprint('CYAN', 'Writing SPI flash image to {}'.format(spi_flash_path))
    return spi_flash_path

def qemu_image_spi(ctx):
    """creates a SPI flash image for qemu"""
    if ctx.env.BOARD.startswith('silk'):
        resources_begin = 0x100000
        image_size = 0x800000
    elif ctx.env.BOARD.startswith('robert') or ctx.env.BOARD.startswith('cutts'):
        resources_begin = 0x200000
        image_size = 0x1000000
    elif ctx.env.MICRO_FAMILY == 'STM32F4':
        resources_begin = 0x380000
        image_size = 0x1000000
    else:
        resources_begin = 0x280000
        image_size = 0x400000

    spi_flash_path = _create_spi_flash_image(ctx, 'qemu_spi_flash.bin')
    with open(spi_flash_path, 'wb') as qemu_spi_img_file:
        # Pad the first section before system resources with FF's'
        qemu_spi_img_file.write("\xff" * resources_begin)

        # Write system resources:
        pbpack = ctx.get_pbpack_node()
        res_img = open(pbpack.path_from(ctx.path), 'rb').read()
        qemu_spi_img_file.write(res_img)

        # Pad with 0xFF up to image size
        tail_padding_size = image_size - resources_begin - len(res_img)
        qemu_spi_img_file.write("\xff" * tail_padding_size)

    with open(os.devnull, 'w') as null:
        qemu_spi_cooker_node = ctx.path.get_bld().make_node('qemu_spi_cooker')
        qemu_spi_cooker_path = qemu_spi_cooker_node.path_from(ctx.path)
        subprocess.check_call([qemu_spi_cooker_path, spi_flash_path], stdout=null)


def mfg_image_spi(ctx):
    """Creates a SPI flash image of PRF for MFG pre-burn. Includes a
    FirmwareDescription struct"""
    import insert_firmware_descr

    if ctx.env.BOARD.startswith('silk'):
        prf_begin = 0x200000
        image_size = 0x800000
    else:
        ctx.fatal("MFG Image not suppored for board: {}".format(ctx.env.BOARD))

    spi_flash_path = _create_spi_flash_image(ctx, 'mfg_prf_image.bin')
    mfg_spi_img_file = open(spi_flash_path, 'wb')

    # Pad the first section before PRF storage
    mfg_spi_img_file.write("\xff" * prf_begin)

    prf_path = ctx.get_tintin_fw_node_prf().path_from(ctx.path)
    prf_image = insert_firmware_descr.insert_firmware_description_struct(prf_path)
    mfg_spi_img_file.write(prf_image)

    # Pad with 0xff up to image size
    tail_padding_size = image_size - prf_begin - len(prf_image)
    mfg_spi_img_file.write("\xff" * tail_padding_size)

def show_ttys(ctx):
    """Displays all available ftdi ports connected to computer"""
    os.system("python ./tools/log_hashing/miniterm_co.py ftdi:///?")


class ConsoleCommand(BuildContext):
    cmd = 'console'
    fun = 'console'


def console(ctx):
    """Starts miniterm with the serial console."""
    # miniterm is not made to be used as a python module, so just shell out:
    tty = ctx.options.tty or _get_dbgserial_tty()

    if _is_pulse_everywhere(ctx):
        os.system("python ./tools/pulse_console.py -t %s" % tty)
    else:
        baudrate = ctx.options.baudrate or 230400
        os.system("python ./tools/log_hashing/miniterm_co.py %s %d" % (tty, baudrate))


class ConsoleCommand(BuildContext):
    cmd = 'console_prf'
    fun = 'console_prf'

def console_prf(ctx):
    os.putenv("PBL_CONSOLE_DICT_PATH", "build/prf/src/fw/loghash_dict.json")
    console(ctx)


class BleConsoleCommand(BuildContext):
    cmd = 'ble_console'
    fun = 'ble_console'


def ble_console(ctx):
    def _get_ble_tty():
        import pebble_tty
        tty = pebble_tty.find_ble_tty()

        if tty is None:
            return None

        waflib.Logs.pprint('GREEN', 'No --tty argument specified, auto-selecting: %s' % tty)
        return tty

    """Starts miniterm with the serial console for the BLE chip."""
    ctx.recurse('platform', mandatory=False)

    # FIXME: We have the ability to progam PIDs into the new round of Big Boards. TTY
    # path discovery should be able to use that (PBL-31111). For now, just make a best
    # guess at what the path should be

    if ctx.is_silk() or ctx.is_robert():
        tty_path = _get_ble_tty()
    # if the bt_controller was chosen explicitly, assume we are using an eval board, which
    # happens to match the path for cutts
    elif ctx.uses_dialog_bluetooth():
        tty_path = "ftdi://ftdi:2232:1/1"
    else:
        waflib.Logs.pprint('CYAN', 'Note: This platform does not have a BLE UART')
        tty_path = _get_dbgserial_tty()

    tty = ctx.options.tty or tty_path
    baudrate = ctx.options.baudrate or 230400

    os.system("python ./tools/log_hashing/miniterm_co.py %s %d" % (tty, baudrate))


class BleConsolePrfCommand(BuildContext):
    cmd = 'ble_console_prf'
    fun = 'ble_console_prf'


def ble_console_prf(ctx):
    os.putenv("PBL_CONSOLE_DICT_PATH", "build/prf/src/fw/loghash_dict.json")
    ble_console(ctx)


def accessory_console(ctx):
    def _get_accessory_tty():
        import pebble_tty
        tty = pebble_tty.find_accessory_tty()

        if tty is None:
            return None

        waflib.Logs.pprint('GREEN', 'No --tty argument specified, auto-selecting: %s' % tty)
        return tty

    """Starts miniterm with the accessory connector console."""
    # miniterm is not made to be used as a python module, so just shell out:
    tty = ctx.options.tty or _get_accessory_tty()
    baudrate = ctx.options.baudrate or 115200
    os.system("python ./tools/log_hashing/miniterm_co.py %s %d" % (tty, baudrate))


def qemu(ctx):
    # Make sure the micro-flash image is up to date. By default, we don't rebuild the
    # SPI flash image in case you want to continue with the stored apps, etc. you had before.
    from waflib import Options
    Options.commands = ['qemu_image_micro', 'qemu_launch'] + Options.commands


def qemu_prf(ctx):
    # Make sure the micro-flash image is up to date. By default, we don't rebuild the
    # SPI flash image in case you want to continue with the stored apps, etc. you had before.
    from waflib import Options
    Options.commands = ['qemu_image_prf_micro', 'qemu_launch'] + Options.commands


class QemuLaunchCommand(BuildContext):
    cmd = 'qemu_launch'
    fun = 'qemu_launch'


def qemu_launch(ctx):
    """Starts up the emulator (qemu) """
    ctx.recurse('platform', mandatory=False)

    qemu_machine = ctx.get_qemu_machine()
    if not qemu_machine or qemu_machine == 'unknown':
        raise Exception("Board type '{}' not supported by QEMU".format(ctx.env.BOARD))

    qemu_micro_flash = ctx.path.get_bld().make_node('qemu_micro_flash.bin')
    qemu_spi_flash = ctx.path.get_bld().make_node('qemu_spi_flash.bin')
    qemu_spi_type = ctx.get_qemu_extflash_device_type()
    if not qemu_spi_type:
        raise Exception("External flash type for '{}' not specified".format(ctx.env.BOARD))

    machine_dep_args = ['-machine', qemu_machine,
                        '-cpu', ctx.get_qemu_cpu(),
                        '-pflash', qemu_micro_flash.path_from(ctx.path),
                        qemu_spi_type, qemu_spi_flash.path_from(ctx.path)]

    if ctx.has_touch():
        machine_dep_args.append('-show-cursor')

    cmd_line = (
        "qemu-system-arm -rtc base=localtime "
        "-monitor stdio "
        "-s "
        "-serial file:uart1.log "
        "-serial tcp::12344,server,nowait "   # Used for bluetooth data
        "-serial tcp::12345,server,nowait "   # Used for console
        ) + ' '.join(machine_dep_args)
    os.system(cmd_line)


class QEMUConsoleCommand(BuildContext):
    cmd = 'qemu_console'
    fun = 'qemu_console'


def qemu_console(ctx):
    """Starts miniterm configured to talk to the emulator (qemu)"""
    # miniterm is not made to be used as a python module, so just shell out:
    host_port = ctx.options.qemu_host or 'localhost:12345'

    # A hacky way to pass an argument
    if _is_pulse_everywhere(ctx):
        os.system("python ./tools/pulse_console.py -t %s" % ('socket://%s' % (host_port)))
    else:
        os.system("python ./tools/log_hashing/miniterm_co.py %s" % ('socket://%s' % (host_port)))


class QemuGdb(BuildContext):
    """Starts up a gdb instance to talk to the emulator """
    cmd = 'qemu_gdb'
    fun = 'qemu_gdb'


def qemu_gdb(ctx):
    # First, startup the gdb proxy
    cmd_line = "python ./tools/qemu/qemu_gdb_proxy.py --port=1233 --target=localhost:1234"
    proc = pexpect.spawn(cmd_line, logfile=sys.stdout)
    proc.expect(["Connected to target", pexpect.TIMEOUT], timeout=10)
    fw_elf = ctx.get_tintin_fw_node().change_ext('.elf')
    run_arm_gdb(ctx, fw_elf, target_server_port=1233)


class QemuGdbBoot(BuildContext):
    """ Starts up a gdb instance to talk to the emulator's boot ROM """
    cmd = 'qemu_gdb_boot'
    fun = 'qemu_gdb_boot'


def qemu_gdb_boot(ctx):
    boot_elf = ctx.get_tintin_boot_node().change_ext('.elf')
    run_arm_gdb(ctx, boot_elf, target_server_port=1234)


class debug(BuildContext):
    """ Alias for gdb """
    cmd = 'debug'

    def execute_build(ctx):
        gdb(ctx)


class Gdb(BuildContext):
    """ Starts GDB and openocd (if not already running) and attaches GDB to
        openocd's GDB server. If openocd is already running, it will be used.
    """
    cmd = 'gdb'
    fun = 'gdb'


def gdb(ctx, fw_elf=None, cfg_file='openocd.cfg', is_ble=False):
    if fw_elf is None:
        fw_elf = ctx.get_tintin_fw_node().change_ext('.elf')
    with waftools.openocd.daemon(ctx, cfg_file,
                                 use_swd=(is_ble or 'swd' in ctx.env.JTAG)):
        run_arm_gdb(ctx, fw_elf, cmd_str='--init-command=".gdbinit"')


class gdb_prf(BuildContext):
    """same as `gdb`, but loading the PRF elf instead"""
    cmd = 'gdb_prf'

    def execute_build(ctx):
        gdb(ctx, ctx.get_tintin_fw_node_prf().change_ext('.elf'))


def openocd(ctx):
    """ Starts openocd and leaves it running. It will reset the board to
        increase the chances of attaching succesfully. """
    waftools.openocd.run_command(ctx, 'init; reset', shutdown=False)


# Image commands
#################################################

def _get_dbgserial_tty():
    import pebble_tty
    tty = pebble_tty.find_dbgserial_tty()

    if tty is None:
        return None

    waflib.Logs.pprint('GREEN', 'No --tty argument specified, auto-selecting: %s' % tty)
    return tty


class ble_send_hci(BuildContext):
    """Puts MCU in HCI bypass mode. Sends specified HCI Command and returns result. i.e:
       ./waf send_hci 0x01 0x03 0x0C 0x00
    """
    cmd = 'ble_send_hci'
    fun = 'ble_send_hci'


def ble_send_hci(ctx):
    import prompt
    import pebble_tty
    from serial_port_wrapper import SerialPortWrapper
    import struct
    from time import sleep
    from waflib import Options

    def _dump_hex_array(prefix, hex_array):
        print prefix + " [",
        for i in range(0, len(hex_array)):
            print "0x%02x " % hex_array[i],
        print "]"

    hci_bytes = [int(i, 16) for i in Options.commands]
    _dump_hex_array("Sent HCI CMD:", hci_bytes)

    try:
        device_tty = pebble_tty.find_dbgserial_tty()
        serial = SerialPortWrapper(device_tty)

        prompt.go_to_prompt(serial)
        prompt.issue_command(serial, "bt test hcipass")
        sleep(0.1)

        serial.write_fast(struct.pack('B'*len(hci_bytes), *hci_bytes))

        response = serial.read()
        response = struct.unpack('%dB' % len(response), response)

        serial.write(struct.pack('B', 0x04))  # issue ctrl-d

        _dump_hex_array(" Got HCI EVT:", response)
    finally:
        # note: random bytes get dropped on subsequent usb ops if you forget to close!
        serial.close()

    # WAF/optparse does not have native support for adding sub-command options
    # or variable length options. Reset the options list to prevent innocuous
    # messaging about unrecognized commands
    Options.commands = []
    return None


class ImageResources(BuildContext):
    """flashes resources"""
    cmd = 'image_resources'
    fun = 'image_resources'


def _is_pulse_everywhere(ctx):
    return "PULSE_EVERYWHERE=1" in ctx.env["DEFINES"]


def _get_pulse_flash_tool(ctx):
    if _is_pulse_everywhere(ctx):
        return "pulse_flash_imaging"
    else:
        return "pulse_legacy_flash_imaging"


def image_resources(ctx):
    tty = ctx.options.tty or _get_dbgserial_tty()
    if tty is None:
        waflib.Logs.pprint('RED', 'Error: --tty not specified')
        return

    tool_name = _get_pulse_flash_tool(ctx)
    pbpack_path = ctx.get_pbpack_node().abspath()
    waflib.Logs.pprint('CYAN', 'Writing pbpack "%s" to tty %s' % (pbpack_path, tty))

    ret = os.system("python ./tools/%s.py -t %s -p resources %s" % (tool_name, tty, pbpack_path))
    if ret != 0:
        ctx.fatal('Imaging failed')


class ImageRecovery(BuildContext):
    """flashes recovery firmware"""
    cmd = 'image_recovery'
    fun = 'image_recovery'


def image_recovery(ctx):
    tty = ctx.options.tty or _get_dbgserial_tty()
    if tty is None:
        waflib.Logs.pprint('RED', 'Error: --tty not specified')
        return

    tool_name = _get_pulse_flash_tool(ctx)
    recovery_bin_path = ctx.options.file or ctx.get_tintin_fw_node_prf().path_from(ctx.path)
    waflib.Logs.pprint('CYAN', 'Writing recovery bin "%s" to tty %s' % (recovery_bin_path, tty))

    ret = os.system("python ./tools/%s.py -t %s -p firmware %s" % (tool_name, tty, recovery_bin_path))
    if ret != 0:
        ctx.fatal('Imaging failed')


# Flash commands
#################################################

class FirmwareTooLargeException(Exception):
    pass


def _check_firmware_image_size(ctx, path):
    BYTES_PER_K = 1024
    firmware_size = os.path.getsize(path)
    # Determine flash and bootloader size so we can calculate the max firmware size
    if ctx.env.MICRO_FAMILY == 'STM32F2':
        # 512k of flash and 16k bootloader
        max_firmware_size = (512 - 16) * BYTES_PER_K
    elif ctx.env.MICRO_FAMILY == 'STM32F4':
        if ctx.env.BOARD.startswith('silk') and ctx.variant == 'prf':
            # silk PRF is limited to 512k to save on SPI flash space
            max_firmware_size = 512 * BYTES_PER_K
        elif ctx.env.BOARD in ('snowy_evt', 'snowy_evt2', 'spalding_evt'):
            # 1024k of flash and 64k bootloader
            max_firmware_size = (1024 - 64) * BYTES_PER_K
        else:
            # 1024k of flash and 16k bootloader
            max_firmware_size = (1024 - 16) * BYTES_PER_K
    elif ctx.env.MICRO_FAMILY == 'STM32F7':
        if ctx.variant == 'prf' and not ctx.env.IS_MFG:
            # Robert PRF is limited to 512k to save on SPI flash space
            max_firmware_size = 512 * BYTES_PER_K
        else:
            # 2048k of flash and 32k bootloader
            max_firmware_size = (2048 - 32) * BYTES_PER_K
    else:
        ctx.fatal('Cannot check firmware size against unknown micro family "{}"'
                  .format(ctx.env.MICRO_FAMILY))

    if firmware_size > max_firmware_size:
        raise FirmwareTooLargeException('Firmware is too large! Size is 0x%x should be less than 0x%x' \
                                        % (firmware_size, max_firmware_size))

    return ('%d / %d bytes used (%d free)' %
            (firmware_size, max_firmware_size, (max_firmware_size - firmware_size)))


class FlashCommand(BuildContext):
    """alias for flash_everything"""
    cmd = 'flash'
    fun = 'flash'


def flash(ctx):
    flash_everything(ctx, ctx.get_tintin_fw_node())


class FlashPrfCommand(BuildContext):
    """flashes recovery firmware as normal firmware"""
    cmd = 'flash_prf'
    fun = 'flash_prf'


def flash_prf(ctx):
    flash_everything(ctx, ctx.get_tintin_fw_node_prf())


class FlashBootCommand(BuildContext):
    cmd = 'flash_boot'
    fun = 'flash_boot'


def flash_boot(ctx):
    """flashes a bootloader"""
    if not ctx.env.BOOTLOADER_HEX:
        ctx.fatal("Target does not have a bootloader binary available")
    waftools.openocd.run_command(ctx, 'init; reset halt; ' +
                                 'flash write_image erase ' + ctx.env.BOOTLOADER_HEX + '; '
                                 'reset;',
                                 expect=["wrote"],
                                 enforce_expect=True)


class FlashFirmware(BuildContext):
    """flashes a firmware"""
    cmd = 'flash_fw'

    def execute_build(ctx):
        flash_fw(ctx, ctx.get_tintin_fw_node())


def flash_fw(ctx, fw_bin):
    _check_firmware_image_size(ctx, fw_bin.path_from(ctx.path))

    hex_path = fw_bin.change_ext('.hex').path_from(ctx.path)
    waftools.openocd.run_command(ctx, 'init; reset halt; ' +
                                 'flash write_image erase {}; '.format(hex_path) +
                                 'reset;',
                                 expect=["wrote"],
                                 enforce_expect=True)


def flash_everything(ctx, fw_bin):
    """flashes a bootloader and firmware"""
    if ctx.env.QEMU:
        ctx.fatal("I'm sorry Dave, I can't let you do that.\n"
                  "QEMU firmwares do not work on physical hardware.\n"
                  "Configure without --qemu and rebuild before trying again.")

    _check_firmware_image_size(ctx, fw_bin.path_from(ctx.path))

    if not ctx.env.BOOTLOADER_HEX:
        ctx.fatal("Target does not have a bootloader binary available")

    hex_path = fw_bin.change_ext('.hex').path_from(ctx.path)
    waftools.openocd.run_command(ctx, 'init; reset halt; '
                                 'flash write_image erase ' + ctx.env.BOOTLOADER_HEX + ';\n'
                                 'flash write_image erase ' + hex_path + '; '
                                 'reset;',
                                 expect=["wrote", "wrote", "shutdown"],
                                 enforce_expect=True)


def force_flash(ctx):
    """forces a connected device into a flashing state"""
    (is_newer_than_0_7_0, _) = waftools.openocd.get_flavor(ctx)
    reset_config = waftools.openocd._get_reset_conf(ctx, is_newer_than_0_7_0, True)
    reset_cmd = "reset_config %s; " % reset_config
    waftools.openocd.run_command(ctx, reset_cmd + 'init; reset halt;', ignore_fail=True)
    waftools.openocd.run_command(ctx, reset_cmd + 'init; stm32x unlock 0;', ignore_fail=True)


def reset(ctx):
    """resets a connected device"""
    waftools.openocd.run_command(ctx, 'init; reset;', expect=["found"])


def bork(ctx):
    """resets and wipes a connected a device"""
    waftools.openocd.run_command(ctx, 'init; reset halt;', ignore_fail=True)
    waftools.openocd.run_command(ctx, 'init; flash erase_sector 0 0 1;', ignore_fail=True)


def make_lang(ctx):
    """generate translation files and update existing ones"""
    ctx.recurse('resources/normal/base/lang')


class PackLangCommand(BuildContext):
    cmd = 'pack_lang'
    fun = 'pack_lang'


def pack_lang(ctx):
    """generates pbpack for langs"""
    ctx.recurse('resources/normal/base/lang')


class PackAllLangsCommand(BuildContext):
    cmd = 'pack_all_langs'
    fun = 'pack_all_langs'


def pack_all_langs(ctx):
    """generates pbpack for all langs"""
    ctx.recurse('resources/normal/base/lang')


# Tool build commands
#################################################


class build_pdc2png(BuildContext):
    """executes the pdc2png build"""
    cmd = 'build_pdc2png'
    variant = 'pdc2png'


class build_tools(BuildContext):
    """build all tools in tools/ dir"""
    cmd = 'build_tools'
    variant = 'tools'

# vim:filetype=python