# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import sys
import re
import os

from .columns import ColumnList
from . import debug

# Check python version to determine which version of ConfirParser to import
if sys.version_info.major >= 3:
    import configparser as ConfigParser
else:
    import ConfigParser


class BomPref:

    SECTION_IGNORE = "IGNORE_COLUMNS"
    SECTION_COLUMN_ORDER = "COLUMN_ORDER"
    SECTION_GENERAL = "BOM_OPTIONS"
    SECTION_ALIASES = "COMPONENT_ALIASES"
    SECTION_GROUPING_FIELDS = "GROUP_FIELDS"
    SECTION_REGEXCLUDES = "REGEX_EXCLUDE"
    SECTION_REGINCLUDES = "REGEX_INCLUDE"

    OPT_PCB_CONFIG = "pcb_configuration"
    OPT_NUMBER_ROWS = "number_rows"
    OPT_GROUP_CONN = "group_connectors"
    OPT_USE_REGEX = "test_regex"
    OPT_USE_ALT = "use_alt"
    OPT_ALT_WRAP = "alt_wrap"
    OPT_MERGE_BLANK = "merge_blank_fields"
    OPT_IGNORE_DNF = "ignore_dnf"
    OPT_GENERATE_DNF = "html_generate_dnf"
    OPT_BACKUP = "make_backup"
    OPT_OUTPUT_FILE_NAME = "output_file_name"
    OPT_VARIANT_FILE_NAME_FORMAT = "variant_file_name_format"
    OPT_DEFAULT_BOARDS = "number_boards"
    OPT_DEFAULT_PCBCONFIG = "board_variant"
    OPT_CONFIG_FIELD = "fit_field"
    OPT_HIDE_HEADERS = "hide_headers"
    OPT_HIDE_PCB_INFO = "hide_pcb_info"

    def __init__(self):
        # List of headings to ignore in BoM generation
        self.ignore = [
            ColumnList.COL_PART_LIB,
            ColumnList.COL_FP_LIB,
        ]

        self.corder = ColumnList._COLUMNS_DEFAULT
        self.useAlt = False  # Use alternate reference representation
        self.altWrap = None  # Wrap to n items when using alt representation
        self.ignoreDNF = True  # Ignore rows for do-not-fit parts
        self.generateDNF = True  # Generate a list of do-not-fit parts
        self.numberRows = True  # Add row-numbers to BoM output
        self.groupConnectors = True  # Group connectors and ignore component value
        self.useRegex = True  # Test various columns with regex

        self.boards = 1  # Quantity of boards to be made
        self.mergeBlankFields = True  # Blanks fields will be merged when possible
        self.hideHeaders = False
        self.hidePcbInfo = False
        self.configField = "Config"  # Default field used for part fitting config
        self.pcbConfig = ["default"]

        self.backup = "%O.tmp"

        self.separatorCSV = None
        self.outputFileName = "%O_bom_%v%V"
        self.variantFileNameFormat = "_(%V)"

        # Default fields used to group components
        self.groups = [
            ColumnList.COL_PART,
            ColumnList.COL_PART_LIB,
            ColumnList.COL_VALUE,
            ColumnList.COL_FP,
            ColumnList.COL_FP_LIB,
            # User can add custom grouping columns in bom.ini
        ]

        self.regIncludes = []  # None by default

        self.regExcludes = [
            [ColumnList.COL_REFERENCE, '^TP[0-9]*'],
            [ColumnList.COL_REFERENCE, '^FID'],
            [ColumnList.COL_PART, 'mount.*hole'],
            [ColumnList.COL_PART, 'solder.*bridge'],
            [ColumnList.COL_PART, 'test.*point'],
            [ColumnList.COL_FP, 'test.*point'],
            [ColumnList.COL_FP, 'mount.*hole'],
            [ColumnList.COL_FP, 'fiducial'],
        ]

        # Default component groupings
        self.aliases = [
            ["c", "c_small", "cap", "capacitor"],
            ["r", "r_small", "res", "resistor"],
            ["sw", "switch"],
            ["l", "l_small", "inductor"],
            ["zener", "zenersmall"],
            ["d", "diode", "d_small"]
        ]

    # Check an option within the SECTION_GENERAL group
    def checkOption(self, parser, opt, default=False):
        if parser.has_option(self.SECTION_GENERAL, opt):
            return parser.get(self.SECTION_GENERAL, opt).lower() in ["1", "true", "yes"]
        else:
            return default

    def checkInt(self, parser, opt, default=False):
        if parser.has_option(self.SECTION_GENERAL, opt):
            return int(parser.get(self.SECTION_GENERAL, opt).lower())
        else:
            return default

    # Read KiBOM preferences from file
    def Read(self, file, verbose=False):
        file = os.path.abspath(file)
        if not os.path.exists(file) or not os.path.isfile(file):
            debug.error("{f} is not a valid file!".format(f=file))
            return

        cf = ConfigParser.RawConfigParser(allow_no_value=True)
        cf.optionxform = str

        cf.read(file)

        # Read general options
        if self.SECTION_GENERAL in cf.sections():
            self.ignoreDNF = self.checkOption(cf, self.OPT_IGNORE_DNF, default=True)
            self.generateDNF = self.checkOption(cf, self.OPT_GENERATE_DNF, default=True)
            self.useAlt = self.checkOption(cf, self.OPT_USE_ALT, default=False)
            self.altWrap = self.checkInt(cf, self.OPT_ALT_WRAP, default=None)
            self.numberRows = self.checkOption(cf, self.OPT_NUMBER_ROWS, default=True)
            self.groupConnectors = self.checkOption(cf, self.OPT_GROUP_CONN, default=True)
            self.useRegex = self.checkOption(cf, self.OPT_USE_REGEX, default=True)
            self.mergeBlankFields = self.checkOption(cf, self.OPT_MERGE_BLANK, default=True)
            self.outputFileName = cf.get(self.SECTION_GENERAL, self.OPT_OUTPUT_FILE_NAME)
            self.variantFileNameFormat = cf.get(self.SECTION_GENERAL, self.OPT_VARIANT_FILE_NAME_FORMAT)

        if cf.has_option(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD):
            self.configField = cf.get(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD)

        if cf.has_option(self.SECTION_GENERAL, self.OPT_DEFAULT_BOARDS):
            self.boards = self.checkInt(cf, self.OPT_DEFAULT_BOARDS, default=None)

        if cf.has_option(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG):
            self.pcbConfig = cf.get(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG).strip().split(",")

        if cf.has_option(self.SECTION_GENERAL, self.OPT_BACKUP):
            self.backup = cf.get(self.SECTION_GENERAL, self.OPT_BACKUP)
        else:
            self.backup = False

        if cf.has_option(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS):
            self.hideHeaders = cf.get(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS) == '1'

        if cf.has_option(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO):
            self.hidePcbInfo = cf.get(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO) == '1'

        # Read out grouping colums
        if self.SECTION_GROUPING_FIELDS in cf.sections():
            self.groups = [i for i in cf.options(self.SECTION_GROUPING_FIELDS)]

        # Read out ignored-rows
        if self.SECTION_IGNORE in cf.sections():
            self.ignore = [i for i in cf.options(self.SECTION_IGNORE)]

        # Read out column order
        if self.SECTION_COLUMN_ORDER in cf.sections():
            self.corder = [i for i in cf.options(self.SECTION_COLUMN_ORDER)]

        # Read out component aliases
        if self.SECTION_ALIASES in cf.sections():
            self.aliases = [re.split('[ \t]+', a) for a in cf.options(self.SECTION_ALIASES)]

        if self.SECTION_REGEXCLUDES in cf.sections():
            self.regExcludes = []
            for pair in cf.options(self.SECTION_REGEXCLUDES):
                if len(re.split('[ \t]+', pair)) == 2:
                    self.regExcludes.append(re.split('[ \t]+', pair))

        if self.SECTION_REGINCLUDES in cf.sections():
            self.regIncludes = []
            for pair in cf.options(self.SECTION_REGINCLUDES):
                if len(re.split('[ \t]+', pair)) == 2:
                    self.regIncludes.append(re.split('[ \t]+', pair))

    # Add an option to the SECTION_GENRAL group
    def addOption(self, parser, opt, value, comment=None):
        if comment:
            if not comment.startswith(";"):
                comment = "; " + comment
            parser.set(self.SECTION_GENERAL, comment)
        parser.set(self.SECTION_GENERAL, opt, "1" if value else "0")

    # Write KiBOM preferences to file
    def Write(self, file):
        file = os.path.abspath(file)

        cf = ConfigParser.RawConfigParser(allow_no_value=True)
        cf.optionxform = str

        cf.add_section(self.SECTION_GENERAL)
        cf.set(self.SECTION_GENERAL, "; General BoM options here")
        self.addOption(cf, self.OPT_IGNORE_DNF, self.ignoreDNF, comment="If '{opt}' option is set to 1, rows that are not to be fitted on the PCB will not be written to the BoM file".format(opt=self.OPT_IGNORE_DNF))
        self.addOption(cf, self.OPT_GENERATE_DNF, self.generateDNF, comment="If '{opt}' option is set to 1, also generate a list of components not fitted on the PCB (HTML only)".format(opt=self.OPT_GENERATE_DNF))
        self.addOption(cf, self.OPT_USE_ALT, self.useAlt, comment="If '{opt}' option is set to 1, grouped references will be printed in the alternate compressed style eg: R1-R7,R18".format(opt=self.OPT_USE_ALT))
        self.addOption(cf, self.OPT_ALT_WRAP, self.altWrap, comment="If '{opt}' option is set to and integer N, the references field will wrap after N entries are printed".format(opt=self.OPT_ALT_WRAP))
        self.addOption(cf, self.OPT_NUMBER_ROWS, self.numberRows, comment="If '{opt}' option is set to 1, each row in the BoM will be prepended with an incrementing row number".format(opt=self.OPT_NUMBER_ROWS))
        self.addOption(cf, self.OPT_GROUP_CONN, self.groupConnectors, comment="If '{opt}' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector".format(opt=self.OPT_GROUP_CONN))
        self.addOption(cf, self.OPT_USE_REGEX, self.useRegex, comment="If '{opt}' option is set to 1, each component group will be tested against a number of regular-expressions (specified, per column, below). If any matches are found, the row is ignored in the output file".format(opt=self.OPT_USE_REGEX))
        self.addOption(cf, self.OPT_MERGE_BLANK, self.mergeBlankFields, comment="If '{opt}' option is set to 1, component groups with blank fields will be merged into the most compatible group, where possible".format(opt=self.OPT_MERGE_BLANK))
        
        cf.set(self.SECTION_GENERAL, "; Specify output file name format, %O is the defined output name, %v is the version, %V is the variant name which will be ammended according to 'variant_file_name_format'.")
        cf.set(self.SECTION_GENERAL, self.OPT_OUTPUT_FILE_NAME, self.outputFileName)

        cf.set(self.SECTION_GENERAL, "; Specify the variant file name format, this is a unique field as the variant is not always used/specified. When it is unused you will want to strip all of this.")
        cf.set(self.SECTION_GENERAL, self.OPT_VARIANT_FILE_NAME_FORMAT, self.variantFileNameFormat)

        cf.set(self.SECTION_GENERAL, '; Field name used to determine if a particular part is to be fitted')
        cf.set(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD, self.configField)

        cf.set(self.SECTION_GENERAL, '; Make a backup of the bom before generating the new one, using the following template')
        cf.set(self.SECTION_GENERAL, self.OPT_BACKUP, self.backup)

        cf.set(self.SECTION_GENERAL, '; Default number of boards to produce if none given on CLI with -n')
        cf.set(self.SECTION_GENERAL, self.OPT_DEFAULT_BOARDS, self.boards)

        cf.set(self.SECTION_GENERAL, '; Default PCB variant if none given on CLI with -r')
        cf.set(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG, self.pcbConfig)

        cf.set(self.SECTION_GENERAL, '; Whether to hide headers from output file')
        cf.set(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS, self.hideHeaders)

        cf.set(self.SECTION_GENERAL, '; Whether to hide PCB info from output file')
        cf.set(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO, self.hidePcbInfo)

        cf.add_section(self.SECTION_IGNORE)
        cf.set(self.SECTION_IGNORE, "; Any column heading that appears here will be excluded from the Generated BoM")
        cf.set(self.SECTION_IGNORE, "; Titles are case-insensitive")

        for i in self.ignore:
            cf.set(self.SECTION_IGNORE, i)

        cf.add_section(self.SECTION_COLUMN_ORDER)
        cf.set(self.SECTION_COLUMN_ORDER, "; Columns will apear in the order they are listed here")
        cf.set(self.SECTION_COLUMN_ORDER, "; Titles are case-insensitive")

        for i in self.corder:
            cf.set(self.SECTION_COLUMN_ORDER, i)

        # Write the component grouping fields
        cf.add_section(self.SECTION_GROUPING_FIELDS)
        cf.set(self.SECTION_GROUPING_FIELDS, '; List of fields used for sorting individual components into groups')
        cf.set(self.SECTION_GROUPING_FIELDS, '; Components which match (comparing *all* fields) will be grouped together')
        cf.set(self.SECTION_GROUPING_FIELDS, '; Field names are case-insensitive')

        for i in self.groups:
            cf.set(self.SECTION_GROUPING_FIELDS, i)

        cf.add_section(self.SECTION_ALIASES)
        cf.set(self.SECTION_ALIASES, "; A series of values which are considered to be equivalent for the part name")
        cf.set(self.SECTION_ALIASES, "; Each line represents a list of equivalent component name values separated by white space")
        cf.set(self.SECTION_ALIASES, "; e.g. 'c c_small cap' will ensure the equivalent capacitor symbols can be grouped together")
        cf.set(self.SECTION_ALIASES, '; Aliases are case-insensitive')

        for a in self.aliases:
            cf.set(self.SECTION_ALIASES, "\t".join(a))

        cf.add_section(self.SECTION_REGINCLUDES)
        cf.set(self.SECTION_REGINCLUDES, '; A series of regular expressions used to include parts in the BoM')
        cf.set(self.SECTION_REGINCLUDES, '; If there are any regex defined here, only components that match against ANY of them will be included in the BOM')
        cf.set(self.SECTION_REGINCLUDES, '; Column names are case-insensitive')
        cf.set(self.SECTION_REGINCLUDES, '; Format is: "[ColumName] [Regex]" (white-space separated)')

        for i in self.regIncludes:
            if not len(i) == 2:
                continue
            cf.set(self.SECTION_REGINCLUDES, i[0] + "\t" + i[1])

        cf.add_section(self.SECTION_REGEXCLUDES)
        cf.set(self.SECTION_REGEXCLUDES, '; A series of regular expressions used to exclude parts from the BoM')
        cf.set(self.SECTION_REGEXCLUDES, '; If a component matches ANY of these, it will be excluded from the BoM')
        cf.set(self.SECTION_REGEXCLUDES, '; Column names are case-insensitive')
        cf.set(self.SECTION_REGEXCLUDES, '; Format is: "[ColumName] [Regex]" (white-space separated)')

        for i in self.regExcludes:
            if not len(i) == 2:
                continue

            cf.set(self.SECTION_REGEXCLUDES, i[0] + "\t" + i[1])

        with open(file, 'w') as configfile:
            cf.write(configfile)
