#!/usr/bin/env python3
# Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
# SPDX-License-Identifier: GPL-3.0-or-later

"""
Create Priority Encoder 8-to-3 IC (with priority: 7 > 6 > ... > 0)

Inputs:
  - data[0..7]: 8 data input bits
  - EI: enable input (74148, active-HIGH). When low, all outputs are forced
    inactive. Tie high for standalone use; for a 16->4 chain drive it from the
    higher-priority encoder's EO.
Outputs:
  - addr[0..2]: 3-bit binary output of the highest-priority active input
  - valid: group-select (74148 GS) = EI AND OR(data[0..7]); 1 iff enabled and
    any input is active
  - EO: enable output (74148) = EI AND no-input-active; chains to the next
    (lower-priority) encoder's EI

Circuit Logic:
- Uses cascaded inhibit signals to ensure only the highest priority input is encoded
- inhibit[i] = NOT(OR of all higher priority inputs)
- selected[i] = data[i] AND inhibit[i] (only true if i is the highest priority active input)
- addr bits encode the selected input using combinational logic

Usage:
    python create_level2_priority_encoder_8to3.py
"""

import asyncio

from element_spacing import HORIZONTAL_GATE_SPACING, VERTICAL_STAGE_SPACING
from ic_builder_base import IC_COMPONENTS_DIR, ICBuilderBase, run_ic_builder


class PriorityEncoder8to3Builder(ICBuilderBase):
    """Builder for Priority Encoder 8-to-3 IC"""

    async def create(self) -> bool:
        """Create the Priority Encoder 8-to-3 IC"""
        await self.begin_build("Priority Encoder 8-to-3")
        # Create new circuit
        if not await self.create_new_circuit():
            return False

        # Position hierarchy
        input_x = 50.0
        input_y = 100.0
        not_inhibit6_x = input_x + HORIZONTAL_GATE_SPACING
        not_inhibit6_y = input_y + VERTICAL_STAGE_SPACING
        or_chain_x = input_x + (2 * HORIZONTAL_GATE_SPACING)
        or_chain_y = input_y + (1.5 * VERTICAL_STAGE_SPACING)
        inhibit_x = input_x + (3 * HORIZONTAL_GATE_SPACING)
        inhibit_base_y = input_y + VERTICAL_STAGE_SPACING
        selected_x = input_x + (4 * HORIZONTAL_GATE_SPACING)
        selected_base_y = input_y + VERTICAL_STAGE_SPACING
        addr_or_x = input_x + (5 * HORIZONTAL_GATE_SPACING)
        # or_addrN_final's labels ("addr1_final", "valid_or", ...) are long
        # enough that a standard 1x step from addr_or_x clears them only by a
        # few px at the default Linux font -- not enough margin for platforms
        # that render the label a bit wider (observed on Windows CI), so this
        # column (and everything to its right, to keep the grid consistent)
        # gets extra clearance.
        addr_or_margin = 32.0
        addr_or_final_x = input_x + (6 * HORIZONTAL_GATE_SPACING) + addr_or_margin
        en_x = input_x + (7 * HORIZONTAL_GATE_SPACING) + addr_or_margin
        output_x = input_x + (8 * HORIZONTAL_GATE_SPACING) + addr_or_margin
        output_base_y = input_y + (1.5 * VERTICAL_STAGE_SPACING)
        inhibit_spacing = VERTICAL_STAGE_SPACING
        selected_spacing = VERTICAL_STAGE_SPACING
        addr_output_spacing = VERTICAL_STAGE_SPACING

        # Create input switches for data bits (8 inputs)
        data_inputs = []
        for i in range(8):
            data_id = await self.create_element(
                "InputSwitch", input_x, 100.0 + i * VERTICAL_STAGE_SPACING, f"data[{i}]"
            )
            if data_id is None:
                return False
            data_inputs.append(data_id)
            await self.log(f"  Created data[{i}] (id={data_id})")

        # Enable input (74148 EI, active-HIGH): when low, all outputs are forced
        # inactive (addr=000, valid=0, EO=0). Tie high for standalone use; for
        # chaining two encoders into 16->4, drive this from the higher-priority
        # encoder's EO.
        ei_input = await self.create_element("InputSwitch", input_x, 100.0 + 8 * VERTICAL_STAGE_SPACING, "EI")
        if ei_input is None:
            return False
        set_ei = await self.mcp.send_command("set_input_value", {"element_id": ei_input, "value": True})
        if not set_ei.success:
            self.log_error("Failed to default EI high")
            return False
        await self.log("  Created enable input EI (default high)")

        # Build inhibit cascade signals
        # inhibit[i] = NOT(OR of all inputs with priority > i)
        # inhibit7: always 1 (data7 has no higher priority inputs)
        # inhibit6: NOT(data[7])
        # inhibit5: NOT(data[7] OR data[6])
        # etc.

        await self.log("  Building inhibit cascade signals:")

        # NOT(data[7]) for inhibit6
        inhibit6 = await self.create_element("Not", not_inhibit6_x, not_inhibit6_y, "inhibit6")
        if inhibit6 is None:
            return False
        if not await self.connect(data_inputs[7], inhibit6):
            return False

        # Build OR gates for inhibit signals (OPTIMIZED - multi-input OR gates)
        # inhibit[i] = NOT(OR of all data with priority > i)
        # This replaces the cascading chain with direct multi-input OR gates
        or_chain = []  # or_chain[i] = OR(data[7..i])

        # or_chain[7] = data[7]
        or_chain.append(data_inputs[7])

        # or_chain[6] = 2-input OR(data[7], data[6])
        or76 = await self.create_element("Or", or_chain_x, or_chain_y, "or_76")
        if or76 is None:
            return False

        if not await self.connect(data_inputs[7], or76):
            return False

        if not await self.connect(data_inputs[6], or76, target_port=1):
            return False
        or_chain.append(or76)

        # Create remaining OR gates with increasing input counts
        # or_chain[5] = 3-input OR(data[7], data[6], data[5])
        # or_chain[4] = 4-input OR(data[7], data[6], data[5], data[4])
        # ... down to OR(data[7..1]), which feeds inhibit0. An OR of all
        # eight bits is never needed (F58: it used to be built and left dead).
        for i in range(5, 0, -1):
            num_inputs = 8 - i  # Number of data inputs to OR together

            # OR gates with 5+ inputs grow taller than the flat 104px row step
            # (each input beyond 4 adds 16px). The 7-input gate (i=1) grows
            # just tall enough to clip the label of the 6-input gate above it
            # (i=2), so give this last row some extra clearance.
            row_y = or_chain_y + (6 - i) * VERTICAL_STAGE_SPACING
            if i == 1:
                row_y += 0.25 * VERTICAL_STAGE_SPACING

            or_gate = await self.create_element("Or", or_chain_x, row_y, f"or_{7}_to_{i}")
            if or_gate is None:
                return False

            # Set OR gate to have correct number of inputs
            if num_inputs > 2:
                # Set input size for gates that need >2 inputs
                set_props = await self.mcp.send_command(
                    "change_input_size", {"element_id": or_gate, "size": num_inputs}
                )
                if not set_props.success:
                    # A gate left at its default size drops inputs silently — the
                    # generated encoder would mis-prioritize. Fail like every
                    # other error path so the corrupt fixture is never saved.
                    print(f"Error: Could not set input_size={num_inputs} for OR gate")
                    return False

            # Connect all required data inputs
            for port_idx in range(num_inputs):
                data_bit_idx = 7 - port_idx  # data[7], then data[6], etc.
                if not await self.connect(data_inputs[data_bit_idx], or_gate, target_port=port_idx):
                    return False

            or_chain.append(or_gate)

        # Build inhibit gates from OR chain
        inhibits: list[int | None] = [None] * 8  # inhibits[i] = NOT(or_chain[7-i])
        inhibits[7] = None  # data[7] is never inhibited

        # inhibit[6] already created above
        inhibits[6] = inhibit6

        # Create inhibit gates for 0-5
        for i in range(5, -1, -1):
            inhibit_gate = await self.create_element(
                "Not", inhibit_x, inhibit_base_y + (5 - i) * inhibit_spacing, f"inhibit{i}"
            )
            if inhibit_gate is None:
                return False
            # Connect OR chain output
            or_chain_idx = 6 - i  # or_chain[6-i] = OR(data[7..i+1])
            if not await self.connect(or_chain[or_chain_idx], inhibit_gate):
                return False
            inhibits[i] = inhibit_gate

        await self.log("  Created inhibit cascade")

        # Create selected signals: selected[i] = data[i] AND inhibit[i]
        selected: list[int | None] = [None] * 8
        selected[7] = data_inputs[7]  # data[7] doesn't need AND gate (no inhibition)

        for i in range(6, -1, -1):
            sel_gate = await self.create_element(
                "And", selected_x, selected_base_y + (6 - i) * selected_spacing, f"sel{i}"
            )
            if sel_gate is None:
                return False
            # Connect data[i]
            if not await self.connect(data_inputs[i], sel_gate):
                return False
            # Connect inhibit[i]
            inhibit_i = inhibits[i]
            assert inhibit_i is not None
            if not await self.connect(inhibit_i, sel_gate, target_port=1):
                return False
            selected[i] = sel_gate

        await self.log("  Created selected signals")

        # Narrow list[int | None] to int: all entries 0-7 are set above
        # (the loop returned False early if any create_element failed)
        sel1, sel2, sel3, sel4, sel5, sel6, sel7 = (
            selected[1],
            selected[2],
            selected[3],
            selected[4],
            selected[5],
            selected[6],
            selected[7],
        )
        assert sel1 is not None and sel2 is not None and sel3 is not None
        assert sel4 is not None and sel5 is not None and sel6 is not None and sel7 is not None

        # Encode the selected inputs
        # addr[2]: 1 if selected[4..7] (indices with bit2=1: 4,5,6,7)
        addr2_gate = await self.create_element("Or", addr_or_x, output_base_y, "addr2")
        if addr2_gate is None:
            return False
        if not await self.connect(sel4, addr2_gate):
            return False
        if not await self.connect(sel5, addr2_gate, target_port=1):
            return False

        # Need more OR gates for addr[2] = selected[4] OR selected[5] OR selected[6] OR selected[7]
        or_addr2_56 = await self.create_element(
            "Or", addr_or_x, output_base_y + (1.5 * VERTICAL_STAGE_SPACING), "or_addr2_56"
        )
        if or_addr2_56 is None:
            return False
        if not await self.connect(sel6, or_addr2_56):
            return False
        if not await self.connect(sel7, or_addr2_56, target_port=1):
            return False

        addr2_final = await self.create_element(
            "Or", addr_or_final_x, output_base_y + (0.75 * VERTICAL_STAGE_SPACING), "addr2_final"
        )
        if addr2_final is None:
            return False
        if not await self.connect(addr2_gate, addr2_final):
            return False
        if not await self.connect(or_addr2_56, addr2_final, target_port=1):
            return False

        # addr[1]: 1 if selected[2..3] OR selected[6..7] (indices with bit1=1: 2,3,6,7)
        or_addr1_23 = await self.create_element(
            "Or", addr_or_x, output_base_y + (3 * VERTICAL_STAGE_SPACING), "or_addr1_23"
        )
        if or_addr1_23 is None:
            return False
        if not await self.connect(sel2, or_addr1_23):
            return False
        if not await self.connect(sel3, or_addr1_23, target_port=1):
            return False

        or_addr1_67 = await self.create_element(
            "Or", addr_or_x, output_base_y + (4 * VERTICAL_STAGE_SPACING), "or_addr1_67"
        )
        if or_addr1_67 is None:
            return False
        if not await self.connect(sel6, or_addr1_67):
            return False
        if not await self.connect(sel7, or_addr1_67, target_port=1):
            return False

        addr1_final = await self.create_element(
            "Or", addr_or_final_x, output_base_y + (3.05 * VERTICAL_STAGE_SPACING), "addr1_final"
        )
        if addr1_final is None:
            return False
        if not await self.connect(or_addr1_23, addr1_final):
            return False
        if not await self.connect(or_addr1_67, addr1_final, target_port=1):
            return False

        # addr[0]: 1 if selected[1] OR selected[3] OR selected[5] OR selected[7] (odd indices)
        or_addr0_13 = await self.create_element(
            "Or", addr_or_x, output_base_y + (5 * VERTICAL_STAGE_SPACING), "or_addr0_13"
        )
        if or_addr0_13 is None:
            return False
        if not await self.connect(sel1, or_addr0_13):
            return False
        if not await self.connect(sel3, or_addr0_13, target_port=1):
            return False

        or_addr0_57 = await self.create_element(
            "Or", addr_or_x, output_base_y + (6 * VERTICAL_STAGE_SPACING), "or_addr0_57"
        )
        if or_addr0_57 is None:
            return False
        if not await self.connect(sel5, or_addr0_57):
            return False
        if not await self.connect(sel7, or_addr0_57, target_port=1):
            return False

        addr0_final = await self.create_element(
            "Or", addr_or_final_x, output_base_y + (5.05 * VERTICAL_STAGE_SPACING), "addr0_final"
        )
        if addr0_final is None:
            return False
        if not await self.connect(or_addr0_13, addr0_final):
            return False
        if not await self.connect(or_addr0_57, addr0_final, target_port=1):
            return False

        await self.log("  Created output encoding logic")

        # "Any input active" = OR(data[0..7]) — drives the group-select (valid)
        # and the EO cascade output. F99 re-introduces the 8-wide OR that F58
        # dropped as dead; here it has a real use.
        valid_or = await self.create_element(
            "Or", addr_or_final_x, output_base_y + (7 * VERTICAL_STAGE_SPACING), "valid_or"
        )
        if valid_or is None:
            return False
        set_props = await self.mcp.send_command("change_input_size", {"element_id": valid_or, "size": 8})
        if not set_props.success:
            # An under-sized valid_or drops data inputs, corrupting valid/EO.
            # Fail like every other error path so the fixture is never saved.
            print("Error: Could not set input_size=8 for valid_or")
            return False
        for i in range(8):
            if not await self.connect(data_inputs[i], valid_or, target_port=i):
                return False

        # ========== Enable gating + cascade output (74148 EI/EO) ==========
        # Every addr/valid output is ANDed with EI (forced to 0 when disabled).
        # EO = EI AND no-input-active, so a chained lower-priority encoder is
        # enabled only when this one sees nothing on its data inputs.
        addr_finals = [addr0_final, addr1_final, addr2_final]
        addr_gated = []
        for i, addr_sig in enumerate(addr_finals):
            g = await self.create_element("And", en_x, output_base_y + i * addr_output_spacing, f"addr{i}_en")
            if g is None:
                return False
            if not await self.connect(addr_sig, g):
                return False
            if not await self.connect(ei_input, g, target_port=1):
                return False
            addr_gated.append(g)

        valid_gated = await self.create_element("And", en_x, output_base_y + 3 * addr_output_spacing, "valid_en")
        if valid_gated is None:
            return False
        if not await self.connect(valid_or, valid_gated):
            return False
        if not await self.connect(ei_input, valid_gated, target_port=1):
            return False

        not_any = await self.create_element("Not", en_x, output_base_y + 4 * addr_output_spacing, "not_any_active")
        if not_any is None:
            return False
        if not await self.connect(valid_or, not_any):
            return False

        eo_gate = await self.create_element("And", en_x, output_base_y + 5 * addr_output_spacing, "eo")
        if eo_gate is None:
            return False
        if not await self.connect(ei_input, eo_gate):
            return False
        if not await self.connect(not_any, eo_gate, target_port=1):
            return False

        await self.log("  Created EI gating and EO cascade output")

        # Create output LEDs: three address bits, valid (GS), and EO
        output_leds = []
        for i in range(3):
            led_id = await self.create_element("Led", output_x, output_base_y + i * addr_output_spacing, f"addr[{i}]")
            if led_id is None:
                return False
            output_leds.append(led_id)

        valid_led = await self.create_element("Led", output_x, output_base_y + 3 * addr_output_spacing, "valid")
        if valid_led is None:
            return False
        # "not_any_active" is a long label; at the standard 1x column gap its
        # text reaches into the EO LED's label on the same row, so give this
        # LED some extra horizontal clearance.
        eo_led = await self.create_element(
            "Led", output_x + (0.5 * HORIZONTAL_GATE_SPACING), output_base_y + 4 * addr_output_spacing, "EO"
        )
        if eo_led is None:
            return False

        # Connect EI-gated address outputs to LEDs
        for i, led_id in enumerate(output_leds):
            if not await self.connect(addr_gated[i], led_id):
                return False

        # Connect valid and EO outputs
        if not await self.connect(valid_gated, valid_led):
            return False
        if not await self.connect(eo_gate, eo_led):
            return False

        await self.log("  Created and connected output LEDs")

        # Save circuit as IC
        output_file = str(IC_COMPONENTS_DIR / "level2_priority_encoder_8to3.panda")
        if not await self.save_circuit(output_file):
            return False

        await self.log(
            f"Successfully created PriorityEncoder8to3 IC"
            f"({self.element_count} elements, {self.connection_count} connections)"
        )
        await self.log(f"   Saved to: {output_file}")
        return True


async def build(mcp) -> bool:
    """Entry point for the builder"""
    builder = PriorityEncoder8to3Builder(mcp, verbose=True)
    return await builder.create()


if __name__ == "__main__":
    import sys
    import traceback

    try:
        exit_code = asyncio.run(run_ic_builder(build, "Priority Encoder 8-to-3 IC"))
        sys.exit(exit_code)
    except Exception as e:
        print(f"Error: {e}")
        traceback.print_exc()
        sys.exit(1)
