#!/usr/bin/env python
import argparse
import ConfigParser
import sys
import os
import logging
from pcbnew import *

"""drill.py
Adam Wolf, wayne@wayneandlayne.com
Wayne and Layne, LLC
May 5th, 2013

PRERELEASE

When completed, this script will use the Kicad scripting interface to generate drill files.
If you do everything exactly like it expects, it will generate drill files.  Not all the
options work.  Not much has been tested.
"""

logging.basicConfig(format='%(levelname)s:%(message)s')
logger = logging.getLogger()

def detect_blind_buried_or_micro_vias(pcb):
    through_vias = 0
    micro_vias = 0
    blind_or_buried_vias = 0

    for track in pcb.GetTracks():
        logger.debug("Checking track.")
        if track.Type() != PCB_VIA_T:
            continue

        if track.GetShape() == VIA_THROUGH:
            through_vias += 1
        elif track.GetShape() == VIA_MICROVIA:
            micro_vias += 1
        elif track.GetShape() == VIA_BLIND_BURIED:
            blind_or_buried_vias += 1

    logger.info("Through Vias: %d" % through_vias)
    logger.info("Micro Vias: %d" % micro_vias)
    logger.info("Blind/Buried Vias: %d" % blind_or_buried_vias)
    if micro_vias or blind_or_buried_vias:
        return {'Micro Vias': micro_vias, 
                'Blind/Buried Vias': blind_or_buried_vias}
    else:
        return False

def detect_NPTH(pcb):
    plated_pads_hole_count = 0
    NPTH_count = 0

    for module in pcb.GetModules():
        logger.debug("Checking module.")
        for pad in module.Pads():
            logger.debug("Checking pad.")
            if pad.GetDrillShape() == PAD_CIRCLE:
                if pad.GetDrillSize().x != 0:
                    if pad.GetAttribute() == PAD_HOLE_NOT_PLATED:
                        NPTH_count += 1
                    else:
                        plated_pads_hole_count += 1
            else:
                if pad.GetDrillSize().x != 0 and pad.GetDrillSize().y != 0:
                    if pad.GetAttribute() == PAD_HOLE_NOT_PLATED:
                        NPTH_count += 1
                    else:
                        plated_pads_hole_count += 1
    logger.debug("Plated pad holes: %d" % plated_pads_hole_count)
    logger.info("NPTH: %d" % NPTH_count)
    return NPTH_count

def generate_drill_file(excellon_writer, NPTH, output_dir, output_prefix):
    layer1 = LAYER_N_BACK
    layer2 = LAYER_N_FRONT
    exclude_through_holes = NPTH
    only_generate_npth = NPTH
    if NPTH:
        logger.info("Generating drill file for NPTH")
    else:
        logger.info("Generating drill file for plated holes")

    excellon_writer.BuildHolesList(layer1, layer2, exclude_through_holes, only_generate_npth)
    if excellon_writer.GetHolesCount() > 0:
        if output_dir:
            path = os.path.join(output_dir, output_prefix)
        else:
            path = output_prefix

        if only_generate_npth:
            path += "-NPTH"

        path += ".drl"

        f = open(path, "w")
        logger.info("Writing drill file.")
        hole_count = excellon_writer.CreateDrillFile( f );
        logger.debug("Written hole count: %d" % hole_count)
    else:
        logger.debug("Drill file not created due to not having holes.")

def main():

    zero_formats = {"decimal-format": EXCELLON_WRITER.DECIMAL_FORMAT,
            "suppress-leading-zeros": EXCELLON_WRITER.SUPPRESS_LEADING,
            "suppress-trailing-zeros": EXCELLON_WRITER.SUPPRESS_TRAILING,
            "keep-zeros": EXCELLON_WRITER.KEEP_ZEROS}

    precisions = {"millimeters" : (3, 3),
            "inches": (2, 4)}
    
    parser = argparse.ArgumentParser(description="Eventually, this will be a slick way to generate drill files for Kicad from the command line.  Currently, it's untested and a proof-of-concept only.  Do not use this, really, for anything.  Do not use with files that aren't backed up.")
    parser.add_argument("--verbose", "-v", action="count", default=0, help="Increase verbosity. May be entered multiple times.")
    parser.add_argument("--drill-units", "-u", choices=["millimeters", "inches"], default="inches", help="Select the drill file units.  Defaults to inches.")
    parser.add_argument("--zero-format", "-z", choices=zero_formats.keys(), default="suppress-leading-zeros", help="Select the zero format.")
    parser.add_argument("--mirror-y-axis", "-m", action="store_true", default=False, help="Select whether to mirror the y axis.  Defaults to false.")
    parser.add_argument("--minimal-header", "-s", action="store_true", default=True, help="Enable a minimal header on the output drill file.")
    parser.add_argument("--drill-origin", "-c", choices=["absolute", "auxillary-axis"], default="absolute", help="Select the drill origin. Defaults to absolute.")
    parser.add_argument("--output-prefix", "-n", help="The prefix for the output file(s).  NPTH drill files will have -NPTH.drl appended, while standard drill files will have .drl appended.  Defaults to the name of the PCB.")
    parser.add_argument("--output-dir", "-d", help="The output directory.  Defaults to the current directory.")
    parser.add_argument("--force", help="Do not use this option.  When a fatal error is encountered, this option stops the script from stopping.  This will almost certainly generate incorrect, broken files.")
    parser.add_argument("pcbfile", metavar="FILE", help="Kicad board file (.kicad_pcb or .brd)")
    args = parser.parse_args()

    if args.verbose == 0:
        logger.setLevel(logging.WARNING)
    elif args.verbose == 1:
        logger.setLevel(logging.INFO)
    elif args.verbose > 1:
        logger.setLevel(logging.DEBUG)
    #logger.info("sample info message")
    #logger.debug("sample debug message")
    
    if args.drill_units == "millimeters" or args.drill_units == "mm":
        args.is_metric = True
    else:
        args.is_metric = False
    
    logger.debug("Parsed args: %s" % args)

    try:
        logger.info("Loading board file %s" % args.pcbfile)
        pcb = LoadBoard(args.pcbfile)
        logger.info("Successfully loaded board file.")
    except IOError:
        logger.error("Unable to load board file %s" % args.pcbfile)
        sys.exit(1)
    
    if args.output_prefix is None:
        args.output_prefix = os.path.splitext(pcb.GetFileName())[0]

    blind_buried_or_micro_vias = detect_blind_buried_or_micro_vias(pcb)
    if blind_buried_or_micro_vias:
        for via_type, count in blind_buried_or_micro_vias.items():
            logger.warning("%s: %d" % (via_type, count))
        logger.critical('Blind, buried, or micro vias detected.  This script currently does not handle layer-by-layer drill files, so it cannot properly process this board.  To override, and create an almost-certainly incorrect file, use the --force option.')
        if not args.force:
            sys.exit(1)
    else:
        logger.info("No blind, buried, or micro vias detected.")





    npth = detect_NPTH(pcb)
    
    if args.drill_origin == "absolute":
        origin_point = wxPoint(0, 0)
    else:
        origin_point = pcb.GetOriginAxisPosition()

    excellon_writer = EXCELLON_WRITER(pcb, origin_point)
    
    zeroes = zero_formats[args.zero_format]

    num_integer_digits, num_mantissa_digits = precisions[args.drill_units]
    
    excellon_writer.SetFormat(args.is_metric,
                              zeroes,
                              num_integer_digits, num_mantissa_digits);

    excellon_writer.SetOptions(args.mirror_y_axis, args.minimal_header, origin_point)
    generate_drill_file(excellon_writer,
            NPTH=False,
            output_dir=args.output_dir,
            output_prefix=args.output_prefix)
    generate_drill_file(excellon_writer,
            NPTH=True,
            output_dir=args.output_dir,
            output_prefix=args.output_prefix)
    logger.info("Completed.")

if __name__ == "__main__":
    main()
