// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "stylesheetmerger.h"

#include <abstractview.h>
#include <bindingproperty.h>
#include <invalididexception.h>
#include <invalidmodelnodeexception.h>
#include <invalidreparentingexception.h>
#include <modelmerger.h>
#include <nodeabstractproperty.h>
#include <nodelistproperty.h>
#include <nodemetainfo.h>
#include <nodeproperty.h>
#include <plaintexteditmodifier.h>
#include <rewriterview.h>
#include <variantproperty.h>

#include <utils/fileutils.h>
#include <utils/qtcassert.h>

#include <QPlainTextEdit>
#include <QQueue>
#include <QRegularExpression>

namespace {

QPoint pointForModelNode(const QmlDesigner::ModelNode &node)
{
    int x = 0;
    if (node.hasVariantProperty("x"))
        x = node.variantProperty("x").value().toInt();

    int y = 0;
    if (node.hasVariantProperty("y"))
        y = node.variantProperty("y").value().toInt();

    return QPoint(x, y);
}

QPoint parentPosition(const QmlDesigner::ModelNode &node)
{
    QPoint p;

    QmlDesigner::ModelNode currentNode = node;
    while (currentNode.hasParentProperty()) {
        currentNode = currentNode.parentProperty().parentModelNode();
        p += pointForModelNode(currentNode);
    }

    return p;
}

bool isTextAlignmentProperty(const QmlDesigner::VariantProperty &property)
{
    return property.name() == "horizontalAlignment"
                || property.name() == "verticalAlignment"
                || property.name() == "elide";
}
} // namespace

namespace QmlDesigner {

static void splitIdInBaseNameAndNumber(const QString &id, QString *baseId, int *number)
{

    int counter = 0;
    while (counter < id.count()) {
        bool canConvertToInteger = false;
        int newNumber = id.right(counter + 1).toInt(&canConvertToInteger);
        if (canConvertToInteger)
            *number = newNumber;
        else
            break;

        counter++;
    }

    *baseId = id.left(id.count() - counter);
}

void StylesheetMerger::syncNodeProperties(ModelNode &outputNode, const ModelNode &inputNode, bool skipDuplicates)
{
    for (const NodeProperty &nodeProperty : inputNode.nodeProperties()) {
        ModelNode oldNode = nodeProperty.modelNode();
        if (m_templateView->hasId(oldNode.id()) && skipDuplicates)
            continue;
        ModelNode newNode = createReplacementNode(oldNode, oldNode);
        // cache the property name as removing it will invalidate it
        PropertyName propertyName = nodeProperty.name();
        // remove property first to prevent invalid reparenting situation
        outputNode.removeProperty(propertyName);
        outputNode.nodeProperty(propertyName).reparentHere(newNode);
    }
}

void StylesheetMerger::syncNodeListProperties(ModelNode &outputNode, const ModelNode &inputNode, bool skipDuplicates)
{
    for (const NodeListProperty &nodeListProperty : inputNode.nodeListProperties()) {
        for (ModelNode node : nodeListProperty.toModelNodeList()) {
            if (m_templateView->hasId(node.id()) && skipDuplicates)
                continue;
            ModelNode newNode = createReplacementNode(node, node);
            outputNode.nodeListProperty(nodeListProperty.name()).reparentHere(newNode);
        }
    }
}

void StylesheetMerger::syncVariantProperties(ModelNode &outputNode, const ModelNode &inputNode)
{
    for (const VariantProperty &variantProperty : inputNode.variantProperties()) {
        outputNode.variantProperty(variantProperty.name()).setValue(variantProperty.value());
    }
}

void StylesheetMerger::syncAuxiliaryProperties(ModelNode &outputNode, const ModelNode &inputNode)
{
    for (const auto &[key, value] : inputNode.auxiliaryData())
        outputNode.setAuxiliaryData(AuxiliaryDataKeyView{key}, value);
}

void StylesheetMerger::syncBindingProperties(ModelNode &outputNode, const ModelNode &inputNode)
{
    for (const BindingProperty &bindingProperty : inputNode.bindingProperties()) {
        outputNode.bindingProperty(bindingProperty.name()).setExpression(bindingProperty.expression());
    }
}

void StylesheetMerger::syncId(ModelNode &outputNode, ModelNode &inputNode)
{
    if (!inputNode.id().isEmpty()) {
        QString id = inputNode.id();
        QString renamedId = m_idReplacementHash.value(inputNode.id());
        inputNode.setIdWithoutRefactoring(renamedId);
        outputNode.setIdWithoutRefactoring(id);
    }
}

void StylesheetMerger::setupIdRenamingHash()
{
    const QList<ModelNode> &nodes = m_templateView->rootModelNode().allSubModelNodesAndThisNode();
    for (const ModelNode &node : nodes) {
        if (!node.id().isEmpty()) {
            QString newId = node.id();
            QString baseId;
            int number = 1;
            splitIdInBaseNameAndNumber(newId, &baseId, &number);

            while (m_templateView->hasId(newId) || std::find(m_idReplacementHash.cbegin(),
                        m_idReplacementHash.cend(), newId) != m_idReplacementHash.cend()) {
                newId = "stylesheet_auto_merge_" + baseId + QString::number(number);
                number++;
            }

            m_idReplacementHash.insert(node.id(), newId);
        }
    }
}

ModelNode StylesheetMerger::createReplacementNode(const ModelNode& styleNode, ModelNode &modelNode)
{
    QList<QPair<PropertyName, QVariant> > propertyList;
    NodeMetaInfo nodeMetaInfo = m_templateView->model()->metaInfo(styleNode.type());

    for (const VariantProperty &variantProperty : modelNode.variantProperties()) {
        if (!nodeMetaInfo.hasProperty(variantProperty.name()))
            continue;
        if (isTextAlignmentProperty(variantProperty) && !m_options.preserveTextAlignment && !styleNode.hasProperty(variantProperty.name()))
            continue;
        propertyList.append(QPair<PropertyName, QVariant>(variantProperty.name(), variantProperty.value()));
    }
    ModelNode newNode(m_templateView->createModelNode(styleNode.type(),
                                                      nodeMetaInfo.majorVersion(),
                                                      nodeMetaInfo.minorVersion(),
                                                      propertyList,
                                                      {},
                                                      styleNode.nodeSource(),
                                                      styleNode.nodeSourceType()));

    syncAuxiliaryProperties(newNode, modelNode);
    syncBindingProperties(newNode, modelNode);
    syncId(newNode, modelNode);
    syncNodeProperties(newNode, modelNode);
    syncNodeListProperties(newNode, modelNode);

    return newNode;
}

StylesheetMerger::StylesheetMerger(AbstractView *templateView, AbstractView *styleView)
    : m_templateView(templateView)
    , m_styleView(styleView)
{
}

bool StylesheetMerger::idExistsInBothModels(const QString& id)
{
    return m_templateView->hasId(id) && m_styleView->hasId(id);
}

void StylesheetMerger::preprocessStyleSheet()
{
    try {
        RewriterTransaction transaction(m_styleView, "preprocess-stylesheet");
        for (const ModelNode &currentStyleNode : m_styleView->rootModelNode().directSubModelNodes()) {
            QString id = currentStyleNode.id();

            if (!idExistsInBothModels(id))
                continue;

            ModelNode templateNode = m_templateView->modelNodeForId(id);
            NodeAbstractProperty templateParentProperty = templateNode.parentProperty();
            if (!templateNode.hasParentProperty()
                    || templateParentProperty.parentModelNode().isRootNode())
                continue;

            ModelNode templateParentNode = templateParentProperty.parentModelNode();
            const QString parentId = templateParentNode.id();
            if (!idExistsInBothModels(parentId))
                continue;

            // Only get the position properties as the node should have a global
            // position in the style sheet.
            const QPoint oldGlobalPos = pointForModelNode(currentStyleNode);

            ModelNode newStyleParent = m_styleView->modelNodeForId(parentId);
            NodeListProperty newParentProperty = newStyleParent.defaultNodeListProperty();
            newParentProperty.reparentHere(currentStyleNode);

            // Get the parent position in global coordinates.
            QPoint parentGlobalPos = parentPosition(currentStyleNode);

            const QPoint newGlobalPos = oldGlobalPos - parentGlobalPos;

            currentStyleNode.variantProperty("x").setValue(newGlobalPos.x());
            currentStyleNode.variantProperty("y").setValue(newGlobalPos.y());

            int templateParentIndex = templateParentProperty.isNodeListProperty()
                    ? templateParentProperty.indexOf(templateNode) : -1;
            int styleParentIndex = newParentProperty.indexOf(currentStyleNode);
            if (templateParentIndex >= 0 && styleParentIndex != templateParentIndex)
                newParentProperty.slide(styleParentIndex, templateParentIndex);
        }
        transaction.commit();
    }catch (InvalidIdException &ide) {
        qDebug().noquote() << "Invalid id exception while preprocessing the style sheet.";
        ide.createWarning();
    } catch (InvalidReparentingException &rpe) {
        qDebug().noquote() << "Invalid reparenting exception while preprocessing the style sheet.";
        rpe.createWarning();
    } catch (InvalidModelNodeException &mne) {
        qDebug().noquote() << "Invalid model node exception while preprocessing the style sheet.";
        mne.createWarning();
    } catch (Exception &e) {
        qDebug().noquote() << "Exception while preprocessing the style sheet.";
        e.createWarning();
    }

}

void StylesheetMerger::replaceNode(ModelNode &replacedNode, ModelNode &newNode)
{
    NodeListProperty replacedNodeParent;
    ModelNode parentModelNode = replacedNode.parentProperty().parentModelNode();
    if (replacedNode.parentProperty().isNodeListProperty())
        replacedNodeParent = replacedNode.parentProperty().toNodeListProperty();
    bool isNodeProperty = false;

    PropertyName reparentName;
    for (const NodeProperty &prop : parentModelNode.nodeProperties()) {
        if (prop.modelNode().id() == replacedNode.id()) {
            isNodeProperty = true;
            reparentName = prop.name();
        }
    }
    ReparentInfo info;
    info.parentIndex = replacedNodeParent.isValid() ? replacedNodeParent.indexOf(replacedNode) : -1;
    info.templateId = replacedNode.id();
    info.templateParentId = parentModelNode.id();
    info.generatedId = newNode.id();

    if (!isNodeProperty) {
        replacedNodeParent.reparentHere(newNode);
        replacedNode.destroy();
        info.alreadyReparented = true;
    } else {
        parentModelNode.removeProperty(reparentName);
        parentModelNode.nodeProperty(reparentName).reparentHere(newNode);
    }
    m_reparentInfoHash.insert(newNode.id(), info);
}

void StylesheetMerger::replaceRootNode(ModelNode& templateRootNode)
{
    try {
        RewriterTransaction transaction(m_templateView, "replace-root-node");
        ModelMerger merger(m_templateView);
        QString rootId = templateRootNode.id();
        // If we shall replace the root node of the template with the style,
        // we first replace the whole model.
        ModelNode rootReplacer = m_styleView->modelNodeForId(rootId);
        merger.replaceModel(rootReplacer);

        // Then reset the id to the old root's one.
        ModelNode newRoot = m_templateView->rootModelNode();
        newRoot.setIdWithoutRefactoring(rootId);
        transaction.commit();
    }  catch (InvalidIdException &ide) {
        qDebug().noquote() << "Invalid id exception while replacing root node of template.";
        ide.createWarning();
    } catch (InvalidReparentingException &rpe) {
        qDebug().noquote() << "Invalid reparenting exception while replacing root node of template.";
        rpe.createWarning();
    } catch (InvalidModelNodeException &mne) {
        qDebug().noquote() << "Invalid model node exception while replacing root node of template.";
        mne.createWarning();
    } catch (Exception &e) {
        qDebug().noquote() << "Exception while replacing root node of template.";
        e.createWarning();
    }
}

// Move the newly created nodes to the correct position in the parent node
void StylesheetMerger::adjustNodeIndex(ModelNode &node)
{
    if (!m_reparentInfoHash.contains(node.id()))
        return;

    ReparentInfo info = m_reparentInfoHash.value(node.id());
    if (info.parentIndex < 0)
        return;

    if (!node.parentProperty().isNodeListProperty())
        return;

    NodeListProperty parentListProperty = node.parentProperty().toNodeListProperty();
    int currentIndex = parentListProperty.indexOf(node);
    if (currentIndex == info.parentIndex)
        return;

    parentListProperty.slide(currentIndex, info.parentIndex);
}

void StylesheetMerger::applyStyleProperties(ModelNode &templateNode, const ModelNode &styleNode)
{
    const QRegularExpression regEx("[a-z]", QRegularExpression::CaseInsensitiveOption);
    for (const VariantProperty &variantProperty : styleNode.variantProperties()) {
        if (templateNode.hasBindingProperty(variantProperty.name())) {
            // if the binding does not contain any alpha letters (i.e. binds to a term rather than a property,
            // replace it with the corresponding variant property.
            if (!templateNode.bindingProperty(variantProperty.name()).expression().contains(regEx)) {
                templateNode.removeProperty(variantProperty.name());
                templateNode.variantProperty(variantProperty.name()).setValue(variantProperty.value());
            }
        } else {
            if (variantProperty.holdsEnumeration()) {
                templateNode.variantProperty(variantProperty.name()).setEnumeration(variantProperty.enumeration().toEnumerationName());
            } else {
                templateNode.variantProperty(variantProperty.name()).setValue(variantProperty.value());
            }
        }
    }
    syncBindingProperties(templateNode, styleNode);
    syncNodeProperties(templateNode, styleNode, true);
    syncNodeListProperties(templateNode, styleNode, true);
}

static void removePropertyIfExists(ModelNode node, const PropertyName &propertyName)
{
    if (node.hasProperty(propertyName))
        node.removeProperty(propertyName);

}

void StylesheetMerger::parseTemplateOptions()
{
    if (!m_templateView->hasId(QStringLiteral("qds_stylesheet_merger_options")))
        return;

    ModelNode optionsNode = m_templateView->modelNodeForId(QStringLiteral("qds_stylesheet_merger_options"));
    if (optionsNode.hasVariantProperty("preserveTextAlignment")) {
        m_options.preserveTextAlignment = optionsNode.variantProperty("preserveTextAlignment").value().toBool();
    }
    if (optionsNode.hasVariantProperty("useStyleSheetPositions")) {
        m_options.useStyleSheetPositions = optionsNode.variantProperty("useStyleSheetPositions").value().toBool();
    }
    try {
        RewriterTransaction transaction(m_templateView, "remove-options-node");
        optionsNode.destroy();
        transaction.commit();
    } catch (InvalidIdException &ide) {
        qDebug().noquote() << "Invalid id exception while removing options from template.";
        ide.createWarning();
    } catch (InvalidReparentingException &rpe) {
        qDebug().noquote() << "Invalid reparenting exception while removing options from template.";
        rpe.createWarning();
    } catch (InvalidModelNodeException &mne) {
        qDebug().noquote() << "Invalid model node exception while removing options from template.";
        mne.createWarning();
    } catch (Exception &e) {
        qDebug().noquote() << "Exception while removing options from template.";
        e.createWarning();
    }
}

void StylesheetMerger::merge()
{
    ModelNode templateRootNode = m_templateView->rootModelNode();
    ModelNode styleRootNode = m_styleView->rootModelNode();

    // first, look if there are any options present in the template
    parseTemplateOptions();

    // second, build up the hierarchy in the style sheet as we have it in the template
    preprocessStyleSheet();

    // build a hash of generated replacement ids
    setupIdRenamingHash();

    //in case we are replacing the root node, just do that and exit
    if (m_styleView->hasId(templateRootNode.id())) {
        replaceRootNode(templateRootNode);
        return;
    }

    QQueue<ModelNode> replacementNodes;

    QList<ModelNode> directRootSubNodes = styleRootNode.directSubModelNodes();
    if (directRootSubNodes.length() == 0 && m_templateView->hasId(styleRootNode.id())) {
        // if the style sheet has only one node, just replace that one
        replacementNodes.enqueue(styleRootNode);
    }
    // otherwise, the nodes to replace are the direct sub nodes of the style sheet's root
    for (const ModelNode &subNode : styleRootNode.allSubModelNodes()) {
        if (m_templateView->hasId(subNode.id())) {
            replacementNodes.enqueue(subNode);
        }
    }

    for (const ModelNode &currentNode : replacementNodes) {

        bool hasPos = false;

        // create the replacement nodes for the styled nodes
        {
            try {
                RewriterTransaction transaction(m_templateView, "create-replacement-node");

                ModelNode replacedNode = m_templateView->modelNodeForId(currentNode.id());
                hasPos = replacedNode.hasProperty("x") || replacedNode.hasProperty("y");

                ModelNode replacementNode = createReplacementNode(currentNode, replacedNode);

                replaceNode(replacedNode, replacementNode);
                transaction.commit();
            } catch (InvalidIdException &ide) {
                qDebug().noquote() << "Invalid id exception while replacing template node";
                ide.createWarning();
                continue;
            } catch (InvalidReparentingException &rpe) {
                qDebug().noquote() << "Invalid reparenting exception while replacing template node";
                rpe.createWarning();
                continue;
            } catch (InvalidModelNodeException &mne) {
                qDebug().noquote() << "Invalid model node exception while replacing template node";
                mne.createWarning();
                continue;
            } catch (Exception &e) {
                qDebug().noquote() << "Exception while replacing template node.";
                e.createWarning();
                continue;
            }
        }
        // sync the properties from the stylesheet
        {
            try {
                RewriterTransaction transaction(m_templateView, "sync-style-node-properties");
                ModelNode templateNode = m_templateView->modelNodeForId(currentNode.id());
                applyStyleProperties(templateNode, currentNode);
                adjustNodeIndex(templateNode);

                /* This we want to do if the parent node in the style is not in the template */
                if (!currentNode.hasParentProperty() ||
                        !m_templateView->modelNodeForId(currentNode.parentProperty().parentModelNode().id()).isValid()) {

                    if (!hasPos && !m_options.useStyleSheetPositions) { //If template had postition retain it
                        removePropertyIfExists(templateNode, "x");
                        removePropertyIfExists(templateNode, "y");
                    }
                    if (templateNode.hasProperty("anchors.fill")) {
                        /* Unfortuntly there are cases were width and height have to be defined - see Button
                     * Most likely we need options for this */
                        //removePropertyIfExists(templateNode, "width");
                        //removePropertyIfExists(templateNode, "height");
                    }
                }
                transaction.commit();
            } catch (InvalidIdException &ide) {
                qDebug().noquote() << "Invalid id exception while syncing style properties to template";
                ide.createWarning();
                continue;
            } catch (InvalidReparentingException &rpe) {
                qDebug().noquote() << "Invalid reparenting exception while syncing style properties to template";
                rpe.createWarning();
                continue;
            } catch (InvalidModelNodeException &mne) {
                qDebug().noquote() << "Invalid model node exception while syncing style properties to template";
                mne.createWarning();
                continue;
            } catch (Exception &e) {
                qDebug().noquote() << "Exception while syncing style properties.";
                e.createWarning();
                continue;
            }
        }
    }
}

void StylesheetMerger::styleMerge(Model *model, const QString &templateFile)
{
    Model *parentModel = model;

    QTC_ASSERT(parentModel, return);

    auto templateModel(Model::create("QtQuick.Item", 2, 1, parentModel));
    Q_ASSERT(templateModel.get());

    templateModel->setFileUrl(parentModel->fileUrl());

    QPlainTextEdit textEditTemplate;
    Utils::FileReader reader;

    QTC_ASSERT(reader.fetch(Utils::FilePath::fromString(templateFile)), return);
    QString qmlTemplateString = QString::fromUtf8(reader.data());
    QString imports;

    for (const Import &import : parentModel->imports()) {
        imports += QStringLiteral("import ") + import.toString(true) + QLatin1Char(';')
                   + QLatin1Char('\n');
    }

    textEditTemplate.setPlainText(imports + qmlTemplateString);
    NotIndentingTextEditModifier textModifierTemplate(&textEditTemplate);

    QScopedPointer<RewriterView> templateRewriterView(new RewriterView(RewriterView::Amend));
    templateRewriterView->setTextModifier(&textModifierTemplate);
    templateModel->attachView(templateRewriterView.data());
    templateRewriterView->setCheckSemanticErrors(false);

    ModelNode templateRootNode = templateRewriterView->rootModelNode();
    QTC_ASSERT(templateRootNode.isValid(), return);

    auto styleModel(Model::create("QtQuick.Item", 2, 1, parentModel));
    Q_ASSERT(styleModel.get());

    styleModel->setFileUrl(parentModel->fileUrl());

    QPlainTextEdit textEditStyle;
    RewriterView *parentRewriterView = parentModel->rewriterView();
    QTC_ASSERT(parentRewriterView, return);
    textEditStyle.setPlainText(parentRewriterView->textModifierContent());
    NotIndentingTextEditModifier textModifierStyle(&textEditStyle);

    QScopedPointer<RewriterView> styleRewriterView(new RewriterView(RewriterView::Amend));
    styleRewriterView->setTextModifier(&textModifierStyle);
    styleModel->attachView(styleRewriterView.data());

    StylesheetMerger merger(templateRewriterView.data(), styleRewriterView.data());

    try {
        merger.merge();
    } catch (Exception &e) {
        e.showException();
    }

    try {
        parentRewriterView->textModifier()->textDocument()->setPlainText(
            templateRewriterView->textModifierContent());
    } catch (Exception &e) {
        e.showException();
    }
}
} // namespace QmlDesigner
