// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#include "pch.h"
#include "CalculationResult.h"
#include "CalculationResultAutomationPeer.h"
#include "CalcViewModel/Common/LocalizationSettings.h"

using namespace CalculatorApp;
using namespace CalculatorApp::Controls;
using namespace CalculatorApp::Common;

using namespace Platform;
using namespace Windows::Devices::Input;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::UI::Xaml::Controls::Primitives;
using namespace Windows::UI::Xaml::Data;
using namespace Windows::UI::Xaml::Input;
using namespace Windows::UI::Xaml::Media;
using namespace Windows::UI::Xaml::Navigation;
using namespace Windows::UI::Xaml::Automation;
using namespace Windows::UI::Xaml::Automation::Peers;
using namespace std;

DEPENDENCY_PROPERTY_INITIALIZATION(CalculationResult, IsActive);
DEPENDENCY_PROPERTY_INITIALIZATION(CalculationResult, MinFontSize);
DEPENDENCY_PROPERTY_INITIALIZATION(CalculationResult, MaxFontSize);
DEPENDENCY_PROPERTY_INITIALIZATION(CalculationResult, DisplayMargin);
DEPENDENCY_PROPERTY_INITIALIZATION(CalculationResult, DisplayValue);
DEPENDENCY_PROPERTY_INITIALIZATION(CalculationResult, IsInError);
DEPENDENCY_PROPERTY_INITIALIZATION(CalculationResult, IsOperatorCommand);

#define SCALEFACTOR 0.357143
#define SMALLHEIGHTSCALEFACTOR 0
#define HEIGHTCUTOFF 100
#define INCREMENTOFFSET 1
#define MAXFONTINCREMENT 5
#define WIDTHTOFONTSCALAR 0.0556513
#define WIDTHTOFONTOFFSET 3
#define WIDTHCUTOFF 50
#define FONTTOLERANCE 0.001
#define SCROLL_RATIO 0.7

// We need a safety margin to guarantee we correctly always show/hide ScrollLeft and ScrollRight buttons when necessary.
// In rare cases, ScrollViewer::HorizontalOffset is a little low by a few (sub)pixels when users scroll to one of the extremity
// and no events are launched when they scroll again in the same direction
#define SCROLL_BUTTONS_APPROXIMATION_RANGE 4

CalculationResult::CalculationResult()
    : m_isScalingText(false)
    , m_haveCalculatedMax(false)
{
}

Platform::String ^ CalculationResult::GetRawDisplayValue()
{
    return LocalizationSettings::GetInstance().RemoveGroupSeparators(DisplayValue);
}

void CalculationResult::OnApplyTemplate()
{
    if (m_textContainer)
    {
        if (m_textContainerLayoutChangedToken.Value != 0)
        {
            m_textContainer->LayoutUpdated -= m_textContainerLayoutChangedToken;
            m_textContainerLayoutChangedToken.Value = 0;
        }
        if (m_textContainerSizeChangedToken.Value != 0)
        {
            m_textContainer->SizeChanged -= m_textContainerSizeChangedToken;
            m_textContainerSizeChangedToken.Value = 0;
        }
        if (m_textContainerViewChangedToken.Value != 0)
        {
            m_textContainer->ViewChanged -= m_textContainerViewChangedToken;
            m_textContainerViewChangedToken.Value = 0;
        }
    }

    if (m_textBlock != nullptr && m_textBlockSizeChangedToken.Value != 0)
    {
        m_textBlock->SizeChanged -= m_textBlockSizeChangedToken;
        m_textBlockSizeChangedToken.Value = 0;
    }

    if (m_scrollLeft != nullptr && m_scrollLeftClickToken.Value != 0)
    {
        m_scrollLeft->Click -= m_scrollLeftClickToken;
        m_scrollLeftClickToken.Value = 0;
    }

    if (m_scrollRight != nullptr && m_scrollRightClickToken.Value != 0)
    {
        m_scrollRight->Click -= m_scrollRightClickToken;
        m_scrollRightClickToken.Value = 0;
    }

    m_textContainer = dynamic_cast<ScrollViewer ^>(GetTemplateChild("TextContainer"));
    if (m_textContainer)
    {
        // We want to know when the size of the container changes so
        // we can rescale the textbox
        m_textContainerSizeChangedToken = m_textContainer->SizeChanged += ref new SizeChangedEventHandler(this, &CalculationResult::OnTextContainerSizeChanged);

        m_textContainerViewChangedToken = m_textContainer->ViewChanged +=
            ref new Windows::Foundation::EventHandler<Windows::UI::Xaml::Controls::ScrollViewerViewChangedEventArgs ^>(
                this, &CalculatorApp::Controls::CalculationResult::OnTextContainerOnViewChanged);

        m_textContainerLayoutChangedToken = m_textContainer->LayoutUpdated +=
            ref new EventHandler<Object ^>(this, &CalculationResult::OnTextContainerLayoutUpdated);

        m_textContainer->ChangeView(m_textContainer->ExtentWidth - m_textContainer->ViewportWidth, nullptr, nullptr);
        m_scrollLeft = dynamic_cast<HyperlinkButton ^>(GetTemplateChild("ScrollLeft"));
        if (m_scrollLeft)
        {
            m_scrollLeftClickToken = m_scrollLeft->Click += ref new RoutedEventHandler(this, &CalculationResult::OnScrollLeftClick);
        }
        m_scrollRight = dynamic_cast<HyperlinkButton ^>(GetTemplateChild("ScrollRight"));
        if (m_scrollRight)
        {
            m_scrollRightClickToken = m_scrollRight->Click += ref new RoutedEventHandler(this, &CalculationResult::OnScrollRightClick);
        }
        m_textBlock = dynamic_cast<TextBlock ^>(GetTemplateChild("NormalOutput"));
        if (m_textBlock)
        {
            m_textBlock->Visibility = ::Visibility::Visible;
            m_textBlockSizeChangedToken = m_textBlock->SizeChanged += ref new SizeChangedEventHandler(this, &CalculationResult::OnTextBlockSizeChanged);
        }
    }
    UpdateVisualState();
    UpdateTextState();
}

void CalculationResult::OnTextContainerLayoutUpdated(Object ^ /*sender*/, Object ^ /*e*/)
{
    if (m_isScalingText)
    {
        UpdateTextState();
    }
}

void CalculationResult::OnTextContainerSizeChanged(Object ^ /*sender*/, SizeChangedEventArgs ^ /*e*/)
{
    UpdateTextState();
}

void CalculationResult::OnIsActivePropertyChanged(bool /*oldValue*/, bool /*newValue */)
{
    UpdateVisualState();
}

void CalculationResult::OnDisplayValuePropertyChanged(String ^ /*oldValue*/, String ^ /*newValue*/)
{
    UpdateTextState();
}

void CalculationResult::OnMinFontSizePropertyChanged(double /*oldValue*/, double /*newValue*/)
{
    UpdateTextState();
}

void CalculationResult::OnMaxFontSizePropertyChanged(double /*oldValue*/, double /*newValue*/)
{
    UpdateTextState();
}

void CalculationResult::OnIsInErrorPropertyChanged(bool /*oldValue*/, bool newValue)
{
    // We need to have a good template for this to work
    if (m_textBlock == nullptr)
    {
        return;
    }

    if (newValue)
    {
        // If there's an error message we need to override the normal display font
        // with the font appropriate for this language. This is because the error
        // message is localized and therefore can contain characters that are not
        // available in the normal font.
        // We use UIText as the font type because this is the most common font type to use
        m_textBlock->FontFamily = LocalizationService::GetInstance()->GetLanguageFontFamilyForType(LanguageFontType::UIText);
    }
    else
    {
        // The error result is no longer an error so we will restore the
        // value to FontFamily property to the value provided in the style
        // for the TextBlock in the template.
        m_textBlock->ClearValue(TextBlock::FontFamilyProperty);
    }
}

void CalculationResult::UpdateVisualState()
{
    if (IsActive)
    {
        VisualStateManager::GoToState(this, "Active", true);
    }
    else
    {
        VisualStateManager::GoToState(this, "Normal", true);
    }
}

void CalculationResult::UpdateTextState()
{
    if ((m_textContainer == nullptr) || (m_textBlock == nullptr))
    {
        return;
    }

    auto containerSize = m_textContainer->ActualWidth;
    String ^ oldText = m_textBlock->Text;
    String ^ newText = Utils::LRO + DisplayValue + Utils::PDF;

    // Initiate the scaling operation
    // UpdateLayout will keep calling us until we make it through the below 2 if-statements
    if (!m_isScalingText || oldText != newText)
    {
        m_textBlock->Text = newText;

        m_isScalingText = true;
        m_haveCalculatedMax = false;
        m_textBlock->InvalidateArrange();
        return;
    }
    if (containerSize > 0)
    {
        double widthDiff = abs(m_textBlock->ActualWidth - containerSize);
        double fontSizeChange = INCREMENTOFFSET;

        if (widthDiff > WIDTHCUTOFF)
        {
            fontSizeChange = min<double>(max<double>(floor(WIDTHTOFONTSCALAR * widthDiff) - WIDTHTOFONTOFFSET, INCREMENTOFFSET), MAXFONTINCREMENT);
        }
        if (m_textBlock->ActualWidth < containerSize && abs(m_textBlock->FontSize - MaxFontSize) > FONTTOLERANCE && !m_haveCalculatedMax)
        {
            ModifyFontAndMargin(m_textBlock, fontSizeChange);
            m_textBlock->InvalidateArrange();
            return;
        }
        if (fontSizeChange < 5)
        {
            m_haveCalculatedMax = true;
        }
        if (m_textBlock->ActualWidth >= containerSize && abs(m_textBlock->FontSize - MinFontSize) > FONTTOLERANCE)
        {
            ModifyFontAndMargin(m_textBlock, -1 * fontSizeChange);
            m_textBlock->InvalidateArrange();
            return;
        }
        assert(m_textBlock->FontSize >= MinFontSize && m_textBlock->FontSize <= MaxFontSize);
        m_isScalingText = false;
        if (IsOperatorCommand)
        {
            m_textContainer->ChangeView(0.0, nullptr, nullptr);
        }
        else
        {
            m_textContainer->ChangeView(m_textContainer->ExtentWidth - m_textContainer->ViewportWidth, nullptr, nullptr);
        }
    }
}

void CalculationResult::ScrollLeft()
{
    if (m_textContainer == nullptr)
    {
        return;
    }
    if (m_textContainer->HorizontalOffset > 0)
    {
        double offset = m_textContainer->HorizontalOffset - (SCROLL_RATIO * m_textContainer->ViewportWidth);
        m_textContainer->ChangeView(offset, nullptr, nullptr);
    }
}

void CalculationResult::ScrollRight()
{
    if (m_textContainer == nullptr)
    {
        return;
    }

    if (m_textContainer->HorizontalOffset < m_textContainer->ExtentWidth - m_textContainer->ViewportWidth)
    {
        double offset = m_textContainer->HorizontalOffset + (SCROLL_RATIO * m_textContainer->ViewportWidth);
        m_textContainer->ChangeView(offset, nullptr, nullptr);
    }
}

void CalculationResult::OnKeyDown(KeyRoutedEventArgs ^ e)
{
    switch (e->Key)
    {
    case Windows::System::VirtualKey::Left:
        this->ScrollLeft();
        break;
    case Windows::System::VirtualKey::Right:
        this->ScrollRight();
        break;
    }
}

void CalculationResult::OnScrollLeftClick(Object ^ sender, RoutedEventArgs ^ /*e*/)
{
    ScrollLeft();
}

void CalculationResult::OnScrollRightClick(Object ^ sender, RoutedEventArgs ^ /*e*/)
{
    ScrollRight();
}

void CalculationResult::UpdateScrollButtons()
{
    if (m_textContainer == nullptr)
    {
        return;
    }

    if (m_scrollLeft != nullptr)
    {
        m_scrollLeft->Visibility = m_textContainer->HorizontalOffset > SCROLL_BUTTONS_APPROXIMATION_RANGE ? ::Visibility::Visible : ::Visibility::Collapsed;
    }

    if (m_scrollRight != nullptr)
    {
        m_scrollRight->Visibility =
            m_textContainer->HorizontalOffset + m_textContainer->ViewportWidth + SCROLL_BUTTONS_APPROXIMATION_RANGE < m_textContainer->ExtentWidth
                ? ::Visibility::Visible
                : ::Visibility::Collapsed;
    }
}

void CalculationResult::ModifyFontAndMargin(TextBlock ^ textBox, double fontChange)
{
    double cur = textBox->FontSize;
    double newFontSize = 0.0;
    double scaleFactor = SCALEFACTOR;
    if (m_textContainer->ActualHeight <= HEIGHTCUTOFF)
    {
        scaleFactor = SMALLHEIGHTSCALEFACTOR;
    }

    newFontSize = min(max(cur + fontChange, MinFontSize), MaxFontSize);
    m_textContainer->Padding = Thickness(0, 0, 0, scaleFactor * abs(cur - newFontSize));
    textBox->FontSize = newFontSize;
}

void CalculationResult::OnTapped(TappedRoutedEventArgs ^ e)
{
    this->Focus(::FocusState::Programmatic);
    RaiseSelectedEvent();
}

void CalculationResult::OnRightTapped(RightTappedRoutedEventArgs ^ e)
{
    auto requestedElement = e->OriginalSource;

    if (requestedElement->Equals(dynamic_cast<Object ^>(m_textBlock)))
    {
        m_textBlock->Focus(::FocusState::Programmatic);
    }
    else
    {
        this->Focus(::FocusState::Programmatic);
    }
}

AutomationPeer ^ CalculationResult::OnCreateAutomationPeer()
{
    return ref new CalculationResultAutomationPeer(this);
}

void CalculationResult::ProgrammaticSelect()
{
    RaiseSelectedEvent();
}

void CalculationResult::RaiseSelectedEvent()
{
    Selected(this);
}

void CalculationResult::OnTextContainerOnViewChanged(Object ^ /*sender*/, ScrollViewerViewChangedEventArgs ^ e)
{
    UpdateScrollButtons();
}

void CalculationResult::OnTextBlockSizeChanged(Object ^ /*sender*/, SizeChangedEventArgs ^ /*e*/)
{
    UpdateScrollButtons();
}
