from __future__ import absolute_import

import os
import logging
from distutils.version import LooseVersion


try:
    # pyocd version >= 0.13
    from pyocd.target import TARGET
    from pyocd.core.helpers import ConnectHelper
    from pyocd import __version__ as pyocd_version
except ImportError as e:
    # old pyocd version
    try:
        from pyOCD.target import TARGET
        from pyOCD.board import MbedBoard
        from pyOCD import __version__ as pyocd_version

    except ImportError:
        pyocd_version = ''
        print('Fatal Error: pyocd is missing, please install it at first!')

from mcutk.debugger.base import DebuggerBase
from mcutk.util import run_command


class PYOCD(DebuggerBase):
    """
    Wrap pyOCD to a standard debugger for pymcutk.

    pyOCD is an open source Python package for programming and debugging Arm Cortex-M microcontrollers
    using multiple supported types of USB debug probes. It is fully cross-platform, with support for Linux,
    macOS, and Windows. For more visit https://github.com/mbedmicro/pyOCD.
    """

    def __init__(self, **kwargs):
        super(PYOCD, self).__init__("pyocd", ".", **kwargs)
        self.version = pyocd_version
        self._target_option = ""

        if self.version and LooseVersion(self.version) >= LooseVersion("0.13.0"):
            self._gdbserver = "pyocd gdbserver"
            self._flashtool = "pyocd flash"
            self._pyocdtool = "pyocd-tool"
        else:
            # windows pyocd tools are regenerated by ourselves
            # use these commands directly.
            if os.name == "nt":
                self._gdbserver = "pyocd-gdbserver"
                self._flashtool = "pyocd-flashtool"
                self._pyocdtool = "pyocd-tool"
            else:
                self._gdbserver = "python -m pyOCD.tools.gdb_server"
                self._flashtool = "python -m pyOCD.tools.flash_tool"
                self._pyocdtool = "python -m pyOCD.tools.pyocd"


    @property
    def is_ready(self):
        return self.version not in (None, '')



    def set_board(self, board):
        self._board = board
        device_name = board.devicename
        if device_name.endswith('pack') and os.path.exists(device_name):
            self._target_option = " --pack %s"%device_name
        else:
            self._target_option = self._overried_target(device_name)


    def _overried_target(self, devicename):
        """Overried target if device name is include in pyOCD.target.TARGET.
        This is very useful if you want to use other device target for debugging.
        """
        if self.is_ready and devicename in TARGET:
            return " -t %s"%devicename

        return ""


    def list_connected_devices(self):
        """List connected CMSIS-DAP devices."""

        probes = ConnectHelper.get_all_connected_probes(blocking=False)
        devices = list()
        for probe in probes:
            device = {
                'usbid': probe.unique_id,
                'name': probe.description,
                'type': 'pyocd'
            }
            devices.append(device)

        return devices



    def test_conn(self):
        """Test debugger connection."""

        if self._board is None:
            raise ValueError("board is not set")

        mbed = None
        msg = "NoError"
        try:
            mbed = MbedBoard.chooseBoard(self._board.usbid)
        except Exception as e:
            msg = "ConnectError: %s"%(str(e))
        finally:
            if mbed:
                mbed.uninit()

        return msg



    def erase(self):
        """Erase flash by: pyocd-tool erase -b <id>
        """
        logging.info("pyocd erase flash")
        if LooseVersion(self.version) >= LooseVersion("0.13.0"):
            command = "pyocd erase --mass-erase -u {0} {1}".format(self._board.usbid, self._target_option)
        else:
            command = "{0} --mass-erase -d error -b {1} {2}".format(self._flashtool, self._board.usbid, self._target_option)
        return run_command(command)[0]



    def reset(self):
        """Reset board by: pyocd-tool reset -b <id>"""

        logging.info("pyocd reset board")
        command = "{0} reset -d error -b {1} {2}".format(self._pyocdtool, self._board.usbid, self._target_option)
        return run_command(command)[0]



    def api_reset(self, usbid):
        """Reset board via pyOCD API."""

        logging.info("resetting board")
        try:
            if self.version and LooseVersion(self.version) >= LooseVersion("0.13.0"):
                with ConnectHelper.session_with_chosen_probe(unique_id=usbid) as session:
                    target = session.board.target
                    target.reset()
            else:
                with MbedBoard.chooseBoard(board_id=usbid) as mboard:
                    mboard.target.reset()

            return True

        except Exception as e:
            logging.exception("resetting board exception")
            return False



    def unlock(self):
        """Unlock board by: pyocd-tool.exe unlock -b <id>"""

        logging.info("unlock board")
        command = "{0} unlock -b {1} {2}".format(self._pyocdtool, self._board.usbid, self._target_option)
        return run_command(command)[0]



    def get_gdbserver(self, **kwargs):
        """Return gdb server command. If configured board.devicename is a target
        name in pyocd.target.TARGET. This will overried the target.
        """

        board = kwargs.get('board')
        port = kwargs.get('port')
        usbid = kwargs.get('usbid')
        devicename = kwargs.get('devicename')

        if board is None:
            board = self._board

        if board:
            port = board.gdbport
            usbid = board.usbid
            devicename = board.devicename

        if self.version and LooseVersion(self.version) >= LooseVersion("0.13.0"):
            command = "pyocd gdbserver --trust-crc"
            if usbid:
                command += " --uid %s"%usbid
            if port:
                command += " --port %s"%port

        else:
            command = self._gdbserver
            if usbid:
                command += " -b %s"%usbid
            if port:
                command += " -p %s"%port

        if devicename:
            command += " " + self._target_option

        return command



    def flash(self, filepath, erase=False, addr=0):
        """Flash chip with filepath. (bin, hex).

        Arguments:
            erase: erase chip yes or not.
            addr: being the integer starting address for the bin file.
        """
        filepath = filepath.replace("\\", "/")

        if LooseVersion(self.version) < LooseVersion("0.13.0"):
            command = "{0} -b {1} -a {2} {3}".format(
                self._flashtool,
                self._board.usbid,
                self._board.start_address,
                filepath)
        else:
            # --trust-crc: use only the CRC of each page to determine if it
            # already has the same data
            command = "{0} -u {1} -a {2} {3} --trust-crc".format(
                self._flashtool,
                self._board.usbid,
                self._board.start_address,
                filepath)

        command += self._target_option

        # call registerd callback function
        self._call_registered_callback("before-load")
        return run_command(command, timeout=300)





    def gdb_init_template(self):
        """Default PyOCD gdb commands temaplte.

        >>>
            set tcp connect-timeout 10
            set remotetimeout 10
            target remote localhost: {gdbport}
            load
            q
        """
        commands = '''set tcp connect-timeout 10
set remotetimeout 100
target remote localhost: {gdbport}
load
monitor reset
q
'''
        return commands



    def __str__(self):
        return "pyocd-%s" % self.version




    @staticmethod
    def get_latest():
        de = PYOCD()
        return de if de.is_ready else None


