# -----------------------------
# DIGITAL LOGIC PYTHON LIBRARY
# -----------------------------

# -----------------------------
# BASIC LOGIC GATES
# -----------------------------
def AND(a, b): return 1 if a == 1 and b == 1 else 0
def OR(a, b): return 1 if a == 1 or b == 1 else 0
def NOT(a): return 0 if a == 1 else 1
def NAND(a, b): return NOT(AND(a, b))
def NOR(a, b): return NOT(OR(a, b))
def XOR(a, b): return 1 if a != b else 0
def XNOR(a, b): return NOT(XOR(a, b))

# -----------------------------
# CONVERSION FUNCTIONS
# -----------------------------
def bin_to_hex(binary_str):
    return hex(int(binary_str, 2))[2:].upper()

def hex_to_bin(hex_str, bits=None):
    bin_str = bin(int(hex_str, 16))[2:]
    if bits: bin_str = bin_str.zfill(bits)
    return bin_str

def bin_to_dec(binary_str):
    return int(binary_str, 2)

def dec_to_bin(decimal, bits=None):
    bin_str = bin(int(decimal))[2:]
    if bits: bin_str = bin_str.zfill(bits)
    return bin_str

def hex_to_dec(hex_str):
    return int(hex_str, 16)

def dec_to_hex(decimal, bits=None):
    hex_str = hex(int(decimal))[2:].upper()
    if bits: hex_str = hex_str.zfill((bits + 3) // 4)
    return hex_str

# -----------------------------
# MULTI-BIT ARITHMETIC
# -----------------------------
def adder(a_bin, b_bin):
    max_len = max(len(a_bin), len(b_bin))
    a_bin = a_bin.zfill(max_len)
    b_bin = b_bin.zfill(max_len)
    result, carry = '', 0
    for i in range(max_len-1, -1, -1):
        total = int(a_bin[i]) + int(b_bin[i]) + carry
        result = str(total % 2) + result
        carry = total // 2
    if carry: result = '1' + result
    return result

def subtractor(a_bin, b_bin):
    diff = int(a_bin, 2) - int(b_bin, 2)
    if diff < 0: raise ValueError("Result would be negative")
    return bin(diff)[2:]

def multiplier(a_bin, b_bin):
    return bin(int(a_bin, 2) * int(b_bin, 2))[2:]

def divider(a_bin, b_bin):
    a_int, b_int = int(a_bin, 2), int(b_bin, 2)
    if b_int == 0: raise ZeroDivisionError("Division by zero")
    return bin(a_int // b_int)[2:], bin(a_int % b_int)[2:]

# -----------------------------
# CIRCUIT SYSTEM
# -----------------------------
class Gate:
    def __init__(self, func, name=None):
        self.func = func
        self.inputs = []
        self.name = name

    def connect(self, *gates_or_values):
        self.inputs = gates_or_values

    def output(self):
        values = [i.output() if isinstance(i, Gate) else i for i in self.inputs]
        return self.func(*values)

def create_gate(gate_type, *inputs, name=None):
    gate_map = {"AND": AND,"OR": OR,"NOT": NOT,"NAND": NAND,"NOR": NOR,"XOR": XOR,"XNOR": XNOR}
    if gate_type not in gate_map: raise ValueError(f"Unknown gate type: {gate_type}")
    gate = Gate(gate_map[gate_type], name)
    gate.connect(*inputs)
    return gate

# -----------------------------
# MULTIPLEXERS / DEMULTIPLEXERS
# -----------------------------
def MUX(a, b, sel):
    return OR(AND(NOT(sel), a), AND(sel, b))

def DEMUX(inp, sel):
    return (AND(inp, NOT(sel)), AND(inp, sel))

# -----------------------------
# ENCODERS / DECODERS
# -----------------------------
class Encoder:
    def __init__(self, mapping):
        self.mapping = mapping  # dict: input tuple -> output binary string
    def encode(self, inp):
        return self.mapping.get(inp, None)

class Decoder:
    def __init__(self, mapping):
        self.mapping = mapping  # dict: binary string -> output tuple
    def decode(self, inp_bin):
        return self.mapping.get(inp_bin, None)

# -----------------------------
# FLIP-FLOPS / LATCHES
# -----------------------------
class DFlipFlop:
    def __init__(self): self.q = 0
    def clock(self, d):
        self.q = d
        return self.q

class TFlipFlop:
    def __init__(self): self.q = 0
    def clock(self, t):
        if t: self.q = NOT(self.q)
        return self.q

class JKFlipFlop:
    def __init__(self): self.q = 0
    def clock(self, j, k):
        if j and k: self.q = NOT(self.q)
        elif j: self.q = 1
        elif k: self.q = 0
        return self.q

class SRFlipFlop:
    def __init__(self): self.q = 0
    def clock(self, s, r):
        if s and r: raise ValueError("Invalid SR inputs: S=R=1")
        elif s: self.q = 1
        elif r: self.q = 0
        return self.q

# -----------------------------
# SHIFT REGISTERS
# -----------------------------
class ShiftRegister:
    def __init__(self, size):
        self.bits = [0]*size
    def shift_in(self, bit):
        self.bits = [bit] + self.bits[:-1]
        return self.bits
    def shift_out(self):
        return self.bits[-1]

# -----------------------------
# COUNTERS
# -----------------------------
class BinaryCounter:
    def __init__(self, bits): self.value = 0; self.bits = bits
    def clock(self):
        self.value = (self.value + 1) % (2**self.bits)
        return dec_to_bin(self.value, self.bits)

class RingCounter:
    def __init__(self, bits):
        self.bits = bits
        self.value = [1] + [0]*(bits-1)
    def clock(self):
        self.value = self.value[-1:] + self.value[:-1]
        return self.value

class JohnsonCounter:
    def __init__(self, bits):
        self.bits = bits
        self.value = [0]*bits
    def clock(self):
        self.value = [NOT(self.value[-1])] + self.value[:-1]
        return self.value

# -----------------------------
# TRUTH TABLE GENERATOR
# -----------------------------
def truth_table(func, num_inputs):
    from itertools import product
    table = []
    for inputs in product([0,1], repeat=num_inputs):
        table.append((*inputs, func(*inputs)))
    return table