# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 Salvador E. Tropea
# Copyright (c) 2020-2023 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...] [-L LOGFILE] [-C | -i | -n] [-m MKFILE] [-A] [-g DEF] ...
         [-E DEF] ... [-w LIST] [--banner N] [TARGET...]
  kibot [-v...] [-b BOARD] [-e SCHEMA] [-c PLOT_CONFIG] [--banner N]
         [-E DEF] ... --list
  kibot [-v...] [-b BOARD] [-d OUT_DIR] [-p | -P] [--banner N] --example
  kibot [-v...] [--start PATH] [-d OUT_DIR] [--dry] [--banner N]
         [-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 [-v...] --help-banners
  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
  --banner N                       Display banner number N (-1 == random)
  -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/.kicad_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)
  -L, --log LOGFILE                Log to LOGFILE using maximum debug level.
                                   Is independent of what is logged to stderr
  -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-banners                   Show all available banners
  --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 datetime import datetime
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 .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'] += os.pathsep+pcbnew_path
    else:
        os.environ['PYTHONPATH'] = pcbnew_path
    nightly = True
from .banner import get_banner, BANNERS
from .gs import GS
from . import dep_downloader
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+)(?:\.(\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_subpatch = 0 if m.group(4) is None else int(m.group(4))
    GS.kicad_version_n = (GS.kicad_version_major*10000000+GS.kicad_version_minor*10000+GS.kicad_version_patch*10 +
                          GS.kicad_version_subpatch)
    GS.ki5 = GS.kicad_version_major < 6
    GS.ki6 = GS.kicad_version_major >= 6
    GS.ki6_only = GS.kicad_version_major == 6
    GS.ki7 = GS.kicad_version_major >= 7
    GS.ki8 = (GS.kicad_version_major == 7 and GS.kicad_version_minor >= 99) or GS.kicad_version_major >= 8
    GS.footprint_gr_type = 'MGRAPHIC' if not GS.ki8 else 'PCB_SHAPE'
    GS.board_gr_type = 'DRAWSEGMENT' if GS.ki5 else 'PCB_SHAPE'
    GS.footprint_update_local_coords = GS.dummy1 if GS.ki8 else GS.footprint_update_local_coords_ki7
    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
    # We can log all the debug info to a separated file
    if args.log:
        if os.path.isfile(args.log):
            os.remove(args.log)
        else:
            os.makedirs(os.path.dirname(os.path.abspath(args.log)), exist_ok=True)
        log.set_file_log(args.log)
        GS.debug_level = 10
    # The log setup finished, this is our first log message
    logger.debug('KiBot {} verbose level: {} started on {}'.format(__version__, args.verbose, datetime.now()))
    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.banner is not None:
        try:
            id = int(args.banner)
        except ValueError:
            logger.error('The banner option needs an integer ({})'.format(id))
            sys.exit(EXIT_BAD_ARGS)
        logger.info(get_banner(id))

    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.help_banners:
        for c, b in enumerate(BANNERS):
            logger.info('Banner '+str(c))
            logger.info(b)
        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
