// Copyright (c) Charles J. Cliffe
// SPDX-License-Identifier: GPL-2.0+

#include "GainCanvas.h"

#include "wx/wxprec.h"

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

#if !wxUSE_GLCANVAS
#error "OpenGL required: set wxUSE_GLCANVAS to 1 and rebuild the library"
#endif

#include "CubicSDR.h"
#include <algorithm>
#include <cmath>

wxBEGIN_EVENT_TABLE(GainCanvas, wxGLCanvas) EVT_PAINT(GainCanvas::OnPaint)
EVT_IDLE(GainCanvas::OnIdle)
EVT_MOTION(GainCanvas::OnMouseMoved)
EVT_LEFT_DOWN(GainCanvas::OnMouseDown)
EVT_LEFT_UP(GainCanvas::OnMouseReleased)
EVT_LEAVE_WINDOW(GainCanvas::OnMouseLeftWindow)
EVT_ENTER_WINDOW(GainCanvas::OnMouseEnterWindow)
EVT_MOUSEWHEEL(GainCanvas::OnMouseWheelMoved)
wxEND_EVENT_TABLE()

GainCanvas::GainCanvas(wxWindow *parent, const wxGLAttributes& dispAttrs) :
        InteractiveCanvas(parent, dispAttrs) {

    glContext = new PrimaryGLContext(this, &wxGetApp().GetContext(this), wxGetApp().GetContextAttributes());
    bgPanel.setCoordinateSystem(GLPanel::GLPANEL_Y_UP);
    bgPanel.setFill(GLPanel::GLPANEL_FILL_GRAD_X);

    numGains = 1;
    spacing = 2.0/numGains;
    barWidth = (1.0/numGains)*0.8;
    startPos = spacing/2.0;
    barHeight = 0.8f;
    refreshCounter = 0;

	userGainAsChanged = false;
}

GainCanvas::~GainCanvas() = default;

void GainCanvas::OnPaint(wxPaintEvent& WXUNUSED(event)) {
  //  wxPaintDC dc(this);
    const wxSize ClientSize = GetClientSize() * GetContentScaleFactor();

    glContext->SetCurrent(*this);
    initGLExtensions();

    glViewport(0, 0, ClientSize.x, ClientSize.y);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    bgPanel.draw();
    
    SwapBuffers();
}

void GainCanvas::OnIdle(wxIdleEvent &event) {
	if (mouseTracker.mouseInView()) {
	    Refresh();
	} else {
		event.Skip();
	}

	bool areGainsChangedHere = false;
    
    for (auto gi : gainPanels) {
        if (gi->getChanged()) {
			areGainsChangedHere  = true;
			// Gain only displays integer gain values, so set the applied gain 
			//value to exactly that. 
            wxGetApp().setGain(gi->getName(), (int)(gi->getValue()));
			//A gain may be exposed as setting also so assure refresh of the menu also.
			wxGetApp().notifyMainUIOfDeviceChange(false); //do not rebuild the gain UI

            gi->setChanged(false);
        }
    }

	//User input has changed the gain, so schedule an update of values
	//in 150ms in the future, else the device may not have taken the value into account.
	if (areGainsChangedHere) {
		userGainAsChanged = true;
		userGainAsChangedDelayTimer.start();
	}
	else {
		userGainAsChangedDelayTimer.update();

		if (!userGainAsChanged || (userGainAsChanged && userGainAsChangedDelayTimer.getMilliseconds() > 150)) {
			
			if (updateGainValues()) {
				Refresh();
			}

			userGainAsChanged = false;
		}
	}
}

void GainCanvas::SetLevel() {
    CubicVR::vec2 mpos = mouseTracker.getGLXY();
    
    for (auto gi : gainPanels) {
        if (gi->isMeterHit(mpos)) {
            float value = gi->getMeterHitValue(mpos);
            
            gi->setValue(value);
            gi->setChanged(true);           
            break;
        }
    }
}

void GainCanvas::OnMouseMoved(wxMouseEvent& event) {
    InteractiveCanvas::OnMouseMoved(event);

    CubicVR::vec2 mpos = mouseTracker.getGLXY();
    
    for (auto gi : gainPanels) {
        if (gi->isMeterHit(mpos)) {
            float value = gi->getMeterHitValue(mpos);
        
            gi->setHighlight(value);
            gi->setHighlightVisible(true);
            wxGetApp().setActiveGainEntry(gi->getName());
        } else {
            gi->setHighlightVisible(false);
        }
    }
    
    if (mouseTracker.mouseDown()) {
        SetLevel();
    }
    else {
        if (!helpTip.empty()) {
            setStatusText(helpTip);
        }
    }
}

void GainCanvas::OnMouseDown(wxMouseEvent& event) {
    InteractiveCanvas::OnMouseDown(event);
    SetLevel();
}

void GainCanvas::OnMouseWheelMoved(wxMouseEvent& event) {
    InteractiveCanvas::OnMouseWheelMoved(event);
    
    CubicVR::vec2 mpos = mouseTracker.getGLXY();
    
    for (auto gi : gainPanels) {
        if (gi->isMeterHit(mpos)) {
            float movement = 3.0 * (float)event.GetWheelRotation();
            gi->setValue(gi->getValue() + ((movement / 100.0) * ((gi->getHigh() - gi->getLow()) / 100.0)));
            gi->setChanged(true);
            break;
        }
    }
}

void GainCanvas::OnMouseReleased(wxMouseEvent& event) {
    InteractiveCanvas::OnMouseReleased(event);
}

void GainCanvas::OnMouseLeftWindow(wxMouseEvent& event) {
    InteractiveCanvas::OnMouseLeftWindow(event);
    SetCursor(wxCURSOR_CROSS);
   
    for (auto gi : gainPanels) {
        gi->setHighlightVisible(false);
    }
    
    Refresh();
}

void GainCanvas::OnMouseEnterWindow(wxMouseEvent& event) {
    InteractiveCanvas::mouseTracker.OnMouseEnterWindow(event);
    SetCursor(wxCURSOR_CROSS);
#ifdef _WIN32
	if (wxGetApp().getAppFrame()->canFocus()) {
		this->SetFocus();
	}
#endif
}

void GainCanvas::setHelpTip(std::string tip) {
    helpTip = tip;
}

void GainCanvas::updateGainUI() {

    SDRDeviceInfo *devInfo = wxGetApp().getDevice();

    //possible if we 'Refresh Devices' then devInfo becomes null
    //until a new device is selected.
    if (devInfo == nullptr) {
        return;
    }

    DeviceConfig *devConfig = wxGetApp().getConfig()->getDevice(devInfo->getDeviceId());
	
	//read the gains from the device.
	//This may be wrong because the device is not started, or has yet 
	//to take into account a user gain change. Doesn't matter,
	//UpdateGainValues() takes cares of updating the true value realtime.
    gains = devInfo->getGains(SOAPY_SDR_RX, 0);
    numGains = gains.size();
    float i = 0;
    
    if (!numGains) {
        return;
    }
    
    spacing = 2.0/numGains;
    barWidth = (1.0/numGains)*0.7;
    startPos = spacing/2.0;
    barHeight = 1.0f;
    
    while (!gainPanels.empty()) {
        MeterPanel *mDel = gainPanels.back();
        gainPanels.pop_back();
        bgPanel.removeChild(mDel);
        delete mDel;
    }

    for (const auto& gi : gains) {
        auto *mPanel = new MeterPanel(gi.first, gi.second.minimum(), gi.second.maximum(), devConfig->getGain(gi.first,wxGetApp().getGain(gi.first)));

        float midPos = -1.0+startPos+spacing*i;
        mPanel->setPosition(midPos, 0);
        mPanel->setSize(barWidth, barHeight);
        bgPanel.addChild(mPanel);
        
        gainPanels.push_back(mPanel);
        i++;
    }
	  
    setThemeColors();
}

// call this to refresh the gain values only, not the whole UI.
bool GainCanvas::updateGainValues() {

	bool isRefreshNeeded = false;

	SDRDeviceInfo *devInfo = wxGetApp().getDevice();

	//possible if we 'Refresh Devices' then devInfo becomes null
	//until a new device is selected.
	//also, do not attempt an update with the device is not started.
	if (devInfo == nullptr || !devInfo->isActive()) {
		return false;
	}

	DeviceConfig *devConfig = wxGetApp().getConfig()->getDevice(devInfo->getDeviceId());

	gains = devInfo->getGains(SOAPY_SDR_RX, 0);

	size_t numGainsToRefresh = std::min(gains.size(), gainPanels.size());
	size_t panelIndex = 0;

	//actually the order of gains iteration should be constant because map of string,
	//and gainPanels were built in that order in updateGainUI()
	for (const auto& gi : gains) {

		if (panelIndex >= numGainsToRefresh) {
			break;
		}

		// do not update if a change is already pending.
		if (!gainPanels[panelIndex]->getChanged()) {

			//read the actual gain from the device, round it
			float actualRoundedGain = (float)std::round(devInfo->getCurrentGain(SOAPY_SDR_RX, 0, gi.first));
			
			//do nothing if the difference is less than 1.0, since the panel do not show it anyway.
			if ((int)actualRoundedGain != (int)(gainPanels[panelIndex]->getValue())) {

				gainPanels[panelIndex]->setValue(actualRoundedGain);

				//update the config with this value : 
				//a consequence of such updates is that the use setting 
				// is overridden by the current one in AGC mode.
				//TODO: if it not desirable, do not update in AGC mode.
				devConfig->setGain(gi.first, actualRoundedGain);

				isRefreshNeeded = true;
			}
		} //end if no external change pending.

		panelIndex++;
	}

	return isRefreshNeeded;
}

void GainCanvas::setThemeColors() {
    RGBA4f c1, c2;
    
    c1 = ThemeMgr::mgr.currentTheme->generalBackground;
    c2 = ThemeMgr::mgr.currentTheme->generalBackground * 0.5;
    c1.a = 1.0;
    c2.a = 1.0;
    bgPanel.setFillColor(c1, c2);

    Refresh();
}

