# Copyright 2022 The Pigweed Authors
#
# 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
#
#     https://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.
"""Creates a new Pigweed module."""

import abc
import argparse
import dataclasses
from dataclasses import dataclass
import datetime
import logging
import os
from pathlib import Path
import re
import sys

from typing import Any, Dict, Iterable, List, Optional, Type, Union

from pw_build import generate_modules_lists

_LOG = logging.getLogger(__name__)

_PIGWEED_LICENSE = f"""
# Copyright {datetime.datetime.now().year} The Pigweed Authors
#
# 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
#
#     https://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.""".lstrip()

_PIGWEED_LICENSE_CC = _PIGWEED_LICENSE.replace('#', '//')


# TODO(frolv): Adapted from pw_protobuf. Consolidate them.
class _OutputFile:
    DEFAULT_INDENT_WIDTH = 2

    def __init__(self, file: Path, indent_width: int = DEFAULT_INDENT_WIDTH):
        self._file = file
        self._content: List[str] = []
        self._indent_width: int = indent_width
        self._indentation = 0

    def line(self, line: str = '') -> None:
        if line:
            self._content.append(' ' * self._indentation)
            self._content.append(line)
        self._content.append('\n')

    def indent(
        self,
        width: Optional[int] = None,
    ) -> '_OutputFile._IndentationContext':
        """Increases the indentation level of the output."""
        return self._IndentationContext(
            self, width if width is not None else self._indent_width
        )

    @property
    def path(self) -> Path:
        return self._file

    @property
    def content(self) -> str:
        return ''.join(self._content)

    def write(self) -> None:
        print('  create  ' + str(self._file.relative_to(Path.cwd())))
        self._file.write_text(self.content)

    class _IndentationContext:
        """Context that increases the output's indentation when it is active."""

        def __init__(self, output: '_OutputFile', width: int):
            self._output = output
            self._width: int = width

        def __enter__(self):
            self._output._indentation += self._width

        def __exit__(self, typ, value, traceback):
            self._output._indentation -= self._width


class _ModuleName:
    _MODULE_NAME_REGEX = '(^[a-zA-Z]{2,})((_[a-zA-Z0-9]+)+)$'

    def __init__(self, prefix: str, main: str) -> None:
        self._prefix = prefix
        self._main = main

    @property
    def full(self) -> str:
        return f'{self._prefix}_{self._main}'

    @property
    def prefix(self) -> str:
        return self._prefix

    @property
    def main(self) -> str:
        return self._main

    @property
    def default_namespace(self) -> str:
        return f'{self._prefix}::{self._main}'

    def upper_camel_case(self) -> str:
        return ''.join(s.capitalize() for s in self._main.split('_'))

    def __str__(self) -> str:
        return self.full

    def __repr__(self) -> str:
        return self.full

    @classmethod
    def parse(cls, name: str) -> Optional['_ModuleName']:
        match = re.fullmatch(_ModuleName._MODULE_NAME_REGEX, name)
        if not match:
            return None

        return cls(match.group(1), match.group(2)[1:])


@dataclass
class _ModuleContext:
    name: _ModuleName
    dir: Path
    root_build_files: List['_BuildFile']
    sub_build_files: List['_BuildFile']
    build_systems: List[str]
    is_upstream: bool

    def build_files(self) -> Iterable['_BuildFile']:
        yield from self.root_build_files
        yield from self.sub_build_files

    def add_docs_file(self, file: Path):
        for build_file in self.root_build_files:
            build_file.add_docs_source(str(file.relative_to(self.dir)))

    def add_cc_target(self, target: '_BuildFile.CcTarget') -> None:
        for build_file in self.root_build_files:
            build_file.add_cc_target(target)

    def add_cc_test(self, target: '_BuildFile.CcTarget') -> None:
        for build_file in self.root_build_files:
            build_file.add_cc_test(target)


class _BuildFile:
    """Abstract representation of a build file for a module."""

    @dataclass
    class Target:
        name: str

        # TODO(frolv): Shouldn't be a string list as that's build system
        # specific. Figure out a way to resolve dependencies from targets.
        deps: List[str] = dataclasses.field(default_factory=list)

    @dataclass
    class CcTarget(Target):
        sources: List[Path] = dataclasses.field(default_factory=list)
        headers: List[Path] = dataclasses.field(default_factory=list)

        def rebased_sources(self, rebase_path: Path) -> Iterable[str]:
            return (str(src.relative_to(rebase_path)) for src in self.sources)

        def rebased_headers(self, rebase_path: Path) -> Iterable[str]:
            return (str(hdr.relative_to(rebase_path)) for hdr in self.headers)

    def __init__(self, path: Path, ctx: _ModuleContext):
        self._path = path
        self._ctx = ctx

        self._docs_sources: List[str] = []
        self._cc_targets: List[_BuildFile.CcTarget] = []
        self._cc_tests: List[_BuildFile.CcTarget] = []

    @property
    def path(self) -> Path:
        return self._path

    @property
    def dir(self) -> Path:
        return self._path.parent

    def add_docs_source(self, filename: str) -> None:
        self._docs_sources.append(filename)

    def add_cc_target(self, target: CcTarget) -> None:
        self._cc_targets.append(target)

    def add_cc_test(self, target: CcTarget) -> None:
        self._cc_tests.append(target)

    def write(self) -> None:
        """Writes the contents of the build file to disk."""
        file = _OutputFile(self._path, self._indent_width())

        if self._ctx.is_upstream:
            file.line(_PIGWEED_LICENSE)
            file.line()

        self._write_preamble(file)

        for target in self._cc_targets:
            file.line()
            self._write_cc_target(file, target)

        for target in self._cc_tests:
            file.line()
            self._write_cc_test(file, target)

        if self._docs_sources:
            file.line()
            self._write_docs_target(file, self._docs_sources)

        file.write()

    @abc.abstractmethod
    def _indent_width(self) -> int:
        """Returns the default indent width for the build file's code style."""

    @abc.abstractmethod
    def _write_preamble(self, file: _OutputFile) -> None:
        """Formats"""

    @abc.abstractmethod
    def _write_cc_target(
        self,
        file: _OutputFile,
        target: '_BuildFile.CcTarget',
    ) -> None:
        """Defines a C++ library target within the build file."""

    @abc.abstractmethod
    def _write_cc_test(
        self,
        file: _OutputFile,
        target: '_BuildFile.CcTarget',
    ) -> None:
        """Defines a C++ unit test target within the build file."""

    @abc.abstractmethod
    def _write_docs_target(
        self,
        file: _OutputFile,
        docs_sources: List[str],
    ) -> None:
        """Defines a documentation target within the build file."""


# TODO(frolv): The Dict here should be Dict[str, '_GnVal'] (i.e. _GnScope),
# but mypy does not yet support recursive types:
# https://github.com/python/mypy/issues/731
_GnVal = Union[bool, int, str, List[str], Dict[str, Any]]
_GnScope = Dict[str, _GnVal]


class _GnBuildFile(_BuildFile):
    _DEFAULT_FILENAME = 'BUILD.gn'
    _INCLUDE_CONFIG_TARGET = 'public_include_path'

    def __init__(
        self,
        directory: Path,
        ctx: _ModuleContext,
        filename: str = _DEFAULT_FILENAME,
    ):
        super().__init__(directory / filename, ctx)

    def _indent_width(self) -> int:
        return 2

    def _write_preamble(self, file: _OutputFile) -> None:
        # Upstream modules always require a tests target, even if it's empty.
        has_tests = len(self._cc_tests) > 0 or self._ctx.is_upstream

        imports = []

        if self._cc_targets:
            imports.append('$dir_pw_build/target_types.gni')

        if has_tests:
            imports.append('$dir_pw_unit_test/test.gni')

        if self._docs_sources:
            imports.append('$dir_pw_docgen/docs.gni')

        file.line('import("//build_overrides/pigweed.gni")\n')
        for imp in sorted(imports):
            file.line(f'import("{imp}")')

        if self._cc_targets:
            file.line()
            _GnBuildFile._target(
                file,
                'config',
                _GnBuildFile._INCLUDE_CONFIG_TARGET,
                {
                    'include_dirs': ['public'],
                    'visibility': [':*'],
                },
            )

        if has_tests:
            file.line()
            _GnBuildFile._target(
                file,
                'pw_test_group',
                'tests',
                {
                    'tests': list(f':{test.name}' for test in self._cc_tests),
                },
            )

    def _write_cc_target(
        self,
        file: _OutputFile,
        target: _BuildFile.CcTarget,
    ) -> None:
        """Defines a GN source_set for a C++ target."""

        target_vars: _GnScope = {}

        if target.headers:
            target_vars['public_configs'] = [
                f':{_GnBuildFile._INCLUDE_CONFIG_TARGET}'
            ]
            target_vars['public'] = list(target.rebased_headers(self.dir))

        if target.sources:
            target_vars['sources'] = list(target.rebased_sources(self.dir))

        if target.deps:
            target_vars['deps'] = target.deps

        _GnBuildFile._target(file, 'pw_source_set', target.name, target_vars)

    def _write_cc_test(
        self,
        file: _OutputFile,
        target: '_BuildFile.CcTarget',
    ) -> None:
        _GnBuildFile._target(
            file,
            'pw_test',
            target.name,
            {
                'sources': list(target.rebased_sources(self.dir)),
                'deps': target.deps,
            },
        )

    def _write_docs_target(
        self,
        file: _OutputFile,
        docs_sources: List[str],
    ) -> None:
        """Defines a pw_doc_group for module documentation."""
        _GnBuildFile._target(
            file,
            'pw_doc_group',
            'docs',
            {
                'sources': docs_sources,
            },
        )

    @staticmethod
    def _target(
        file: _OutputFile,
        target_type: str,
        name: str,
        args: _GnScope,
    ) -> None:
        """Formats a GN target."""

        file.line(f'{target_type}("{name}") {{')

        with file.indent():
            _GnBuildFile._format_gn_scope(file, args)

        file.line('}')

    @staticmethod
    def _format_gn_scope(file: _OutputFile, scope: _GnScope) -> None:
        """Formats all of the variables within a GN scope to a file.

        This function does not write the enclosing braces of the outer scope to
        support use from multiple formatting contexts.
        """
        for key, val in scope.items():
            if isinstance(val, int):
                file.line(f'{key} = {val}')
                continue

            if isinstance(val, str):
                file.line(f'{key} = {_GnBuildFile._gn_string(val)}')
                continue

            if isinstance(val, bool):
                file.line(f'{key} = {str(val).lower()}')
                continue

            if isinstance(val, dict):
                file.line(f'{key} = {{')
                with file.indent():
                    _GnBuildFile._format_gn_scope(file, val)
                file.line('}')
                continue

            # Format a list of strings.
            # TODO(frolv): Lists of other types?
            assert isinstance(val, list)

            if not val:
                file.line(f'{key} = []')
                continue

            if len(val) == 1:
                file.line(f'{key} = [ {_GnBuildFile._gn_string(val[0])} ]')
                continue

            file.line(f'{key} = [')
            with file.indent():
                for string in sorted(val):
                    file.line(f'{_GnBuildFile._gn_string(string)},')
            file.line(']')

    @staticmethod
    def _gn_string(string: str) -> str:
        """Converts a Python string into a string literal within a GN file.

        Accounts for the possibility of variable interpolation within GN,
        removing quotes if unnecessary:

            "string"           ->  "string"
            "string"           ->  "string"
            "$var"             ->  var
            "$var2"            ->  var2
            "$3var"            ->  "$3var"
            "$dir_pw_foo"      ->  dir_pw_foo
            "$dir_pw_foo:bar"  ->  "$dir_pw_foo:bar"
            "$dir_pw_foo/baz"  ->  "$dir_pw_foo/baz"
            "${dir_pw_foo}"    ->  dir_pw_foo

        """

        # Check if the entire string refers to a interpolated variable.
        #
        # Simple case: '$' followed a single word, e.g. "$my_variable".
        # Note that identifiers can't start with a number.
        if re.fullmatch(r'^\$[a-zA-Z_]\w*$', string):
            return string[1:]

        # GN permits wrapping an interpolated variable in braces.
        # Check for strings of the format "${my_variable}".
        if re.fullmatch(r'^\$\{[a-zA-Z_]\w*\}$', string):
            return string[2:-1]

        return f'"{string}"'


class _BazelBuildFile(_BuildFile):
    _DEFAULT_FILENAME = 'BUILD.bazel'

    def __init__(
        self,
        directory: Path,
        ctx: _ModuleContext,
        filename: str = _DEFAULT_FILENAME,
    ):
        super().__init__(directory / filename, ctx)

    def _indent_width(self) -> int:
        return 4

    def _write_preamble(self, file: _OutputFile) -> None:
        imports = ['//pw_build:pigweed.bzl']
        if self._cc_targets:
            imports.append('pw_cc_library')

        if self._cc_tests:
            imports.append('pw_cc_test')

        file.line('load(')
        with file.indent():
            for imp in sorted(imports):
                file.line(f'"{imp}",')
        file.line(')\n')

        file.line('package(default_visibility = ["//visibility:public"])\n')
        file.line('licenses(["notice"])')

    def _write_cc_target(
        self,
        file: _OutputFile,
        target: _BuildFile.CcTarget,
    ) -> None:
        _BazelBuildFile._target(
            file,
            'pw_cc_library',
            target.name,
            {
                'srcs': list(target.rebased_sources(self.dir)),
                'hdrs': list(target.rebased_headers(self.dir)),
                'includes': ['public'],
            },
        )

    def _write_cc_test(
        self,
        file: _OutputFile,
        target: '_BuildFile.CcTarget',
    ) -> None:
        _BazelBuildFile._target(
            file,
            'pw_cc_test',
            target.name,
            {
                'srcs': list(target.rebased_sources(self.dir)),
                'deps': target.deps,
            },
        )

    def _write_docs_target(
        self,
        file: _OutputFile,
        docs_sources: List[str],
    ) -> None:
        file.line('# Bazel does not yet support building docs.')
        _BazelBuildFile._target(
            file, 'filegroup', 'docs', {'srcs': docs_sources}
        )

    @staticmethod
    def _target(
        file: _OutputFile,
        target_type: str,
        name: str,
        keys: Dict[str, List[str]],
    ) -> None:
        file.line(f'{target_type}(')

        with file.indent():
            file.line(f'name = "{name}",')

            for k, vals in keys.items():
                if len(vals) == 1:
                    file.line(f'{k} = ["{vals[0]}"],')
                    continue

                file.line(f'{k} = [')
                with file.indent():
                    for val in sorted(vals):
                        file.line(f'"{val}",')
                file.line('],')

        file.line(')')


class _CmakeBuildFile(_BuildFile):
    _DEFAULT_FILENAME = 'CMakeLists.txt'

    def __init__(
        self,
        directory: Path,
        ctx: _ModuleContext,
        filename: str = _DEFAULT_FILENAME,
    ):
        super().__init__(directory / filename, ctx)

    def _indent_width(self) -> int:
        return 2

    def _write_preamble(self, file: _OutputFile) -> None:
        file.line('include($ENV{PW_ROOT}/pw_build/pigweed.cmake)')

    def _write_cc_target(
        self,
        file: _OutputFile,
        target: _BuildFile.CcTarget,
    ) -> None:
        if target.name == self._ctx.name.full:
            target_name = target.name
        else:
            target_name = f'{self._ctx.name.full}.{target.name}'

        _CmakeBuildFile._target(
            file,
            'pw_add_module_library',
            target_name,
            {
                'sources': list(target.rebased_sources(self.dir)),
                'headers': list(target.rebased_headers(self.dir)),
                'public_includes': ['public'],
            },
        )

    def _write_cc_test(
        self,
        file: _OutputFile,
        target: '_BuildFile.CcTarget',
    ) -> None:
        _CmakeBuildFile._target(
            file,
            'pw_auto_add_module_tests',
            self._ctx.name.full,
            {'private_deps': []},
        )

    def _write_docs_target(
        self,
        file: _OutputFile,
        docs_sources: List[str],
    ) -> None:
        file.line('# CMake does not yet support building docs.')

    @staticmethod
    def _target(
        file: _OutputFile,
        target_type: str,
        name: str,
        keys: Dict[str, List[str]],
    ) -> None:
        file.line(f'{target_type}({name}')

        with file.indent():
            for k, vals in keys.items():
                file.line(k.upper())
                with file.indent():
                    for val in sorted(vals):
                        file.line(val)

        file.line(')')


class _LanguageGenerator:
    """Generates files for a programming language in a new Pigweed module."""

    def __init__(self, ctx: _ModuleContext) -> None:
        self._ctx = ctx

    @abc.abstractmethod
    def create_source_files(self) -> None:
        """Creates the boilerplate source files required by the language."""


class _CcLanguageGenerator(_LanguageGenerator):
    """Generates boilerplate source files for a C++ module."""

    def __init__(self, ctx: _ModuleContext) -> None:
        super().__init__(ctx)

        self._public_dir = ctx.dir / 'public'
        self._headers_dir = self._public_dir / ctx.name.full

    def create_source_files(self) -> None:
        self._headers_dir.mkdir(parents=True)

        main_header = self._new_header(self._ctx.name.main)
        main_source = self._new_source(self._ctx.name.main)
        test_source = self._new_source(f'{self._ctx.name.main}_test')

        # TODO(frolv): This could be configurable.
        namespace = self._ctx.name.default_namespace

        main_source.line(
            f'#include "{main_header.path.relative_to(self._public_dir)}"\n'
        )
        main_source.line(f'namespace {namespace} {{\n')
        main_source.line('int magic = 42;\n')
        main_source.line(f'}}  // namespace {namespace}')

        main_header.line(f'namespace {namespace} {{\n')
        main_header.line('extern int magic;\n')
        main_header.line(f'}}  // namespace {namespace}')

        test_source.line(
            f'#include "{main_header.path.relative_to(self._public_dir)}"\n'
        )
        test_source.line('#include "gtest/gtest.h"\n')
        test_source.line(f'namespace {namespace} {{')
        test_source.line('namespace {\n')
        test_source.line(
            f'TEST({self._ctx.name.upper_camel_case()}, GeneratesCorrectly) {{'
        )
        with test_source.indent():
            test_source.line('EXPECT_EQ(magic, 42);')
        test_source.line('}\n')
        test_source.line('}  // namespace')
        test_source.line(f'}}  // namespace {namespace}')

        self._ctx.add_cc_target(
            _BuildFile.CcTarget(
                name=self._ctx.name.full,
                sources=[main_source.path],
                headers=[main_header.path],
            )
        )

        self._ctx.add_cc_test(
            _BuildFile.CcTarget(
                name=f'{self._ctx.name.main}_test',
                deps=[f':{self._ctx.name.full}'],
                sources=[test_source.path],
            )
        )

        main_header.write()
        main_source.write()
        test_source.write()

    def _new_source(self, name: str) -> _OutputFile:
        file = _OutputFile(self._ctx.dir / f'{name}.cc')

        if self._ctx.is_upstream:
            file.line(_PIGWEED_LICENSE_CC)
            file.line()

        return file

    def _new_header(self, name: str) -> _OutputFile:
        file = _OutputFile(self._headers_dir / f'{name}.h')

        if self._ctx.is_upstream:
            file.line(_PIGWEED_LICENSE_CC)

        file.line('#pragma once\n')
        return file


_BUILD_FILES: Dict[str, Type[_BuildFile]] = {
    'bazel': _BazelBuildFile,
    'cmake': _CmakeBuildFile,
    'gn': _GnBuildFile,
}

_LANGUAGE_GENERATORS: Dict[str, Type[_LanguageGenerator]] = {
    'cc': _CcLanguageGenerator,
}


def _check_module_name(
    module: str,
    is_upstream: bool,
) -> Optional[_ModuleName]:
    """Checks whether a module name is valid."""

    name = _ModuleName.parse(module)
    if not name:
        _LOG.error(
            '"%s" does not conform to the Pigweed module name format', module
        )
        return None

    if is_upstream and name.prefix != 'pw':
        _LOG.error('Modules within Pigweed itself must start with "pw_"')
        return None

    return name


def _create_main_docs_file(ctx: _ModuleContext) -> None:
    """Populates the top-level docs.rst file within a new module."""

    docs_file = _OutputFile(ctx.dir / 'docs.rst')
    docs_file.line(f'.. _module-{ctx.name}:\n')

    title = '=' * len(ctx.name.full)
    docs_file.line(title)
    docs_file.line(ctx.name.full)
    docs_file.line(title)
    docs_file.line(f'This is the main documentation file for {ctx.name}.')

    ctx.add_docs_file(docs_file.path)

    docs_file.write()


def _basic_module_setup(
    module_name: _ModuleName,
    module_dir: Path,
    build_systems: Iterable[str],
    is_upstream: bool,
) -> _ModuleContext:
    """Creates the basic layout of a Pigweed module."""
    module_dir.mkdir()

    ctx = _ModuleContext(
        name=module_name,
        dir=module_dir,
        root_build_files=[],
        sub_build_files=[],
        build_systems=list(build_systems),
        is_upstream=is_upstream,
    )

    ctx.root_build_files.extend(
        _BUILD_FILES[build](module_dir, ctx) for build in ctx.build_systems
    )

    _create_main_docs_file(ctx)

    return ctx


def _create_module(
    module: str, languages: Iterable[str], build_systems: Iterable[str]
) -> None:
    project_root = Path(os.environ.get('PW_PROJECT_ROOT', ''))
    assert project_root.is_dir()

    is_upstream = os.environ.get('PW_ROOT') == str(project_root)

    module_name = _check_module_name(module, is_upstream)
    if not module_name:
        sys.exit(1)

    if not is_upstream:
        _LOG.error(
            '`pw module create` is experimental and does '
            'not yet support downstream projects.'
        )
        sys.exit(1)

    module_dir = project_root / module

    if module_dir.is_dir():
        _LOG.error('Module %s already exists', module)
        sys.exit(1)

    if module_dir.is_file():
        _LOG.error(
            'Cannot create module %s as a file of that name already exists',
            module,
        )
        sys.exit(1)

    ctx = _basic_module_setup(
        module_name, module_dir, build_systems, is_upstream
    )

    try:
        generators = list(_LANGUAGE_GENERATORS[lang](ctx) for lang in languages)
    except KeyError as key:
        _LOG.error('Unsupported language: %s', key)
        sys.exit(1)

    for generator in generators:
        generator.create_source_files()

    for build_file in ctx.build_files():
        build_file.write()

    if is_upstream:
        modules_file = project_root / 'PIGWEED_MODULES'
        if not modules_file.exists():
            _LOG.error(
                'Could not locate PIGWEED_MODULES file; '
                'your repository may be in a bad state.'
            )
            return

        modules_gni_file = (
            project_root / 'pw_build' / 'generated_pigweed_modules_lists.gni'
        )

        # Cut off the extra newline at the end of the file.
        modules_list = modules_file.read_text().split('\n')[:-1]
        modules_list.append(module_name.full)
        modules_list.sort()
        modules_list.append('')
        modules_file.write_text('\n'.join(modules_list))
        print('  modify  ' + str(modules_file.relative_to(Path.cwd())))

        generate_modules_lists.main(
            root=project_root,
            modules_list=modules_file,
            modules_gni_file=modules_gni_file,
            mode=generate_modules_lists.Mode.UPDATE,
        )
        print('  modify  ' + str(modules_gni_file.relative_to(Path.cwd())))

    print()
    _LOG.info(
        'Module %s created at %s',
        module_name,
        module_dir.relative_to(Path.cwd()),
    )


def register_subcommand(parser: argparse.ArgumentParser) -> None:
    csv = lambda s: s.split(',')

    parser.add_argument(
        '--build-systems',
        help=(
            'Comma-separated list of build systems the module supports. '
            f'Options: {", ".join(_BUILD_FILES.keys())}'
        ),
        type=csv,
        default=_BUILD_FILES.keys(),
        metavar='BUILD[,BUILD,...]',
    )
    parser.add_argument(
        '--languages',
        help=(
            'Comma-separated list of languages the module will use. '
            f'Options: {", ".join(_LANGUAGE_GENERATORS.keys())}'
        ),
        type=csv,
        default=[],
        metavar='LANG[,LANG,...]',
    )
    parser.add_argument(
        'module', help='Name of the module to create.', metavar='MODULE_NAME'
    )
    parser.set_defaults(func=_create_module)
