# Copyright (c) 2018 Open Source Foundries Limited.
#
# SPDX-License-Identifier: Apache-2.0

'''Deprecated; do not use.

Preserved for Zephyr v1.14 LTS compatibility only. This should never
have been part of west, and will be removed when Zephyr v1.14 is
obsoleted.
'''

from collections import OrderedDict
import os.path
import re
import subprocess
import shutil
import warnings

from west import log
from west.util import quote_sh_list

# This has no effect by default from 'west build' unless explicitly
# enabled, e.g. with PYTHONWARNINGS.
warnings.warn(
    'west.cmake was deprecated after west v0.6, and will be removed after '
    'Zephyr v1.14 is obsoleted',
    DeprecationWarning)

__all__ = ['run_cmake', 'run_build',
           'make_c_identifier',
           'CMakeCacheEntry', 'CMakeCache']

DEFAULT_CACHE = 'CMakeCache.txt'

def run_cmake(args, quiet=False):
    '''Run cmake to (re)generate a build system'''
    cmake = shutil.which('cmake')
    if cmake is None:
        log.die('CMake is not installed or cannot be found; cannot build.')
    cmd = [cmake] + args
    kwargs = dict()
    if quiet:
        kwargs['stdout'] = subprocess.DEVNULL
        kwargs['stderr'] = subprocess.STDOUT
    log.dbg('Running CMake:', cmd, level=log.VERBOSE_VERY)
    log.dbg('As command:', quote_sh_list(cmd), level=log.VERBOSE_VERY)
    subprocess.check_call(cmd, **kwargs)

def run_build(build_directory, extra_args=(), quiet=False):
    '''Run cmake in build tool mode in `build_directory`'''
    run_cmake(['--build', build_directory] + list(extra_args), quiet=quiet)

def make_c_identifier(string):
    '''Make a C identifier from a string in the same way CMake does.
    '''
    # The behavior of CMake's string(MAKE_C_IDENTIFIER ...)  is not
    # precisely documented. This behavior matches the test case
    # that introduced the function:
    #
    # https://gitlab.kitware.com/cmake/cmake/commit/0ab50aea4c4d7099b339fb38b4459d0debbdbd85
    ret = []

    alpha_under = re.compile('[A-Za-z_]')
    alpha_num_under = re.compile('[A-Za-z0-9_]')

    if not alpha_under.match(string):
        ret.append('_')
    for c in string:
        if alpha_num_under.match(c):
            ret.append(c)
        else:
            ret.append('_')

    return ''.join(ret)

class CMakeCacheEntry:
    '''Represents a CMake cache entry.

    This class understands the type system in a CMakeCache.txt, and
    converts the following cache types to Python types:

    Cache Type    Python type
    ----------    -------------------------------------------
    FILEPATH      str
    PATH          str
    STRING        str OR list of str (if ';' is in the value)
    BOOL          bool
    INTERNAL      str OR list of str (if ';' is in the value)
    ----------    -------------------------------------------
    '''

    # Regular expression for a cache entry.
    #
    # CMake variable names can include escape characters, allowing a
    # wider set of names than is easy to match with a regular
    # expresion. To be permissive here, use a non-greedy match up to
    # the first colon (':'). This breaks if the variable name has a
    # colon inside, but it's good enough.
    CACHE_ENTRY = re.compile(
        r'''(?P<name>.*?)                               # name
         :(?P<type>FILEPATH|PATH|STRING|BOOL|INTERNAL)  # type
         =(?P<value>.*)                                 # value
        ''', re.X)

    @classmethod
    def _to_bool(cls, val):
        # Convert a CMake BOOL string into a Python bool.
        #
        #   "True if the constant is 1, ON, YES, TRUE, Y, or a
        #   non-zero number. False if the constant is 0, OFF, NO,
        #   FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in
        #   the suffix -NOTFOUND. Named boolean constants are
        #   case-insensitive. If the argument is not one of these
        #   constants, it is treated as a variable."
        #
        # https://cmake.org/cmake/help/v3.0/command/if.html
        val = val.upper()
        if val in ('ON', 'YES', 'TRUE', 'Y'):
            return True
        elif val in ('OFF', 'NO', 'FALSE', 'N', 'IGNORE', 'NOTFOUND', ''):
            return False
        elif val.endswith('-NOTFOUND'):
            return False
        else:
            try:
                v = int(val)
                return v != 0
            except ValueError as exc:
                raise ValueError(f'invalid bool {val}') from exc

    @classmethod
    def from_line(cls, line, line_no):
        # Comments can only occur at the beginning of a line.
        # (The value of an entry could contain a comment character).
        if line.startswith('//') or line.startswith('#'):
            return None

        # Whitespace-only lines do not contain cache entries.
        if not line.strip():
            return None

        m = cls.CACHE_ENTRY.match(line)
        if not m:
            return None

        name, type_, value = (m.group(g) for g in ('name', 'type', 'value'))
        if type_ == 'BOOL':
            try:
                value = cls._to_bool(value)
            except ValueError as exc:
                args = exc.args + (f'on line {line_no}: {line}',)
                raise ValueError(args) from exc
        elif type_ == 'STRING' or type_ == 'INTERNAL':
            # If the value is a CMake list (i.e. is a string which
            # contains a ';'), convert to a Python list.
            if ';' in value:
                value = value.split(';')

        return CMakeCacheEntry(name, value)

    def __init__(self, name, value):
        self.name = name
        self.value = value

    def __str__(self):
        return f'CMakeCacheEntry(name={self.name}, value={self.value})'

class CMakeCache:
    '''Parses and represents a CMake cache file.'''

    @staticmethod
    def from_build_dir(build_dir):
        return CMakeCache(os.path.join(build_dir, DEFAULT_CACHE))

    def __init__(self, cache_file):
        self.cache_file = cache_file
        self.load(cache_file)

    def load(self, cache_file):
        entries = []
        with open(cache_file, 'r', encoding='utf-8') as cache:
            for line_no, line in enumerate(cache):
                entry = CMakeCacheEntry.from_line(line, line_no)
                if entry:
                    entries.append(entry)
        self._entries = OrderedDict((e.name, e) for e in entries)

    def get(self, name, default=None):
        entry = self._entries.get(name)
        if entry is not None:
            return entry.value
        else:
            return default

    def get_list(self, name, default=None):
        if default is None:
            default = []
        entry = self._entries.get(name)
        if entry is not None:
            value = entry.value
            if isinstance(value, list):
                return value
            elif isinstance(value, str):
                return [value] if value else []
            else:
                raise RuntimeError(f'invalid value {value} type {type(value)}')
        else:
            return default

    def __contains__(self, name):
        return name in self._entries

    def __getitem__(self, name):
        return self._entries[name].value

    def __setitem__(self, name, entry):
        if not isinstance(entry, CMakeCacheEntry):
            raise TypeError(f'improper type {type(entry)} for value {entry}, '
                            'expecting CMakeCacheEntry')
        self._entries[name] = entry

    def __delitem__(self, name):
        del self._entries[name]

    def __iter__(self):
        return iter(self._entries.values())
