/*
 * Copyright (c) Contributors to the Open 3D Engine Project.
 * For complete copyright and license terms please see the LICENSE at the root of this distribution.
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 *
 */


#include "EditorDefs.h"

#include "TrackViewCurveEditor.h"

AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
#include "TrackView/ui_TrackViewCurveEditor.h"
AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING


#define IDC_TRACKVIEWGRAPH_CURVE 1
#define IDC_TIMELINE             2

#define IDC_HORIZON_SLIDER      3
#define IDC_VERTICAL_SLIDER     4

//! It's for mapping from a slider control range to a real zoom range, and vice versa.
#define SLIDER_MULTIPLIER       100.f
#define SLIDERRANGE_TO_ZOOM(SLIDERVALUE)    (float)SLIDERVALUE / SLIDER_MULTIPLIER
#define ZOOMRANGE_TO_SLIDER(ZOOMVALUE)      (int)(ZOOMVALUE * SLIDER_MULTIPLIER)


//////////////////////////////////////////////////////////////////////////
TrackViewCurveEditorDialog::TrackViewCurveEditorDialog(QWidget* parent)
    : QWidget(parent)
{
    m_widget = new CTrackViewCurveEditor(this);
    QVBoxLayout* l = new QVBoxLayout;
    l->setMargin(0);
    l->addWidget(m_widget);
    setLayout(l);
}

//////////////////////////////////////////////////////////////////////////
CTrackViewCurveEditor::CTrackViewCurveEditor(QWidget* parent)
    : QWidget(parent)
    , m_ui(new Ui::TrackViewCurveEditor)
{
    m_ui->setupUi(this);
    m_bLevelClosing = false;
    m_bIgnoreSelfEvents = false;
    GetIEditor()->RegisterNotifyListener(this);
    GetIEditor()->GetAnimation()->AddListener(this);

    m_timelineCtrl.SetTimeRange(Range(0, 1));
    m_timelineCtrl.SetTicksTextScale(1.0f);

    m_ui->m_wndSpline->SetTimelineCtrl(&m_timelineCtrl);

    connect(&m_timelineCtrl, &TimelineWidget::change, this, &CTrackViewCurveEditor::OnTimelineChange);
    connect(m_ui->m_wndSpline, &SplineWidget::change, this, &CTrackViewCurveEditor::OnSplineChange);
    connect(m_ui->m_wndSpline, &SplineWidget::timeChange, this, &CTrackViewCurveEditor::OnSplineTimeMarkerChange);

    connect(m_ui->buttonTangentAuto,      &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_TANGENT_AUTO); });
    connect(m_ui->buttonTangentInZero,    &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_TANGENT_IN_ZERO); });
    connect(m_ui->buttonTangentInStep,    &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_TANGENT_IN_STEP); });
    connect(m_ui->buttonTangentInLinear,  &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_TANGENT_IN_LINEAR); });
    connect(m_ui->buttonTangentOutZero,   &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_TANGENT_OUT_ZERO); });
    connect(m_ui->buttonTangentOutStep,   &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_TANGENT_OUT_STEP); });
    connect(m_ui->buttonTangentOutLinear, &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_TANGENT_OUT_LINEAR); });
    connect(m_ui->buttonSplineFitX,       &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_SPLINE_FIT_X); });
    connect(m_ui->buttonSplineFitY,       &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_SPLINE_FIT_Y); });
    connect(m_ui->buttonSplineSnapGridX,  &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_SPLINE_SNAP_GRID_X); });
    connect(m_ui->buttonSplineSnapGridY,  &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_SPLINE_SNAP_GRID_Y); });
    connect(m_ui->buttonTangentUnify,     &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_TANGENT_UNIFY); });
    connect(m_ui->buttonFreezeKeys,       &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_FREEZE_KEYS); });
    connect(m_ui->buttonFreezeTangents,   &QToolButton::clicked, this, [&]() {OnSplineCmd(ID_FREEZE_TANGENTS); });
}

//////////////////////////////////////////////////////////////////////////
CTrackViewCurveEditor::~CTrackViewCurveEditor()
{
    GetIEditor()->GetAnimation()->RemoveListener(this);
    GetIEditor()->UnregisterNotifyListener(this);
}



// CTrackViewGraph message handlers

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnSequenceChanged([[maybe_unused]] CTrackViewSequence* pSequence)
{
    UpdateSplines();
    update();
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnEditorNotifyEvent(EEditorNotifyEvent event)
{
    if (m_bIgnoreSelfEvents)
    {
        return;
    }
    switch (event)
    {
    case eNotify_OnCloseScene:
        m_ui->m_wndSpline->RemoveAllSplines();
        m_bLevelClosing = true;
        break;
    case eNotify_OnBeginNewScene:
    case eNotify_OnBeginSceneOpen:
        m_bLevelClosing = false;
        break;
    }
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::UpdateSplines()
{
    CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence();

    if (!pSequence || m_bLevelClosing)
    {
        // No sequence selected, remove any splines.
        if (nullptr != m_ui)
        {
            if (nullptr != m_ui->m_wndSpline)
            {
                m_ui->m_wndSpline->RemoveAllSplines();
            }
        }

        return;
    }

    CTrackViewTrackBundle selectedTracks;
    selectedTracks = pSequence->GetSelectedTracks();

    std::set<CTrackViewTrack*> oldTracks;
    for (auto iter = m_ui->m_wndSpline->GetTracks().begin(); iter != m_ui->m_wndSpline->GetTracks().end(); ++iter)
    {
        CTrackViewTrack* pTrack = *iter;
        oldTracks.insert(pTrack);
    }

    std::set<CTrackViewTrack*> newTracks;
    if (selectedTracks.AreAllOfSameType())
    {
        for (unsigned int i = 0; i < selectedTracks.GetCount(); i++)
        {
            CTrackViewTrack* pTrack = selectedTracks.GetTrack(i);

            if (pTrack->IsCompoundTrack())
            {
                unsigned int numChildTracks = pTrack->GetChildCount();
                for (unsigned int ii = 0; ii < numChildTracks; ++ii)
                {
                    CTrackViewTrack* pChildTrack = static_cast<CTrackViewTrack*>(pTrack->GetChild(ii));
                    newTracks.insert(pChildTrack);
                }
            }
            else
            {
                newTracks.insert(pTrack);
            }
        }
    }

    if (oldTracks == newTracks)
    {
        return;
    }

    m_ui->m_wndSpline->RemoveAllSplines();
    for (auto iter = newTracks.begin(); iter != newTracks.end(); ++iter)
    {
        AddSpline(*iter);
    }

    UpdateTimeRange(pSequence);

    // If it is a rotation track, adjust the default value range properly to accommodate some degree values.
    if (selectedTracks.HasRotationTrack())
    {
        m_ui->m_wndSpline->SetDefaultValueRange(Range(-180.0f, 180.0f));
    }
    else
    {
        m_ui->m_wndSpline->SetDefaultValueRange(Range(-1.1f, 1.1f));
    }

    ResetSplineCtrlZoomLevel();

    return;
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::AddSpline(CTrackViewTrack* pTrack)
{
    if (!pTrack->GetSpline())
    {
        return;
    }

    const int subTrackIndex = pTrack->GetSubTrackIndex();

    if (subTrackIndex >= 0)
    {
        QColor trackColor = QColor(255, 0, 0);
        switch (subTrackIndex)
        {
        case 0:
            trackColor = QColor(255, 0, 0);
            break;
        case 1:
            trackColor = QColor(0, 255, 0);
            break;
        case 2:
            trackColor = QColor(0, 0, 255);
            break;
        case 3:
            trackColor = QColor(255, 255, 0);
            break;
        }

        m_ui->m_wndSpline->AddSpline(pTrack->GetSpline(), pTrack, trackColor);
    }
    else
    {
        QColor    afColorArray[4];
        afColorArray[0] = QColor(255, 0, 0);
        afColorArray[1] = QColor(0, 255, 0);
        afColorArray[2] = QColor(0, 0, 255);
        afColorArray[3] = QColor(255, 0, 255); //Pink... so you know it's wrong if you see it.

        m_ui->m_wndSpline->AddSpline(pTrack->GetSpline(), pTrack, afColorArray);
    }
}

void CTrackViewCurveEditor::showEvent(QShowEvent* event)
{
    QWidget::showEvent(event);
    OnSplineCmdUpdateUI();
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnSplineChange()
{
    CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence();
    if (pSequence)
    {
        pSequence->OnKeysChanged();
    }

    // In the end, focus this again in order to properly catch 'KeyDown' messages.
    m_ui->m_wndSpline->setFocus();
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnSplineCmd(UINT cmd)
{
    m_ui->m_wndSpline->OnUserCommand(cmd);
    OnSplineCmdUpdateUI();
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnSplineCmdUpdateUI()
{
    CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence();

    if (m_bLevelClosing || !pSequence)
    {
        return;
    }

    m_ui->buttonSplineSnapGridX->setChecked(m_ui->m_wndSpline->IsSnapTime());
    m_ui->buttonSplineSnapGridY->setChecked(m_ui->m_wndSpline->IsSnapValue());
    m_ui->buttonTangentUnify->setChecked(m_ui->m_wndSpline->IsUnifiedKeyCurrentlySelected());
    m_ui->buttonFreezeKeys->setChecked(m_ui->m_wndSpline->IsKeysFrozen());
    m_ui->buttonFreezeTangents->setChecked(m_ui->m_wndSpline->IsTangentsFrozen());
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnTimeChanged(float newTime)
{
    m_ui->m_wndSpline->SetTimeMarker(newTime);
    m_ui->m_wndSpline->update();
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::SetEditLock(bool bLock)
{
    m_ui->m_wndSpline->SetEditLock(bLock);
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnTimelineChange()
{
    float fTime = m_timelineCtrl.GetTimeMarker();
    GetIEditor()->GetAnimation()->SetTime(fTime);
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnSplineTimeMarkerChange()
{
    float fTime = m_ui->m_wndSpline->GetTimeMarker();
    GetIEditor()->GetAnimation()->SetTime(fTime);
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::SetFPS(float fps)
{
    m_timelineCtrl.SetFPS(fps);
}

//////////////////////////////////////////////////////////////////////////
float CTrackViewCurveEditor::GetFPS() const
{
    return m_timelineCtrl.GetFPS();
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::SetTickDisplayMode(ETVTickMode mode)
{
    if (mode == eTVTickMode_InFrames)
    {
        m_timelineCtrl.SetMarkerStyle(TimelineWidget::MARKER_STYLE_FRAMES);
        m_ui->m_wndSpline->SetTooltipValueScale(GetFPS(), 1.0f);
    }
    else if (mode == eTVTickMode_InSeconds)
    {
        m_timelineCtrl.SetMarkerStyle(TimelineWidget::MARKER_STYLE_SECONDS);
        m_ui->m_wndSpline->SetTooltipValueScale(1.0f, 1.0f);
    }

    m_timelineCtrl.update();
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::ResetSplineCtrlZoomLevel()
{
    m_ui->m_wndSpline->FitSplineToViewHeight();
    m_ui->m_wndSpline->FitSplineToViewWidth();
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnKeysChanged([[maybe_unused]] CTrackViewSequence* pSequence)
{
    m_ui->m_wndSpline->update();
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnKeyAdded(CTrackViewKeyHandle& addedKeyHandle)
{
    EAnimCurveType trType = addedKeyHandle.GetTrack()->GetCurveType();
    if (trType == eAnimCurveType_BezierFloat)
    {
        // we query the added key's track to find the default tangent flags to use for newly created keys
        const int tangentFlagsForNewKeys = addedKeyHandle.GetTrack()->GetAnimNode()->GetDefaultKeyTangentFlags();
        I2DBezierKey bezierKey;
        addedKeyHandle.GetKey(&bezierKey);

        // clear any existing in and out tangent flags
        bezierKey.flags &= ~SPLINE_KEY_TANGENT_ALL_MASK;

        // set tangent flags to the default tangent flags used for the track's animNode and save them
        bezierKey.flags |= tangentFlagsForNewKeys;
        addedKeyHandle.SetKey(&bezierKey);
    }
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnKeySelectionChanged([[maybe_unused]] CTrackViewSequence* pSequence)
{
    if (isVisible())
    {
        m_ui->m_wndSpline->update();
        m_ui->buttonTangentUnify->setChecked(m_ui->m_wndSpline->IsUnifiedKeyCurrentlySelected());
    }
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnNodeChanged([[maybe_unused]] CTrackViewNode* pNode, ENodeChangeType type)
{
    if (isVisible() && type == ITrackViewSequenceListener::eNodeChangeType_Removed)
    {
        UpdateSplines();
    }
}

//////////////////////////////////////////////////////////////////////////
void CTrackViewCurveEditor::OnNodeSelectionChanged([[maybe_unused]] CTrackViewSequence* pSequence)
{
    if (isVisible())
    {
        UpdateSplines();
    }
}

void CTrackViewCurveEditor::OnSequenceSettingsChanged(CTrackViewSequence* pSequence)
{
    if (isVisible())
    {
        UpdateTimeRange(pSequence);
        m_timelineCtrl.update();
        m_ui->m_wndSpline->update();
    }
}

void CTrackViewCurveEditor::UpdateTimeRange(CTrackViewSequence* pSequence)
{
    Range timeRange =   pSequence->GetTimeRange();
    m_ui->m_wndSpline->SetTimeRange(timeRange);
    m_timelineCtrl.SetTimeRange(timeRange);
    m_ui->m_wndSpline->SetValueRange(Range(-2000.0f, 2000.0f));
}

void CTrackViewCurveEditor::SetPlayCallback(const std::function<void()>& callback)
{
    m_ui->m_wndSpline->SetPlayCallback(callback);
    m_timelineCtrl.SetPlayCallback(callback);
}

CTrackViewSplineCtrl& CTrackViewCurveEditor::GetSplineCtrl()
{
    return *m_ui->m_wndSpline;
}
