// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
// SPDX-License-Identifier: GPL-3.0-or-later

#include "App/Element/GraphicElements/Demux.h"

#include <cmath>

#include <QPainter>

#include "App/Core/Constants.h"
#include "App/Element/ElementFactory.h"
#include "App/Element/ElementInfo.h"
#include "App/Wiring/Port.h"

template<>
struct ElementInfo<Demux> {
    static constexpr ElementConstraints constraints{
        .type = ElementType::Demux,
        .group = ElementGroup::Mux,
        .minInputSize = 2,
        .maxInputSize = 4,
        .minOutputSize = 2,
        .maxOutputSize = 8,
        .canChangeAppearance = true,
    };
    static_assert(validate(constraints));

    static ElementMetadata metadata()
    {
        auto meta = metadataFromConstraints(constraints);
        meta.pixmapPath = []{ return QStringLiteral(":/Components/Logic/demux.svg"); };
        meta.titleText = QT_TRANSLATE_NOOP("Demux", "DEMULTIPLEXER");
        meta.translatedName = QT_TRANSLATE_NOOP("Demux", "Demux");
        meta.trContext = "Demux";
        meta.defaultAppearances = QStringList({":/Components/Logic/demux.svg"});
        return meta;
    }

    static inline const bool registered = []() {
        ElementMetadataRegistry::registerMetadata(metadata());
        ElementFactory::registerCreator(constraints.type, [] { return new Demux(); });
        return true;
    }();
};

Demux::Demux(QGraphicsItem *parent)
    : GraphicElement(ElementType::Demux, parent)
{
    Demux::updatePortsProperties();
}

void Demux::setInputSize(const int size)
{
    // Input size is derived from output size and cannot be set directly
    // This is a private override to prevent accidental calls
    Q_UNUSED(size)
    // Do nothing - this method should not be called
}

void Demux::setOutputSize(const int size)
{
    // Validate size is within bounds
    if ((size < minOutputSize()) || (size > maxOutputSize())) {
        return;  // Silently ignore invalid sizes, stays at current size
    }

    // Calculate required input ports: 1 data + log2(size) select lines
    const int numSelectLines = calculateSelectLines(size);
    int requiredInputs = 1 + numSelectLines;

    // Update output size FIRST to ensure outputSize() reflects new value
    // This prevents updatePortsProperties() from using stale output count
    GraphicElement::setOutputSize(size);

    // Then, update input size to match the select lines needed
    // This will trigger updatePortsProperties() with both input and output sizes correct
    GraphicElement::setInputSize(requiredInputs);

}

void Demux::updatePortsProperties()
{
    // Determine number of select lines based on number of outputs
    const int numSelectLines = calculateSelectLines(outputSize());

    const int step = Constants::gridSize / 2; // 8

    // Calculate element height to fit all output ports with padding
    int outputPortsSpan = (outputSize() - 1) * step * 2;
    int elementHeight = (std::max)(64, outputPortsSpan + step * 6);
    // Snap to multiple of 16
    elementHeight = ((elementHeight + 15) / 16) * 16;
    int centerY = elementHeight / 2;

    // Position data input on the left, centered
    inputPort(0)->setPos(16, centerY);
    inputPort(0)->setName("In");

    // Position select lines at the bottom, spaced by gridSize to avoid overlap
    int selectY = elementHeight - step;
    for (int i = 0; i < numSelectLines; ++i) {
        if ((1 + i) < inputSize()) {
            inputPort(1 + i)->setPos(32 + (i * step * 2), selectY);
            inputPort(1 + i)->setName(QString("S%1").arg(i));
        }
    }

    // Position output ports on the right, centered vertically
    int y = centerY - (outputSize() - 1) * step;
    for (int i = 0; i < outputSize(); ++i) {
        if (outputPort(i) != nullptr) {
            outputPort(i)->setPos(48, y);
            outputPort(i)->setName(QString("Out%1").arg(i));
        }
        y += step * 2;
    }

    generatePixmap();
}

QRectF Demux::boundingRect() const
{
    return renderBodyBounds();
}

void Demux::generatePixmap()
{
    // The body is drawn as vectors in drawBody()/paint(); m_pixmap is kept only so the base
    // pixmapCenter()/boundingRect() have the right size (its image content is never displayed).
    const int height = static_cast<int>(renderBodyBounds().height());

    QPixmap sizingPixmap(64, height);
    sizingPixmap.fill(Qt::transparent);
    m_appearance.setRenderPixmap(sizingPixmap);
    update();
}

void Demux::drawBody(QPainter *painter)
{
    painter->save();
    painter->setRenderHint(QPainter::Antialiasing);
    // boundingRect()'s top-left may be negative when ports extend past the 64×64 body; align the
    // local origin with it so the body lands exactly where the old rasterised pixmap was blitted.
    painter->translate(boundingRect().topLeft());

    const int height = pixmap().rect().height();

    // Trapezoid body: narrower on left (input side), wider on right (output side)
    const int bodyTop = 8;
    const int bodyBottom = height - 10;
    const int leftInset = 8; // fixed inset for the narrower left side

    // Outer dark shape
    painter->setBrush(QColor(38, 38, 38));
    painter->setPen(QPen(QColor(38, 38, 38), 3, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));

    QPolygonF outer;
    outer << QPointF(22, bodyTop + leftInset)
          << QPointF(22, bodyBottom - leftInset)
          << QPointF(42, bodyBottom)
          << QPointF(42, bodyTop);
    painter->drawPolygon(outer);

    // Inner white fill
    painter->setBrush(QColor(252, 252, 252));
    painter->setPen(QPen(QColor(252, 252, 252), 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));

    const int fillInset = 4;
    QPolygonF inner;
    inner << QPointF(22 + fillInset, bodyTop + leftInset + fillInset - 2)
          << QPointF(22 + fillInset, bodyBottom - leftInset - fillInset + 2)
          << QPointF(42 - fillInset, bodyBottom - fillInset)
          << QPointF(42 - fillInset, bodyTop + fillInset);
    painter->drawPolygon(inner);

    painter->restore();
}

void Demux::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(widget)
    Q_UNUSED(option)

    if (isSelected()) {
        painter->save();
        painter->setBrush(m_appearance.selectionBrush());
        painter->setPen(QPen(m_appearance.selectionPen(), 0.5, Qt::SolidLine));
        painter->drawRoundedRect(boundingRect(), 5, 5);
        painter->restore();
    }

    // Draw the body as vectors (crisp at any zoom) rather than blitting a fixed-resolution pixmap.
    drawBody(painter);
}

void Demux::updateLogic()
{
    if (!simUpdateInputsAllowUnknown()) {
        return;
    }

    const Status data = simInputs().at(0);

    // Calculate select lines from current output count
    const int numSelectLines = calculateSelectLines(outputSize());

    // If any select line is Unknown/Error, all outputs are indeterminate
    for (int i = 0; i < numSelectLines; i++) {
        const Status sel = simInputs().at(1 + i);
        if (sel == Status::Unknown || sel == Status::Error) {
            for (int j = 0; j < outputSize(); j++) {
                setOutputValue(j, sel);
            }
            return;
        }
    }

    const int selectValue = decodeSelectValue(1, numSelectLines);

    // Mirror Mux: a select value that addresses no output (possible whenever
    // the output count is not a power of two) is indeterminate routing — all
    // outputs become Unknown instead of silently dropping the data.
    if (selectValue >= outputSize()) {
        for (int i = 0; i < outputSize(); i++) {
            setOutputValue(i, Status::Unknown);
        }
        return;
    }

    for (int i = 0; i < outputSize(); i++) {
        setOutputValue(i, (i == selectValue) ? data : Status::Inactive);
    }
}

int Demux::calculateSelectLines(int dataCount)
{
    int k = 1;
    while ((1 << k) < dataCount) {
        k++;
    }
    return k;
}
