"""
mbed SDK
Copyright (c) 2011-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.

Author: Przemyslaw Wirkus <Przemyslaw.Wirkus@arm.com>
"""

import os
import re
import json
from os import walk
try:
    from contextlib import suppress
except ImportError:
    from contextlib import contextmanager
    @contextmanager
    def suppress(*excs):
        try:
            yield
        except excs:
            pass
from mbed_greentea.mbed_common_api import run_cli_process
from mbed_greentea.mbed_greentea_log import gt_logger


## Information about some properties of targets (platforms)
#
#  "default" entry is used to fetch "global" properties if they are not
#  specified with each platform
#

TARGET_INFO_MAPPING = {
    "default" : {
        "program_cycle_s": 4,
        "binary_type": ".bin",
        "copy_method": "default",
        "reset_method": "default"
    },

    "K64F" : {
        "yotta_targets": [
                {
                    "yotta_target": "frdm-k64f-gcc",
                    "mbed_toolchain": "GCC_ARM"
                },
                {
                    "yotta_target": "frdm-k64f-armcc",
                    "mbed_toolchain": "ARM"
                }
             ],
        "properties" : {
                "binary_type": ".bin",
                "copy_method": "default",
                "reset_method": "default",
                "program_cycle_s": 4
            }
    },
    "NUCLEO_F401RE" : {
        "yotta_targets": [
                {
                    "yotta_target": "st-nucleo-f401re-gcc",
                    "mbed_toolchain": "GCC_ARM"
                }
             ],
        "properties" : {
                "binary_type": ".bin",
                "copy_method": "cp",
                "reset_method": "default",
                "program_cycle_s": 4
            }
        },
    "NRF51_DK" : {
        "yotta_targets": [
                {
                    "yotta_target": "nrf51dk-gcc",
                    "mbed_toolchain": "GCC_ARM"
                },
                {
                    "yotta_target": "nrf51dk-armcc",
                    "mbed_toolchain": "ARM"
                }
             ],
        "properties" : {
                "binary_type": "-combined.hex",
                "copy_method": "shell",
                "reset_method": "default",
                "program_cycle_s": 4
            }
        },
    "NRF51822" : {
        "yotta_targets": [
                {
                    "yotta_target": "mkit-gcc",
                    "mbed_toolchain": "GCC_ARM"
                },
                {
                    "yotta_target": "mkit-armcc",
                    "mbed_toolchain": "ARM"
                }
             ],
        "properties" : {
                "binary_type": "-combined.hex",
                "copy_method": "shell",
                "reset_method": "default",
                "program_cycle_s": 4
            }
        },
    "ARCH_BLE" : {
        "yotta_targets": [
                {
                    "yotta_target": "tinyble-gcc",
                    "mbed_toolchain": "GCC_ARM"
                }
             ],
        "properties" : {
                "binary_type": "-combined.hex",
                "copy_method": "shell",
                "reset_method": "default",
                "program_cycle_s": 4
            }
        }
}

TARGET_TOOLCAHINS = {
    '-armcc': 'ARM',
    '-gcc': 'GCC_ARM',
    '-iar': 'IAR',
}

def get_mbed_target_call_yotta_target():
    """! Calls yotta's 'yotta target' command to get information about
    """
    cmd = ['yotta', '--plain', 'target']
    gt_logger.gt_log("checking yotta target in current directory")
    gt_logger.gt_log_tab("calling yotta: %s"% " ".join(cmd))
    _stdout, _stderr, _ret = run_cli_process(cmd)
    return _stdout, _stderr, _ret

def parse_yotta_json_for_build_name(yotta_json_content):
    """! Function parse .yotta.json to fetch set yotta target
    @param yotta_json_content Content of .yotta_json file
    @return String with set yotta target name, None if no target found
    """
    try:
        return yotta_json_content['build']['target'].split(',')[0]
    except KeyError:
        return None

def get_yotta_target_from_local_config(yotta_json='.yotta.json'):
    """! Load yotta target from local configuration file
    @param yotta_json File in format of .yotta.json which stores current target names
    @return Yotta target set in currect directory, None if no info is available
    @details
    Example structure of .yotta.json file:
    {
      "build": {
        "target": "frdm-k64f-gcc,*",
        "targetSetExplicitly": true
      }
    }
    """
    if not os.path.exists(yotta_json):
        return None

    try:
        gt_logger.gt_log("parsing local file '%s' for target information"% yotta_json)

        with open(yotta_json, 'r') as f:
            return parse_yotta_json_for_build_name(json.load(f))
    except (IOError, ValueError) as e:
        gt_logger.gt_log(str(e))
    return None

def get_mbed_target_from_current_dir():
    """! Function uses yotta target command to check current target
    @return Returns current target or None if target not found (e.g. not yotta package)
    """
    # We will first try to load current target name using .yotta.json file
    result = get_yotta_target_from_local_config()

    # If we can't read .yotta.json, we will try to use command line to fetch target name
    if not result:
        _stdout, _stderr, _ret = get_mbed_target_call_yotta_target()
        if not _ret:
            for line in _stdout.splitlines():
                target = parse_yotta_target_cmd_output(line)
                if target:
                    result = target
                    break
    return result

def parse_yotta_target_cmd_output(line):
    """! Function parsed output from command 'yotta --plain target'
         looking for valid target names. First one will be used as 'default'
         of currently set yotta target
    @param line Line of text from 'yotta target' command
    @return Yotta target name, None if not parsed
    @details

    Example call to 'yotta target' command (all lines)
    $ yotta --plain target
    frdm-k64f-gcc 2.0.0
    kinetis-k64-gcc 2.2.0
    mbed-gcc 1.2.2

    """
    # Regular expression to parse stings like: 'frdm-k64f-gcc 2.0.0'
    m = re.search(r'[\w\d_-]+ \d+\.\d+\.\d+', line)
    if m and len(m.group()):
        result = line.split()[0]
        return result
    return None

def get_mbed_targets_from_yotta_local_module(mbed_classic_name, yotta_targets_path='./yotta_targets'):
    """! Function is parsing local yotta targets to fetch matching mbed device target's name
    @return Function returns list of possible targets or empty list if value not found
    """
    result = []

    if not os.path.exists(yotta_targets_path):
        return result

    # All local directories with yotta targets
    target_dirs = [target_dir_name for target_dir_name in os.listdir(yotta_targets_path) if os.path.isdir(os.path.join(yotta_targets_path, target_dir_name))]

    gt_logger.gt_log("local yotta target search in '%s' for compatible mbed-target '%s'"% (gt_logger.gt_bright(yotta_targets_path), gt_logger.gt_bright(mbed_classic_name.lower().strip())))

    for target_dir in target_dirs:
        path = os.path.join(yotta_targets_path, target_dir, 'target.json')
        try:
            with open(path, 'r') as data_file:
                target_json_data = json.load(data_file)
                yotta_target_name = parse_mbed_target_from_target_json(mbed_classic_name, target_json_data)
                if yotta_target_name:
                    target_dir_name = os.path.join(yotta_targets_path, target_dir)
                    gt_logger.gt_log_tab("inside '%s' found compatible target '%s'"% (gt_logger.gt_bright(target_dir_name), gt_logger.gt_bright(yotta_target_name)))
                    result.append(yotta_target_name)
        except IOError as e:
            gt_logger.gt_log_err(str(e))
    return result

def parse_mbed_target_from_target_json(mbed_classic_name, target_json_data):
    if (not target_json_data or
        'keywords' not in target_json_data or
        'name' not in target_json_data):
        return None

    for keyword in target_json_data['keywords']:
        target, _, name = keyword.partition(':')
        if  (target == "mbed-target" and
             name.lower() == mbed_classic_name.lower()):
            return target_json_data['name']

    return None

def get_mbed_targets_from_yotta(mbed_classic_name):
    """! Function is using 'yotta search' command to fetch matching mbed device target's name
    @return Function returns list of possible targets or empty list if value not found
    @details Example:
             $ yt search -k mbed-target:k64f target
             frdm-k64f-gcc 0.0.16: Official mbed build target for the mbed frdm-k64f development board.
             frdm-k64f-armcc 0.0.10: Official mbed build target for the mbed frdm-k64f development board, using the armcc toolchain.

             Note: Function prints on console
    """
    result = []
    cmd = ['yotta', '--plain', 'search', '-k', 'mbed-target:%s'% mbed_classic_name.lower().strip(), 'target']
    gt_logger.gt_log("yotta search for mbed-target '%s'"% gt_logger.gt_bright(mbed_classic_name.lower().strip()))
    gt_logger.gt_log_tab("calling yotta: %s"% " ".join(cmd))
    _stdout, _stderr, _ret = run_cli_process(cmd)
    if not _ret:
        for line in _stdout.splitlines():
            yotta_target_name = parse_yotta_search_cmd_output(line)
            if yotta_target_name:
                if yotta_target_name and yotta_target_name not in result:
                    result.append(yotta_target_name)
                    gt_logger.gt_log_tab("found target '%s'" % gt_logger.gt_bright(yotta_target_name))
    else:
        gt_logger.gt_log_err("calling yotta search failed!")
    return result

def parse_yotta_search_cmd_output(line):
    m = re.search('([\w\d-]+) \d+\.\d+\.\d+[$:]?', line)
    if m and len(m.groups()):
        yotta_target_name = m.groups()[0]
        return yotta_target_name
    return None

def add_target_info_mapping(mbed_classic_name, map_platform_to_yt_target=None, use_yotta_registry=False):
    """! Adds more target information to TARGET_INFO_MAPPING by searching in yotta registry
    @return Returns TARGET_INFO_MAPPING updated with new targets
    @details Note: function mutates TARGET_INFO_MAPPING
    """
    yotta_target_search = get_mbed_targets_from_yotta_local_module(mbed_classic_name)
    if use_yotta_registry:
        # We can also use yotta registry to check for target compatibility (slower)
        yotta_registry_target_search = get_mbed_targets_from_yotta(mbed_classic_name)
        yotta_target_search.extend(yotta_registry_target_search)
        yotta_target_search = list(set(yotta_target_search))    # Reduce repeated values

    # Add extra targets to already existing and detected in the system platforms
    if map_platform_to_yt_target and mbed_classic_name in map_platform_to_yt_target:
        yotta_target_search = list(set(yotta_target_search + map_platform_to_yt_target[mbed_classic_name]))

    # Check if this targets are already there
    if mbed_classic_name not in TARGET_INFO_MAPPING:
        TARGET_INFO_MAPPING[mbed_classic_name] = {
        "yotta_targets": [],
        "properties" : {
                "binary_type": ".bin",
                "copy_method": "shell",
                "reset_method": "default",
                "program_cycle_s": 6
            }
        }

    target_desc = TARGET_INFO_MAPPING[mbed_classic_name]
    if 'yotta_targets' not in target_desc:
        return TARGET_INFO_MAPPING

    # All yt targets supported by 'mbed_classic_name' board
    mbeds_yt_targets = []
    for target in target_desc['yotta_targets']:
        mbeds_yt_targets.append(target['yotta_target'])

    # Check if any of yotta targets is new to TARGET_INFO_MAPPING
    for new_yt_target in yotta_target_search:
        if new_yt_target in mbeds_yt_targets:
            continue

        gt_logger.gt_log_tab("discovered extra target '%s'"% new_yt_target)
        # We want to at least guess toolchain type by target's name suffix
        mbed_toolchain = 'UNKNOWN'
        for toolchain_suffix in TARGET_TOOLCAHINS:
            if new_yt_target.endswith(toolchain_suffix):
                mbed_toolchain = TARGET_TOOLCAHINS[toolchain_suffix]
                break

        TARGET_INFO_MAPPING[mbed_classic_name]['yotta_targets'].append({
            'yotta_target': new_yt_target,
            'mbed_toolchain': mbed_toolchain
            })

    return TARGET_INFO_MAPPING

def get_mbed_clasic_target_info(mbed_classic_name, map_platform_to_yt_target=None, use_yotta_registry=False):
    """! Function resolves meta-data information about target given as mbed classic name.
    @param mbed_classic_name Mbed classic (mbed 2.0) name e.g. K64F, LPC1768 etc.
    @param map_platform_to_yt_target User defined mapping platform:supported target
    @details Function first updated TARGET_INFO_MAPPING structure and later checks if mbed classic name is available in mapping structure
    @return Returns information about yotta target for specific toolchain
    """
    TARGET_INFO_MAPPING = add_target_info_mapping(mbed_classic_name, map_platform_to_yt_target, use_yotta_registry)
    return TARGET_INFO_MAPPING[mbed_classic_name] if mbed_classic_name in TARGET_INFO_MAPPING else None

def get_binary_type_for_platform(platform):
    """
    Gives binary type for the given platform.

    :param platform:
    :return:
    """
    #return TARGET_INFO_MAPPING[platform]['properties']["binary_type"]
    return get_platform_property(platform, 'binary_type')

def get_platform_property(platform, property):
    """
    Gives platform property.

    :param platform:
    :return: property value, None if property not found
    """

    default = _get_platform_property_from_default(property)
    from_targets_json = _get_platform_property_from_targets(
        platform, property, default)
    if from_targets_json:
        return from_targets_json
    from_info_mapping = _get_platform_property_from_info_mapping(platform, property)
    if from_info_mapping:
        return from_info_mapping
    return default

def _get_platform_property_from_default(property):
    with suppress(KeyError):
        return TARGET_INFO_MAPPING['default'][property]

def _get_platform_property_from_info_mapping(platform, property):
    with suppress(KeyError):
        return TARGET_INFO_MAPPING[platform]['properties'][property]

def _platform_property_from_targets_json(targets, platform, property, default):
    """! Get a platforms's property from the target data structure in
            targets.json. Takes into account target inheritance.
    @param targets Data structure parsed from targets.json
    @param platform Name of the platform
    @param property Name of the property
    @param default the fallback value if none is found, but the target exists
    @return property value, None if property not found

    """
    with suppress(KeyError):
        return targets[platform][property]
    with suppress(KeyError):
        for inherited_target in targets[platform]['inherits']:
            result = _platform_property_from_targets_json(targets, inherited_target, property, None)
            if result:
                return result
    if platform in targets:
        return default

IGNORED_DIRS = ['.build', 'BUILD', 'tools']

def _find_targets_json(path):
    for root, dirs, files in walk(path, followlinks=True):
        for ignored_dir in IGNORED_DIRS:
            if ignored_dir in dirs:
                dirs.remove(ignored_dir)
        if 'targets.json' in files:
            yield os.path.join(root, 'targets.json')

def _get_platform_property_from_targets(platform, property, default):
    """
    Load properties from targets.json file somewhere in the project structure

    :param platform:
    :return: property value, None if property not found
    """
    for targets_path in _find_targets_json(os.getcwd()):
        with suppress(IOError, ValueError):
            with open(targets_path, 'r') as f:
                targets = json.load(f)
                result = _platform_property_from_targets_json(targets, platform, property, default)
                if result:
                    return result
