/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "stdafx.h"
#include "SpriteBorderEditorCommon.h"


//-------------------------------------------------------------------------------

namespace
{
    // Various pixel values that affect layout and appearance of items within
    // the Sprite Editor dialog.
    const int sectionContentLeftMargin = 24;
    const int sectionContentTopMargin = sectionContentLeftMargin / 2;
    const int sectionContentBottomMargin = sectionContentLeftMargin / 2;
    const int interElementSpacing = 16;
    const int textInputWidth = 100;
}

CellSelectRectItem* CellSelectRectItem::s_currentSelection = nullptr;

SpriteBorderEditor::SpriteBorderEditor(const char* path, QWidget* parent)
    : QDialog(parent)
    , m_spritePath(path)
{
    memset(m_manipulators, 0, sizeof(m_manipulators));

    // Remove the ability to resize this window.
    setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::MSWindowsFixedSizeDialogHint);

    // Make sure the sprite can load and be displayed before continuing
    m_sprite = gEnv->pLyShine->LoadSprite(m_spritePath.toLatin1().constData());
    QString fullPath = Path::GamePathToFullPath(m_sprite->GetTexturePathname().c_str());
    const bool imageWontLoad = !m_sprite || QPixmap(fullPath).isNull();
    if (imageWontLoad)
    {
        m_hasBeenInitializedProperly = false;
        return;
    }

    // Store a copy of the sprite-sheet's current configuration in case the
    // user decides to cancel this dialog
    m_restoreInfo.spriteSheetCells = m_sprite->GetSpriteSheetCells();
    m_restoreInfo.borders = m_sprite->GetBorders();

    CreateLayout();

    setWindowTitle("Sprite Editor");
    setModal(true);
    setWindowModality(Qt::ApplicationModal);

    layout()->setSizeConstraint(QLayout::SetFixedSize);

    // Position the widget centered around cursor.
    {
        QSize halfSize = (layout()->sizeHint() / 2);
        move(QCursor::pos() - QPoint(halfSize.width(), halfSize.height()));
    }

    // Set the "configure as sprite-sheet" flag if we start with a 
    // sprite-sheet. This resolves a bug where we start with a sprite-sheet
    // and the user decides to change the row and col to 1x1 which results
    // in the dialog having the sprite-sheet sections removed, but still
    // retain the original dialog size. If the user "removes" the sprite-sheet
    // data by setting row and col to 1x1, then they'll see the basic dialog
    // next time they open the editor.
    if (m_sprite->IsSpriteSheet())
    {
        m_configureAsSpriteSheet = true;
        m_resizeOnce = false;
    }
}

SpriteBorderEditor::~SpriteBorderEditor()
{
    m_sprite->Release();
}

bool SpriteBorderEditor::GetHasBeenInitializedProperly()
{
    return m_hasBeenInitializedProperly;
}

void SpriteBorderEditor::CreateLayout()
{
    // The layout.
    QGridLayout* outerGrid = new QGridLayout(this);

    QGridLayout* innerGrid = new QGridLayout();
    outerGrid->addLayout(innerGrid, 0, 0, 1, 2);

    int layoutRowIncrement = 0;

    if (IsConfiguringSpriteSheet())
    {
        AddConfigureSection(innerGrid, layoutRowIncrement);
        AddSeparator(innerGrid, layoutRowIncrement);

        AddSelectCellSection(innerGrid, layoutRowIncrement);
        AddSeparator(innerGrid, layoutRowIncrement);
    }

    AddPropertiesSection(innerGrid, layoutRowIncrement);
    AddButtonsSection(outerGrid, layoutRowIncrement);

    // If dialog is closed without saving, restore original border values.
    {
        ISprite::Borders originalBorders = m_sprite->GetBorders();
        QObject::connect(this,
            &QDialog::rejected,
            this,
            [this, originalBorders]()
        {
            // Restore original borders.
            m_sprite->SetBorders(originalBorders);
        });
    }

    // Default to displaying the first cell of the spritesheet
    static const int firstCellIndex = 0;
    DisplaySelectedCell(firstCellIndex);

    // CreateLayout can be called multiple times, so make sure we only resize
    // the window once.
    if (m_configureAsSpriteSheet && m_resizeOnce)
    {
        m_resizeOnce = false;

        // Scale the height and width of the window to account for the 
        // additional space required by the spritesheet configuration 
        // sections. Probably the "correct" way to solve this would be 
        // dynamically recreating (or somehow updating) the QLayout of the
        // window.
        const float heightScale = 2.15f;
        const float widthScale = 1.15f;
        QSize currentSize(size());
        
        setFixedSize(aznumeric_cast<int>(currentSize.width() * widthScale), aznumeric_cast<int>(currentSize.height() * heightScale));
    }
}

void SpriteBorderEditor::ResetUi()
{
    emit ResettingUi();

    // Disconnect all objects from the sprite editor's signals
    disconnect();

    ClearLayout();

    // Repopulate the window contents on the next Qt event loop tick.
    QMetaObject::invokeMethod(this, "CreateLayout", Qt::QueuedConnection);
}

void SpriteBorderEditor::ClearLayout()
{
    // Remove all children from the dialog
    auto childWidgets = children();
    for (auto childWidget : childWidgets)
    {
        // We deleteLater in case this window still has events sitting on
        // the event queue for this particular tick of the Qt event loop.
        childWidget->deleteLater();
    }

    m_cellPropertiesPixmap = nullptr;
    m_cellPropertiesGraphicsScene = nullptr;
    m_textureSizeLabel = nullptr;
    m_cellAliasLineEdit = nullptr;

    CellSelectRectItem::ClearSelection();
}

void SpriteBorderEditor::UpdateSpriteSheetCellInfo(int newNumRows, int newNumCols, ISprite* sprite)
{
    // Because the row/column sprite-sheet configuration is changing, we need
    // to remove the current sprite-sheet configuration for this sprite.
    sprite->ClearSpriteSheetCells();

    m_numRows = newNumRows;
    m_numCols = newNumCols;

    float floatNumRows = aznumeric_cast<float>(m_numRows);
    float floatNumCols = aznumeric_cast<float>(m_numCols);

    // Calculate uniformly sized sprite-sheet cell UVs based on the given
    // row and column cell configuration.
    for (int row = 0; row < m_numRows; ++row)
    {
        for (int col = 0; col < m_numCols; ++col)
        {
            AZ::Vector2 min(col / floatNumCols, row / floatNumRows);
            AZ::Vector2 max((col + 1) / floatNumCols, (row + 1) / floatNumRows);

            UiTransformInterface::RectPoints uvCellCoords;
            uvCellCoords.TopLeft() = AZ::Vector2(min.GetX(), min.GetY());
            uvCellCoords.BottomLeft() = AZ::Vector2(min.GetX(), max.GetY());
            uvCellCoords.TopRight() = AZ::Vector2(max.GetX(), min.GetY());
            uvCellCoords.BottomRight() = AZ::Vector2(max.GetX(), max.GetY());

            ISprite::SpriteSheetCell newCell;
            newCell.uvCellCoords = uvCellCoords;

            sprite->AddSpriteSheetCell(newCell);
        }
    }

    // Dialog needs to be updated to reflect new sprite-sheet cell info
    ResetUi();
}

void SpriteBorderEditor::DisplaySelectedCell(AZ::u32 cellIndex)
{
    m_currentCellIndex = cellIndex;
    emit SelectedCellChanged(m_sprite, cellIndex);

    // A new cell has been selected, so remove the currently
    // displayed image/cell from the view
    m_cellPropertiesGraphicsScene->removeItem(m_cellPropertiesPixmap);

    // Determine how much we need to scale the view to fit the cell 
    // contents to the displayed properties image.
    const AZ::Vector2 cellSize = m_sprite->GetCellSize(cellIndex);
    const AZ::Vector2 cellScale = AZ::Vector2(m_unscaledSpriteSheet.size().width() / cellSize.GetX(), m_unscaledSpriteSheet.size().height() / cellSize.GetY());

    // Scale-to-fit, while preserving aspect ratio.
    QRect croppedRect = m_unscaledSpriteSheet.rect();
    {
        const UiTransformInterface::RectPoints& cellUvCoords = m_sprite->GetSourceCellUvCoords(cellIndex);
        int minX = aznumeric_cast<int>(cellUvCoords.TopLeft().GetX() > 0.0f ? croppedRect.right() * cellUvCoords.TopLeft().GetX() : 0);
        int maxX = aznumeric_cast<int>(cellUvCoords.BottomRight().GetX() > 0.0f ? croppedRect.right() * cellUvCoords.BottomRight().GetX() : 0);
        int minY = aznumeric_cast<int>(cellUvCoords.TopLeft().GetY() > 0.0f ? croppedRect.bottom() * cellUvCoords.TopLeft().GetY() : 0);
        int maxY = aznumeric_cast<int>(cellUvCoords.BottomRight().GetY() > 0.0f ? croppedRect.bottom() * cellUvCoords.BottomRight().GetY() : 0);
        croppedRect.setCoords(minX, minY, maxX, maxY);
    }

    // Finally, display the cropped pixmap to show the 
    // selected cell.
    QPixmap croppedPixmap = m_unscaledSpriteSheet.copy(croppedRect).scaled(UICANVASEDITOR_SPRITEBORDEREDITOR_SCENE_WIDTH, UICANVASEDITOR_SPRITEBORDEREDITOR_SCENE_HEIGHT, Qt::KeepAspectRatio);
    m_cellPropertiesPixmap = m_cellPropertiesGraphicsScene->addPixmap(croppedPixmap);

    // Render the sprite-sheet cell before the first slice manipulator so the
    // cell doesn't render on top (and occlude) the manipulator.
    m_cellPropertiesPixmap->stackBefore(m_manipulators[0]);

    // Adjust the slice manipulators to the selected cell's border values
    for (int i = 0; i < numSliceBorders; ++i)
    {
        const SpriteBorder border = static_cast<SpriteBorder>(i);
        const float sizeInPixels = IsBorderVertical(border) ? cellSize.GetX() : cellSize.GetY();

        m_manipulators[i]->SetCellIndex(cellIndex);
        m_manipulators[i]->SetPixmapSizes(QSize(aznumeric_cast<int>(cellSize.GetX()), aznumeric_cast<int>(cellSize.GetY())), croppedPixmap.size());
        m_manipulators[i]->setPixelPosition(GetBorderValueInPixels(m_sprite, border, sizeInPixels, cellIndex));
    }

    // Update the texture size text to accurately reflect the new selection
    SetDisplayedTextureSize(cellSize.GetX(), cellSize.GetY());

    // Update cell alias field info
    m_cellAliasLineEdit->setText(m_sprite->GetCellAlias(cellIndex).c_str());
}

void SpriteBorderEditor::AddConfigureSection(QGridLayout* gridLayout, int& rowNum)
{
    QLabel* labelHeader = new QLabel(QString("<h2>Configure Spritesheet</h2>"), this);
    gridLayout->addWidget(labelHeader, rowNum++, 0, 1, 6);

    // Derive row/col based on spritesheet cell UV coord info. Assumes 
    // uniform grid of cells.
    AZStd::unordered_set<float> uSet, vSet;
    for (auto spriteSheetCell : m_sprite->GetSpriteSheetCells())
    {
        uSet.insert(spriteSheetCell.uvCellCoords.TopLeft().GetX());
        uSet.insert(spriteSheetCell.uvCellCoords.TopRight().GetX());

        vSet.insert(spriteSheetCell.uvCellCoords.TopLeft().GetY());
        vSet.insert(spriteSheetCell.uvCellCoords.BottomLeft().GetY());
    }

    // Count the number of unique entries along each axis to determine number
    // of rows/cols contained within the spritesheet.
    m_numRows = vSet.size() > 1 ? vSet.size() - 1 : 1;
    m_numCols = uSet.size() > 1 ? uSet.size() - 1 : 1;

    // Text input fields displaying row/col information for auto-extracting 
    // spritesheet cells
    QLineEdit* numRowsLineEdit = new QLineEdit(QString::number(m_numRows), this);
    QLineEdit* numColsLineEdit = new QLineEdit(QString::number(m_numCols), this);

    numRowsLineEdit->setFixedWidth(textInputWidth);
    numColsLineEdit->setFixedWidth(textInputWidth);

    // Once the user enters in the new row/col information, this callback
    // will notify the SpriteBorderEditor so that the UV information can
    // be auto-generated for each of the cells.
    AZStd::function<void()> rowColChangedCallback =
        [this, numRowsLineEdit, numColsLineEdit]()
        {
            bool rowConversionSuccess = false;
            int newNumRows = numRowsLineEdit->text().toInt(&rowConversionSuccess);

            bool colConversionSuccess = false;
            int newNumCols = numColsLineEdit->text().toInt(&colConversionSuccess);

            const bool positiveInputs = newNumRows > 0 && newNumCols > 0;
            const bool valueChanged = m_numRows != newNumRows || m_numCols != newNumCols;

            // This number of cells is just nearly unusable in the sprite editor UI. Supporting
            // more would likely require reworking of UX/UI and even implementation.
            static const int maxNumCellsSupported = 32 * 32;

            const bool numCellsSupported = newNumRows * newNumCols <= maxNumCellsSupported;
            const bool inputValid = rowConversionSuccess && colConversionSuccess && positiveInputs && valueChanged && numCellsSupported;
            if (inputValid)
            {
                UpdateSpriteSheetCellInfo(newNumRows, newNumCols, m_sprite);
            }
            else 
            {
                // Restore the current values
                numRowsLineEdit->setText(QString::number(m_numRows));
                numColsLineEdit->setText(QString::number(m_numCols));

                if (!numCellsSupported)
                {
                    QString warningMessage = QString(
                        "Too many rows and columns have been specified!\n"
                        "The maximum number of sprite-sheet cells is limited to %1").arg(maxNumCellsSupported);
                    QMessageBox(QMessageBox::Warning,
                        "Warning",
                        warningMessage,
                        QMessageBox::Ok, QApplication::activeWindow()).exec();
                }
            }
        };

    // Hook up the callback to the text input fields
    QObject::connect(numRowsLineEdit, &QLineEdit::editingFinished, this, rowColChangedCallback);
    QObject::connect(numColsLineEdit, &QLineEdit::editingFinished, this,rowColChangedCallback);

    // Create a new "inner layout" for this section of the window so we can 
    // properly align the content of this section with the other sections by
    // setting margins for the content. This could also possibly be achieved
    // via QSpacerItems.
    QGridLayout* innerLayout = new QGridLayout();
    gridLayout->addLayout(innerLayout, rowNum++, 0, 1, 6, Qt::AlignLeft);

    // These margins effectively indent the content of this entire section to
    // align with the rest of the window contents.
    int left, top, right, bottom;
    innerLayout->getContentsMargins(&left, &top, &right, &bottom);
    innerLayout->setContentsMargins(
        sectionContentLeftMargin, 
        sectionContentTopMargin, 
        right, 
        sectionContentBottomMargin);

    // Finally, add the widgets to the layout
    int innerLayoutCol = 0;
    innerLayout->addWidget(new QLabel("Rows", this),                0, innerLayoutCol++, Qt::AlignLeft);
    innerLayout->addItem(new QSpacerItem(interElementSpacing, 0),   0, innerLayoutCol++, Qt::AlignLeft);
    innerLayout->addWidget(numRowsLineEdit,                         0, innerLayoutCol++, Qt::AlignLeft);
    innerLayout->addItem(new QSpacerItem(interElementSpacing, 0),   0, innerLayoutCol++, Qt::AlignLeft);
    innerLayout->addWidget(new QLabel("Columns", this),             0, innerLayoutCol++, Qt::AlignLeft);
    innerLayout->addItem(new QSpacerItem(interElementSpacing, 0),   0, innerLayoutCol++, Qt::AlignLeft);
    innerLayout->addWidget(numColsLineEdit,                         0, innerLayoutCol++, Qt::AlignLeft);

    // Configure tab order for fields
    QWidget::setTabOrder(numRowsLineEdit, numColsLineEdit);

    // Prime for transition to next "tab-able" field 
    m_prevTabField = numColsLineEdit;
}

void SpriteBorderEditor::AddSelectCellSection(QGridLayout* gridLayout, int& rowNum)
{
    static const int cellSelectionLabelRowSpan = 1;
    static const int cellSelectionLabelColSpan = 6;
    gridLayout->addWidget(new QLabel(QString("<h2>Select cell</h2>"), this), rowNum++, 0, cellSelectionLabelRowSpan, cellSelectionLabelColSpan);

    // The border margin is used to reserve space along the X and Y axes to
    // insert a border in the graphics scene. This way the image fits within
    // the border rather than behind it.
    static const float borderMargin = 2.0f;

    // Total amount of space the border margin occupies along a single axis
    // (which is just double the border margin since the border appears on
    // all edges of the image).
    static const float borderMarginTotal = borderMargin * 2.0f;

    // If the sprite-sheet has a width at least twice as big as its height,
    // then display the image at a bigger size to fill up the contents of
    // the dialog in a more visually appealing way.
    int widthMultiplier = 1;

    // Load the full spritesheet image and scale it to fit the view
    QPixmap scaledPixmap;
    {
        QString fullPath = Path::GamePathToFullPath(m_sprite->GetTexturePathname().c_str());
        m_unscaledSpriteSheet = QPixmap(fullPath);
        if ((m_unscaledSpriteSheet.height() <= 0) &&
            (m_unscaledSpriteSheet.width() <= 0))
        {
            m_hasBeenInitializedProperly = false;
            return;
        }

        float widthToHeightRatio = static_cast<float>(m_unscaledSpriteSheet.width()) / m_unscaledSpriteSheet.height();
        bool isVertical = (m_unscaledSpriteSheet.height() > m_unscaledSpriteSheet.width());

        if (isVertical)
        {
            scaledPixmap = m_unscaledSpriteSheet.scaledToHeight(aznumeric_cast<int>(UICANVASEDITOR_SPRITEBORDEREDITOR_SCENE_HEIGHT - (borderMarginTotal)));
        }
        else
        {
            widthMultiplier = widthToHeightRatio >= 2.0f ? 2 : 1;
            scaledPixmap = m_unscaledSpriteSheet.scaledToWidth(aznumeric_cast<int>(UICANVASEDITOR_SPRITEBORDEREDITOR_SCENE_WIDTH * widthMultiplier - (borderMarginTotal)));
        }
    }

    // Create the graphics scene area to include enough space for the scaled
    // pixmap and the border.
    QGraphicsScene* graphicsScene = new QGraphicsScene(0.0f, 0.0f, scaledPixmap.width() + borderMarginTotal, scaledPixmap.height() + borderMarginTotal, this);

    // Offset the pixmap to fit within the border margins
    QGraphicsPixmapItem* pixmapItem = graphicsScene->addPixmap(scaledPixmap);
    const QPointF scaledPixmapOffset(borderMargin, borderMargin);
    pixmapItem->setOffset(scaledPixmapOffset);

    // Create an "inner layout" to set margins on the pixmap to align with 
    // the rest of the contents of the dialog
    QGridLayout* innerLayout = new QGridLayout();
    {
        gridLayout->addLayout(innerLayout, rowNum, 0, 1, 6, Qt::AlignLeft);

        // Set content margins
        int left, top, right, bottom;
        innerLayout->getContentsMargins(&left, &top, &right, &bottom);
        innerLayout->setContentsMargins(
            sectionContentLeftMargin,
            sectionContentTopMargin,
            right,
            sectionContentBottomMargin);
    }

    static const int cellSelectionRowSpan = 1;
    static const int cellSelectionColSpan = 4;
    QGraphicsView* slicerView = new SlicerView(graphicsScene, this);
    innerLayout->addWidget(slicerView, 0, 0, cellSelectionRowSpan, cellSelectionColSpan, Qt::AlignHCenter | Qt::AlignVCenter);

    // Multiplier to scale individual cells by to fit selection view
    AZ::Vector2 scaleMultiplierWithoutBorder(
        static_cast<float>(scaledPixmap.width() + borderMarginTotal) / m_unscaledSpriteSheet.width(),
        static_cast<float>(scaledPixmap.height() + borderMarginTotal) / m_unscaledSpriteSheet.height());

    // Size of an individual cell after being scaled to fit selection view
    AZ::Vector2 cellSize(
        (m_unscaledSpriteSheet.width() / m_numCols) * scaleMultiplierWithoutBorder.GetX(),
        (m_unscaledSpriteSheet.height() / m_numRows) * scaleMultiplierWithoutBorder.GetY());

    // Add grid overlay on-top of spritesheet image to help visualize 
    // row/col grid of sprite-sheet cells.
    {
        QPen cellDividerPenWhite;
        cellDividerPenWhite.setStyle(Qt::DashLine);
        static const float dashedPenWidth = 2.0f;
        cellDividerPenWhite.setWidthF(dashedPenWidth);
        cellDividerPenWhite.setColor(Qt::white);

        QPen cellDividerPenBlack(cellDividerPenWhite);
        cellDividerPenBlack.setColor(Qt::black);
        cellDividerPenBlack.setStyle(Qt::SolidLine);
        cellDividerPenBlack.setWidthF(dashedPenWidth * 2.0f);

        for (uint row = 0; row < m_numRows; ++row)
        {
            const float yOffset = row * cellSize.GetY();

            // Only add the dashed border to the bottom of this row if we're
            // not on the final/bottom row of the spritehset. The outer sprite-sheet
            // image already has a border.
            const bool finalRow = row == m_numRows - 1;
            if (!finalRow)
            {
                const float bottomOfCellOffset = cellSize.GetY();

                QLineF bottomDashedRowBorderBlack(
                    0.0f,
                    yOffset + bottomOfCellOffset,
                    scaledPixmap.width() + borderMarginTotal,
                    yOffset + bottomOfCellOffset);
                graphicsScene->addLine(bottomDashedRowBorderBlack, cellDividerPenBlack);

                QLineF bottomDashedRowBorderWhite(
                    0.0f,
                    yOffset + bottomOfCellOffset,
                    scaledPixmap.width() + borderMarginTotal,
                    yOffset + bottomOfCellOffset);
                graphicsScene->addLine(bottomDashedRowBorderWhite, cellDividerPenWhite);
            }
        }

        for (uint col = 1; col < m_numCols; ++col)
        {
            const float xOffset = col * cellSize.GetX();

            // Only add the dashed border to the right of the cell if we're
            // not on the last column of the row. The outer sprite-sheet 
            // image already has a border.
            QLineF rightDashedCellBorderBlack(
                xOffset,
                0.0f,
                xOffset,
                scaledPixmap.height() + borderMarginTotal);
            graphicsScene->addLine(rightDashedCellBorderBlack, cellDividerPenBlack);

            QLineF rightDashedCellBorderWhite(
                xOffset,
                0.0f,
                xOffset,
                scaledPixmap.height() + borderMarginTotal);
            graphicsScene->addLine(rightDashedCellBorderWhite, cellDividerPenWhite);
        }
    }

    // Add image border to the scene
    {
        static const float outerPenWidth = borderMargin;
        static const float innerPenWidth = outerPenWidth * 0.5f;

        AZ::Vector2 scaleMultiplierWithBorder(
            static_cast<float>(scaledPixmap.width()) / m_unscaledSpriteSheet.width(),
            static_cast<float>(scaledPixmap.height()) / m_unscaledSpriteSheet.height());

        // Outer, black border
        {
            QPen wholeImageBorder;
            wholeImageBorder.setWidthF(outerPenWidth);
            wholeImageBorder.setColor(Qt::black);
            wholeImageBorder.setJoinStyle(Qt::MiterJoin);

            QPointF topLeft(outerPenWidth * 0.5f, outerPenWidth * 0.5f);
            QPointF bottomRight(
                outerPenWidth + m_unscaledSpriteSheet.width() * scaleMultiplierWithBorder.GetX() + 1.0f,
                outerPenWidth + m_unscaledSpriteSheet.height() * scaleMultiplierWithBorder.GetY() + 1.0f);
            QRectF cellRect(topLeft, bottomRight);

            graphicsScene->addRect(cellRect, wholeImageBorder);
        }

        // Inner, white border
        {
            QPen wholeImageBorder;
            wholeImageBorder.setWidthF(innerPenWidth);
            wholeImageBorder.setColor(Qt::white);
            wholeImageBorder.setJoinStyle(Qt::MiterJoin);

            QPointF topLeft(outerPenWidth, outerPenWidth);
            QPointF bottomRight(
                topLeft.x() + m_unscaledSpriteSheet.width() * scaleMultiplierWithBorder.GetX() - outerPenWidth + 1.0f,
                topLeft.y() + m_unscaledSpriteSheet.height() * scaleMultiplierWithBorder.GetY() - outerPenWidth + 1.0f);
            QRectF cellRect(topLeft, bottomRight);

            graphicsScene->addRect(cellRect, wholeImageBorder);
        }
    }

    // Finally, add invisible rect items to the scene that correspond to each
    // cell of the sprite-sheet. Each rect item has a callback that processes
    // which cell of the sprite-sheet was selected.
    for (uint row = 0; row < m_numRows; ++row)
    {
        const float yOffset = row * cellSize.GetY();

        for (uint col = 0; col < m_numCols; ++col)
        {
            const bool topRow = row == 0;
            const bool firstColumnInRow = col == 0;
            const bool lastColumnInRow = col == m_numCols - 1;
            const bool bottomRow = row == m_numRows - 1;

            const float xOffset = col * cellSize.GetX();
            const float borderMarginRectOffset = borderMargin * 0.5f + 1.0f;
            const float topLeftXOffset = firstColumnInRow ? borderMarginRectOffset : 0.0f;
            const float topLeftYOffset = topRow ? borderMarginRectOffset : 0.0f;
            const float bottomRightYOffset = bottomRow ? borderMarginRectOffset : 0.0f;

            // The right border of the cell selection rect gets clipped (due
            // to the way the QPen renders) when the last column cell in the
            // row is selected.
            const float lastColOffset = lastColumnInRow ? 2.0f : 0.0f;

            // Calculate the top-left and bottom-right coordinates for this
            // cell within the cell selection graphics view.
            QPointF topLeft(xOffset + topLeftXOffset, yOffset + topLeftYOffset);
            QPointF bottomRight(xOffset + cellSize.GetX() - lastColOffset, yOffset + cellSize.GetY() - bottomRightYOffset);

            // Create the graphics rect item with a custom mouse press event
            // that allows us to get information of the selected cell.
            QRectF cellRect(topLeft, bottomRight);
            int cellIndex = row * m_numCols + col;
            CellSelectRectItem* cellSelectRectItem = new CellSelectRectItem(
                cellRect,
                [this, cellIndex]()
            {
                DisplaySelectedCell(cellIndex);
            });

            QObject::connect(
                this,
                &SpriteBorderEditor::ResettingUi,
                cellSelectRectItem,
                &CellSelectRectItem::StopProcessingInput);

            cellSelectRectItem->setPen(Qt::NoPen);

            graphicsScene->addItem(cellSelectRectItem);

            // Pre-select the first cell
            const bool firstCell = row == 0 && col == 0;
            if (firstCell)
            {
                cellSelectRectItem->SelectCell();
            }
        }
    }

    rowNum += cellSelectionRowSpan;
}

void SpriteBorderEditor::AddPropertiesSection(QGridLayout* gridLayout, int& rowNum)
{
    gridLayout->addWidget(new QLabel(QString("<h2>Border Properties</h2>"), this), rowNum++, 0, 1, 6);

    // Create an "inner layout" to set margins on the pixmap to align with 
    // the rest of the contents of the dialog.
    QGridLayout* innerLayout = new QGridLayout();
    {
        gridLayout->addLayout(innerLayout, rowNum, 0, 6, 8);

        // Set content margins
        int left, top, right, bottom;
        innerLayout->getContentsMargins(&left, &top, &right, &bottom);
        innerLayout->setContentsMargins(
            sectionContentLeftMargin,
            sectionContentTopMargin,
            right,
            sectionContentBottomMargin);
    }

    // The scene and view responsible for displaying the image (or image of
    // a specific spritesheet cell).
    m_cellPropertiesGraphicsScene = new QGraphicsScene(0.0f, 0.0f, UICANVASEDITOR_SPRITEBORDEREDITOR_SCENE_WIDTH, UICANVASEDITOR_SPRITEBORDEREDITOR_SCENE_HEIGHT, this);
    innerLayout->addWidget(new SlicerView(m_cellPropertiesGraphicsScene, this), 0, 0, 1, 1, Qt::AlignLeft);

    // The image (or spritesheet cell).
    QSize unscaledPixmapSize;
    QSize scaledPixmapSize;
    {
        QString fullPath = Path::GamePathToFullPath(m_sprite->GetTexturePathname().c_str());
        m_unscaledSpriteSheet = QPixmap(fullPath);
        if ((m_unscaledSpriteSheet.size().height() <= 0) &&
            (m_unscaledSpriteSheet.size().width() <= 0))
        {
            m_hasBeenInitializedProperly = false;
            return;
        }

        bool isVertical = (m_unscaledSpriteSheet.size().height() > m_unscaledSpriteSheet.size().width());

        // Scale-to-fit, while preserving aspect ratio.
        QPixmap scaledPixmap;

        if (isVertical)
        {
            scaledPixmap = m_unscaledSpriteSheet.scaledToHeight(UICANVASEDITOR_SPRITEBORDEREDITOR_SCENE_HEIGHT);
        }
        else
        {
            scaledPixmap = m_unscaledSpriteSheet.scaledToWidth(UICANVASEDITOR_SPRITEBORDEREDITOR_SCENE_WIDTH);
        }

        m_cellPropertiesPixmap = m_cellPropertiesGraphicsScene->addPixmap(scaledPixmap);

        unscaledPixmapSize = m_unscaledSpriteSheet.size();
        scaledPixmapSize = m_cellPropertiesPixmap->pixmap().size();
    }

    // The properties section is divided into two columns. The left column
    // displays the currently selected cell, and this right column displays
    // the modifiable border properties. A separate grid layout is created to
    // achieve desired visual layout.
    QGridLayout* rightColumnLayout= new QGridLayout();
    {
        innerLayout->addLayout(rightColumnLayout, 0, 1, 1, 1);

        // Separate layout for formatting texture/cell size label
        QGridLayout* textureSizeLabelLayout = new QGridLayout();
        {
            rightColumnLayout->addLayout(textureSizeLabelLayout, 0, 0, 1, 3, Qt::AlignTop);

            int left, top, right, bottom;
            textureSizeLabelLayout->getContentsMargins(&left, &top, &right, &bottom);
            textureSizeLabelLayout->setContentsMargins(
                interElementSpacing,
                top,
                right,
                bottom);

            m_textureSizeLabel = new QLabel(this);
            textureSizeLabelLayout->addWidget(m_textureSizeLabel, 0, 0, Qt::AlignLeft);
            SetDisplayedTextureSize(aznumeric_cast<float>(unscaledPixmapSize.width()), aznumeric_cast<float>(unscaledPixmapSize.height()));
        }

        // Separate layout for border property fields
        QGridLayout* propertyFieldsLayout = new QGridLayout();
        {
            rightColumnLayout->addLayout(propertyFieldsLayout, 1, 0, 8, 3, Qt::AlignTop | Qt::AlignLeft);

            int left, top, right, bottom;
            propertyFieldsLayout->getContentsMargins(&left, &top, &right, &bottom);
            propertyFieldsLayout->setContentsMargins(
                interElementSpacing,
                top,
                right,
                bottom);

            // Row value/iterator for row placement within layout
            int row = 0;

            // Text field for modifying cell string alias
            int columnCount = 0;
            propertyFieldsLayout->addWidget(new QLabel("Alias", this), row, columnCount++, Qt::AlignLeft);
            propertyFieldsLayout->addItem(new QSpacerItem(interElementSpacing, 0), row, columnCount++, Qt::AlignLeft);

            m_cellAliasLineEdit = new QLineEdit(this);
            m_cellAliasLineEdit->setFixedWidth(textInputWidth);
            propertyFieldsLayout->addWidget(m_cellAliasLineEdit, row, columnCount++, Qt::AlignLeft);

            // Editing finished callback for setting alias value after being entered
            QObject::connect(m_cellAliasLineEdit, &QLineEdit::editingFinished, this,
                [this]()
                {
                    // Remove preceding or trailing whitespace and tab/newline chars
                    const QString lineEditText = m_cellAliasLineEdit->text().simplified();

                    // Only allow alphanumeric and whitespace chars
                    QRegExp re("([A-Z]|[a-z]|[0-9]|\\s)*");
                    const bool containsOnlyAlphaNumeric = re.exactMatch(lineEditText);
                    const bool hasValidLength = lineEditText.length() <= 128;
                    const bool lineEditTextValid = containsOnlyAlphaNumeric && hasValidLength;
                    if (lineEditTextValid)
                    {
                        m_sprite->SetCellAlias(m_currentCellIndex, lineEditText.toUtf8().constData());

                        const bool wasSimplified = lineEditText != m_cellAliasLineEdit->text();
                        if (wasSimplified)
                        {
                            // Update line edit text to simplified value
                            m_cellAliasLineEdit->setText(lineEditText);

                            // Tell the user that the value was simplified, but not in the case where
                            // the string is empty anywas (user accidentally hits space character or
                            // something).
                            if (!lineEditText.isEmpty())
                            {
                                QMessageBox(QMessageBox::Information,
                                    "Alias Value Updated",
                                    "The cell alias that was entered has been modified to remove additional whitespace characters.",
                                    QMessageBox::Ok, QApplication::activeWindow()).exec();
                            }
                        }
                    }
                    else
                    {
                        if (!containsOnlyAlphaNumeric)
                        {
                            QMessageBox(QMessageBox::Warning,
                                "Warning",
                                "Unable to set cell alias value. Only alphanumeric characters are supported.",
                                QMessageBox::Ok, QApplication::activeWindow()).exec();
                        }
                        else
                        {
                            QMessageBox(QMessageBox::Warning,
                                "Warning",
                                "Unable to set cell alias value. The alias is too long.",
                                QMessageBox::Ok, QApplication::activeWindow()).exec();
                        }

                        // Restore original line edit text value
                        m_cellAliasLineEdit->setText(m_sprite->GetCellAlias(m_currentCellIndex).c_str());
                    }
                    
                });

            // Prime row value for the following (border value) fields
            ++row;

            // Used for setting tab order
            SlicerEdit* prevEditField = nullptr;

            for (const auto b : SpriteBorder())
            {
                SlicerEdit* edit = new SlicerEdit(
                    this,
                    b,
                    unscaledPixmapSize,
                    m_sprite);

                SlicerManipulator* manipulator = new SlicerManipulator(b,
                    unscaledPixmapSize,
                    scaledPixmapSize,
                    m_sprite,
                    m_cellPropertiesGraphicsScene,
                    edit);

                int manipulatorArrayIndex = static_cast<int>(b);
                m_manipulators[manipulatorArrayIndex] = manipulator;

                edit->SetManipulator(manipulator);
                edit->setFixedWidth(textInputWidth);

                int innerLayoutCol = 0;
                propertyFieldsLayout->addWidget(new QLabel(SpriteBorderToString(b), this), row, innerLayoutCol++, Qt::AlignLeft);
                propertyFieldsLayout->addItem(new QSpacerItem(interElementSpacing, 0), row, innerLayoutCol++, Qt::AlignLeft);
                propertyFieldsLayout->addWidget(edit, row, innerLayoutCol++, Qt::AlignLeft);
                propertyFieldsLayout->addWidget(new QLabel("px", this), row, innerLayoutCol++, Qt::AlignLeft);
                row++;

                // Setup tab order
                if (prevEditField)
                {
                    QWidget::setTabOrder(prevEditField, edit);
                }
                else
                {
                    // Tab from previous tab-able field to alias field
                    if (m_prevTabField)
                    {
                        QWidget::setTabOrder(m_prevTabField, m_cellAliasLineEdit);
                    }
                    
                    // Need to transition from alias field to first border
                    // edit field since the alias field comes first.
                    QWidget::setTabOrder(m_cellAliasLineEdit, edit);
                }

                prevEditField = edit;
            }
        }
    }
}

void SpriteBorderEditor::AddButtonsSection(QGridLayout* gridLayout, int& rowNum)
{
    // Add a button to allow users to configure this image as a sprite-sheet,
    // otherwise hide it if they already are working with a sprite-sheet.
    if (!IsConfiguringSpriteSheet())
    {
        // Left-align the button
        QGridLayout* leftAlignedLayout = new QGridLayout();
        gridLayout->addLayout(leftAlignedLayout, rowNum, 0, Qt::AlignLeft);

        QPushButton* configureButton = new QPushButton("Configure Spritesheet", this);

        QObject::connect(configureButton,
            &QPushButton::clicked, this,
            [this](bool checked)
        {
            m_configureAsSpriteSheet = true;
            ResetUi();
        });

        leftAlignedLayout->addWidget(configureButton, rowNum, 0);
    }

    // Needed to right-align buttons next to eachother
    QGridLayout* innerLayout = new QGridLayout();
    gridLayout->addLayout(innerLayout, rowNum, 1, Qt::AlignRight);

    // Add buttons.
    {
        // Save button.
        QPushButton* saveButton = new QPushButton("Save", this);
        QObject::connect(saveButton,
            &QPushButton::clicked, this,
            [this](bool checked)
            {
                // Sanitize values.
                //
                // This is the simplest way to sanitize the
                // border values. Otherwise, we need to prevent
                // flipping the manipulators in the UI.
                {
                    ISprite::Borders b = m_sprite->GetBorders();

                    if (b.m_top > b.m_bottom)
                    {
                        std::swap(b.m_top, b.m_bottom);
                    }

                    if (b.m_left > b.m_right)
                    {
                        std::swap(b.m_left, b.m_right);
                    }

                    m_sprite->SetBorders(b);
                }

                // the sprite file may not exist yet. If it does not then GamePathToFullPath
                // will give a path in the project even if the texture is in a gem.
                // The texture is guaranteed to exist so use that to get the full path.
                QString fullTexturePath = Path::GamePathToFullPath(m_sprite->GetTexturePathname().c_str());
                const char* const spriteExtension = "sprite";
                string fullSpritePath = PathUtil::ReplaceExtension(fullTexturePath.toUtf8().data(), spriteExtension);

                FileHelpers::SourceControlAddOrEdit(fullSpritePath.c_str(), QApplication::activeWindow());

                bool saveSuccessful = m_sprite->SaveToXml(fullSpritePath.c_str());

                if (saveSuccessful)
                {
                    accept();
                    return;
                }

                QMessageBox(QMessageBox::Critical,
                    "Error",
                    "Unable to save file. Is the file read-only?",
                    QMessageBox::Ok, QApplication::activeWindow()).exec();
            });
        saveButton->setProperty("class", "Primary");
        innerLayout->addWidget(saveButton, rowNum, 0);

        // Cancel button.
        QPushButton* cancelButton = new QPushButton("Cancel", this);
        QObject::connect(cancelButton,
            &QPushButton::clicked, this,
            [this](bool checked)
        {
            // Since we're cancelling the dialog, restore the original sprite
            // configuration from when the dialog originally opened.
            m_sprite->SetSpriteSheetCells(m_restoreInfo.spriteSheetCells);
            m_sprite->SetBorders(m_restoreInfo.borders);

            reject();
        });
        innerLayout->addWidget(cancelButton, rowNum, 1);
    }
}

void SpriteBorderEditor::AddSeparator(QGridLayout* gridLayout, int& rowNum)
{
    QFrame* line = new QFrame();
    line->setFrameShape(QFrame::HLine);
    line->setFrameShadow(QFrame::Sunken);
    const int firstColumnPosition = 0;
    const int singleRowSpan = 1;
    const int fullWindowWidthColumnSpan = 8;
    gridLayout->addWidget(line, rowNum++, firstColumnPosition, singleRowSpan, fullWindowWidthColumnSpan);
}

void SpriteBorderEditor::SetDisplayedTextureSize(float width, float height)
{
    QString imageDescription = m_sprite->GetSpriteSheetCells().size() <= 1 ? "Texture" : "Cell size";
    m_textureSizeLabel->setText(QString("%1 is %2 x %3").arg(imageDescription).arg(QString::number(width)).arg(QString::number(height)));
}

bool SpriteBorderEditor::IsConfiguringSpriteSheet() const
{
    return m_sprite->IsSpriteSheet() || m_configureAsSpriteSheet;
}

CellSelectRectItem::CellSelectRectItem(const QRectF& rect, const AZStd::function<void()>& clickCallback)
    : QGraphicsRectItem(rect)
    , m_clickCallback(clickCallback) 
{ 
}

CellSelectRectItem::~CellSelectRectItem()
{
    // We assume that the layout is being reset/cleared when this
    // dtor is getting called. It's possible that a mousePressEvent
    // has already been invoked on a newer CellSelectRectItem. If that's
    // the case, the current selection ptr will be dangling, so just clear
    // it here.
    ClearSelection();
}

void CellSelectRectItem::SelectCell()
{
    CellSelectRectItem* currentSelection = s_currentSelection;

    if (s_currentSelection)
    {
        s_currentSelection->setPen(Qt::NoPen);
    }

    s_currentSelection = this;

    QPen solidPenStyle;
    static const QColor orangeQColor(255, 165, 0);
    solidPenStyle.setColor(orangeQColor);
    solidPenStyle.setStyle(Qt::SolidLine);
    solidPenStyle.setWidth(4);
    solidPenStyle.setJoinStyle(Qt::MiterJoin);
    setPen(solidPenStyle);
}

void CellSelectRectItem::mousePressEvent(QGraphicsSceneMouseEvent* mouseEvent)
{
    if (m_processInput)
    {
        SelectCell();
        m_clickCallback();
    }
}

#include <SpriteBorderEditor.moc>