# Copyright 2024 Google LLC
#
# 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.

import contextlib
import pexpect
import re
import string
import subprocess
import sys
import waflib

from waflib import Logs


JTAG_OPTIONS = {'olimex': 'source [find interface/ftdi/olimex-arm-usb-ocd-h.cfg]',
                'fixture': 'source [find interface/flossjtag-noeeprom.cfg]',
                'bb2': 'source waftools/openocd_bb2_ftdi.cfg',
                'bb2-legacy': 'source waftools/openocd_bb2_ft2232.cfg',
                'jtag_ftdi': 'source waftools/openocd_jtag_ftdi.cfg',
                'swd_ftdi': 'source waftools/openocd_swd_ftdi.cfg',
                'swd_jlink': 'source waftools/openocd_swd_jlink.cfg',
                'swd_stlink': 'source [find interface/stlink-v2.cfg]',
                }

OPENOCD_TELNET_PORT = 4444


@contextlib.contextmanager
def daemon(ctx, cfg_file, use_swd=False):
    if _is_openocd_running():
        yield
    else:
        if use_swd:
            expect_str = "SWD IDCODE"
        else:
            expect_str = "device found"

        proc = pexpect.spawn('openocd', ['-f', cfg_file], logfile=sys.stdout)
        # Wait for OpenOCD to connect to the board:
        result = proc.expect([expect_str, pexpect.TIMEOUT], timeout=10)
        if result == 0:
            yield
        else:
            raise Exception("Timed out connecting OpenOCD to development board...")
        proc.close()


def _has_openocd(ctx):
    try:
        ctx.cmd_and_log(['which', 'openocd'], quiet=waflib.Context.BOTH)
        return True
    except:
        return False


def _is_openocd_running():
    import socket
    import errno
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        s.bind(('', OPENOCD_TELNET_PORT))
        s.close()
    except socket.error as e:
        s.close()
        return e[0] == errno.EADDRINUSE
    return False


def run_command(ctx, cmd, ignore_fail=False, expect=[], timeout=40,
                shutdown=True, enforce_expect=False, cfg_file="openocd.cfg"):
    if _is_openocd_running():
        import telnetlib
        t = telnetlib.Telnet('', OPENOCD_TELNET_PORT)
        Logs.info("Sending commands to OpenOCD daemon:\n%s\n..." % cmd)
        t.write("%s\n" % cmd)
        for regex in expect:
            idx, match, text = t.expect([regex], timeout)
            if enforce_expect and idx == -1:
                # They'll see the full story in another window
                ctx.fatal("OpenOCD expectation '%s' unfulfilled" % regex)
        t.close()
    else:
        fail_handling = ' || true ' if ignore_fail else ''
        if shutdown:
            # append 'shutdown' to make openocd exit:
            cmd = "%s ; shutdown" % cmd
        ctx.exec_command('openocd -f %s -c "%s" 2>&1 | tee .waf.openocd.log %s' %
                         (cfg_file, cmd, fail_handling), stdout=None, stderr=None)
        if enforce_expect:
            # Read the result
            with open(".waf.openocd.log", "r") as result_file:
                result = result_file.read()
                match_start = 0
                for regex in expect:
                    expect_match = re.search(regex, result[match_start:])
                    if not expect_match:
                        ctx.fatal("OpenOCD expectation '%s' unfulfilled" % regex)
                    match_start = expect_match.end()



def _get_supported_interfaces(ctx):
    if not _has_openocd(ctx):
        return []
    # Ugh, openocd exits with status 1 when not specifying an interface...
    try:
        ctx.cmd_and_log(['openocd', '-c', '"interface_list"'],
                        quiet=waflib.Context.BOTH,
                        output=waflib.Context.STDERR)
    except Exception as e:
        # Ugh, openocd prints the output to stderr...
        out = e.stderr
    out_lines = out.splitlines()
    interfaces = []
    for line in out_lines:
        matches = re.search("\d+: (\w+)", line)
        if matches:
            interfaces.append(matches.groups()[0])
    return interfaces


def get_flavor(conf):
    """ Returns a 2-tuple (is_newer_than_0_7_0, is_pebble_flavor) """

    try:
        version_string = conf.cmd_and_log(['openocd', '--version'],
                                          quiet=waflib.Context.BOTH,
                                          output=waflib.Context.STDERR)
        version_string = version_string.splitlines()[0]
        matches = re.search("(\d+)\.(\d+)\.(\d+)", version_string)
        version = map(int, matches.groups())
        return (version[0] >= 0 and version[1] >= 7,
                'pebble' in version_string)
    except Exception:
        Logs.error("Couldn't parse openocd version")
        return (False, False)


def _get_reset_conf(conf, is_newer_than_0_7_0, should_connect_assert_srst):
    if is_newer_than_0_7_0:
        options = ['trst_and_srst', 'srst_nogate']
        if should_connect_assert_srst:
            options.append('connect_assert_srst')
        return ' '.join(options)
    else:
        return 'trst_and_srst'


def write_cfg(conf):
    jtag = conf.env.JTAG
    if jtag == 'bb2':
        if 'ftdi' not in _get_supported_interfaces(conf):
            jtag = 'bb2-legacy'
            Logs.warn('OpenOCD is not compiled with --enable-ftdi, falling'
                      ' back to legacy ft2232 driver.')

    if conf.env.MICRO_FAMILY == 'STM32F2':
        target = 'stm32f2x.cfg'
    elif conf.env.MICRO_FAMILY == 'STM32F4':
        target = 'stm32f4x.cfg'
    elif conf.env.MICRO_FAMILY == 'STM32F7':
        target = 'stm32f7x.cfg'

    (is_newer_than_0_7_0, is_pebble_flavor) = get_flavor(conf)

    reset_config = _get_reset_conf(conf, is_newer_than_0_7_0, False)
    Logs.info("reset_config: %s" % reset_config)

    if is_pebble_flavor:
        Logs.info("openocd is Pebble flavored!")
        os_name = 'Pebble_FreeRTOS'
    else:
        os_name = 'FreeRTOS'

    openocd_cfg = OPENOCD_CFG_TEMPLATE.substitute(jtag=JTAG_OPTIONS[jtag],
                                                  target=target,
                                                  reset_config=reset_config,
                                                  os_name=os_name)
    waflib.Utils.writef('./openocd.cfg', openocd_cfg)


OPENOCD_CFG_TEMPLATE = string.Template("""
# THIS IS A GENERATED FILE: See waftools/openocd.py for details

${jtag}
source [find target/${target}]

reset_config ${reset_config}

$$_TARGETNAME configure -rtos ${os_name}
$$_TARGETNAME configure -event gdb-attach {
    echo "Halting target because GDB is attaching..."
    halt
}
$$_TARGETNAME configure -event gdb-detach {
    echo "Resuming target because GDB is detaching..."
    resume
}
""")