/////////////////////////////////////////////////////////////////////////////
// Name:        oscopectrl.cpp
// Purpose:     wxOScopeData and OScopeCtrl
// Author:      Francesco Montorsi
// Created:     2006/11/14
// RCS-ID:      $Id: oscopectrl.cpp,v 1.3 2007/01/14 15:19:22 frm Exp $
// Copyright:   (c) 2006 Francesco Montorsi
//              (inspired from the aMule's OScopeCtrl which is copyrighted
//               by the aMule Team - http://www.amule.org)
// Licence:     wxWidgets licence
/////////////////////////////////////////////////////////////////////////////


// For compilers that support precompilation, includes "wx.h".
#include "wx/wxprec.h"

#ifdef __BORLANDC__
#pragma hdrstop
#endif

#ifndef WX_PRECOMP
    #include "wx/wx.h"
#endif

#include <wx/defs.h>
#include <wx/dcmemory.h>
#include "wx/oscopectrl.h"

#include <wx/arrimpl.cpp> // this is a magic incantation which must be done!
WX_DEFINE_OBJARRAY(wxOScopeDataArray);
WX_DEFINE_OBJARRAY(wxArrayFloat);


// ----------------------------------------------------------------------------
// globals
// ----------------------------------------------------------------------------

// a constant used in all generic calculations of borders among
// labels, plot rect, window client size, etc
#define wxOSC_BORDER                5

// define to 1 to enable some raw clip code
#define wxOSC_DO_RAW_CLIP           0


// ----------------------------------------------------------------------------
// wxOScopeData
// ----------------------------------------------------------------------------

void wxOScopeData::SetBestRange(float lowerOffset, float upperOffset)
{
    if (m_data.GetCount() == 0)
    {
        m_fLowerLimit = lowerOffset;
        m_fUpperLimit = upperOffset;
        return;
    }

    m_fLowerLimit = m_data[0];
    m_fUpperLimit = m_data[0];
    for (size_t i=1; i<m_data.GetCount(); i++)
    {
        m_fUpperLimit = wxMax(m_fUpperLimit, m_data[i]);
        m_fLowerLimit = wxMin(m_fLowerLimit, m_data[i]);
    }

    m_fLowerLimit += lowerOffset;
    m_fUpperLimit += upperOffset;

    wxASSERT(m_fLowerLimit < m_fUpperLimit);
}

void wxOScopeData::AddSampleAndUpdateRange(float f, float lowerOffset, float upperOffset)
{
    // AddSample() will update m_fMax and m_fMin
    AddSample(f);

    m_fLowerLimit = m_fMin + lowerOffset;
    m_fUpperLimit = m_fMax + upperOffset;

    wxASSERT(m_fLowerLimit < m_fUpperLimit);
}



// ----------------------------------------------------------------------------
// wxOScopeCtrl
// ----------------------------------------------------------------------------

BEGIN_EVENT_TABLE(wxOScopeCtrl, wxControl)
    EVT_PAINT(wxOScopeCtrl::OnPaint)
    EVT_SIZE(wxOScopeCtrl::OnSize)
END_EVENT_TABLE()

bool wxOScopeCtrl::Create(wxWindow *parent, wxWindowID id, const wxPoint &pos, 
                        const wxSize &sz, long style, const wxString &name)
{
    if (!wxControl::Create(parent, id, pos, sz, style, wxDefaultValidator, name))
        return false;

    // do not erase background for us, we don't need it and it only
    // generates flickering!
    SetBackgroundStyle(wxBG_STYLE_CUSTOM);      

    // ensure defaults
    SetBackgroundColour(*wxBLACK);
    SetForegroundColour(*wxGREEN);
    SetFont(*wxSWISS_FONT);
    CreateBitmaps(GetClientSize());

    return true;
}

wxSize wxOScopeCtrl::DoGetBestSize() const
{
    // this control's best size is quite arbitrary
    // but this is still better than wxControl's defaults
    return wxSize(200, 100);
}

size_t wxOScopeCtrl::GetTimeRange() const
{
    if (IsXRangeFixed())
    {
        // no need to compute anything - just use the user's provided
        // time range. Note that this is the delta of the time displayed
        // on the axis: i.e. if the lowest X on the axis is associated
        // with instant T, then the highest X on the axis represents 
        // the instant T+m_nTimeRange.
        return m_nTimeRange;
    }

    // the best time range which allows us to plot all 
    // sample points in the m_rectPlot.width pixels is
    // [0; maxTime]
    return GetMaxTime();
}

size_t wxOScopeCtrl::GetMaxTime() const
{
    size_t maxTime = 0;
    for (size_t i=0; i<m_trends.GetCount(); i++)
    {
        size_t tmax = m_trends[i].GetTimeFor(m_trends[i].GetSampleCount()-1);
        maxTime = wxMax(maxTime, tmax);
    }

    return maxTime;
}

wxString wxOScopeCtrl::GetYMax() const
{
    if (m_strYMax.IsEmpty())
        return wxString::Format(wxT("%.*f"), m_nYDecimals, GetUpperLimit());
    
    return m_strYMax;
}

wxString wxOScopeCtrl::GetYMin() const
{
    if (m_strYMin.IsEmpty())
        return wxString::Format(wxT("%.*f"), m_nYDecimals, GetLowerLimit());
    
    return m_strYMin;
}

wxString wxOScopeCtrl::GetXMax() const
{
    return m_strXMax;
}

wxString wxOScopeCtrl::GetXMin() const
{
    return m_strXMin;
}

wxString wxOScopeCtrl::GetXUnits() const
{
    if (IsXRangeFixed() && !m_strXUnits.IsEmpty())
        return m_strXUnits;     // use user's custom label

    // format our current time range appropriately
    size_t nMilliseconds = GetTimeRange();
    wxDateTime dt((nMilliseconds/3600000)%60, // hours
                (nMilliseconds/60000)%60,   // minutes
                (nMilliseconds/1000)%60,    // seconds
                nMilliseconds%1000);
    
    wxString strTemp;
    if (dt.GetHour() > 0) 
        strTemp = dt.Format(wxT("%H:%M:%S"));
    else if (dt.GetMinute() > 0)
        strTemp = dt.Format(wxT("%M min, %S.%l sec"));
    else if (dt.GetSecond() > 0)
        strTemp = dt.Format(wxT("%S.%l sec"));
    else if (dt.GetMillisecond() > 0)
        strTemp = dt.Format(wxT("%l millisec"));

    return strTemp;
}

void wxOScopeCtrl::AddSampleData(size_t i, float newvalue)
{
    m_trends[i].AddSample(newvalue);

    if (UpdateLimits() || !IsXRangeFixed())
        UpdateGrid();
    UpdateGraph();
    Refresh();
}

void wxOScopeCtrl::AddSampleDataAndUpdateRange(size_t i, float newvalue,
                                            float lowerOffset, float upperOffset)
{
    m_trends[i].AddSampleAndUpdateRange(newvalue, lowerOffset, upperOffset);

    if (UpdateLimits() || !IsXRangeFixed())
        UpdateGrid();
    UpdateGraph();
    Refresh();
}

void wxOScopeCtrl::SetFixedXRange(size_t newrange)
{
    m_nTimeRange = newrange;
}

void wxOScopeCtrl::CreateBitmaps(const wxSize &sz)
{
    // recreate internal bitmaps if needed
    if (!m_bmpGrid.IsOk() ||
        sz.GetWidth() != m_bmpGrid.GetWidth() ||
        sz.GetHeight() != m_bmpGrid.GetHeight())
    {
        m_bmpGrid.Create(sz.GetWidth(), sz.GetHeight());
    }

    if (!m_bmpPlot.IsOk() ||
        sz.GetWidth() != m_bmpPlot.GetWidth() ||
        sz.GetHeight() != m_bmpPlot.GetHeight())
    {
        m_dcPlot.SelectObject(wxNullBitmap);

        // we can now safely recreate the bitmap
        m_bmpPlot.Create(sz.GetWidth(), sz.GetHeight());

        m_dcPlot.SelectObject(m_bmpPlot);
    }
}


// ----------------------------------------------------------------------------
// wxOScopeCtrl - update functions
// ----------------------------------------------------------------------------

bool wxOScopeCtrl::UpdateLimits()
{
    if (m_bFixedYLimits)
        return false;       // do not update - use user's custom limits

    // find the ymin & ymax looking at our data arrays:

    float max = 1.0;
    for (size_t i=0; i<m_trends.GetCount(); i++)
        max = wxMax(max, m_trends[i].GetUpperLimit());

    float min = 0.0;
    for (size_t i=0; i<m_trends.GetCount(); i++)
        min = wxMin(min, m_trends[i].GetLowerLimit());

    if (min != m_fLowerLimit || max != m_fUpperLimit)
    {
        m_fLowerLimit = min;
        m_fUpperLimit = max;

        // limits have been changed - grid needs to be updated
        return true;
    }

    return false;
}

void wxOScopeCtrl::UpdatePlotRect()
{
    wxMemoryDC memDC;
    wxSize sz, sz2;

    memDC.SetFont(this->GetFont());
    sz = memDC.GetTextExtent(GetYMin());
    sz2 = memDC.GetTextExtent(GetYMax());

    // line height for all labels should be the same,
    // but just in case some strange font is used, we get
    // the mean value:
    int lineHeight = (sz.GetHeight() + sz2.GetHeight())/2;

    // border needs to be doubled because it's left on both right and
    // left side of the m_strYMin/max strings
    m_rectPlot.x = wxOSC_BORDER*2 + wxMax(sz.x, sz2.x);
    m_rectPlot.y = wxOSC_BORDER;

    // make sure there's enough space for y max label
    m_rectPlot.y += lineHeight/2;

    // set plot height after last change to m_rectPlot.y
    m_rectPlot.height = GetClientSize().GetHeight() - m_rectPlot.y;

    // leave enough space also for the X units
    m_rectPlot.height -= wxOSC_BORDER + lineHeight + wxOSC_BORDER;

    // make sure there's enough space for y units (which is drawn rotated 90 degrees)
    m_rectPlot.x = wxMax(m_rectPlot.x, wxOSC_BORDER*2 + lineHeight);

    // set plot width after last update to m_rectPlot.x
    m_rectPlot.width = GetClientSize().GetWidth() - m_rectPlot.x - wxOSC_BORDER;
    m_rectPlot.width -= wxOSC_BORDER;  // just because I prefer it a bit less wide
}

void wxOScopeCtrl::UpdateGrid()
{
    // clear our bitmap to the background colour of this window
    wxMemoryDC memDC;
    memDC.SelectObject(m_bmpGrid);
    memDC.SetBackground(wxBrush(this->GetBackgroundColour()));
    memDC.Clear();

    if (!HasFlag(wxOSC_NO_GRID))
    {
        // draw the horizontal lines
        memDC.SetPen(m_penGrid);
        for (int y=m_rectPlot.GetTop(); y<m_rectPlot.GetBottom(); y+=m_nYGridLineGap)
            memDC.DrawLine(m_rectPlot.GetLeft(), y,
                        m_rectPlot.GetRight(), y);

        // be sure the line with y = m_rectPlot.GetBottom() gets drawn:
        memDC.DrawLine(m_rectPlot.GetLeft(), m_rectPlot.GetBottom(),
                    m_rectPlot.GetRight(), m_rectPlot.GetBottom());


        // draw the vertical lines
        for (int x=m_rectPlot.GetLeft(); x<m_rectPlot.GetRight(); x+=m_nXGridLineGap)
            memDC.DrawLine(x, m_rectPlot.GetTop(),
                        x, m_rectPlot.GetBottom());

        // be sure the line with x = m_rectPlot.GetRight() gets drawn:
        memDC.DrawLine(m_rectPlot.GetRight(), m_rectPlot.GetTop(),
                    m_rectPlot.GetRight(), m_rectPlot.GetBottom());
    }

    if (HasFlag(wxOSC_NO_AXIS_LABELS))
        return;

    // set up DC for text drawing
    memDC.SetFont(this->GetFont());
    memDC.SetTextForeground(this->GetForegroundColour());

    wxString strTemp;
    wxSize sz;

    // draw y max
    strTemp = GetYMax();
    sz = memDC.GetTextExtent(strTemp);
    memDC.DrawText(strTemp, m_rectPlot.x - sz.x - wxOSC_BORDER, wxOSC_BORDER);

    // draw y min
    strTemp = GetYMin();
    sz = memDC.GetTextExtent(strTemp);
    memDC.DrawText(strTemp, m_rectPlot.x - sz.x - wxOSC_BORDER, 
                            m_rectPlot.GetBottom() - sz.y/2);

    // draw y units
    if (!m_strYUnits.IsEmpty()) 
    {
        sz = memDC.GetTextExtent(m_strYUnits);
        memDC.DrawRotatedText(m_strYUnits,
                            m_rectPlot.x - sz.y - wxOSC_BORDER,
                            m_rectPlot.y + m_rectPlot.height/2 + sz.x/2, 
                            90.0);
    }

    // draw x units
    strTemp = GetXUnits();
    if (!strTemp.IsEmpty())
    {
        sz = memDC.GetTextExtent(strTemp);
        memDC.DrawText(strTemp, m_rectPlot.x + m_rectPlot.width/2 - sz.x/2, 
                                m_rectPlot.GetBottom() + wxOSC_BORDER);
    }
}

void wxOScopeCtrl::UpdateGraph()
{
    wxASSERT(m_bmpPlot.IsOk() && m_bmpGrid.IsOk());
    wxASSERT(m_rectPlot.width > 0 && m_rectPlot.height > 0);

    // use the grid as background of m_bmpPlot 
    m_dcPlot.DestroyClippingRegion();
    m_dcPlot.DrawBitmap(m_bmpGrid, 0, 0, false);

    // after blitting the grid, we want to clip anything outside m_rectPlot
    m_dcPlot.SetClippingRegion(m_rectPlot);

    // some constants used for scaling values to pixels
    float yscale = m_rectPlot.height/(GetUpperLimit()-GetLowerLimit());
    float xscale = (float)m_rectPlot.width / (float)GetTimeRange();

    int xOffset = 0;
    if (IsXRangeFixed())
    {
        // AUTO-SCROLLING feature is enabled:
        // compute the max X which we need to draw
        int maxX = (int)(GetMaxTime()*xscale);
        xOffset = maxX - m_rectPlot.width;
    }

    for (size_t iTrend=0; iTrend<m_trends.GetCount(); ++iTrend)
    {
        // select iTrend-th plot colour
        const wxOScopeData &t = m_trends[iTrend];
        m_dcPlot.SetPen(t.GetPlotPen());

        // precalc some constants
        // NB: use signed types here to avoid weird effects!
        int xStart = m_rectPlot.x,
            yStart = m_rectPlot.y + m_rectPlot.height;

        size_t firstToDraw;
        if (IsXRangeFixed())
        {
            // AUTO-SCROLLING feature:

            // compute the first point which needs to be drawn
            // skipping the "oldest" points: note that xOffset/xscale
            // is the time (in millisecs) of the first point which must
            // be drawn
            if (xOffset > 0)
                firstToDraw = t.GetIdxOfFirstSamplePreceding((size_t)(xOffset / xscale));
            else
                firstToDraw = 0;
        }
        else
        {
            // we always want to draw all the points and we can do
            // that because we choose a scale for axis X which provides
            // enough space for *all* points
            firstToDraw = 0;
        }

        // does current trend have any point in the visible area of the plot?
        if (t.GetSampleCount() <= firstToDraw)
            continue;       // skip this trend

        // init xPrev & yPrev with first point values
        int xPrev = xStart - xOffset + (int)(t.GetTimeFor(firstToDraw)*xscale),
            yPrev = yStart - (int)((t.GetSample(firstToDraw) - t.GetLowerLimit())*yscale),
            x, y;

#if wxOSC_DO_RAW_CLIP
        // clip xPrev & yPrev
        if (yPrev < m_rectPlot.y) 
            yPrev = m_rectPlot.y;
        else if (yPrev > yStart) 
            yPrev = yStart;
        if (xPrev < m_rectPlot.x) 
            xPrev = m_rectPlot.x;
#endif

        for (size_t i=firstToDraw+1; i < t.GetSampleCount(); i++)
        {
            // compute i-th point coords
            x = xStart - xOffset + (int)(t.GetTimeFor(i)*xscale);
            y = yStart - (int)((t.GetSample(i) - t.GetLowerLimit())*yscale);

#if wxOSC_DO_RAW_CLIP
            // clip the x
            if (x < m_rectPlot.x) 
                x = m_rectPlot.x;

            // clip the y
            if (y < m_rectPlot.y) 
                y = m_rectPlot.y;
            else if (y > yStart) 
                y = yStart;
#endif

            // draw the line from (i-1)-th to i-th point
            m_dcPlot.DrawLine(xPrev, yPrev, x, y);
            xPrev = x;
            yPrev = y;
        }
    }
}

void wxOScopeCtrl::UpdateAll()
{
    // NB: order is important!

    UpdateLimits();
    UpdatePlotRect();

    if (m_rectPlot.width <= 0 ||
        m_rectPlot.height <= 0)
        return;     // invalid size - cannot update graphs!

    UpdateGrid();
    UpdateGraph();
}




// ----------------------------------------------------------------------------
// wxOScopeCtrl - event handlers
// ----------------------------------------------------------------------------

void wxOScopeCtrl::OnPaint(wxPaintEvent& WXUNUSED(evt)) 
{	
    wxPaintDC dc(this);

    if (m_bmpPlot.IsOk())
    {
        // NOTE: m_dcPlot already has the m_bmpPlot selected inside!
        dc.Blit(0, 0, m_bmpPlot.GetWidth(), m_bmpPlot.GetHeight(),
                &m_dcPlot, 0, 0, wxCOPY, false /* no mask */);
    }
}

void wxOScopeCtrl::OnSize(wxSizeEvent& evt)
{
    const wxSize &sz = evt.GetSize();

    CreateBitmaps(sz);
    UpdateAll();
}


