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

'''West's bootstrap/wrapper script.
'''

import argparse
import os
import platform
import subprocess
import sys

import west._bootstrap.version as version

if sys.version_info < (3,):
    sys.exit('fatal error: you are running Python 2')


#
# Special files and directories in the west installation.
#
# These are given variable names for clarity, but they can't be
# changed without propagating the changes into west itself.
#

# Top-level west directory, containing west itself and the manifest.
WEST_DIR = 'west'
# Subdirectory to check out the west source repository into.
WEST = 'west'
# Default west repository URL.
WEST_DEFAULT = 'https://github.com/zephyrproject-rtos/west'
# Default revision to check out of the west repository.
WEST_REV_DEFAULT = 'master'
# File inside of WEST_DIR which marks it as the top level of the
# Zephyr project installation.
#
# (The WEST_DIR name is not distinct enough to use when searching for
# the top level; other directories named "west" may exist elsewhere,
# e.g. zephyr/doc/west.)
WEST_MARKER = '.west_topdir'

# Manifest repository directory under WEST_DIR.
MANIFEST = 'manifest'
# Default manifest repository URL.
MANIFEST_DEFAULT = 'https://github.com/zephyrproject-rtos/manifest'
# Default revision to check out of the manifest repository.
MANIFEST_REV_DEFAULT = 'master'


#
# Helpers shared between init and wrapper mode
#


class WestError(RuntimeError):
    pass


class WestNotFound(WestError):
    '''Neither the current directory nor any parent has a West installation.'''


def find_west_topdir(start):
    '''Find the top-level installation directory, starting at ``start``.

    If none is found, raises WestNotFound.'''
    # If you change this function, make sure to update west.util.west_topdir().

    cur_dir = start

    while True:
        if os.path.isfile(os.path.join(cur_dir, WEST_DIR, WEST_MARKER)):
            return cur_dir

        parent_dir = os.path.dirname(cur_dir)
        if cur_dir == parent_dir:
            # At the root
            raise WestNotFound('Could not find a West installation '
                               'in this or any parent directory')
        cur_dir = parent_dir


def clone(url, rev, dest):
    if os.path.exists(dest):
        raise WestError('refusing to clone into existing location ' + dest)

    if not url.startswith(('http:', 'https:', 'git:', 'git+shh:', 'file:')):
        raise WestError('Unknown URL scheme for repository: {}'.format(url))

    subprocess.check_call(('git', 'clone', '-b', rev, '--', url, dest))


#
# west init
#


def init(argv):
    '''Command line handler for ``west init`` invocations.

    This exits the program with a nonzero exit code if fatal errors occur.'''
    init_parser = argparse.ArgumentParser(
        prog='west init',
        description='Bootstrap initialize a Zephyr installation')
    init_parser.add_argument(
        '-b', '--base-url',
        help='''Base URL for both 'manifest' and 'zephyr' repositories; cannot
        be given if either -u or -w are''')
    init_parser.add_argument(
        '-u', '--manifest-url',
        help='Zephyr manifest fetch URL, default ' + MANIFEST_DEFAULT)
    init_parser.add_argument(
        '--mr', '--manifest-rev', default=MANIFEST_REV_DEFAULT,
        dest='manifest_rev',
        help='Manifest revision to fetch, default ' + MANIFEST_REV_DEFAULT)
    init_parser.add_argument(
        '-w', '--west-url',
        help='West fetch URL, default ' + WEST_DEFAULT)
    init_parser.add_argument(
        '--wr', '--west-rev', default=WEST_REV_DEFAULT, dest='west_rev',
        help='West revision to fetch, default ' + WEST_REV_DEFAULT)
    init_parser.add_argument(
        'directory', nargs='?', default=None,
        help='Initializes in this directory, creating it if necessary')

    args = init_parser.parse_args(args=argv)
    directory = args.directory or os.getcwd()

    if args.base_url:
        if args.manifest_url or args.west_url:
            sys.exit('fatal error: -b is incompatible with -u and -w')
        args.manifest_url = args.base_url.rstrip('/') + '/manifest'
        args.west_url = args.base_url.rstrip('/') + '/west'
    else:
        if not args.manifest_url:
            args.manifest_url = MANIFEST_DEFAULT
        if not args.west_url:
            args.west_url = WEST_DEFAULT

    try:
        topdir = find_west_topdir(directory)
        init_reinit(topdir, args)
    except WestNotFound:
        init_bootstrap(directory, args)


def hide_file(path):
    '''Ensure path is a hidden file.

    On Windows, this uses attrib to hide the file manually.

    On UNIX systems, this just checks that the path's basename begins
    with a period ('.'), for it to be hidden already. It's a fatal
    error if it does not begin with a period in this case.

    On other systems, this just prints a warning.
    '''
    system = platform.system()

    if system == 'Windows':
        subprocess.check_call(['attrib', '+H', path])
    elif os.name == 'posix':  # Try to check for all Unix, not just macOS/Linux
        if not os.path.basename(path).startswith('.'):
            sys.exit("internal error: {} can't be hidden on UNIX".format(path))
    else:
        print("warning: unknown platform {}; {} may not be hidden"
              .format(system, path), file=sys.stderr)


def init_bootstrap(directory, args):
    '''Bootstrap a new manifest + West installation in the given directory.'''
    if not os.path.isdir(directory):
        try:
            print('Initializing in new directory', directory)
            os.makedirs(directory, exist_ok=False)
        except PermissionError:
            sys.exit('Cannot initialize in {}: permission denied'.format(
                directory))
        except FileExistsError:
            sys.exit('Something else created {} concurrently; quitting'.format(
                directory))
        except Exception as e:
            sys.exit("Can't create directory {}: {}".format(
                directory, e.args))
    else:
        print('Initializing in', directory)

    # Clone the west source code and the manifest into west/. Git will create
    # the west/ directory if it does not exist.

    clone(args.west_url, args.west_rev,
          os.path.join(directory, WEST_DIR, WEST))

    clone(args.manifest_url, args.manifest_rev,
          os.path.join(directory, WEST_DIR, MANIFEST))

    # Create a dotfile to mark the installation. Hide it on Windows.

    with open(os.path.join(directory, WEST_DIR, WEST_MARKER), 'w') as f:
        hide_file(f.name)


def init_reinit(directory, args):
    # TODO
    sys.exit('Re-initializing an existing installation is not yet supported.')


#
# Wrap a West command
#

def append_to_pythonpath(directory):
    pp = os.environ.get('PYTHONPATH')
    os.environ['PYTHONPATH'] = ':'.join(([pp] if pp else []) + [directory])


def wrap(argv):
    printing_version = False

    if argv and argv[0] in ('-V', '--version'):
        print('West bootstrapper version: v{} ({})'.format(version.__version__,
                                                    os.path.dirname(__file__)))
        printing_version = True

    start = os.getcwd()
    try:
        topdir = find_west_topdir(start)
    except WestNotFound:
        if printing_version:
            sys.exit(0)         # run outside of an installation directory
        else:
            sys.exit('Error: not a Zephyr directory (or any parent): {}\n'
                     'Use "west init" to install Zephyr here'.format(start))

    west_git_repo = os.path.join(topdir, WEST_DIR, WEST)
    if printing_version:
        try:
            git_describe = subprocess.check_output(
                ['git', 'describe', '--tags'],
                stderr=subprocess.DEVNULL,
                cwd=west_git_repo).decode(sys.getdefaultencoding()).strip()
            print('West repository version: {} ({})'.format(git_describe,
                                                            west_git_repo))
        except subprocess.CalledProcessError:
            print('West repository verison: unknown; no tags were found')
        sys.exit(0)

    # Replace the wrapper process with the "real" west

    # sys.argv[1:] strips the argv[0] of the wrapper script itself
    argv = ([sys.executable,
             os.path.join(west_git_repo, 'src', 'west', 'main.py')] +
            argv)

    append_to_pythonpath(os.path.join(west_git_repo, 'src'))
    try:
        subprocess.check_call(argv)
    except subprocess.CalledProcessError as e:
        sys.exit(1)


#
# Main entry point
#


def main(wrap_argv=None):
    '''Entry point to the wrapper script.'''
    if wrap_argv is None:
        wrap_argv = sys.argv[1:]

    if not wrap_argv or wrap_argv[0] != 'init':
        wrap(wrap_argv)
    else:
        init(wrap_argv[1:])
        sys.exit(0)


if __name__ == '__main__':
    main()
