# -*- coding: utf-8 -*-
# Copyright (c) 2020-2022 Salvador E. Tropea
# Copyright (c) 2020-2022 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
import os
from .gs import GS
from .optionable import Optionable
from .kicad.config import expand_env
from .macros import macros, document  # noqa: F401
from .pre_filters import FiltersOptions
from .log import get_logger, set_filters
from .misc import W_MUSTBEINT
from .kicad.sexpdata import load, SExpData, sexp_iter, Symbol
from .kicad.v6_sch import PCBLayer


class Environment(Optionable):
    """ Used to define the KiCad environment vars """
    def __init__(self):
        super().__init__()
        self._unkown_is_error = True
        with document:
            self.symbols = ''
            """ System level symbols dir. KiCad 5: KICAD_SYMBOL_DIR. KiCad 6: KICAD6_SYMBOL_DIR """
            self.footprints = ''
            """ System level footprints (aka modules) dir. KiCad 5: KICAD_FOOTPRINT_DIR and KISYSMOD.
                KiCad 6: KICAD6_FOOTPRINT_DIR """
            self.models_3d = ''
            """ System level 3D models dir. KiCad 5: KISYS3DMOD. KiCad 6: KICAD6_3DMODEL_DIR """
            self.templates = ''
            """ System level templates dir. KiCad 5: KICAD_TEMPLATE_DIR. KiCad 6: KICAD6_TEMPLATE_DIR """
            self.user_templates = ''
            """ User level templates dir. KiCad 5/6: KICAD_USER_TEMPLATE_DIR """
            self.third_party = ''
            """ 3rd party dir. KiCad 6: KICAD6_3RD_PARTY """
            self.define_old = False
            """ Also define legacy versions of the variables.
                Useful when using KiCad 6 and some libs uses old KiCad 5 names """

    def define_k5_vars(self, defs):
        if self.symbols:
            defs['KICAD_SYMBOL_DIR'] = self.symbols
        if self.footprints:
            defs['KICAD_FOOTPRINT_DIR'] = self.symbols
            defs['KISYSMOD'] = self.symbols
        if self.models_3d:
            defs['KISYS3DMOD'] = self.models_3d
        if self.templates:
            defs['KICAD_TEMPLATE_DIR'] = self.templates
        if self.user_templates:
            defs['KICAD_USER_TEMPLATE_DIR'] = self.user_templates

    def define_k6_vars(self, defs):
        if self.symbols:
            defs['KICAD6_SYMBOL_DIR'] = self.symbols
        if self.footprints:
            defs['KICAD6_FOOTPRINT_DIR'] = self.symbols
        if self.models_3d:
            defs['KICAD6_3DMODEL_DIR'] = self.models_3d
        if self.templates:
            defs['KICAD6_TEMPLATE_DIR'] = self.templates
        if self.user_templates:
            defs['KICAD_USER_TEMPLATE_DIR'] = self.user_templates
        if self.third_party:
            defs['KICAD6_3RD_PARTY'] = self.third_party

    def config(self, parent):
        super().config(parent)
        defs = {}
        if GS.ki5():
            self.define_k5_vars(defs)
        else:
            self.define_k6_vars(defs)
            if self.define_old:
                self.define_k5_vars(defs)
        if len(defs):
            logger.debug('Defining environment vars from the global section')
            env = {}
            if GS.pcb_file:
                env['KIPRJMOD'] = os.path.dirname(GS.pcb_file)
            elif GS.sch_file:
                env['KIPRJMOD'] = os.path.dirname(GS.sch_file)
            for n, v in defs.items():
                v = expand_env(v, env, os.environ)
                logger.debug('- {} = "{}"'.format(n, v))
                os.environ[n] = v


class Globals(FiltersOptions):
    """ Global options """
    def __init__(self):
        super().__init__()
        with document:
            self.castellated_pads = False
            """ Has the PCB castelletad pads?
                KiCad 6: you should set this in the Board Setup -> Board Finish -> Has castellated pads """
            self.copper_finish = None
            """ {pcb_finish} """
            self.copper_thickness = 35
            """ [number|string] Copper thickness in micrometers (1 Oz is 35 micrometers).
                KiCad 6: you should set this in the Board Setup -> Physical Stackup """
            self.date_format = '%Y-%m-%d'
            """ Format used for the day we started the script.
                Is also used for the PCB/SCH date formatting when `time_reformat` is enabled (default behavior).
                Uses the `strftime` format """
            self.date_time_format = '%Y-%m-%d_%H-%M-%S'
            """ Format used for the PCB and schematic date when using the file timestamp. Uses the `strftime` format """
            self.dir = ''
            """ Default pattern for the output directories """
            self.drill_size_increment = 0.05
            """ This is the difference between drill tools in millimeters.
                A manufacturer with 0.05 of increment has drills for 0.1, 0.15, 0.2, 0.25, etc. """
            self.edge_connector = 'no'
            """ [yes,no,bevelled] Has the PCB edge connectors?
                KiCad 6: you should set this in the Board Setup -> Board Finish -> Edge card connectors """
            self.edge_plating = False
            """ Has the PCB a plated board edge?
                KiCad 6: you should set this in the Board Setup -> Board Finish -> Plated board edge """
            self.extra_pth_drill = 0.1
            """ How many millimeters the manufacturer will add to plated holes.
                This is because the plating reduces the hole, so you need to use a bigger drill.
                For more information consult: https://www.eurocircuits.com/pcb-design-guidelines/drilled-holes/ """
            self.field_3D_model = '_3D_model'
            """ Name for the field controlling the 3D models used for a component """
            self.kiauto_time_out_scale = 0.0
            """ Time-out multiplier for KiAuto operations """
            self.kiauto_wait_start = 0
            """ Time to wait for KiCad in KiAuto operations """
            self.impedance_controlled = False
            """ The PCB needs specific dielectric characteristics.
                KiCad 6: you should set this in the Board Setup -> Physical Stackup """
            self.output = GS.def_global_output
            """ Default pattern for output file names """
            self.pcb_finish = 'HAL'
            """ Finishing used to protect pads. Currently used for documentation and to choose default colors.
                KiCad 6: you should set this in the Board Setup -> Board Finish -> Copper Finish option.
                Currently known are None, HAL, HASL, HAL SnPb, HAL lead-free, ENIG, ENEPIG, Hard gold, ImAg, Immersion Silver,
                Immersion Ag, ImAu, Immersion Gold, Immersion Au, Immersion Tin, Immersion Nickel, OSP and HT_OSP """
            self.pcb_material = 'FR4'
            """ PCB core material. Currently used for documentation and to choose default colors.
                Currently known are FR1 to FR5 """
            self.silk_screen_color = 'white'
            """ Color for the markings. Currently used for documentation and to choose default colors.
                KiCad 6: you should set this in the Board Setup -> Physical Stackup.
                Currently known are black and white """
            self.silk_screen_color_top = ''
            """ Color for the top silk screen. When not defined `silk_screen_color` is used.
                Read `silk_screen_color` help """
            self.silk_screen_color_bottom = ''
            """ Color for the bottom silk screen. When not defined `silk_screen_color` is used.
                Read `silk_screen_color` help """
            self.solder_mask_color = 'green'
            """ Color for the solder mask. Currently used for documentation and to choose default colors.
                KiCad 6: you should set this in the Board Setup -> Physical Stackup.
                Currently known are green, black, white, yellow, purple, blue and red """
            self.solder_mask_color_top = ''
            """ Color for the top solder mask. When not defined `solder_mask_color` is used.
                Read `solder_mask_color` help """
            self.solder_mask_color_bottom = ''
            """ Color for the bottom solder mask. When not defined `solder_mask_color` is used.
                Read `solder_mask_color` help """
            self.time_format = '%H-%M-%S'
            """ Format used for the time we started the script. Uses the `strftime` format """
            self.time_reformat = True
            """ Tries to reformat the PCB/SCH date using the `date_format`.
                This assumes you let KiCad fill this value and hence the time is in ISO format (YY-MM-DD) """
            self.units = ''
            """ [millimeters,inches,mils] Default units. Affects `position` and `bom` outputs. Also KiCad 6 dimensions """
            self.variant = ''
            """ Default variant to apply to all outputs """
            self.out_dir = ''
            """ Base output dir, same as command line `--out-dir` """
            self.environment = Environment
            """ [dict] Used to define environment variables used by KiCad.
                The values defined here are exported as environment variables and has
                more precedence than KiCad paths defined in the GUI.
                You can make reference to any OS environment variable using ${VARIABLE}.
                The KIPRJMOD is also available for expansion """
        self.set_doc('filters', " [list(dict)] KiBot warnings to be ignored ")
        self._filter_what = 'KiBot warnings'
        self._unkown_is_error = True
        self._error_context = 'global '

    def set_global(self, opt):
        # Command Line value has more priority
        cli_val = GS.cli_global_defs.get(opt, None)
        if cli_val is not None:
            logger.info('Using command line value `{}` for global option `{}`'.format(cli_val, opt))
            return cli_val
        # Now try to apply our default
        return getattr(self, opt)

    def get_data_from_layer(self, ly, materials, thicknesses):
        if ly.name == "F.SilkS":
            if ly.color:
                self.silk_screen_color_top = ly.color.lower()
                logger.debug("- F.SilkS color: "+ly.color)
        elif ly.name == "B.SilkS":
            if ly.color:
                self.silk_screen_color_bottom = ly.color.lower()
                logger.debug("- B.SilkS color: "+ly.color)
        elif ly.name == "F.Mask":
            if ly.color:
                self.solder_mask_color_top = ly.color.lower()
                logger.debug("- F.Mask color: "+ly.color)
        elif ly.name == "B.Mask":
            if ly.color:
                self.solder_mask_color_bottom = ly.color.lower()
                logger.debug("- B.Mask color: "+ly.color)
        elif ly.material:
            if not len(materials):
                materials.add(ly.material)
                self.pcb_material = ly.material
            elif ly.material not in materials:
                materials.add(ly.material)
                self.pcb_material += ' / '+ly.material
        elif ly.type == 'copper' and ly.thickness:
            if not len(thicknesses):
                thicknesses.add(ly.thickness)
                self.copper_thickness = str(int(ly.thickness))
            elif ly.thickness not in thicknesses:
                thicknesses.add(ly.thickness)
                self.copper_thickness += ' / '+str(int(ly.thickness))

    def get_stack_up(self):
        logger.debug("Looking for stack-up information in the PCB")
        pcb = None
        with open(GS.pcb_file, 'rt') as fh:
            try:
                pcb = load(fh)
            except SExpData as e:
                # Don't make it an error, will be detected and reported latter
                logger.debug("- Failed to load the PCB "+str(e))
        if pcb is None:
            return
        iter = sexp_iter(pcb, 'kicad_pcb/setup/stackup')
        if iter is None:
            return
        sp = next(iter, None)
        if sp is None:
            return
        logger.debug("- Found stack-up information")
        stackup = []
        materials = set()
        thicknesses = set()
        for e in sp[1:]:
            if isinstance(e, list) and isinstance(e[0], Symbol):
                name = e[0].value()
                value = None
                if len(e) > 1:
                    if isinstance(e[1], Symbol):
                        value = e[1].value()
                    else:
                        value = str(e[1])
                if name == 'copper_finish':
                    self.pcb_finish = value
                    logger.debug("- Copper finish: "+self.pcb_finish)
                elif name == 'edge_connector':
                    self.edge_connector = value
                    logger.debug("- Edge connector: "+self.edge_connector)
                elif name == 'castellated_pads':
                    self.castellated_pads = value == 'yes'
                    logger.debug("- Castellated pads: "+value)
                elif name == 'edge_plating':
                    self.edge_plating = value == 'yes'
                    logger.debug("- Edge plating: "+value)
                elif name == 'dielectric_constraints':
                    self.impedance_controlled = value == 'yes'
                    logger.debug("- Impedance controlled: "+value)
                elif name == 'layer':
                    ly = PCBLayer.parse(e)
                    stackup.append(ly)
                    self.get_data_from_layer(ly, materials, thicknesses)
        if stackup:
            GS.stackup = stackup
        if len(materials):
            logger.debug("- PCB Material/s: "+self.pcb_material)
        if len(thicknesses):
            logger.debug("- Copper thickness: "+self.copper_thickness)

    def config(self, parent):
        if GS.ki6() and GS.pcb_file and os.path.isfile(GS.pcb_file):
            self.get_stack_up()
        super().config(parent)
        # Transfer options to the GS globals
        for option in filter(lambda x: x[0] != '_', self.__dict__.keys()):
            gl = 'global_'+option
            if hasattr(GS, gl):
                setattr(GS, gl, self.set_global(option))
        # Special cases
        if not GS.out_dir_in_cmd_line and self.out_dir:
            GS.out_dir = os.path.join(os.getcwd(), self.out_dir)
        if GS.global_kiauto_wait_start and int(GS.global_kiauto_wait_start) != GS.global_kiauto_wait_start:
            GS.global_kiauto_wait_start = int(GS.global_kiauto_wait_start)
            logger.warning(W_MUSTBEINT+'kiauto_wait_start must be integer, truncating to '+str(GS.global_kiauto_wait_start))
        # - Solder mask
        if GS.global_solder_mask_color_top and GS.global_solder_mask_color_bottom:
            # Top and bottom defined, use the top as general
            GS.global_solder_mask_color = GS.global_solder_mask_color_top
        else:
            if not GS.global_solder_mask_color_top:
                GS.global_solder_mask_color_top = GS.global_solder_mask_color
            if not GS.global_solder_mask_color_bottom:
                GS.global_solder_mask_color_bottom = GS.global_solder_mask_color
        # - Silk screen
        if GS.global_silk_screen_color_top and GS.global_silk_screen_color_bottom:
            GS.global_silk_screen_color = GS.global_silk_screen_color_top
        else:
            if not GS.global_silk_screen_color_top:
                GS.global_silk_screen_color_top = GS.global_silk_screen_color
            if not GS.global_silk_screen_color_bottom:
                GS.global_silk_screen_color_bottom = GS.global_silk_screen_color
        set_filters(self.unparsed)


logger = get_logger(__name__)
GS.class_for_global_opts = Globals
