# MIT license
#
# Copyright (C) 2016 by XESS Corporation.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


"""
Parsers for LiCad schematic symbol library files.
"""



from __future__ import unicode_literals
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
from builtins import open
from future import standard_library
standard_library.install_aliases()

import argparse as ap
import os
import sys
import re
from pyparsing import *

from .py_2_3 import *

from .__init__ import __version__

THIS_MODULE = locals()

def _parse_lib_kicad(text):
    """
    Return a pyparsing object storing the contents of a KiCad symbol library.
    """

    # Basic parser elements.
    terminator_char = '|'
    terminator = Literal(terminator_char + '\n').suppress()
    fnum = Group(Word(nums) + Optional(Literal('.') + Optional(Word(nums))))
    string = Word(printables, excludeChars=terminator_char)
    qstring = dblQuotedString() ^ sglQuotedString()
    qstring.addParseAction(removeQuotes)
    anystring = qstring ^ string

    #------------------ Schematic symbol parser.--------------------

    # Field section parser. (Fields are ignored.)
    field_start = CaselessLiteral('F') + Word(nums)('field_num')
    field = field_start + restOfLine
    fields = ZeroOrMore(field).suppress()  # Just ignore all fields.

    # Part aliases. (Aliases are ignored.)
    aliases = ZeroOrMore(CaselessKeyword('ALIAS') + restOfLine()).suppress()

    # Footprint section. (Footprints are just skipped over and ignored.)
    foot_start = CaselessKeyword('$FPLIST') + terminator
    foot_end = CaselessKeyword('$ENDFPLIST') + terminator
    footprints = Optional(foot_start + SkipTo(foot_end) + foot_end).suppress()

    # Draw section parser.
    draw_start = (CaselessKeyword('DRAW') + restOfLine).suppress()
    draw_end = (CaselessKeyword('ENDDRAW') + terminator).suppress()
    draw_pin = Group(Word('Xx', exact=1).suppress() + anystring('name') + anystring('num') + \
                (anystring * 3).suppress() + anystring('orientation') + \
                (anystring * 2).suppress() + anystring('unit') + anystring.suppress() + \
                anystring('type') + Optional(anystring)('style') + terminator)
    draw_other = (Word('AaCcPpSsTt', exact=1) + White() + restOfLine).suppress()
    draw_element = draw_pin ^ draw_other
    draw = Group(draw_start + ZeroOrMore(draw_element) + draw_end)('pins')

    # Complete schematic symbol definition parser.
    def_start = CaselessKeyword('DEF').suppress() + anystring('name') + anystring('ref_id') + restOfLine.suppress()
    def_end = (CaselessKeyword('ENDDEF') + terminator).suppress()
    defn = Group(def_start + (fields & aliases & footprints & draw) + def_end)

    # Parser for set of schematic symbol definitions.
    defs = ZeroOrMore(defn)

    # Parser for entire library.
    version = CaselessKeyword('version').suppress() + fnum
    start = CaselessKeyword('EESchema-LIBRARY').suppress() + \
            SkipTo(version) + version('version') + restOfLine.suppress()
    end_of_file = Optional(White()) + stringEnd
    lib = start + defs('parts') + end_of_file

    #---------------------- End of parser -------------------------

    # Remove all comments from the text to be parsed but leave the lines blank.
    # (Don't delete the lines or else it becomes hard to find the line in the file
    # that made the parser fail.)
    text = re.sub('(^|\n)#.*?(?=\n)', '\n', text)

    # Terminate all lines with the terminator string.
    # (This makes it easier to handle each line without accidentally getting
    # stuff from the next line.)
    text = re.sub('\n', ' '+terminator_char + '\n', text)

    # Remove the terminator from all lines that just have the terminator character on them.
    # (Don't delete the lines or else it becomes hard to find the line in the file
    # that made the parser fail.)
    text = re.sub('\n *\\' + terminator_char + ' *(?=\n)', '\n', text)

    # Return the parsed text.
    return lib.parseString(text)


def _parse_lib(src, tool='kicad'):
    """
    Return a pyparsing object storing the contents of a schematic symbol library.

    Args:
        src: Either a text string, or a filename, or a file object that stores
            the netlist.

    Returns:
        A pyparsing object that stores the library contents.

    Exception:
        PyparsingException.
    """

    try:
        text = src.read()
    except Exception:
        try:
            text = open(src,'r').read()
        except Exception:
            text = src

    if not isinstance(text, basestring):
        raise Exception("What is this shit you're handing me? [{}]\n".format(src))

    try:
        # Use the tool name to find the function for loading the library.
        func_name = '_parse_lib_{}'.format(tool)
        parse_func = THIS_MODULE[func_name]
        return parse_func(text)
    except KeyError:
        # OK, that didn't work so well...
        logger.error('Unsupported ECAD tool library: {}'.format(tool))
        raise Exception

def _gen_csv(parsed_lib):
    '''Return multi-line CSV string for the parts in a parsed schematic library.'''

    type_tbl = {'I':'in', 'O':'out', 'B':'bidir', 'T':'tri', 'P':'passive', 'U':'unspecified', 'W':'pwr', 'w':'pwr_out', 'C':'open_collector', 'E':'open_emitter', 'N':'NC'}
    orientation_tbl = {'R':'left', 'L':'right', 'U':'bottom', 'D':'top'}
    style_tbl = {'':'', 'I':'inv', 'C':'clk', 'IC':'inv_clk', 'L':'input_low', 'CL':'clk_low', 'V':'output_low', 'F':'falling_clk', 'X':'non_logic'}

    csv = ''
    for part in parsed_lib.parts:
        csv += '{part.name},{part.ref_id},,,,,\n'.format(**locals())
        csv += 'Pin,Name,Type,Side,Unit,Style,Hidden\n'

        def zero_pad_nums(s):
            # Pad all numbers in the string with leading 0's.
            # Thus, 'A10' and 'A2' will become 'A00010' and 'A00002' and A2 will
            # appear before A10 in a list.
            try:
                return re.sub(r'\d+', lambda mtch: '0' * (8 - len(mtch.group(0))) + mtch.group(0), s)
            except TypeError:
                return s # The input is probably not a string, so just return it unchanged.

        def num_key(pin):
            '''Generate a key from a pin's number so they are sorted by position on the package.'''

            # Pad all numeric strings in the pin name with leading 0's.
            # Thus, 'A10' and 'A2' will become 'A00010' and 'A00002' and A2 will
            # appear before A10 in a list.
            return zero_pad_nums(pin.unit) + zero_pad_nums(pin.num)

        for p in sorted(part.pins, key=num_key):
            # Replace commas in pin numbers, names and units so it doesn't screw-up the CSV file.
            p.num = re.sub(',', ';', p.num)
            p.name = re.sub(',', ';', p.name)
            p.unit = re.sub(',', ';', p.unit)

            is_hidden = ''

            if p.style.find('N') != -1:
                p.style = p.style.replace('N', '')
                is_hidden = 'Y'

            csv += ','.join([p.num, p.name, type_tbl[p.type], orientation_tbl[p.orientation], p.unit, style_tbl[p.style], is_hidden]) + '\n'
        csv += ',,,,,,\n'
    csv += '\n'
    return csv

def main():
    parser = ap.ArgumentParser(
        description=
        'Convert a KiCad schematic symbol library file into a CSV file for KiPart.')

    parser.add_argument('-v', '--version',
        action='version',
        version='kilib2csv ' + __version__)
    parser.add_argument(
        'input_files',
        nargs='+',
        type=str,
        metavar='file.lib',
        help='KiCad schematic symbol library.')
    parser.add_argument('-o', '--output',
        nargs='?',
        type=str,
        metavar='file.csv',
        help='CSV file created from schematic library file.')
    parser.add_argument('-a', '--append',
        action='store_true',
        help='Append to an existing CSV file.')
    parser.add_argument('-w', '--overwrite',
        action='store_true',
        help='Allow overwriting of an existing CSV file.')

    args = parser.parse_args()

    if args.output == None:
        issue('No output file specified. Using kipart.csv as the default.')
        args.output = 'kipart.csv'
    else:
        args.output = os.path.splitext(args.output)[0] + '.csv'

    if os.path.isfile(args.output):
        if not (args.overwrite or args.append):
            print('Output file {} already exists! Use the --overwrite option to replace it or the --append option to append to it.'.format(
                args.output))
            sys.exit(1)

    file_mode = 'a' if args.append else 'w'
    with open(args.output, file_mode) as out_file:
        for lib in args.input_files:
            parsed_lib = _parse_lib(lib)
            out_file.write(_gen_csv(parsed_lib))

# main entrypoint.
if __name__ == '__main__':
    main()

