# -*- coding: utf-8 -*-

# The MIT License (MIT) - Copyright (c) Dave Vandenbout.

"""
Handles part pins and their connections to nets.

This module provides the Pin class which represents electronic component pins. 
Pins can be connected to nets and other pins, and have various electrical properties 
like input, output, bidirectional, etc.
"""

import random
import re
import sys
from collections import defaultdict
from collections.abc import Iterable
from copy import copy
from enum import IntEnum
from functools import total_ordering

from .logger import active_logger
from .skidlbaseobj import ERROR, OK, WARNING, SkidlBaseObject
from .utilities import (
    expand_buses,
    expand_indices,
    export_to_all,
    find_num_copies,
    flatten,
    from_iadd,
    rmv_iadd,
    set_iadd,
    to_list,
)


# Various types of pins.
pin_types = IntEnum(
    "pin_types",
    (
    "INPUT",
    "OUTPUT",
    "BIDIR",
    "TRISTATE",
    "PASSIVE",
    "UNSPEC",
    "PWRIN",
    "PWROUT",
    "OPENCOLL",
    "OPENEMIT",
    "PULLUP",
    "PULLDN",
    "NOCONNECT",
    "FREE",
    )
)

# Various drive levels a pin can output.
# The order of these is important! The first entry has the weakest
# drive and the drive increases for each successive entry.
pin_drives = IntEnum(
    "pin_drives",
    (
        "NOCONNECT",  #  NC pin drive.
        "NONE",  # No drive capability (like an input pin).
        "PASSIVE",  # Small drive capability, but less than a pull-up or pull-down.
        "PULLUPDN",  # Pull-up or pull-down capability.
        "ONESIDE",  # Can pull high (open-emitter) or low (open-collector).
        "TRISTATE",  # Can pull high/low and be in high-impedance state.
        "PUSHPULL",  # Can actively drive high or low.
        "POWER",  # A power supply or ground line.
    ),
)

# Information about the various types of pins:
#   function: A string describing the pin's function.
#   drive: The drive capability of the pin.
#   rcv_min: The minimum amount of drive the pin must receive to function.
#   rcv_max: The maximum amount of drive the pin can receive and still function.
pin_info = {
    pin_types.INPUT: {
        "function": "INPUT",
        "func_str": "INPUT",
        "drive": pin_drives.NONE,
        "max_rcv": pin_drives.POWER,
        "min_rcv": pin_drives.PASSIVE,
    },
    pin_types.OUTPUT: {
        "function": "OUTPUT",
        "func_str": "OUTPUT",
        "drive": pin_drives.PUSHPULL,
        "max_rcv": pin_drives.PASSIVE,
        "min_rcv": pin_drives.NONE,
    },
    pin_types.BIDIR: {
        "function": "BIDIRECTIONAL",
        "func_str": "BIDIR",
        "drive": pin_drives.TRISTATE,
        "max_rcv": pin_drives.POWER,
        "min_rcv": pin_drives.NONE,
    },
    pin_types.TRISTATE: {
        "function": "TRISTATE",
        "func_str": "TRISTATE",
        "drive": pin_drives.TRISTATE,
        "max_rcv": pin_drives.TRISTATE,
        "min_rcv": pin_drives.NONE,
    },
    pin_types.PASSIVE: {
        "function": "PASSIVE",
        "func_str": "PASSIVE",
        "drive": pin_drives.PASSIVE,
        "max_rcv": pin_drives.POWER,
        "min_rcv": pin_drives.NONE,
    },
    pin_types.PULLUP: {
        "function": "PULLUP",
        "func_str": "PULLUP",
        "drive": pin_drives.PULLUPDN,
        "max_rcv": pin_drives.POWER,
        "min_rcv": pin_drives.NONE,
    },
    pin_types.PULLDN: {
        "function": "PULLDN",
        "func_str": "PULLDN",
        "drive": pin_drives.PULLUPDN,
        "max_rcv": pin_drives.POWER,
        "min_rcv": pin_drives.NONE,
    },
    pin_types.UNSPEC: {
        "function": "UNSPECIFIED",
        "func_str": "UNSPEC",
        "drive": pin_drives.NONE,
        "max_rcv": pin_drives.POWER,
        "min_rcv": pin_drives.NONE,
    },
    pin_types.PWRIN: {
        "function": "POWER-IN",
        "func_str": "PWRIN",
        "drive": pin_drives.NONE,
        "max_rcv": pin_drives.POWER,
        "min_rcv": pin_drives.POWER,
    },
    pin_types.PWROUT: {
        "function": "POWER-OUT",
        "func_str": "PWROUT",
        "drive": pin_drives.POWER,
        "max_rcv": pin_drives.PASSIVE,
        "min_rcv": pin_drives.NONE,
    },
    pin_types.OPENCOLL: {
        "function": "OPEN-COLLECTOR",
        "func_str": "OPENCOLL",
        "drive": pin_drives.ONESIDE,
        "max_rcv": pin_drives.TRISTATE,
        "min_rcv": pin_drives.NONE,
    },
    pin_types.OPENEMIT: {
        "function": "OPEN-EMITTER",
        "func_str": "OPENEMIT",
        "drive": pin_drives.ONESIDE,
        "max_rcv": pin_drives.TRISTATE,
        "min_rcv": pin_drives.NONE,
    },
    pin_types.NOCONNECT: {
        "function": "NO-CONNECT",
        "func_str": "NOCONNECT",
        "drive": pin_drives.NOCONNECT,
        "max_rcv": pin_drives.NOCONNECT,
        "min_rcv": pin_drives.NOCONNECT,
    },
    pin_types.FREE: {
        "function": "FREE",
        "func_str": "FREE",
        "drive": pin_drives.NONE,
        "max_rcv": pin_drives.POWER,
        "min_rcv": pin_drives.NOCONNECT,
    },
}


@export_to_all
@total_ordering
class Pin(SkidlBaseObject):
    """
    A class for storing data about pins for electronic parts.

    Args:
        attribs: Key/value pairs of attributes to add to the pin.

    Attributes:
        nets (list): The electrical nets this pin is connected to (can be >1).
        part (Part): Link to the Part object this pin belongs to.
        name (str): Name of the pin (e.g., "GND", "VCC").
        num (str): Pin number (e.g., "1", "2", "A5").
        func (int): Pin function from pin_types enum (e.g., INPUT, OUTPUT, BIDIR).
        do_erc (bool): When False, the pin is not checked for ERC violations.
        stub (bool): When True, the pin is only for internal schematic connections.
        aliases (list): Alternative names for this pin.
    """

    # Maximum allowable pin number.
    MAX_PIN_NUM = sys.maxsize >> 3

    types = pin_types
    funcs = pin_types  # A synonym for types.
    drives = pin_drives

    def __init__(self, **attribs):
        super().__init__()

        self.nets = []
        self.part = None
        self.name = ""
        self.num = ""
        self.stub = False
        self.do_erc = True
        self.func = pin_types.UNSPEC  # Pin function defaults to unspecified.

        # Set pin number as a random integer so if Pin() is called multiple
        # times it will give pins that are distinct according to __eq__.
        # This pin number gets overridden if the num is set in attribs.
        # Checking the pin number will also detect if the pin has been
        # assigned a real pin number for a part.
        self.num = random.randint(self.MAX_PIN_NUM+1, sys.maxsize)

        # Attach additional attributes to the pin.
        for k, v in list(attribs.items()):
            setattr(self, k, v)

    def __str__(self):
        """
        Return a description of this pin as a string.
        
        Returns:
            str: String formatted as "Pin ref/num/names/func"
        """
        ref = getattr(self.part, "ref", "???")
        num, names, func = self.get_pin_info()
        return f"Pin {ref}/{num}/{names}/{func}"

    __repr__ = __str__

    def __bool__(self):
        """
        Any valid Pin is considered True in boolean context.
        
        Returns:
            bool: Always True for valid pins
        """
        return True

    __nonzero__ = __bool__  # Python 2 compatibility.

    def __lt__(self, o):
        """
        Compare pins for sorting based on normalized pin numbers.
        
        Args:
            o (Pin): Another pin to compare with
            
        Returns:
            bool: True if this pin's normalized number is less than the other pin's
            
        Raises:
            ValueError: If comparing pins from different parts
        """
        if not isinstance(o, type(self)):
            return NotImplemented
        if self.part != o.part:
            raise ValueError("Comparing pins on different parts not supported.")
        return self._normalize_num() < o._normalize_num()

    def __eq__(self, o):
        """
        Check if two pins are equivalent (same part and normalized number).
        
        Args:
            o (Pin): Another pin to compare with
            
        Returns:
            bool: True if pins are equivalent, False otherwise
        """
        if not isinstance(o, type(self)):
            return NotImplemented
        return self.part == o.part and self._normalize_num() == o._normalize_num()

    def __and__(self, obj):
        """
        Attach a pin and another part/pin/net in serial.
        
        Args:
            obj: Another part, pin, or net
            
        Returns:
            Network: A network with the pin and object connected in series
        """
        from .network import Network

        return Network(self) & obj

    def __rand__(self, obj):
        """
        Support for right-side serial attachment.
        
        Args:
            obj: Another part, pin, or net
            
        Returns:
            Network: A network with the object and pin connected in series
        """
        from .network import Network

        return obj & Network(self)

    def __or__(self, obj):
        """
        Attach a pin and another part/pin/net in parallel.
        
        Args:
            obj: Another part, pin, or net
            
        Returns:
            Network: A network with the pin and object connected in parallel
        """
        from .network import Network

        return Network(self) | obj

    def __ror__(self, obj):
        """
        Support for right-side parallel attachment.
        
        Args:
            obj: Another part, pin, or net
            
        Returns:
            Network: A network with the object and pin connected in parallel
        """
        from .network import Network

        return obj | Network(self)

    # Defining an __eq__ method will make Pins unhashable unless we
    # explicitly define a hash method.
    __hash__ = SkidlBaseObject.__hash__

    # Make copies with the multiplication operator or by calling the object.
    def __call__(self, num_copies=None, **attribs):
        """
        Return copy or list of copies of a pin including any net connection.

        Args:
            num_copies (int, optional): Number of copies to make of pin.

        Keyword Args:
            attribs: Name/value pairs for setting attributes for the pin.

        Returns:
            Pin or list[Pin]: Copy or list of copies of the pin

        Notes:
            An instance of a pin can be copied just by calling it like so::

                p = Pin()     # Create a pin.
                p_copy = p()  # This is a copy of the pin.
        """

        return self.copy(num_copies=num_copies, **attribs)

    def __mul__(self, num_copies):
        """
        Create multiple copies of a pin.
        
        Args:
            num_copies (int): Number of copies to create
            
        Returns:
            Pin or list[Pin]: Single pin or list of pins
        """
        if num_copies is None:
            num_copies = 0
        return self.copy(num_copies=num_copies)

    __rmul__ = __mul__

    def __getitem__(self, *ids):
        """
        Return the pin if the indices resolve to a single index of 0.

        Args:
            ids: A list of indices. These can be individual
                numbers, net names, nested lists, or slices.

        Returns:
            Pin: The pin if index is 0, otherwise None or raises Exception.
            
        Raises:
            ValueError: If a non-zero index is used or multiple indices are provided
        """

        # Resolve the indices.
        indices = list(set(expand_indices(0, self.width - 1, False, *ids)))
        if indices is None or len(indices) == 0:
            return None
        if len(indices) > 1:
            active_logger.raise_(ValueError, "Can't index a pin with multiple indices.")
        if indices[0] != 0:
            active_logger.raise_(ValueError, "Can't use a non-zero index for a pin.")
        return self

    def __setitem__(self, ids, *pins_nets_buses):
        """
        You can't assign to Pins. You must use the += operator.

        This method is a work-around that allows the use of the += for making
        connections to pins while prohibiting direct assignment. Python
        processes something like net[0] += Net() as follows::

            1. Pin.__getitem__ is called with '0' as the index. This
               returns a single Pin.
            2. The Pin.__iadd__ method is passed the pin and
               the thing to connect to it (a Net in this case). This
               method makes the actual connection to the net. Then
               it creates an iadd_flag attribute in the object it returns.
            3. Finally, Pin.__setitem__ is called. If the iadd_flag attribute
               is true in the passed argument, then __setitem__ was entered
               as part of processing the += operator. If there is no
               iadd_flag attribute, then __setitem__ was entered as a result
               of using a direct assignment, which is not allowed.
        """

        # If the iadd_flag is set, then it's OK that we got
        # here and don't issue an error. Also, delete the flag.
        if from_iadd(pins_nets_buses):
            rmv_iadd(pins_nets_buses)
            return

        # No iadd_flag or it wasn't set. This means a direct assignment
        # was made to the pin, which is not allowed.
        active_logger.raise_(TypeError, "Can't assign to a Net! Use the += operator.")

    def __iter__(self):
        """
        Return an iterator for stepping through the pin.
        
        Returns:
            iterator: Generator that yields this pin once
        """
        # You can only iterate a Pin one time.
        return (self for i in [0])  # Return generator expr.

    def __iadd__(self, *pins_nets_buses):
        """
        Return the pin after connecting it to one or more nets or pins using the += operator.

        Args:
            pins_nets_buses: One or more Pin, Net or Bus objects or
                lists/tuples of them.

        Returns:
            Pin: The updated pin with the new connections.

        Notes:
            You can connect nets or pins to a pin like so::

                p = Pin()     # Create a pin.
                n = Net()     # Create a net.
                p += net      # Connect the net to the pin.
        """
        return self.connect(*pins_nets_buses)

    def copy(self, num_copies=None, **attribs):
        """
        Return copy or list of copies of a pin including any net connection.

        Args:
            num_copies (int, optional): Number of copies to make of pin.

        Keyword Args:
            attribs: Name/value pairs for setting attributes for the pin.

        Returns:
            Pin or list[Pin]: A single copy or a list of copies.

        Notes:
            An instance of a pin can be copied just by calling it like so::

                p = Pin()     # Create a pin.
                p_copy = p()  # This is a copy of the pin.
        """

        # If the number of copies is None, then a single copy will be made
        # and returned as a scalar (not a list). Otherwise, the number of
        # copies will be set by the num_copies parameter or the number of
        # values supplied for each part attribute.
        num_copies_attribs = find_num_copies(**attribs)
        return_list = (num_copies is not None) or (num_copies_attribs > 1)
        if num_copies is None:
            num_copies = max(1, num_copies_attribs)

        # Check that a valid number of copies is requested.
        if not isinstance(num_copies, int):
            active_logger.raise_(
                ValueError,
                f"Can't make a non-integer number ({num_copies}) of copies of a pin!"
            )
        if num_copies < 0:
            active_logger.raise_(
                ValueError,
                f"Can't make a negative number ({num_copies}) of copies of a pin!"
            )

        # Skip some Pin attributes that would cause an infinite recursion exception
        # or naming clashes.
        skip_attrs = ('nets', 'num')

        copies = []
        for _ in range(num_copies):

            # Create a new pin to store the copy.
            cpy = Pin()

            # Copy stuff from the original pin to the copy.
            for k,v in self.__dict__.items():
                if k in skip_attrs:
                    continue
                if isinstance(v, Iterable) and not isinstance(v, str):
                    # Copy the list with shallow copies of its items to the copy.
                    setattr(cpy, k, copy(v))
                else:
                    setattr(cpy, k, v)

            # Copy gets the same pin number if the source is an actual pin.
            # Otherwise, the copy will keep the random pin number it was given
            # when it was created.
            if self.is_assigned():
                cpy.num = self.num

            # Attach additional attributes to the pin.
            for k, v in list(attribs.items()):
                setattr(cpy, k, v)

            # Connect the new pin to the same net as the original.
            if self.nets:
                self.nets[0] += cpy

            copies.append(cpy)

        # Return a list of the copies made or just a single copy.
        if return_list:
            return copies
        return copies[0]

    def is_assigned(self):
        """
        Return true if the pin has been assigned a valid pin number.

        Returns:
            bool: True if the pin is assigned (either has a non-integer number or 
                  an integer number less than or equal to MAX_PIN_SIZE), False otherwise.
        Note:
            A pin is considered assigned if it has either a string/non-integer identifier
            or an integer pin number within the valid range (1 to MAX_PIN_SIZE).
        """
        return not isinstance(self.num, int) or self.num <= self.MAX_PIN_NUM

    def is_connected(self):
        """
        Return true if a pin is connected to a net (but not a no-connect net).
        
        Returns:
            bool: True if connected to a normal net, False otherwise
            
        Raises:
            ValueError: If pin is connected to both normal and no-connect nets
        """

        from .net import NCNet, Net

        if not self.nets:
            # This pin is not connected to any nets.
            return False

        # Get the types of things this pin is connected to.
        net_types = set([type(n) for n in self.nets])

        if set([NCNet]) == net_types:
            # This pin is only connected to no-connect nets.
            return False
        if set([Net]) == net_types:
            # This pin is only connected to normal nets.
            return True
        if set([Net, NCNet]) == net_types:
            # Can't be connected to both normal and no-connect nets!
            active_logger.raise_(
                ValueError,
                f"{self.erc_desc()} is connected to both normal and no-connect nets!"
            )
        # This is just strange...
        active_logger.raise_(
            ValueError,
            f"{self.erc_desc()} is connected to something strange: {self.nets}."
        )

    def is_attached(self, pin_net_bus):
        """
        Return true if this pin is attached to the given pin, net or bus.
        
        Args:
            pin_net_bus: A Pin, Net or Bus object
            
        Returns:
            bool: True if attached, False otherwise
            
        Raises:
            ValueError: If attempting to check attachment to an invalid object type
        """

        from .net import Net
        from .pin import Pin
        from .bus import Bus

        if not self.is_connected():
            return False
        if isinstance(pin_net_bus, Pin):
            if pin_net_bus.is_connected():
                return pin_net_bus.net.is_attached(self.net)
            return False
        if isinstance(pin_net_bus, Net):
            return pin_net_bus.is_attached(self.net)
        if isinstance(pin_net_bus, Bus):
            for net in pin_net_bus[:]:
                if self.net.is_attached(net):
                    return True
            return False
        active_logger.raise_(
            ValueError,
            f"Pins can't be attached to {type(pin_net_bus)}!",
        )

    def split_name(self, delimiters):
        """
        Use chars in divider to split a pin name and add substrings to aliases.
        
        Args:
            delimiters (str): String of characters used to split pin names
        """

        # Split pin name and add subnames as aliases.
        self.aliases += re.split("[" + re.escape(delimiters) + "]", self.name)

        # Remove any empty aliases.
        self.aliases.clean()

    def connect(self, *pins_nets_buses):
        """
        Return the pin after connecting it to one or more nets or pins.

        Args:
            pins_nets_buses: One or more Pin, Net or Bus objects or
                lists/tuples of them.

        Returns:
            Pin: The updated pin with the new connections.

        Notes:
            You can connect nets or pins to a pin like so::

                p = Pin()     # Create a pin.
                n = Net()     # Create a net.
                p += net      # Connect the net to the pin.
        """

        from .net import Net

        # Go through all the pins and/or nets and connect them to this pin.
        for pn in expand_buses(flatten(pins_nets_buses)):
            if isinstance(pn, Pin):
                # Connecting pin-to-pin.
                if self.is_connected():
                    # If self is already connected to a net, then add the
                    # other pin to the same net.
                    self.nets[0] += pn
                elif pn.is_connected():
                    # If self is unconnected but the other pin is, then
                    # connect self to the other pin's net.
                    pn.nets[0] += self
                else:
                    # Neither pin is connected to a net, so create a net
                    # in the same circuit as the pin and attach both to it.
                    Net(circuit=self.part.circuit).connect(self, pn)
            elif isinstance(pn, Net):
                # Connecting pin-to-net, so just connect the pin to the net.
                pn += self
            else:
                active_logger.raise_(
                    TypeError,
                    f"Cannot attach non-Pin/non-Net {type(pn)} to {self.erc_desc()}."
                )

        # Set the flag to indicate this result came from the += operator.
        set_iadd(self, True)

        return self

    def disconnect(self):
        """
        Disconnect this pin from all nets.
        """
        if not self.net:
            return
        for n in self.nets:
            n.disconnect(self)
            n.merge_names()  # Clean-up the net after removing a pin.
        self.nets = []

    def move(self, net):
        """
        Move pin to another net.

        Args:
            net (Net): Destination net for pin.
        """

        self.disconnect()
        net += self

    def get_nets(self):
        """
        Return a list containing the Net objects connected to this pin.
        
        Returns:
            list: List of Net objects connected to this pin
        """
        return self.nets

    def get_pins(self):
        """
        Return a list containing this pin.
        
        Returns:
            list: List containing just this pin
        """
        return to_list(self)

    def create_network(self):
        """
        Create a network from a single pin.
        
        Returns:
            Network: A network containing just this pin
        """
        from .network import Network

        ntwk = Network()
        ntwk.append(self)
        return ntwk

    def chk_conflict(self, other_pin):
        """
        Check for electrical rule conflicts between this pin and another.
        
        Args:
            other_pin (Pin): The other pin to check against
        """

        if not self.do_erc or not other_pin.do_erc:
            return

        [erc_result, erc_msg] = conflict_matrix[self.func][other_pin.func]

        # Return if the pins are compatible.
        if erc_result == OK:
            return

        # Otherwise, generate an error or warning message.
        if not erc_msg:
            erc_msg = " ".join(
                (
                    pin_info[self.func]["function"],
                    "connected to",
                    pin_info[other_pin.func]["function"],
                )
            )
        n = self.net.name
        p1 = self.erc_desc()
        p2 = other_pin.erc_desc()
        msg = f"Pin conflict on net {n}, {p1} <==> {p2} ({erc_msg})"
        if erc_result == WARNING:
            active_logger.warning(msg)
        else:
            active_logger.error(msg)

    def erc_desc(self):
        """
        Return a string describing this pin for ERC.
        
        Returns:
            str: Description string for the pin
        """
        desc = "{func} pin {num}/{name} of {part}".format(
            part=self.part.erc_desc(),
            num=self.num,
            name=self.name,
            func=pin_info[self.func]["function"],
        )
        return desc

    def get_pin_info(self):
        """
        Get basic pin information.
        
        Returns:
            tuple: (pin number, pin names/aliases, pin function)
        """
        num = getattr(self, "num", "???")
        names = [getattr(self, "name", "???")]
        names.extend(self.aliases)
        names = ",".join(names)
        func = pin_info[self.func]["function"]
        return num, names, func

    def export(self):
        """
        Return a string to recreate a Pin object.
        
        Returns:
            str: Python code string that recreates the pin when evaluated
        """
        attribs = []
        for k in ["num", "name", "func", "unit"]:
            v = getattr(self, k, None)
            if v:
                if k == "func":
                    # Assign the pin function using the actual name of the
                    # function, not its numerical value (in case that changes
                    # in the future if more pin functions are added).
                    v = "pin_types." + pin_info[v]["func_str"]
                else:
                    v = repr(v)
                attribs.append("{}={}".format(k, v))
        return "Pin({})".format(",".join(attribs))

    def _normalize_num(self):
        """
        Normalize pin numbers into a tuple for comparison purposes.

        Returns:
            tuple: Tuple consisting of BGA row identifier and numeric column.
                If it's not a BGA pin, then the tuple contains just a single number.
        """

        # Split the pin number into an initial alpha BGA row followed by column number.
        n = list(re.match(r"(\D*)(.*)", str(self.num)).group(1, 2))

        # Uppercase the BGA row. This has no effect if it's not a BGA.
        n[0] = n[0].upper()

        # Convert the column number (or just the single pin number) to an integer if possible.
        try:
            n[-1] = int(n[-1])
        except ValueError:
            pass

        # return the pin number tuple.
        return n

    @property
    def num(self):
        """
        Get the pin number.
        
        Returns:
            str or int: Pin number
        """
        return self._num
    
    @num.setter
    def num(self, num):
        """
        Set the pin number.
        
        Args:
            num (str or int): Pin number to assign
        """
        del self.num  # Remove any pre-existing num.
        num = str(num) if num is not None else ""  # Store num as a string.
        self._num = num
        
        # Only add a pin alias for the number if it's a non-empty string.
        if num:
            self.aliases += f"p{num}"  # Add new num to aliases.

    @num.deleter
    def num(self):
        """
        Delete the pin number.
        """
        try:
            self.aliases.discard(self._num)
            self._num = None
        except AttributeError:
            pass


    @property
    def pins(self):
        """
        Get a list of pins (just this pin for a Pin object).
        
        Returns:
            list: List containing just this pin
        """
        return self.get_pins()

    @property
    def net(self):
        """
        Return one of the nets the pin is connected to.
        
        Returns:
            Net or None: The first net the pin is connected to, or None
        """
        if self.nets:
            return self.nets[0]
        return None

    @property
    def width(self):
        """
        Return width of a Pin, which is always 1.
        
        Returns:
            int: Always 1
        """
        return 1

    @property
    def drive(self):
        """
        Get the drive strength of this pin.
        
        Returns:
            int: Drive strength value from pin_drives enum
        """
        try:
            return self._drive
        except AttributeError:
            # Drive unspecified, so use default drive for this type of pin.
            return pin_info[self.func]["drive"]

    @drive.setter
    def drive(self, drive):
        """
        Set the drive strength of this pin.
        
        Args:
            drive (int): Drive strength from pin_drives enum
        """
        self._drive = drive

    @drive.deleter
    def drive(self):
        """
        Delete the explicit drive strength, reverting to the default.
        """
        try:
            del self._drive
        except AttributeError:
            pass

    @property
    def ref(self):
        """
        Return the reference of the part the pin belongs to.
        
        Returns:
            str: Reference designator of the parent part
        """
        return self.part.ref

    @property
    def circuit(self):
        """
        Return the circuit of the part the pin belongs to.
        
        Returns:
            Circuit: The circuit containing the parent part
        """
        return self.part.circuit


##############################################################################


@export_to_all
class PhantomPin(Pin):
    """
    A pin type that exists solely to tie two pinless nets together.
    
    It will not participate in generating any netlists.
    """

    def __init__(self, **attribs):
        super().__init__(**attribs)
        self.nets = []
        self.part = None
        self.do_erc = False


##############################################################################


# Create the pin conflict matrix as a defaultdict of defaultdicts which
# returns OK if the given element is not in the matrix. This would indicate
# the pin types used to index that element have no contention if connected.
conflict_matrix = defaultdict(lambda: defaultdict(lambda: [OK, ""]))

# Add the non-OK pin connections to the matrix.
conflict_matrix[pin_types.OUTPUT][pin_types.OUTPUT] = [ERROR, ""]
conflict_matrix[pin_types.TRISTATE][pin_types.OUTPUT] = [WARNING, ""]
conflict_matrix[pin_types.UNSPEC][pin_types.INPUT] = [WARNING, ""]
conflict_matrix[pin_types.UNSPEC][pin_types.OUTPUT] = [WARNING, ""]
conflict_matrix[pin_types.UNSPEC][pin_types.BIDIR] = [WARNING, ""]
conflict_matrix[pin_types.UNSPEC][pin_types.TRISTATE] = [WARNING, ""]
conflict_matrix[pin_types.UNSPEC][pin_types.PASSIVE] = [WARNING, ""]
conflict_matrix[pin_types.UNSPEC][pin_types.PULLUP] = [WARNING, ""]
conflict_matrix[pin_types.UNSPEC][pin_types.PULLDN] = [WARNING, ""]
conflict_matrix[pin_types.UNSPEC][pin_types.UNSPEC] = [WARNING, ""]
conflict_matrix[pin_types.PWRIN][pin_types.TRISTATE] = [WARNING, ""]
conflict_matrix[pin_types.PWRIN][pin_types.UNSPEC] = [WARNING, ""]
conflict_matrix[pin_types.PWROUT][pin_types.OUTPUT] = [ERROR, ""]
conflict_matrix[pin_types.PWROUT][pin_types.BIDIR] = [WARNING, ""]
conflict_matrix[pin_types.PWROUT][pin_types.TRISTATE] = [ERROR, ""]
conflict_matrix[pin_types.PWROUT][pin_types.UNSPEC] = [WARNING, ""]
conflict_matrix[pin_types.PWROUT][pin_types.PWROUT] = [ERROR, ""]
conflict_matrix[pin_types.OPENCOLL][pin_types.OUTPUT] = [ERROR, ""]
conflict_matrix[pin_types.OPENCOLL][pin_types.BIDIR] = [WARNING, ""]
conflict_matrix[pin_types.OPENCOLL][pin_types.TRISTATE] = [ERROR, ""]
conflict_matrix[pin_types.OPENCOLL][pin_types.UNSPEC] = [WARNING, ""]
conflict_matrix[pin_types.OPENCOLL][pin_types.PWROUT] = [ERROR, ""]
conflict_matrix[pin_types.OPENEMIT][pin_types.OUTPUT] = [ERROR, ""]
conflict_matrix[pin_types.OPENEMIT][pin_types.BIDIR] = [WARNING, ""]
conflict_matrix[pin_types.OPENEMIT][pin_types.TRISTATE] = [ERROR, ""]
conflict_matrix[pin_types.OPENEMIT][pin_types.UNSPEC] = [WARNING, ""]
conflict_matrix[pin_types.OPENEMIT][pin_types.PWROUT] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.INPUT] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.OUTPUT] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.BIDIR] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.TRISTATE] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.PASSIVE] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.PULLUP] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.PULLDN] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.UNSPEC] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.PWRIN] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.PWROUT] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.OPENCOLL] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.OPENEMIT] = [ERROR, ""]
conflict_matrix[pin_types.NOCONNECT][pin_types.NOCONNECT] = [ERROR, ""]
conflict_matrix[pin_types.PULLUP][pin_types.PULLUP] = [
    WARNING,
    "Multiple pull-ups connected.",
]
conflict_matrix[pin_types.PULLDN][pin_types.PULLDN] = [
    WARNING,
    "Multiple pull-downs connected.",
]
conflict_matrix[pin_types.PULLUP][pin_types.PULLDN] = [
    ERROR,
    "Pull-up connected to pull-down.",
]

# Fill-in the other half of the symmetrical contention matrix by looking
# for entries that != OK at position (r,c) and copying them to position
# (c,r).
cols = list(conflict_matrix.keys())
for c in cols:
    for r in list(conflict_matrix[c].keys()):
        conflict_matrix[r][c] = conflict_matrix[c][r]
