# -*- coding: utf-8 -*-
# Copyright (c) 2020-2022 Salvador E. Tropea
# Copyright (c) 2020-2022 Instituto Nacional de Tecnología Industrial
# Copyright (c) 2018 John Beard
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
# Adapted from: https://github.com/johnbeard/kiplot
"""KiBot: KiCad automation tool for documents generation

Usage:
  kibot [-b BOARD] [-e SCHEMA] [-c CONFIG] [-d OUT_DIR] [-s PRE] [-D]
         [-q | -v...] [-C | -i | -n] [-m MKFILE] [-A] [-g DEF] ...
         [-E DEF] ... [-w LIST] [TARGET...]
  kibot [-v...] [-b BOARD] [-e SCHEMA] [-c PLOT_CONFIG] [-E DEF] ... --list
  kibot [-v...] [-b BOARD] [-d OUT_DIR] [-p | -P] --example
  kibot [-v...] [--start PATH] [-d OUT_DIR] [--dry] [-t, --type TYPE]...
         --quick-start
  kibot [-v...] --help-filters
  kibot [-v...] [--markdown|--json] --help-dependencies
  kibot [-v...] --help-global-options
  kibot [-v...] --help-list-outputs
  kibot [-v...] --help-output=HELP_OUTPUT
  kibot [-v...] --help-outputs
  kibot [-v...] --help-preflights
  kibot [-v...] --help-variants
  kibot -h | --help
  kibot --version

Arguments:
  TARGET    Outputs to generate, default is all

Options:
  -A, --no-auto-download           Disable dependencies auto-download
  -b BOARD, --board-file BOARD     The PCB .kicad-pcb board file
  -c CONFIG, --plot-config CONFIG  The plotting config file to use
  -C, --cli-order                  Generate outputs using the indicated order
  -d OUT_DIR, --out-dir OUT_DIR    The output directory [default: .]
  -D, --dont-stop                  Try to continue if an output fails
  -e SCHEMA, --schematic SCHEMA    The schematic file (.sch)
  -E DEF, --define DEF             Define preprocessor value (VAR=VAL)
  -g DEF, --global-redef DEF       Overwrite a global value (VAR=VAL)
  -i, --invert-sel                 Generate the outputs not listed as targets
  -l, --list                       List available outputs (in the config file)
  -m MKFILE, --makefile MKFILE     Generate a Makefile (no targets created)
  -n, --no-priority                Don't sort targets by priority
  -p, --copy-options               Copy plot options from the PCB file
  -P, --copy-and-expand            As -p but expand the list of layers
  -q, --quiet                      Remove information logs
  -s PRE, --skip-pre PRE           Skip preflights, comma separated or `all`
  -v, --verbose                    Show debugging information
  -V, --version                    Show program's version number and exit
  -w, --no-warn LIST               Exclude the mentioned warnings (comma sep)
  -x, --example                    Create a template configuration file

Quick start options:
  --quick-start                    Generates demo config files and their outputs
  --dry                            Just generate the config files
  --start PATH                     Starting point for the search [default: .]
  -t, --type TYPE                  Generate examples only for the indicated type/s

Help options:
  -h, --help                       Show this help message and exit
  --help-dependencies              List dependencies in human readable format
  --help-filters                   List supported filters and details
  --help-global-options            List supported global variables
  --help-list-outputs              List supported outputs
  --help-output HELP_OUTPUT        Help for this particular output
  --help-outputs                   List supported outputs and details
  --help-preflights                List supported preflights and details
  --help-variants                  List supported variants and details

"""
from glob import glob
import gzip
import locale
import os
import platform
import re
import sys
from sys import path as sys_path
from . import __version__, __copyright__, __license__
# Import log first to set the domain
from . import log
log.set_domain('kibot')
logger = log.init()
from . import dep_downloader
from .docopt import docopt
# GS will import pcbnew, so we must solve the nightly setup first
# Check if we have to run the nightly KiCad build
nightly = False
if os.environ.get('KIAUS_USE_NIGHTLY'):  # pragma: no cover (nightly)
    # Path to the Python module
    pcbnew_path = '/usr/lib/kicad-nightly/lib/python3/dist-packages'
    sys_path.insert(0, pcbnew_path)
    # This helps other tools like iBoM to pick-up the right pcbnew module
    if 'PYTHONPATH' in os.environ:
        os.environ['PYTHONPATH'] += ':'+pcbnew_path
    else:
        os.environ['PYTHONPATH'] = pcbnew_path
    nightly = True
from .gs import GS
from .misc import EXIT_BAD_ARGS, W_VARCFG, NO_PCBNEW_MODULE, W_NOKIVER, hide_stderr, TRY_INSTALL_CHECK, W_ONWIN
from .pre_base import BasePreFlight
from .error import KiPlotConfigurationError, config_error
from .config_reader import (CfgYamlReader, print_outputs_help, print_output_help, print_preflights_help, create_example,
                            print_filters_help, print_global_options_help, print_dependencies, print_variants_help)
from .kiplot import (generate_outputs, load_actions, config_output, generate_makefile, generate_examples, solve_schematic,
                     solve_board_file, solve_project_file, check_board_file)
GS.kibot_version = __version__


def list_pre_and_outs(logger, outputs):
    logger.info('Available actions:\n')
    pf = BasePreFlight.get_in_use_objs()
    if len(pf):
        logger.info('Pre-flight:')
        for c in pf:
            logger.info('- '+str(c))
    if len(outputs):
        logger.info('Outputs:')
        for o in outputs:
            # Note: we can't do a `dry` config because some layer and field names can be validated only if we
            # load the schematic and the PCB.
            config_output(o, dry=False)
            logger.info('- '+str(o))


def solve_config(a_plot_config):
    plot_config = a_plot_config
    if not plot_config:
        plot_configs = glob('*.kibot.yaml')+glob('*.kiplot.yaml')+glob('*.kibot.yaml.gz')
        if len(plot_configs) == 1:
            plot_config = plot_configs[0]
            logger.info('Using config file: '+os.path.relpath(plot_config))
        elif len(plot_configs) > 1:
            plot_config = plot_configs[0]
            logger.warning(W_VARCFG + 'More than one config file found in current directory.\n'
                           '  Using '+plot_config+' if you want to use another use -c option.')
        else:
            logger.error('No config file found (*.kibot.yaml), use -c to specify one.')
            sys.exit(EXIT_BAD_ARGS)
    if not os.path.isfile(plot_config):
        logger.error("Plot config file not found: "+plot_config)
        sys.exit(EXIT_BAD_ARGS)
    logger.debug('Using configuration file: `{}`'.format(plot_config))
    return plot_config


def set_locale():
    """ Try to set the locale for all the cataegories.
        If it fails try with LC_NUMERIC (the one we need for tests). """
    try:
        locale.setlocale(locale.LC_ALL, '')
        return
    except locale.Error:
        pass
    try:
        locale.setlocale(locale.LC_NUMERIC, '')
        return
    except locale.Error:
        pass


def detect_kicad():
    try:
        import pcbnew
    except ImportError:
        logger.error("Failed to import pcbnew Python module."
                     " Is KiCad installed?"
                     " Do you need to add it to PYTHONPATH?")
        logger.error(TRY_INSTALL_CHECK)
        sys.exit(NO_PCBNEW_MODULE)
    try:
        GS.kicad_version = pcbnew.GetBuildVersion()
    except AttributeError:
        logger.warning(W_NOKIVER+"Unknown KiCad version, please install KiCad 5.1.6 or newer")
        # Assume the best case
        GS.kicad_version = '5.1.5'
    try:
        # Debian sid may 2021 mess:
        really_index = GS.kicad_version.index('really')
        GS.kicad_version = GS.kicad_version[really_index+6:]
    except ValueError:
        pass

    m = re.search(r'(\d+)\.(\d+)\.(\d+)', GS.kicad_version)
    if m is None:
        logger.error("Unable to detect KiCad version, got: `{}`".format(GS.kicad_version))
        sys.exit(NO_PCBNEW_MODULE)
    GS.kicad_version_major = int(m.group(1))
    GS.kicad_version_minor = int(m.group(2))
    GS.kicad_version_patch = int(m.group(3))
    GS.kicad_version_n = GS.kicad_version_major*1000000+GS.kicad_version_minor*1000+GS.kicad_version_patch
    GS.ki6 = GS.kicad_version_major >= 6
    GS.ki5 = GS.kicad_version_major < 6
    logger.debug('Detected KiCad v{}.{}.{} ({} {})'.format(GS.kicad_version_major, GS.kicad_version_minor,
                 GS.kicad_version_patch, GS.kicad_version, GS.kicad_version_n))
    # Used to look for plug-ins.
    # KICAD_PATH isn't good on my system.
    # The kicad-nightly package overwrites the regular package!!
    GS.kicad_share_path = '/usr/share/kicad'
    if GS.ki6:
        GS.kicad_conf_path = pcbnew.GetSettingsManager().GetUserSettingsPath()
        if nightly:
            # Nightly Debian packages uses `/usr/share/kicad-nightly/kicad-nightly.env` as an environment extension
            # This script defines KICAD_CONFIG_HOME="$HOME/.config/kicadnightly"
            # So we just patch it, as we patch the name of the binaries
            # No longer needed for 202112021512+6.0.0+rc1+287+gbb08ef2f41+deb11
            # GS.kicad_conf_path = GS.kicad_conf_path.replace('/kicad/', '/kicadnightly/')
            GS.kicad_share_path = GS.kicad_share_path.replace('/kicad/', '/kicad-nightly/')
            GS.kicad_dir = 'kicad-nightly'
        GS.pro_ext = '.kicad_pro'
        # KiCad 6 doesn't support the Rescue layer
        GS.work_layer = 'User.9'
    else:
        # Bug in KiCad (#6989), prints to stderr:
        # `../src/common/stdpbase.cpp(62): assert "traits" failed in Get(test_dir): create wxApp before calling this`
        # Found in KiCad 5.1.8, 5.1.9
        # So we temporarily suppress stderr
        with hide_stderr():
            GS.kicad_conf_path = pcbnew.GetKicadConfigPath()
        GS.pro_ext = '.pro'
        GS.work_layer = 'Rescue'
    # Dirs to look for plugins
    GS.kicad_plugins_dirs = []
    # /usr/share/kicad/*
    GS.kicad_plugins_dirs.append(os.path.join(GS.kicad_share_path, 'scripting'))
    GS.kicad_plugins_dirs.append(os.path.join(GS.kicad_share_path, 'scripting', 'plugins'))
    GS.kicad_plugins_dirs.append(os.path.join(GS.kicad_share_path, '3rdparty', 'plugins'))  # KiCad 6.0 PCM
    # ~/.config/kicad/*
    GS.kicad_plugins_dirs.append(os.path.join(GS.kicad_conf_path, 'scripting'))
    GS.kicad_plugins_dirs.append(os.path.join(GS.kicad_conf_path, 'scripting', 'plugins'))
    # ~/.kicad_plugins and ~/.kicad
    if 'HOME' in os.environ:
        home = os.environ['HOME']
        GS.kicad_plugins_dirs.append(os.path.join(home, '.kicad_plugins'))
        GS.kicad_plugins_dirs.append(os.path.join(home, '.kicad', 'scripting'))
        GS.kicad_plugins_dirs.append(os.path.join(home, '.kicad', 'scripting', 'plugins'))
        if GS.kicad_version_major >= 6:
            ver_dir = str(GS.kicad_version_major)+'.'+str(GS.kicad_version_minor)
            local_share = os.path.join(home, '.local', 'share', 'kicad', ver_dir)
            GS.kicad_plugins_dirs.append(os.path.join(local_share, 'scripting'))
            GS.kicad_plugins_dirs.append(os.path.join(local_share, 'scripting', 'plugins'))
            GS.kicad_plugins_dirs.append(os.path.join(local_share, '3rdparty', 'plugins'))   # KiCad 6.0 PCM
    if GS.debug_level > 1:
        logger.debug('KiCad config path {}'.format(GS.kicad_conf_path))


def parse_defines(args):
    for define in args.define:
        if '=' not in define:
            logger.error('Malformed `define` option, must be VARIABLE=VALUE ({})'.format(define))
            sys.exit(EXIT_BAD_ARGS)
        var = define.split('=')[0]
        GS.cli_defines[var] = define[len(var)+1:]


def parse_global_redef(args):
    for redef in args.global_redef:
        if '=' not in redef:
            logger.error('Malformed global-redef option, must be VARIABLE=VALUE ({})'.format(redef))
            sys.exit(EXIT_BAD_ARGS)
        var = redef.split('=')[0]
        GS.cli_global_defs[var] = redef[len(var)+1:]


class SimpleFilter(object):
    def __init__(self, num):
        self.number = num
        self.regex = re.compile('')
        self.error = ''


def apply_warning_filter(args):
    if args.no_warn:
        try:
            log.set_filters([SimpleFilter(int(n)) for n in args.no_warn.split(',')])
        except ValueError:
            logger.error('-w/--no-warn must specify a comma separated list of numbers ({})'.format(args.no_warn))
            sys.exit(EXIT_BAD_ARGS)


def debug_arguments(args):
    if GS.debug_level > 1:
        logger.debug('Command line arguments:\n'+str(sys.argv))
        logger.debug('Command line parsed:\n'+str(args))


def detect_windows():
    if platform.system() != 'Windows':
        return
    # Note: We assume this is the Python from KiCad, but we should check it ...
    GS.on_windows = True
    logger.warning(W_ONWIN+'Running on Windows, this is experimental, please report any problem')


def main():
    set_locale()
    ver = 'KiBot '+__version__+' - '+__copyright__+' - License: '+__license__
    GS.out_dir_in_cmd_line = '-d' in sys.argv or '--out-dir' in sys.argv
    args = docopt(__doc__, version=ver, options_first=True)

    # Set the specified verbosity
    GS.debug_enabled = log.set_verbosity(logger, args.verbose, args.quiet)
    log.debug_level = GS.debug_level = args.verbose
    logger.debug('KiBot {} verbose level: {}'.format(__version__, args.verbose))
    apply_warning_filter(args)
    # Now we have the debug level set we can check (and optionally inform) KiCad info
    detect_kicad()
    detect_windows()
    debug_arguments(args)

    # Force iBoM to avoid the use of graphical stuff
    os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = 'True'

    # Parse global overwrite options
    parse_global_redef(args)

    # Disable auto-download if needed
    if args.no_auto_download:
        dep_downloader.disable_auto_download = True

    # Output dir: relative to CWD (absolute path overrides)
    GS.out_dir = os.path.join(os.getcwd(), args.out_dir)

    # Load output and preflight plugins
    load_actions()

    if args.help_outputs or args.help_list_outputs:
        print_outputs_help(details=args.help_outputs)
        sys.exit(0)
    if args.help_output:
        print_output_help(args.help_output)
        sys.exit(0)
    if args.help_preflights:
        print_preflights_help()
        sys.exit(0)
    if args.help_variants:
        print_variants_help()
        sys.exit(0)
    if args.help_filters:
        print_filters_help()
        sys.exit(0)
    if args.help_global_options:
        print_global_options_help()
        sys.exit(0)
    if args.help_dependencies:
        print_dependencies(args.markdown, args.json)
        sys.exit(0)
    if args.example:
        check_board_file(args.board_file)
        if args.copy_options and not args.board_file:
            logger.error('Asked to copy options but no PCB specified.')
            sys.exit(EXIT_BAD_ARGS)
        create_example(args.board_file, GS.out_dir, args.copy_options, args.copy_and_expand)
        sys.exit(0)
    if args.quick_start:
        # Some kind of wizard to get usable examples
        generate_examples(args.start, args.dry, args.type)
        sys.exit(0)

    # Determine the YAML file
    plot_config = solve_config(args.plot_config)
    # Determine the SCH file
    GS.set_sch(solve_schematic('.', args.schematic, args.board_file, plot_config))
    # Determine the PCB file
    GS.set_pcb(solve_board_file('.', args.board_file))
    # Determine the project file
    GS.set_pro(solve_project_file())

    # Parse preprocessor defines
    parse_defines(args)

    # Read the config file
    cr = CfgYamlReader()
    outputs = None
    try:
        # The Python way ...
        with gzip.open(plot_config) as cf_file:
            try:
                outputs = cr.read(cf_file)
            except KiPlotConfigurationError as e:
                config_error(str(e))
    except OSError:
        pass
    if outputs is None:
        with open(plot_config) as cf_file:
            try:
                outputs = cr.read(cf_file)
            except KiPlotConfigurationError as e:
                config_error(str(e))

    # Is just "list the available targets"?
    if args.list:
        list_pre_and_outs(logger, outputs)
        sys.exit(0)

    if args.makefile:
        # Only create a makefile
        generate_makefile(args.makefile, plot_config, outputs)
    else:
        # Do all the job (preflight + outputs)
        generate_outputs(outputs, args.target, args.invert_sel, args.skip_pre, args.cli_order, args.no_priority,
                         dont_stop=args.dont_stop)
    # Print total warnings
    logger.log_totals()


if __name__ == "__main__":
    main()  # pragma: no cover
