﻿// MyGraph.cpp

#include "stdafx.h"
#include "MyGraph.h"
#include "BufferDC.h"
#include "Theme.h"

#include <cmath>
// ReSharper disable once CppInconsistentNaming
#define _USE_MATH_DEFINES
#include <math.h> // for M_PI
#include <memory>

#ifdef _DEBUG
// ReSharper disable once CppInconsistentNaming
#    define new DEBUG_NEW
#    undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// This macro can be called at the beginning and ending of every
// method.  It is identical to saying "ASSERT_VALID(); ASSERT_KINDOF();"
// but is written like this so that VALIDATE can be a macro.  It is useful
// as an "early warning" that something has gone wrong with "this" object.
#ifndef VALIDATE
#    ifdef _DEBUG
#        define VALIDATE                                      \
            ::AfxAssertValidObject(this, __FILE__, __LINE__); \
            _ASSERTE(IsKindOf(GetRuntimeClass()));
#    else
#        define VALIDATE
#    endif
#endif

/////////////////////////////////////////////////////////////////////////////
// Constants.

#define TICK_PIXELS                   4 // Size of tick marks.
#define GAP_PIXELS                    6 // Better if an even value.
#define LEGEND_COLOR_BAR_WIDTH_PIXELS 50 // Width of color bar.
#define LEGEND_COLOR_BAR_GAP_PIXELS   1 // Space between color bars.
#define Y_AXIS_TICK_COUNT_TARGET      5 // How many ticks should be there on the y axis.
#define MIN_FONT_SIZE                 70 // The minimum font-size in pt*10.
#define LEGEND_VISIBILITY_THRESHOLD   300 // The width of the graph in pixels when the legend gets hidden.

#define INTERSERIES_PERCENT_USED 0.85 // How much of the graph is \
                                      // used for bars/pies (the  \
                                      // rest is for inter-series \
                                      // spacing).

#define TITLE_DIVISOR        5 // Scale font to graph width.
#define LEGEND_DIVISOR       8 // Scale font to graph height.
#define Y_AXIS_LABEL_DIVISOR 6 // Scale font to graph height.

#ifndef M_PI
const double M_PI = 3.1415926535897932384626433832795;
#endif

/////////////////////////////////////////////////////////////////////////////
// MyGraphSeries

// Constructor.
MyGraphSeries::MyGraphSeries(const CString& sLabel /* = "" */)
    : m_sLabel(sLabel)
{
}

// Destructor.
/* virtual */ MyGraphSeries::~MyGraphSeries()
{
    for (int nGroup = 0; nGroup < m_oaRegions.GetSize(); ++nGroup)
    {
        delete m_oaRegions.GetAt(nGroup);
    }
}

//
void MyGraphSeries::SetLabel(const CString& sLabel)
{
    VALIDATE;

    m_sLabel = sLabel;
}

//
void MyGraphSeries::SetData(int nGroup, int nValue)
{
    VALIDATE;
    _ASSERTE(0 <= nGroup);

    m_dwaValues.SetAtGrow(nGroup, nValue);
}

//
void MyGraphSeries::SetTipRegion(int nGroup, const CRect& rc)
{
    VALIDATE;

    auto prgnNew = std::make_unique<CRgn>();
    ASSERT_VALID(prgnNew.get());

    VERIFY(prgnNew->CreateRectRgnIndirect(rc));
    SetTipRegion(nGroup, prgnNew.release());
}

//
void MyGraphSeries::SetTipRegion(int nGroup, CRgn* prgn)
{
    VALIDATE;
    _ASSERTE(0 <= nGroup);
    ASSERT_VALID(prgn);

    // If there is an existing region, delete it.
    CRgn* prgnOld = nullptr;

    if (nGroup < m_oaRegions.GetSize())
    {
        prgnOld = m_oaRegions.GetAt(nGroup);
        ASSERT_NULL_OR_POINTER(prgnOld, CRgn);
    }

    delete prgnOld;
    prgnOld = nullptr;

    // Add the new region.
    m_oaRegions.SetAtGrow(nGroup, prgn);

    _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
}

//
CString MyGraphSeries::GetLabel() const
{
    VALIDATE;

    return m_sLabel;
}

//
int MyGraphSeries::GetData(int nGroup) const
{
    VALIDATE;
    _ASSERTE(0 <= nGroup);
    _ASSERTE(m_dwaValues.GetSize() > nGroup);

    return m_dwaValues[nGroup];
}

// Returns the largest data value in this series.
int MyGraphSeries::GetMaxDataValue(bool bStackedGraph) const
{
    VALIDATE;

    int nMax(0);

    for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup)
    {
        if (!bStackedGraph)
        {
            nMax = max(nMax, static_cast<int>(m_dwaValues.GetAt(nGroup)));
        }
        else
        {
            nMax += static_cast<int>(m_dwaValues.GetAt(nGroup));
        }
    }

    return nMax;
}

// Returns the average data value in this series.
int MyGraphSeries::GetAverageDataValue() const
{
    VALIDATE;

    int nTotal = 0;

    for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup)
    {
        nTotal += static_cast<int>(m_dwaValues.GetAt(nGroup));
    }

    if (m_dwaValues.GetSize() == 0)
        return 0;

    return nTotal / static_cast<int>(m_dwaValues.GetSize());
}

// Returns the number of data points that are not zero.
int MyGraphSeries::GetNonZeroElementCount() const
{
    VALIDATE;

    int nCount(0);

    for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup)
    {
        if (m_dwaValues.GetAt(nGroup))
        {
            ++nCount;
        }
    }

    return nCount;
}

// Returns the sum of the data points for this series.
int MyGraphSeries::GetDataTotal() const
{
    VALIDATE;

    int nTotal(0);

    for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup)
    {
        nTotal += m_dwaValues.GetAt(nGroup);
    }

    return nTotal;
}

// Returns which group (if any) the sent point lies within in this series.
int MyGraphSeries::HitTest(const CPoint& pt, int searchStart = 0) const
{
    VALIDATE;

    for (int nGroup = searchStart; nGroup < m_oaRegions.GetSize(); ++nGroup)
    {
        CRgn* prgnData = m_oaRegions.GetAt(nGroup);
        ASSERT_NULL_OR_POINTER(prgnData, CRgn);

        if (prgnData && prgnData->PtInRegion(pt))
        {
            return nGroup;
        }
    }

    return -1;
}

// Get the series portion of the tip for this group in this series.
CString MyGraphSeries::GetTipText(int nGroup, const CString& unitString) const
{
    VALIDATE;
    _ASSERTE(0 <= nGroup);
    _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());

    CString sTip;

    sTip.Format(L"%d %s (%d%%)", m_dwaValues.GetAt(nGroup),
                static_cast<LPCWSTR>(unitString),
                GetDataTotal() ? static_cast<int>(100.0 * static_cast<double>(m_dwaValues.GetAt(nGroup)) /
                                                  static_cast<double>(GetDataTotal()))
                               : 0);

    return sTip;
}

/////////////////////////////////////////////////////////////////////////////
// MyGraph

// Constructor.
MyGraph::MyGraph(GraphType eGraphType /* = MyGraph::Pie */, bool bStackedGraph /* = false */)
    : m_nXAxisWidth(0)
    , m_nYAxisHeight(0)
    , m_nAxisLabelHeight(0)
    , m_nAxisTickLabelHeight(0)
    , m_eGraphType(eGraphType)
    , m_bStackedGraph(bStackedGraph)
{
    m_ptOrigin.x = m_ptOrigin.y = 0;
    m_rcGraph.SetRectEmpty();
    m_rcLegend.SetRectEmpty();
    m_rcTitle.SetRectEmpty();
}

// Destructor.
/* virtual */ MyGraph::~MyGraph()
{
}

BEGIN_MESSAGE_MAP(MyGraph, CStatic)
    //{{AFX_MSG_MAP(MyGraph)
    ON_WM_PAINT()
    ON_WM_SIZE()
    //}}AFX_MSG_MAP
    ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnNeedText)
    ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnNeedText)
END_MESSAGE_MAP()

// Called by the framework to allow other necessary sub classing to occur
// before the window is sub classed.
void MyGraph::PreSubclassWindow()
{
    VALIDATE;

    CStatic::PreSubclassWindow();

    VERIFY(EnableToolTips(true));
}

/////////////////////////////////////////////////////////////////////////////
// MyGraph message handlers

// Handle the tooltip messages.  Returns true to mean message was handled.
// ReSharper disable once CppMemberFunctionMayBeConst
BOOL MyGraph::OnNeedText(UINT /*uiId*/, NMHDR* pNMHDR, LRESULT* pResult)
{
    _ASSERTE(pNMHDR && "Bad parameter passed");
    _ASSERTE(pResult && "Bad parameter passed");

    bool     bReturn(false);
    UINT_PTR uiID(pNMHDR->idFrom);

    // Notification in NT from automatically created tooltip.
    if (0U != uiID)
    {
        bReturn = true;

        // Need to handle both ANSI and UNICODE versions of the message.
        TOOLTIPTEXTA* pTTTA = reinterpret_cast<TOOLTIPTEXTA*>(pNMHDR);
        ASSERT_POINTER(pTTTA, TOOLTIPTEXTA);

        TOOLTIPTEXTW* pTTTW = reinterpret_cast<TOOLTIPTEXTW*>(pNMHDR);
        ASSERT_POINTER(pTTTW, TOOLTIPTEXTW);

        CString sTipText(GetTipText());

#ifndef _UNICODE
        if (TTN_NEEDTEXTA == pNMHDR->code)
        {
            lstrcpyn(pTTTA->szText, sTipText, _countof(pTTTA->szText) - 1);
        }
        else
        {
            _mbstowcsz(pTTTW->szText, sTipText, _countof(pTTTA->szText));
        }
#else
        if (pNMHDR->code == TTN_NEEDTEXTA)
        {
            _wcstombsz(pTTTA->szText, sTipText, _countof(pTTTA->szText));
        }
        else
        {
            lstrcpyn(pTTTW->szText, sTipText, _countof(pTTTA->szText) - 1LL);
        }
#endif

        *pResult = 0;
    }

    return bReturn;
}

// The framework calls this member function to determine whether a point is in
// the bounding rectangle of the specified tool.
INT_PTR MyGraph::OnToolHitTest(CPoint point, TOOLINFO* pTi) const
{
    _ASSERTE(pTi && "Bad parameter passed");

    // This works around the problem of the tip remaining visible when you move
    // the mouse to various positions over this control.
    INT_PTR       nReturn(0);
    static bool   bTipPopped(false);
    static CPoint ptPrev(-1, -1);

    if (point != ptPrev)
    {
        ptPrev = point;

        if (bTipPopped)
        {
            bTipPopped = false;
            nReturn    = -1;
        }
        else
        {
            ::Sleep(50);
            bTipPopped = true;

            pTi->hwnd     = m_hWnd;
            pTi->uId      = reinterpret_cast<UINT_PTR>(m_hWnd);
            pTi->lpszText = LPSTR_TEXTCALLBACK;

            CRect rcWnd;
            GetClientRect(&rcWnd);
            pTi->rect = rcWnd;
            nReturn   = 1;
        }
    }
    else
    {
        nReturn = 1;
    }

    MyGraph::SpinTheMessageLoop();

    return nReturn;
}

// Build the tip text for the part of the graph that the mouse is currently
// over.
CString MyGraph::GetTipText() const
{
    VALIDATE;

    CString sTip("");

    // Get the position of the mouse.
    CPoint pt;
    VERIFY(::GetCursorPos(&pt));
    ScreenToClient(&pt);

    // Ask each part of the graph to check and see if the mouse is over it.
    if (m_rcLegend.PtInRect(pt))
    {
        sTip = "Legend";
    }
    else if (m_rcTitle.PtInRect(pt))
    {
        sTip = "Title";
    }
    else
    {
        int maxXAxis = m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2));
        if (pt.x >= m_ptOrigin.x && pt.x <= maxXAxis)
        {
            int    average       = GetAverageDataValue();
            int    nMaxDataValue = max(GetMaxDataValue(), 1);
            double barTop        = m_ptOrigin.y - static_cast<double>(m_nYAxisHeight) *
                                               (average / static_cast<double>(nMaxDataValue));
            if (pt.y >= barTop - 2 && pt.y <= barTop + 2)
            {
                sTip.Format(_T("Average: %d %s (%d%%)"), average, static_cast<LPCWSTR>(m_sYAxisLabel), nMaxDataValue ? (100 * average / nMaxDataValue) : 0);
                return sTip;
            }
        }

        POSITION pos(m_olMyGraphSeries.GetHeadPosition());

        while (pos && sTip.IsEmpty())
        {
            MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
            ASSERT_VALID(pSeries);

            int nGroup(0);
            do
            {
                nGroup = pSeries->HitTest(pt, nGroup);

                if (-1 != nGroup)
                {
                    if (!sTip.IsEmpty())
                    {
                        sTip += L", ";
                    }
                    sTip += m_saLegendLabels.GetAt(nGroup) + L": ";
                    sTip += pSeries->GetTipText(nGroup, m_sYAxisLabel);
                    nGroup++;
                }
            } while (-1 != nGroup);
        }
    }

    return sTip;
}

// Handle WM_PAINT.
void MyGraph::OnPaint()
{
    VALIDATE;

    CBufferDC dc(this);
    DrawGraph(dc);
}

// Handle WM_SIZE.
void MyGraph::OnSize(UINT nType, int cx, int cy)
{
    VALIDATE;

    CStatic::OnSize(nType, cx, cy);

    Invalidate();
}

// Change the type of the graph; the caller should call Invalidate() on this
// window to make the effect of this change visible.
void MyGraph::SetGraphType(GraphType e, bool bStackedGraph)
{
    VALIDATE;

    m_eGraphType    = e;
    m_bStackedGraph = bStackedGraph;
}

// Calculate the current max legend label length in pixels.
int MyGraph::GetMaxLegendLabelLength(CDC& dc) const
{
    VALIDATE;
    ASSERT_VALID(&dc);

    CString sMax;
    int     nMaxChars(-1);
    CSize   siz(-1, -1);

    // First get max number of characters.
    for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup)
    {
        int nLabelLength(m_saLegendLabels.GetAt(nGroup).GetLength());

        if (nMaxChars < nLabelLength)
        {
            nMaxChars = nLabelLength;
            sMax      = m_saLegendLabels.GetAt(nGroup);
        }
    }

    // Now calculate the pixels.
    siz = dc.GetTextExtent(sMax);

    _ASSERTE(-1 < siz.cx);

    return siz.cx;
}

// Returns the largest number of data points in any series.
int MyGraph::GetMaxSeriesSize() const
{
    VALIDATE;

    int      nMax(0);
    POSITION pos(m_olMyGraphSeries.GetHeadPosition());

    while (pos)
    {
        MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
        ASSERT_VALID(pSeries);

        nMax = max(nMax, static_cast<int>(pSeries->m_dwaValues.GetSize()));
    }

    return nMax;
}

// Returns the largest number of non-zero data points in any series.
int MyGraph::GetMaxNonZeroSeriesSize() const
{
    VALIDATE;

    int      nMax(0);
    POSITION pos(m_olMyGraphSeries.GetHeadPosition());

    while (pos)
    {
        MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
        ASSERT_VALID(pSeries);

        nMax = max(nMax, pSeries->GetNonZeroElementCount());
    }

    return nMax;
}

// Get the largest data value in all series.
int MyGraph::GetMaxDataValue() const
{
    VALIDATE;

    int      nMax(0);
    POSITION pos(m_olMyGraphSeries.GetHeadPosition());

    while (pos)
    {
        MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
        ASSERT_VALID(pSeries);

        nMax = max(nMax, pSeries->GetMaxDataValue(m_bStackedGraph));
    }

    return nMax;
}

// Get the average data value in all series.
int MyGraph::GetAverageDataValue() const
{
    VALIDATE;

    int      nTotal = 0, nCount = 0;
    POSITION pos(m_olMyGraphSeries.GetHeadPosition());

    while (pos)
    {
        MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
        ASSERT_VALID(pSeries);

        nTotal += pSeries->GetAverageDataValue();
        ++nCount;
    }

    if (nCount == 0)
        return 0;

    return nTotal / nCount;
}

// How many series are populated?
int MyGraph::GetNonZeroSeriesCount() const
{
    VALIDATE;

    int      nCount(0);
    POSITION pos(m_olMyGraphSeries.GetHeadPosition());

    while (pos)
    {
        MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
        ASSERT_VALID(pSeries);

        if (0 < pSeries->GetNonZeroElementCount())
        {
            ++nCount;
        }
    }

    return nCount ? nCount : 1;
}

// Returns the group number for the sent label; -1 if not found.
int MyGraph::LookupLabel(const CString& sLabel) const
{
    VALIDATE;
    _ASSERTE(!sLabel.IsEmpty());

    for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup)
    {
        if (0 == sLabel.CompareNoCase(m_saLegendLabels.GetAt(nGroup)))
        {
            return nGroup;
        }
    }

    return -1;
}

void MyGraph::Clear()
{
    m_dwaColors.RemoveAll();
    m_saLegendLabels.RemoveAll();
    m_olMyGraphSeries.RemoveAll();
}

//
void MyGraph::AddSeries(MyGraphSeries& rMyGraphSeries)
{
    VALIDATE;
    ASSERT_VALID(&rMyGraphSeries);
    _ASSERTE(m_saLegendLabels.GetSize() == rMyGraphSeries.m_dwaValues.GetSize());

    m_olMyGraphSeries.AddTail(&rMyGraphSeries);
}

//
void MyGraph::SetXAxisLabel(const CString& sLabel)
{
    VALIDATE;
    _ASSERTE(!sLabel.IsEmpty());

    m_sXAxisLabel = sLabel;
}

//
void MyGraph::SetYAxisLabel(const CString& sLabel)
{
    VALIDATE;
    _ASSERTE(!sLabel.IsEmpty());

    m_sYAxisLabel = sLabel;
}

// Returns the group number added.  Also, makes sure that all the series have
// this many elements.
int MyGraph::AppendGroup(const CString& sLabel)
{
    VALIDATE;

    // Add the group.
    int nGroup(static_cast<int>(m_saLegendLabels.GetSize()));
    SetLegend(nGroup, sLabel);

    // Make sure that all series have this element.
    POSITION pos(m_olMyGraphSeries.GetHeadPosition());

    while (pos)
    {
        MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
        ASSERT_VALID(pSeries);

        if (nGroup >= pSeries->m_dwaValues.GetSize())
        {
            pSeries->m_dwaValues.SetAtGrow(nGroup, 0);
        }
    }

    return nGroup;
}

// Set this value to the legend.
void MyGraph::SetLegend(int nGroup, const CString& sLabel)
{
    VALIDATE;
    _ASSERTE(0 <= nGroup);

    m_saLegendLabels.SetAtGrow(nGroup, sLabel);
}

//
void MyGraph::SetGraphTitle(const CString& sTitle)
{
    VALIDATE;
    _ASSERTE(!sTitle.IsEmpty());

    m_sTitle = sTitle;
}

//
void MyGraph::DrawGraph(CDC& dc)
{
    VALIDATE;
    ASSERT_VALID(&dc);

    if (GetMaxSeriesSize())
    {
        dc.SetBkMode(TRANSPARENT);

        dc.SetTextColor(CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor : GetSysColor(COLOR_WINDOWTEXT));
        auto themePen      = CreatePen(PS_SOLID, 1, CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor : GetSysColor(COLOR_WINDOWTEXT));
        auto themeBrush    = CreateSolidBrush(CTheme::Instance().IsDarkTheme() ? CTheme::darkBkColor : GetSysColor(COLOR_WINDOW));
        auto oldThemePen   = dc.SelectObject(themePen);
        auto oldThemeBrush = dc.SelectObject(themeBrush);

        // Populate the colors as a group of evenly spaced colors of maximum
        // saturation.
        int nColorsDelta(240 / GetMaxSeriesSize());

        int   baseColorL      = 120;
        int   diffColorL      = 60;
        DWORD backgroundColor = CTheme::Instance().IsDarkTheme() ? CTheme::darkBkColor : GetSysColor(COLOR_WINDOW);
        // If graph is a non-stacked line graph, use darker colors if system window color is light.
#if 0
        if (m_eGraphType == MyGraph::Line && !m_bStackedGraph) {
            int backgroundLuma = (GetRValue(backgroundColor) + GetGValue(backgroundColor) + GetBValue(backgroundColor)) / 3;
            if (backgroundLuma > 128) {
                baseColorL = 70;
                diffColorL = 50;
            }
        }
#endif
        for (WORD nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup)
        {
            WORD     colorH = static_cast<WORD>(nColorsDelta * nGroup);
            WORD     colorL = static_cast<WORD>(baseColorL + (diffColorL * (nGroup % 2)));
            WORD     colorS = static_cast<WORD>(180) + (30 * ((1 - nGroup % 2) * (nGroup % 3)));
            COLORREF cr(MyGraph::HLStoRGB(colorH, colorL, colorS)); // Populate colors cleverly
            m_dwaColors.SetAtGrow(nGroup, cr);
        }

        // Reduce the graphable area by the frame window and status bar.  We will
        // leave GAP_PIXELS pixels blank on all sides of the graph.  So top-left
        // side of graph is at GAP_PIXELS,GAP_PIXELS and the bottom-right side
        // of graph is at (m_rcGraph.Height() - GAP_PIXELS), (m_rcGraph.Width() -
        // GAP_PIXELS).  These settings are altered by axis labels and legends.
        CRect rcWnd;
        GetClientRect(&rcWnd);
        m_rcGraph.left   = GAP_PIXELS;
        m_rcGraph.top    = GAP_PIXELS;
        m_rcGraph.right  = rcWnd.Width() - GAP_PIXELS;
        m_rcGraph.bottom = rcWnd.Height() - GAP_PIXELS;

        CBrush br;
        VERIFY(br.CreateSolidBrush(backgroundColor));
        dc.FillRect(rcWnd, &br);
        br.DeleteObject();

        // Draw graph title.
        DrawTitle(dc);

        // Set the axes and origin values.
        SetupAxes(dc);

        // Draw legend if there is one and there's enough space.
        if (m_saLegendLabels.GetSize() && m_rcGraph.right - m_rcGraph.left > LEGEND_VISIBILITY_THRESHOLD)
        {
            DrawLegend(dc);
        }
        else
        {
            m_rcLegend.SetRectEmpty();
        }

        // Draw axes unless it's a pie.
        if (m_eGraphType != MyGraph::PieChart)
        {
            DrawAxes(dc);
        }

        // Draw series data and labels.
        switch (m_eGraphType)
        {
            case MyGraph::Bar:
                DrawSeriesBar(dc);
                break;
            case MyGraph::Line:
                if (m_bStackedGraph)
                    DrawSeriesLineStacked(dc);
                else
                    DrawSeriesLine(dc);
                break;
            case MyGraph::PieChart:
                DrawSeriesPie(dc);
                break;
            default:
                _ASSERTE(!"Bad default case");
                break;
        }
        dc.SelectObject(oldThemePen);
        dc.SelectObject(oldThemeBrush);
        DeleteObject(themeBrush);
        DeleteObject(themePen);
    }
}

// Draw graph title; size is proportionate to width.
void MyGraph::DrawTitle(CDC& dc)
{
    VALIDATE;
    ASSERT_VALID(&dc);

    // Create the title font.
    CFont fontTitle;
    VERIFY(fontTitle.CreatePointFont(max(m_rcGraph.Width() / TITLE_DIVISOR, MIN_FONT_SIZE),
                                     L"Arial", &dc));
    CFont* pFontOld = dc.SelectObject(&fontTitle);
    ASSERT_VALID(pFontOld);

    // Draw the title.
    m_rcTitle.SetRect(GAP_PIXELS, GAP_PIXELS, m_rcGraph.Width() + GAP_PIXELS,
                      m_rcGraph.Height() + GAP_PIXELS);

    dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE | DT_TOP | DT_CALCRECT);

    m_rcTitle.right = m_rcGraph.Width() + GAP_PIXELS;

    dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE | DT_TOP);

    VERIFY(dc.SelectObject(pFontOld));
    fontTitle.DeleteObject();
}

// Set the axes and origin values.
void MyGraph::SetupAxes(CDC& dc)
{
    VALIDATE;
    ASSERT_VALID(&dc);

    // Since pie has no axis lines, set to full size minus GAP_PIXELS on each
    // side.  These are needed for legend to plot itself.
    if (MyGraph::PieChart == m_eGraphType)
    {
        m_nXAxisWidth  = m_rcGraph.Width() - (GAP_PIXELS * 2);
        m_nYAxisHeight = m_rcGraph.Height() - m_rcTitle.bottom;
        m_ptOrigin.x   = GAP_PIXELS;
        m_ptOrigin.y   = m_rcGraph.Height() - GAP_PIXELS;
    }
    else
    {
        // Bar and Line graphs.

        // Need to find out how wide the biggest Y-axis tick label is

        // Get and store height of axis label font.
        m_nAxisLabelHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);
        // Get and store height of tick label font.
        m_nAxisTickLabelHeight = max(static_cast<int>(m_nAxisLabelHeight * 0.8), MIN_FONT_SIZE);

        CFont fontTickLabels;
        VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, L"Arial", &dc));
        // Select font and store the old.
        CFont* pFontOld = dc.SelectObject(&fontTickLabels);
        ASSERT_VALID(pFontOld);

        // Obtain tick label dimensions.
        CString sTickLabel;
        sTickLabel.Format(L"%d", GetMaxDataValue());
        CSize sizTickLabel(dc.GetTextExtent(sTickLabel));

        // Set old font object again and delete temporary font object.
        VERIFY(dc.SelectObject(pFontOld));
        fontTickLabels.DeleteObject();

        // Determine axis specifications.
        m_ptOrigin.x = m_rcGraph.left + m_nAxisLabelHeight / 10 + 2 * GAP_PIXELS + sizTickLabel.cx + GAP_PIXELS + TICK_PIXELS;
        m_ptOrigin.y = m_rcGraph.bottom - m_nAxisLabelHeight / 10 - 2 * GAP_PIXELS -
                       sizTickLabel.cy - GAP_PIXELS - TICK_PIXELS;
        m_nYAxisHeight = m_ptOrigin.y - m_rcTitle.bottom - (2 * GAP_PIXELS);
        m_nXAxisWidth  = (m_rcGraph.Width() - GAP_PIXELS) - m_ptOrigin.x;
    }
}

//
void MyGraph::DrawLegend(CDC& dc)
{
    VALIDATE;
    ASSERT_VALID(&dc);

    // Create the legend font.
    CFont fontLegend;
    int   pointFontHeight = max(m_rcGraph.Height() / LEGEND_DIVISOR, MIN_FONT_SIZE);
    VERIFY(fontLegend.CreatePointFont(pointFontHeight, L"Arial", &dc));

    // Get the height of each label.
    LOGFONT lf = {0};
    VERIFY(fontLegend.GetLogFont(&lf));
    // just in case the font height is invalid (zero), use min().
    int nLabelHeight = max(1l, abs(lf.lfHeight));

    // Get number of legend entries
    int nLegendEntries = max(1, GetMaxSeriesSize());

    // Calculate optimal label height = AvailableLegendHeight/AllAuthors
    // Use a buffer of (GAP_PIXELS / 2) on each side inside the legend, and in addition the same
    // gab above and below the legend frame, so in total 2*GAP_PIXELS
    double optimalLabelHeight = static_cast<double>(m_rcGraph.Height() - 2 * GAP_PIXELS) / nLegendEntries;

    // Now relate the LabelHeight to the PointFontHeight
    int optimalPointFontHeight = static_cast<int>(pointFontHeight * optimalLabelHeight / nLabelHeight);

    // Limit the optimal PointFontHeight to the available range
    optimalPointFontHeight = min(max(optimalPointFontHeight, MIN_FONT_SIZE), pointFontHeight);

    // If the optimalPointFontHeight is different from the initial one, create a new legend font
    if (optimalPointFontHeight != pointFontHeight)
    {
        fontLegend.DeleteObject();
        VERIFY(fontLegend.CreatePointFont(optimalPointFontHeight, L"Arial", &dc));
        VERIFY(fontLegend.GetLogFont(&lf));
        nLabelHeight = abs(lf.lfHeight);
    }

    // Calculate maximum number of authors that can be shown with the current label height
    int nShownAuthors = (m_rcGraph.Height() - 2 * GAP_PIXELS) / nLabelHeight - 1;
    // Fix rounding errors.
    if (nShownAuthors + 1 == GetMaxSeriesSize())
        ++nShownAuthors;

    // Get number of authors to be shown.
    nShownAuthors = min(nShownAuthors, GetMaxSeriesSize());
    // nShownAuthors contains now the number of authors

    CFont* pFontOld = dc.SelectObject(&fontLegend);
    ASSERT_VALID(pFontOld);

    // Determine actual size of legend.  A buffer of (GAP_PIXELS / 2) on each side,
    // plus the height of each label based on the pint size of the font.
    int nLegendHeight = (GAP_PIXELS / 2) + (nShownAuthors * nLabelHeight) + (GAP_PIXELS / 2);
    // Draw the legend border.  Allow LEGEND_COLOR_BAR_PIXELS pixels for
    // display of label bars.
    m_rcLegend.top    = (m_rcGraph.Height() - nLegendHeight) / 2;
    m_rcLegend.bottom = m_rcLegend.top + nLegendHeight;
    m_rcLegend.right  = m_rcGraph.Width() - GAP_PIXELS;
    m_rcLegend.left   = m_rcLegend.right - GetMaxLegendLabelLength(dc) -
                      LEGEND_COLOR_BAR_WIDTH_PIXELS;
    VERIFY(dc.Rectangle(m_rcLegend));

    int skippedRow = -1; // if != -1, this is the row that we show the ... in
    if (nShownAuthors < GetMaxSeriesSize())
        skippedRow = nShownAuthors - 2;

    dc.SetTextColor(CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor : GetSysColor(COLOR_WINDOWTEXT));
    // Draw each group's label and bar.
    for (int nGroup = 0; nGroup < nShownAuthors; ++nGroup)
    {
        int nLabelTop(m_rcLegend.top + (nGroup * nLabelHeight) +
                      (GAP_PIXELS / 2));

        int nShownGroup = nGroup; // introduce helper variable to avoid code duplication

        // Do we have a skipped row?
        if (skippedRow != -1)
        {
            if (nGroup == skippedRow)
            {
                // draw the dots
                VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop, L"..."));
                continue;
            }
            if (nGroup == nShownAuthors - 1)
            {
                // we show the last group instead of the scheduled group
                nShownGroup = GetMaxSeriesSize() - 1;
            }
        }
        // Draw the label.
        VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop,
                          m_saLegendLabels.GetAt(nShownGroup)));

        // Determine the bar.
        CRect rcBar;
        rcBar.left   = m_rcLegend.left + GAP_PIXELS + GetMaxLegendLabelLength(dc) + GAP_PIXELS;
        rcBar.top    = nLabelTop + LEGEND_COLOR_BAR_GAP_PIXELS;
        rcBar.right  = m_rcLegend.right - GAP_PIXELS;
        rcBar.bottom = rcBar.top + nLabelHeight - LEGEND_COLOR_BAR_GAP_PIXELS;
        VERIFY(dc.Rectangle(rcBar));

        // Draw bar for group.
        COLORREF crBar(m_dwaColors.GetAt(nShownGroup));
        CBrush   br(crBar);

        CBrush* pBrushOld = dc.SelectObject(&br);
        ASSERT_VALID(pBrushOld);

        rcBar.DeflateRect(LEGEND_COLOR_BAR_GAP_PIXELS, LEGEND_COLOR_BAR_GAP_PIXELS);
        dc.FillRect(rcBar, &br);

        dc.SelectObject(pBrushOld);
        br.DeleteObject();
    }

    VERIFY(dc.SelectObject(pFontOld));
    fontLegend.DeleteObject();
}

//
void MyGraph::DrawAxes(CDC& dc) const
{
    VALIDATE;
    ASSERT_VALID(&dc);
    _ASSERTE(MyGraph::PieChart != m_eGraphType);

    dc.SetTextColor(CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor : GetSysColor(COLOR_WINDOWTEXT));

    // Draw y axis.
    dc.MoveTo(m_ptOrigin);
    VERIFY(dc.LineTo(m_ptOrigin.x, m_ptOrigin.y - m_nYAxisHeight));

    // Draw x axis.
    dc.MoveTo(m_ptOrigin);

    if (m_saLegendLabels.GetSize())
    {
        VERIFY(dc.LineTo(m_ptOrigin.x +
                             (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)),
                         m_ptOrigin.y));
    }
    else
    {
        VERIFY(dc.LineTo(m_ptOrigin.x + m_nXAxisWidth, m_ptOrigin.y));
    }

    // Note: m_nAxisLabelHeight and m_nAxisTickLabelHeight have been calculated in SetupAxis()

    // Create the x-axis label font.
    CFont fontXAxis;
    VERIFY(fontXAxis.CreatePointFont(m_nAxisLabelHeight, L"Arial", &dc));

    // Obtain the height of the font in device coordinates.
    LOGFONT pLF;
    VERIFY(fontXAxis.GetLogFont(&pLF));
    int fontHeightDC = pLF.lfHeight;

    // Create the y-axis label font.
    CFont fontYAxis;
    VERIFY(fontYAxis.CreateFont(
        /* nHeight */ fontHeightDC,
        /* nWidth */ 0,
        /* nEscapement */ 90 * 10,
        /* nOrientation */ 0,
        /* nWeight */ FW_DONTCARE,
        /* bItalic */ false,
        /* bUnderline */ false,
        /* cStrikeOut */ 0,
        ANSI_CHARSET,
        OUT_DEFAULT_PRECIS,
        CLIP_DEFAULT_PRECIS,
        PROOF_QUALITY,
        VARIABLE_PITCH | FF_DONTCARE,
        L"Arial"));

    // Set the y-axis label font and draw the label.
    CFont* pFontOld = dc.SelectObject(&fontYAxis);
    ASSERT_VALID(pFontOld);
    CSize sizYLabel(dc.GetTextExtent(m_sYAxisLabel));
    VERIFY(dc.TextOut(GAP_PIXELS, (m_rcGraph.Height() + sizYLabel.cx) / 2,
                      m_sYAxisLabel));

    // Set the x-axis label font and draw the label.
    VERIFY(dc.SelectObject(&fontXAxis));
    CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));
    VERIFY(dc.TextOut(m_ptOrigin.x + (m_nXAxisWidth - sizXLabel.cx) / 2,
                      m_rcGraph.bottom - GAP_PIXELS - sizXLabel.cy, m_sXAxisLabel));

    // chose suitable tick step (1, 2, 5, 10, 20, 50, etc.)
    int nMaxDataValue(GetMaxDataValue());
    nMaxDataValue = max(nMaxDataValue, 1);
    int nTickStep = 1;
    while (10 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
        nTickStep *= 10;

    if (5 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
        nTickStep *= 5;
    if (2 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
        nTickStep *= 2;

    // We hardwire TITLE_DIVISOR y-axis ticks here for simplicity.
    int    nTickCount(nMaxDataValue / nTickStep);
    double tickSpace = static_cast<double>(m_nYAxisHeight) * nTickStep / static_cast<double>(nMaxDataValue);

    // create tick label font and set it in the device context
    CFont fontTickLabels;
    VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, L"Arial", &dc));
    VERIFY(dc.SelectObject(&fontTickLabels));

    for (int nTick = 0; nTick < nTickCount; ++nTick)
    {
        int nTickYLocation = static_cast<int>(m_ptOrigin.y - tickSpace * (nTick + 1LL) + 0.5);
        dc.MoveTo(m_ptOrigin.x - TICK_PIXELS, nTickYLocation);
        VERIFY(dc.LineTo(m_ptOrigin.x + TICK_PIXELS, nTickYLocation));

        // Draw tick label.
        CString sTickLabel;
        sTickLabel.Format(L"%d", nTickStep * (nTick + 1));
        CSize sizTickLabel(dc.GetTextExtent(sTickLabel));

        VERIFY(dc.TextOut(m_ptOrigin.x - GAP_PIXELS - sizTickLabel.cx - TICK_PIXELS,
                          nTickYLocation - sizTickLabel.cy / 2, sTickLabel));
    }

    // Draw X axis tick marks.
    POSITION pos(m_olMyGraphSeries.GetHeadPosition());
    int      nSeries(0);

    while (pos)
    {
        MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
        ASSERT_VALID(pSeries);

        // Ignore unpopulated series if bar chart.
        if (m_eGraphType != MyGraph::Bar ||
            0 < pSeries->GetNonZeroElementCount())
        {
            // Get the spacing of the series.
            int nSeriesSpace(0);

            if (m_saLegendLabels.GetSize())
            {
                nSeriesSpace =
                    (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
                    (m_eGraphType == MyGraph::Bar ? GetNonZeroSeriesCount() : static_cast<int>(m_olMyGraphSeries.GetCount()));
            }
            else
            {
                nSeriesSpace = m_nXAxisWidth / (m_eGraphType == MyGraph::Bar ? GetNonZeroSeriesCount() : static_cast<int>(m_olMyGraphSeries.GetCount()));
            }

            int nTickXLocation(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) -
                               (nSeriesSpace / 2));

            dc.MoveTo(nTickXLocation, m_ptOrigin.y - TICK_PIXELS);
            VERIFY(dc.LineTo(nTickXLocation, m_ptOrigin.y + TICK_PIXELS));

            // Draw x-axis tick label.
            CString sTickLabel(pSeries->GetLabel());
            CSize   sizTickLabel(dc.GetTextExtent(sTickLabel));

            VERIFY(dc.TextOut(nTickXLocation - (sizTickLabel.cx / 2),
                              m_ptOrigin.y + TICK_PIXELS + GAP_PIXELS, sTickLabel));

            ++nSeries;
        }
    }

    VERIFY(dc.SelectObject(pFontOld));
    fontXAxis.DeleteObject();
    fontYAxis.DeleteObject();
    fontTickLabels.DeleteObject();
}

//
void MyGraph::DrawSeriesBar(CDC& dc) const
{
    VALIDATE;
    ASSERT_VALID(&dc);

    // How much space does each series get (includes inter series space)?
    // We ignore series whose members are all zero.
    double availableSpace = m_saLegendLabels.GetSize()
                                ? m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)
                                : m_nXAxisWidth;

    double seriesSpace = availableSpace / static_cast<double>(GetNonZeroSeriesCount());

    // Determine width of bars.  Data points with a value of zero are assumed
    // to be empty.  This is a bad assumption.
    double barWidth(0.0);

    // This is the width of the largest series (no inter series space).
    double maxSeriesPlotSize(0.0);

    if (!m_bStackedGraph)
    {
        int seriesSize = GetMaxNonZeroSeriesSize();
        barWidth       = seriesSize ? seriesSpace / seriesSize : 0;
        if (1 < GetNonZeroSeriesCount())
        {
            barWidth *= INTERSERIES_PERCENT_USED;
        }
        maxSeriesPlotSize = GetMaxNonZeroSeriesSize() * barWidth;
    }
    else
    {
        barWidth          = seriesSpace * INTERSERIES_PERCENT_USED;
        maxSeriesPlotSize = barWidth;
    }

    // Iterate the series.
    POSITION pos(m_olMyGraphSeries.GetHeadPosition());
    int      nSeries(0);

    while (pos)
    {
        MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
        ASSERT_VALID(pSeries);

        // Ignore unpopulated series.
        if (0 < pSeries->GetNonZeroElementCount())
        {
            // Draw each bar; empty bars are not drawn.
            double runningLeft(m_ptOrigin.x + (nSeries + 1LL) * seriesSpace -
                               maxSeriesPlotSize);

            double stackAccumulator(0.0);

            for (int nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup)
            {
                if (pSeries->GetData(nGroup))
                {
                    int nMaxDataValue(GetMaxDataValue());
                    nMaxDataValue = max(nMaxDataValue, 1);
                    double barTop = m_ptOrigin.y - static_cast<double>(m_nYAxisHeight) * pSeries->GetData(nGroup) / static_cast<double>(nMaxDataValue) - stackAccumulator;

                    CRect rcBar;
                    rcBar.left = static_cast<int>(runningLeft);
                    rcBar.top  = static_cast<int>(barTop);
                    // Make adjacent bar borders overlap, so there's only one pixel border line between them.
                    rcBar.right  = static_cast<int>(runningLeft + barWidth) + 1;
                    rcBar.bottom = static_cast<int>(static_cast<double>(m_ptOrigin.y) - stackAccumulator) + 1;

                    if (m_bStackedGraph)
                    {
                        stackAccumulator = static_cast<double>(m_ptOrigin.y) - barTop;
                    }

                    pSeries->SetTipRegion(nGroup, rcBar);

                    COLORREF crBar(m_dwaColors.GetAt(nGroup));
                    CBrush   br(crBar);
                    CBrush*  pBrushOld = dc.SelectObject(&br);
                    ASSERT_VALID(pBrushOld);

                    VERIFY(dc.Rectangle(rcBar));
                    dc.SelectObject(pBrushOld);
                    br.DeleteObject();

                    if (!m_bStackedGraph)
                    {
                        runningLeft += barWidth;
                    }
                }
            }

            ++nSeries;
        }
    }
    if (!m_bStackedGraph)
    {
        int    nMaxDataValue = max(GetMaxDataValue(), 1);
        double barTop        = m_ptOrigin.y - static_cast<double>(m_nYAxisHeight) *
                                           (GetAverageDataValue() / static_cast<double>(nMaxDataValue));
        dc.MoveTo(m_ptOrigin.x, static_cast<int>(barTop));
        VERIFY(dc.LineTo(m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)), static_cast<int>(barTop)));
    }
}

//
void MyGraph::DrawSeriesLine(CDC& dc) const
{
    VALIDATE;
    ASSERT_VALID(&dc);
    _ASSERTE(!m_bStackedGraph);

    // Iterate the groups.
    CPoint ptLastLoc(0, 0);
    int    dataLastLoc(0);

    for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++)
    {
        // How much space does each series get (includes inter series space)?
        int nSeriesSpace(0);

        if (m_saLegendLabels.GetSize())
        {
            nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
                           static_cast<int>(m_olMyGraphSeries.GetCount());
        }
        else
        {
            nSeriesSpace = m_nXAxisWidth / static_cast<int>(m_olMyGraphSeries.GetCount());
        }

        // Determine width of bars.
        int nMaxSeriesSize(GetMaxSeriesSize());
        nMaxSeriesSize = max(nMaxSeriesSize, 1);
        int nBarWidth(nSeriesSpace / nMaxSeriesSize);

        if (1 < m_olMyGraphSeries.GetCount())
        {
            nBarWidth = static_cast<int>(static_cast<double>(nBarWidth) * INTERSERIES_PERCENT_USED);
        }

        // This is the width of the largest series (no inter series space).
        //int nMaxSeriesPlotSize(GetMaxSeriesSize() * nBarWidth);

        // Iterate the series.
        POSITION pos(m_olMyGraphSeries.GetHeadPosition());

        // Build objects.
        COLORREF crLine(m_dwaColors.GetAt(nGroup));
        CBrush   br(crLine);
        CBrush*  pBrushOld = dc.SelectObject(&br);
        ASSERT_VALID(pBrushOld);
        CPen  penLine(PS_SOLID, 1, crLine);
        CPen* pPenOld = dc.SelectObject(&penLine);
        ASSERT_VALID(pPenOld);

        for (int nSeries = 0; nSeries < m_olMyGraphSeries.GetCount(); ++nSeries)
        {
            MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
            ASSERT_VALID(pSeries);

            // Get x and y location of center of ellipse.
            CPoint ptLoc(0, 0);

            ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
                                      (nSeriesSpace / 2));

            int nMaxDataValue(GetMaxDataValue());
            nMaxDataValue = max(nMaxDataValue, 1);
            double dLineHeight(pSeries->GetData(nGroup) * m_nYAxisHeight /
                               static_cast<double>(nMaxDataValue));

            ptLoc.y = static_cast<int>(static_cast<double>(m_ptOrigin.y) - dLineHeight);

            // Draw line back to last data member.
            if (nSeries > 0 && (pSeries->GetData(nGroup) != 0 || dataLastLoc != 0))
            {
                dc.MoveTo(ptLastLoc.x, ptLastLoc.y - 1);
                VERIFY(dc.LineTo(ptLoc.x - 1, ptLoc.y - 1));
            }

            // Now draw ellipse.
            CRect rcEllipse(ptLoc.x - 3, ptLoc.y - 3, ptLoc.x + 3, ptLoc.y + 3);
            if (pSeries->GetData(nGroup) != 0)
            {
                VERIFY(dc.Ellipse(rcEllipse));
            }
            if (m_olMyGraphSeries.GetCount() < 40)
            {
                pSeries->SetTipRegion(nGroup, rcEllipse);
            }

            // Save last pt and data
            ptLastLoc   = ptLoc;
            dataLastLoc = pSeries->GetData(nGroup);
        }
        VERIFY(dc.SelectObject(pPenOld));
        penLine.DeleteObject();
        VERIFY(dc.SelectObject(pBrushOld));
        br.DeleteObject();
    }

    int    nMaxDataValue = max(GetMaxDataValue(), 1);
    double barTop        = m_ptOrigin.y - static_cast<double>(m_nYAxisHeight) *
                                       (GetAverageDataValue() / static_cast<double>(nMaxDataValue));
    dc.MoveTo(m_ptOrigin.x, static_cast<int>(barTop));
    VERIFY(dc.LineTo(m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)), static_cast<int>(barTop)));
}

//
void MyGraph::DrawSeriesLineStacked(CDC& dc) const
{
    VALIDATE;
    ASSERT_VALID(&dc);
    _ASSERTE(m_bStackedGraph);

    int nSeriesCount = static_cast<int>(m_olMyGraphSeries.GetCount());

    CArray<int> stackAccumulator;
    stackAccumulator.SetSize(nSeriesCount);

    CArray<CPoint> polygon;
    // Special case: if we only have single series, make polygon
    // a bar instead of one pixel line.
    polygon.SetSize(nSeriesCount == 1 ? 4 : nSeriesCount * 2);

    // How much space does each series get?
    int nSeriesSpace(0);
    if (m_saLegendLabels.GetSize())
    {
        nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
                       nSeriesCount;
    }
    else
    {
        nSeriesSpace = m_nXAxisWidth / nSeriesCount;
    }

    int nMaxDataValue(GetMaxDataValue());
    nMaxDataValue    = max(nMaxDataValue, 1);
    double dYScaling = static_cast<double>(m_nYAxisHeight) / nMaxDataValue;

    // Iterate the groups.
    for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++)
    {
        // Build objects.
        COLORREF crGroup(m_dwaColors.GetAt(nGroup));
        CBrush   br(crGroup);
        CBrush*  pBrushOld = dc.SelectObject(&br);
        ASSERT_VALID(pBrushOld);
        // For polygon outline, use average of this and previous color, and darken it.
        COLORREF crPrevGroup(nGroup > 0 ? m_dwaColors.GetAt(nGroup - 1LL) : crGroup);
        COLORREF crOutline = RGB(
            (GetRValue(crGroup) + GetRValue(crPrevGroup)) / 3,
            (GetGValue(crGroup) + GetGValue(crPrevGroup)) / 3,
            (GetBValue(crGroup) + GetBValue(crPrevGroup)) / 3);
        CPen  penLine(PS_SOLID, 1, crOutline);
        CPen* pPenOld = dc.SelectObject(&penLine);
        ASSERT_VALID(pPenOld);

        // Construct bottom part of polygon from current stack accumulator
        for (int nPolyBottom = 0; nPolyBottom < nSeriesCount; ++nPolyBottom)
        {
            CPoint ptLoc;
            ptLoc.x = m_ptOrigin.x + (((nPolyBottom + 1) * nSeriesSpace) - (nSeriesSpace / 2));
            double dLineHeight((stackAccumulator[nPolyBottom]) * dYScaling);
            ptLoc.y = static_cast<int>(static_cast<double>(m_ptOrigin.y) - dLineHeight);

            if (nSeriesCount > 1)
            {
                polygon[nSeriesCount - nPolyBottom - 1LL] = ptLoc;
            }
            else
            {
                // special case: when there's one series, make polygon a bar
                polygon[0] = CPoint(ptLoc.x - GAP_PIXELS / 2, ptLoc.y);
                polygon[1] = CPoint(ptLoc.x + GAP_PIXELS / 2, ptLoc.y);
            }
        }

        // Iterate the series, construct upper part of polygon and upadte stack accumulator
        POSITION pos(m_olMyGraphSeries.GetHeadPosition());
        for (int nSeries = 0; nSeries < nSeriesCount; ++nSeries)
        {
            MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
            ASSERT_VALID(pSeries);

            CPoint ptLoc;
            ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
                                      (nSeriesSpace / 2));
            double dLineHeight((pSeries->GetData(nGroup) + stackAccumulator[nSeries]) * dYScaling);
            ptLoc.y = static_cast<int>(static_cast<double>(m_ptOrigin.y) - dLineHeight);
            if (nSeriesCount > 1)
            {
                polygon[nSeriesCount + nSeries] = ptLoc;
            }
            else
            {
                // special case: when there's one series, make polygon a bar
                polygon[2] = CPoint(ptLoc.x + GAP_PIXELS / 2, ptLoc.y);
                polygon[3] = CPoint(ptLoc.x - GAP_PIXELS / 2, ptLoc.y);
            }

            stackAccumulator[nSeries] += pSeries->GetData(nGroup);
        }

        // Draw polygon
        VERIFY(dc.Polygon(polygon.GetData(), static_cast<int>(polygon.GetSize())));

        VERIFY(dc.SelectObject(pPenOld));
        penLine.DeleteObject();
        VERIFY(dc.SelectObject(pBrushOld));
        br.DeleteObject();
    }
}

//
void MyGraph::DrawSeriesPie(CDC& dc) const
{
    VALIDATE;
    ASSERT_VALID(&dc);

    // Determine width of pie display area (pie and space).
    int nSeriesSpace(0);

    int seriesCount = GetNonZeroSeriesCount();
    int horizontalSpace(0);

    if (m_saLegendLabels.GetSize())
    {
        // With legend box.

        horizontalSpace = m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2);
        int nPieAndSpaceWidth(horizontalSpace / (seriesCount ? seriesCount : 1));

        // Height is limiting factor.
        if (nPieAndSpaceWidth > m_nYAxisHeight - (GAP_PIXELS * 2))
        {
            nSeriesSpace = (m_nYAxisHeight - (GAP_PIXELS * 2));
        }
        else
        {
            // Width is limiting factor.
            nSeriesSpace = nPieAndSpaceWidth;
        }
    }
    else
    {
        // No legend box.

        horizontalSpace = m_nXAxisWidth;

        // Height is limiting factor.
        if (m_nXAxisWidth > m_nYAxisHeight * (seriesCount ? seriesCount : 1))
        {
            nSeriesSpace = m_nYAxisHeight;
        }
        else
        {
            // Width is limiting factor.
            nSeriesSpace = m_nXAxisWidth / (seriesCount ? seriesCount : 1);
        }
    }

    // Make pies be centered horizontally
    int xOrigin = m_ptOrigin.x + GAP_PIXELS + (horizontalSpace - nSeriesSpace * seriesCount) / 2;

    // Create font for labels.
    CFont fontLabels;
    int   pointFontHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);
    VERIFY(fontLabels.CreatePointFont(pointFontHeight, L"Arial", &dc));
    CFont* pFontOld = dc.SelectObject(&fontLabels);
    ASSERT_VALID(pFontOld);

    // Draw each pie.
    int      nPie(0);
    int      nRadius(static_cast<int>(nSeriesSpace * INTERSERIES_PERCENT_USED / 2.0));
    POSITION pos(m_olMyGraphSeries.GetHeadPosition());

    while (pos)
    {
        MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
        ASSERT_VALID(pSeries);

        // Don't leave a space for empty pies.
        if (0 < pSeries->GetNonZeroElementCount())
        {
            // Locate this pie.
            CPoint ptCenter;
            ptCenter.x = xOrigin + (nSeriesSpace * nPie) + nSeriesSpace / 2;
            ptCenter.y = m_ptOrigin.y - m_nYAxisHeight / 2;

            CRect rcPie;
            rcPie.left   = ptCenter.x - nRadius;
            rcPie.right  = ptCenter.x + nRadius;
            rcPie.top    = ptCenter.y - nRadius;
            rcPie.bottom = ptCenter.y + nRadius;

            // Draw series label.
            CSize sizPieLabel(dc.GetTextExtent(pSeries->GetLabel()));

            VERIFY(dc.TextOut(ptCenter.x - (sizPieLabel.cx / 2),
                              ptCenter.y + nRadius + GAP_PIXELS, pSeries->GetLabel()));

            // How much do the wedges total to?
            double dPieTotal(pSeries->GetDataTotal());

            // Draw each wedge in this pie.
            CPoint ptStart(rcPie.left, ptCenter.y);
            double dRunningWedgeTotal(0.0);

            for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup)
            {
                // Ignore empty wedges.
                if (0 < pSeries->GetData(nGroup))
                {
                    // Get the degrees of this wedge.
                    dRunningWedgeTotal += pSeries->GetData(nGroup);
                    double dPercent(dRunningWedgeTotal * 100.0 / dPieTotal);
                    double degrees(360.0 * dPercent / 100.0);

                    // Find the location of the wedge's endpoint.
                    CPoint ptEnd(WedgeEndFromDegrees(degrees, ptCenter, nRadius));

                    // Special case: a wedge that takes up the whole pie would
                    // otherwise be confused with an empty wedge.
                    bool drawEmptyWedges = false;
                    if (1 == pSeries->GetNonZeroElementCount())
                    {
                        _ASSERTE(360 == static_cast<int>(degrees) && ptStart == ptEnd && "This is the problem we're correcting");
                        --ptEnd.y;
                        drawEmptyWedges = true;
                    }

                    // If the wedge is zero size or very narrow, don't paint it.
                    // If pie is small, and wedge data is small, we might get a wedges
                    // where center and both endpoints lie on the same coordinate,
                    // and endpoints differ only in one pixel. GDI draws such pie as whole pie,
                    // so we just skip them instead.
                    int distance = abs(ptStart.x - ptEnd.x) + abs(ptStart.y - ptEnd.y);
                    if (drawEmptyWedges || distance > 1)
                    {
                        // Draw wedge.
                        COLORREF crWedge(m_dwaColors.GetAt(nGroup));
                        CBrush   br(crWedge);
                        CBrush*  pBrushOld = dc.SelectObject(&br);
                        ASSERT_VALID(pBrushOld);
                        VERIFY(dc.Pie(rcPie, ptStart, ptEnd));

                        // Create a region from the path we create.
                        VERIFY(dc.BeginPath());
                        VERIFY(dc.Pie(rcPie, ptStart, ptEnd));
                        VERIFY(dc.EndPath());
                        auto prgnWedge = std::make_unique<CRgn>();
                        VERIFY(prgnWedge->CreateFromPath(&dc));
                        pSeries->SetTipRegion(nGroup, prgnWedge.release());

                        // Cleanup.
                        dc.SelectObject(pBrushOld);
                        br.DeleteObject();
                        ptStart = ptEnd;
                    }
                }
            }

            ++nPie;
        }
    }

    // Draw X axis title
    CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));
    VERIFY(dc.TextOut(xOrigin + (nSeriesSpace * nPie - sizXLabel.cx) / 2,
                      m_ptOrigin.y - m_nYAxisHeight / 2 + nRadius + GAP_PIXELS * 2 + sizXLabel.cy, m_sXAxisLabel));

    VERIFY(dc.SelectObject(pFontOld));
    fontLabels.DeleteObject();
}

// Convert degrees to x and y coords.
CPoint MyGraph::WedgeEndFromDegrees(double degrees, const CPoint& ptCenter,
                                    double radius) const
{
    VALIDATE;

    CPoint pt;

    double radians = degrees / 360.0 * M_PI * 2.0;

    pt.x = static_cast<int>(radius * cos(radians));
    pt.x = ptCenter.x - pt.x;

    pt.y = static_cast<int>(radius * sin(radians));
    pt.y = ptCenter.y + pt.y;

    return pt;
}

// Spin The Message Loop: C++ version.  See "Advanced Windows Programming",
// M. Heller, p. 153, and the MS TechNet CD, PSS ID Number: Q99999.
/* static */ UINT MyGraph::SpinTheMessageLoop(bool bNoDrawing /* = false */,
                                              bool bOnlyDrawing /* = false */,
                                              UINT uiMsgAllowed /* = WM_NULL */)
{
    MSG msg = {nullptr};
    while (::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
    {
        // Do painting only.
        if (bOnlyDrawing && WM_PAINT == msg.message)
        {
            ::TranslateMessage(&msg);
            ::DispatchMessage(&msg);

            // Update user interface.
            AfxGetApp()->OnIdle(0);
        }
        // Do everything *but* painting.
        else if (bNoDrawing && WM_PAINT == msg.message)
        {
            break;
        }
        // Special handling for this message.
        else if (WM_QUIT == msg.message)
        {
            ::PostQuitMessage(static_cast<int>(msg.wParam));
            break;
        }
        // Allow one message (like WM_LBUTTONDOWN).
        else if (uiMsgAllowed == msg.message && !AfxGetApp()->PreTranslateMessage(&msg))
        {
            ::TranslateMessage(&msg);
            ::DispatchMessage(&msg);
            break;
        }
        // This is the general case.
        else if (!bOnlyDrawing && !AfxGetApp()->PreTranslateMessage(&msg))
        {
            ::TranslateMessage(&msg);
            ::DispatchMessage(&msg);

            // Update user interface, then free temporary objects.
            AfxGetApp()->OnIdle(0);
            AfxGetApp()->OnIdle(1);
        }
    }

    return msg.message;
}

/////////////////////////////////////////////////////////////////////////////
// Conversion routines: RGB to HLS (Red-Green-Blue to Hue-Luminosity-Saturation).
// See Microsoft KnowledgeBase article Q29240.

#define HLSMAX 240 // H,L, and S vary over 0-HLSMAX
#define RGBMAX 255 // R,G, and B vary over 0-RGBMAX \
                   // HLSMAX BEST IF DIVISIBLE BY 6 \
                   // RGBMAX, HLSMAX must each fit in a byte (255).

#define UNDEFINED (HLSMAX * 2 / 3) // Hue is undefined if Saturation is 0  \
                                   // (grey-scale).  This value determines \
                                   // where the Hue scrollbar is initially \
                                   // set for achromatic colors.

// Convert HLS to RGB.
/* static */ COLORREF MyGraph::HLStoRGB(WORD wH, WORD wL, WORD wS)
{
    _ASSERTE(0 <= wH && 240 >= wH && "Illegal hue value");
    _ASSERTE(0 <= wL && 240 >= wL && "Illegal lum value");
    _ASSERTE(0 <= wS && 240 >= wS && "Illegal sat value");

    WORD wR(0);
    WORD wG(0);
    WORD wB(0);

    // Achromatic case.
    if (0 == wS)
    {
        wR = wG = wB = (wL * RGBMAX) / HLSMAX;

        if (UNDEFINED != wH)
        {
            _ASSERTE(!"ERROR");
        }
    }
    else
    {
        // Chromatic case.
        WORD magic1(0);
        WORD magic2(0);

        // Set up magic numbers.
        if (wL <= HLSMAX / 2)
        {
            magic2 = (wL * (HLSMAX + wS) + (HLSMAX / 2)) / HLSMAX;
        }
        else
        {
            magic2 = wL + wS - ((wL * wS) + (HLSMAX / 2)) / HLSMAX;
        }

        magic1 = 2 * wL - magic2;

        // Get RGB, change units from HLSMAX to RGBMAX.
        wR = (HueToRGB(magic1, magic2, wH + (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
        wG = (HueToRGB(magic1, magic2, wH) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
        wB = (HueToRGB(magic1, magic2, wH - (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
    }

    return RGB(wR, wG, wB);
}

// Utility routine for HLStoRGB.
/* static */ WORD MyGraph::HueToRGB(WORD w1, WORD w2, WORD wH)
{
    // Range check: note values passed add/subtract thirds of range.
    if (wH > HLSMAX)
    {
        wH -= HLSMAX;
    }

    // Return r, g, or b value from this tridrant.
    if (wH < HLSMAX / 6)
    {
        return w1 + (((w2 - w1) * wH + (HLSMAX / 12)) / (HLSMAX / 6));
    }

    if (wH < HLSMAX / 2)
    {
        return w2;
    }

    if (wH < (HLSMAX * 2) / 3)
    {
        return w1 + (((w2 - w1) * (((HLSMAX * 2) / 3) - wH) + (HLSMAX / 12)) / (HLSMAX / 6));
    }
    else
    {
        return w1;
    }
}
