/*
    ------------------------------------------------------------------

    This file is part of the Open Ephys GUI
    Copyright (C) 2013 Open Ephys

    ------------------------------------------------------------------

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "SpikeSorterCanvas.h"


SpikeSorterCanvas::SpikeSorterCanvas(SpikeSorter* n) :
    processor(n), newSpike(false)
{
    electrode = nullptr;
    viewport = new Viewport();
    spikeDisplay = new SpikeThresholdDisplay(n,this, viewport);

    viewport->setViewedComponent(spikeDisplay, false);
    viewport->setScrollBarsShown(true, true);

    inDrawingPolygonMode = false;
    scrollBarThickness = viewport->getScrollBarThickness();

    addUnitButton = new UtilityButton("New box unit", Font("Small Text", 13, Font::plain));
    addUnitButton->setRadius(3.0f);
    addUnitButton->addListener(this);
    addAndMakeVisible(addUnitButton);

    addPolygonUnitButton = new UtilityButton("New polygon", Font("Small Text", 13, Font::plain));
    addPolygonUnitButton->setRadius(3.0f);
    addPolygonUnitButton->addListener(this);
    addAndMakeVisible(addPolygonUnitButton);

    addBoxButton = new UtilityButton("Add box", Font("Small Text", 13, Font::plain));
    addBoxButton->setRadius(3.0f);
    addBoxButton->addListener(this);
    addAndMakeVisible(addBoxButton);

    delUnitButton = new UtilityButton("Delete", Font("Small Text", 13, Font::plain));
    delUnitButton->setRadius(3.0f);
    delUnitButton->addListener(this);
    addAndMakeVisible(delUnitButton);

    rePCAButton = new UtilityButton("Re-PCA", Font("Small Text", 13, Font::plain));
    rePCAButton->setRadius(3.0f);
    rePCAButton->addListener(this);
    addAndMakeVisible(rePCAButton);

    newIDbuttons = new UtilityButton("New IDs", Font("Small Text", 13, Font::plain));
    newIDbuttons->setRadius(3.0f);
    newIDbuttons->addListener(this);
    addAndMakeVisible(newIDbuttons);

    deleteAllUnits = new UtilityButton("Delete All", Font("Small Text", 13, Font::plain));
    deleteAllUnits->setRadius(3.0f);
    deleteAllUnits->addListener(this);
    addAndMakeVisible(deleteAllUnits);

    nextElectrode = new UtilityButton("Next Electrode", Font("Small Text", 13, Font::plain));
    nextElectrode->setRadius(3.0f);
    nextElectrode->addListener(this);
    addAndMakeVisible(nextElectrode);

    prevElectrode = new UtilityButton("Prev Electrode", Font("Small Text", 13, Font::plain));
    prevElectrode->setRadius(3.0f);
    prevElectrode->addListener(this);
    addAndMakeVisible(prevElectrode);

    addAndMakeVisible(viewport);

    setWantsKeyboardFocus(true);

    update();

}

SpikeSorterCanvas::~SpikeSorterCanvas()
{

}

void SpikeSorterCanvas::beginAnimation()
{
    std::cout << "SpikeSorterCanvas beginning animation." << std::endl;

    startCallbacks();
}

void SpikeSorterCanvas::endAnimation()
{
    std::cout << "SpikeSorterCanvas ending animation." << std::endl;

    stopCallbacks();
}

void SpikeSorterCanvas::update()
{

    std::cout << "Updating SpikeSorterCanvas" << std::endl;

    int nPlots = processor->getNumElectrodes();
    processor->removeSpikePlots();
    spikeDisplay->removePlots();

    if (nPlots > 0)
    {
        // Plot only active electrode
        int currentElectrode = processor->getCurrentElectrodeIndex();
        electrode = processor->getActiveElectrode();
        SpikeHistogramPlot* sp = spikeDisplay->addSpikePlot(processor->getNumberOfChannelsForElectrode(currentElectrode), electrode->electrodeID,
                                                            processor->getNameForElectrode(currentElectrode));
        processor->addSpikePlotForElectrode(sp, currentElectrode);
        electrode->spikePlot->setFlipSignal(processor->getFlipSignalState());
        electrode->spikePlot->updateUnitsFromProcessor();

    }
    spikeDisplay->resized();
    spikeDisplay->repaint();
}


void SpikeSorterCanvas::refreshState()
{
    // called when the component's tab becomes visible again
    resized();
}

void SpikeSorterCanvas::resized()
{
    viewport->setBounds(130,0,getWidth()-140,getHeight());

    spikeDisplay->setBounds(0,0,getWidth()-140, spikeDisplay->getTotalHeight());

    nextElectrode->setBounds(0, 20, 120,30);
    prevElectrode->setBounds(0, 60, 120,30);

    addUnitButton->setBounds(0, 120, 120,20);
    addPolygonUnitButton->setBounds(0, 150, 120,20);
    addBoxButton->setBounds(0, 180, 120,20);
    delUnitButton->setBounds(0, 210, 120,20);

    rePCAButton->setBounds(0, 240, 120,20);

    newIDbuttons->setBounds(0, 270, 120,20);
    deleteAllUnits->setBounds(0, 300, 120,20);

}

void SpikeSorterCanvas::paint(Graphics& g)
{

    g.fillAll(Colours::darkgrey);

}

void SpikeSorterCanvas::refresh()
{
    // called every 10 Hz
    processSpikeEvents();

    repaint();
}


void SpikeSorterCanvas::processSpikeEvents()
{


    //Electrode* e = ((SpikeSorter*) processor)->getActiveElectrode();

    //	e->spikeSort->lstLastSpikes
    //    processor->setParameter(2, 0.0f); // request redraw

}





void SpikeSorterCanvas::removeUnitOrBox()
{
    int electrodeID = processor->getActiveElectrode()->electrodeID;
    int unitID, boxID;
    processor->getActiveElectrode()->spikePlot->getSelectedUnitAndBox(unitID, boxID);
    bool selectNewBoxUnit = false;
    bool selectNewPCAUnit = false;
    if (unitID > 0)
    {
        if (boxID >= 0)
        {
            // box unit
            int numBoxes = processor->getActiveElectrode()->spikeSort->getNumBoxes(unitID);
            if (numBoxes > 1)
            {
                // delete box, but keep unit
                processor->getActiveElectrode()->spikeSort->removeBoxFromUnit(unitID, boxID);
                electrode->spikePlot->updateUnitsFromProcessor();
                electrode->spikePlot->setSelectedUnitAndBox(unitID,0);
            }
            else
            {
                // delete unit
                processor->getActiveElectrode()->spikeSort->removeUnit(unitID);
                electrode->spikePlot->updateUnitsFromProcessor();
                processor->removeUnit(electrodeID, unitID);

                std::vector<BoxUnit> boxunits = processor->getActiveElectrode()->spikeSort->getBoxUnits();
                std::vector<PCAUnit> pcaunits = processor->getActiveElectrode()->spikeSort->getPCAUnits();
                if (boxunits.size() > 0)
                {
                    selectNewBoxUnit = true;
                }
                else if (pcaunits.size() > 0)
                {
                    selectNewPCAUnit = true;
                }
                else
                {
                    electrode->spikePlot->setSelectedUnitAndBox(-1,-1);
                }
            }
        }
        else
        {
            // pca unit
            processor->getActiveElectrode()->spikeSort->removeUnit(unitID);
            electrode->spikePlot->updateUnitsFromProcessor();
            processor->removeUnit(electrodeID, unitID);

            std::vector<BoxUnit> boxunits = processor->getActiveElectrode()->spikeSort->getBoxUnits();
            std::vector<PCAUnit> pcaunits = processor->getActiveElectrode()->spikeSort->getPCAUnits();
            if (pcaunits.size() > 0)
            {
                selectNewPCAUnit = true;
            }
            else if (boxunits.size() > 0)
            {
                selectNewBoxUnit = true;
            }
            else
            {
                electrode->spikePlot->setSelectedUnitAndBox(-1,-1);
            }


        }
        if (selectNewPCAUnit)
        {
            // set new selected unit to be the last existing unit
            std::vector<PCAUnit> u = processor->getActiveElectrode()->spikeSort->getPCAUnits();
            if (u.size() > 0)
            {
                electrode->spikePlot->setSelectedUnitAndBox(u[u.size()-1].getUnitID(),-1);
            }
            else
            {
                electrode->spikePlot->setSelectedUnitAndBox(-1,-1);
            }
        }
        if (selectNewBoxUnit)
        {
            // set new selected unit to be the last existing unit
            std::vector<BoxUnit> u = processor->getActiveElectrode()->spikeSort->getBoxUnits();
            if (u.size() > 0)
            {
                electrode->spikePlot->setSelectedUnitAndBox(u[u.size()-1].getUnitID(),0);
            }
            else
            {
                electrode->spikePlot->setSelectedUnitAndBox(-1,-1);
            }
        }



    }

}

bool SpikeSorterCanvas::keyPressed(const KeyPress& key)
{

    KeyPress c = KeyPress::createFromDescription("c");
    KeyPress e = KeyPress::createFromDescription("escape");
    KeyPress d = KeyPress::createFromDescription("delete");

    if (key.isKeyCode(c.getKeyCode())) // C
    {
        spikeDisplay->clear();

        std::cout << "Clearing display" << std::endl;
        return true;
    }
    else  if (key.isKeyCode(e.getKeyCode()))   // ESC
    {
        spikeDisplay->setPolygonMode(false);
        return true;
    }
    else  if (key.isKeyCode(d.getKeyCode()))   // Delete
    {
        removeUnitOrBox();
        return true;
    }

    return false;

}

void SpikeSorterCanvas::buttonClicked(Button* button)
{
    int channel = 0;
    int unitID = -1;
    int boxID = -1;
    Time t;

    if (button == addPolygonUnitButton)
    {
        inDrawingPolygonMode = true;
        addPolygonUnitButton->setToggleState(true, dontSendNotification);
        electrode->spikePlot->setPolygonDrawingMode(true);
    }
    else if (button == addUnitButton)
    {
        Electrode* e = processor->getActiveElectrode();

        if (e != nullptr)
        {
            int electrodeID = processor->getActiveElectrode()->electrodeID;

            std::cout << "Adding box unit to electrode " << e->electrodeID << std::endl;
            int newUnitID = processor->getActiveElectrode()->spikeSort->addBoxUnit(0);

            uint8 r, g, b;
            processor->getActiveElectrode()->spikeSort->getUnitColor(newUnitID, r, g, b);
            electrode->spikePlot->updateUnitsFromProcessor();
            electrode->spikePlot->setSelectedUnitAndBox(newUnitID, 0);
            processor->addNewUnit(electrodeID, newUnitID, r, g, b);
        }

    }
    else if (button == delUnitButton)
    {
        removeUnitOrBox();

    }
    else if (button == addBoxButton)
    {

        processor->getActiveElectrode()->spikePlot->getSelectedUnitAndBox(unitID, boxID);

        if (unitID > 0)
        {
            std::cout << "Adding box to channel " << channel << " with unitID " << unitID << std::endl;
            processor->getActiveElectrode()->spikeSort->addBoxToUnit(channel, unitID);
            electrode->spikePlot->updateUnitsFromProcessor();
        }
    }
    else if (button == rePCAButton)
    {
        processor->getActiveElectrode()->spikeSort->RePCA();
    }
    else if (button == nextElectrode)
    {
        SpikeSorterEditor* ed = (SpikeSorterEditor*)processor->getEditor();
        ed->setElectrodeComboBox(1);
    }
    else if (button == prevElectrode)
    {

        SpikeSorterEditor* ed = (SpikeSorterEditor*)processor->getEditor();

        ed->setElectrodeComboBox(-1);

    }
    else if (button == newIDbuttons)
    {
        // generate new IDs
        processor->getActiveElectrode()->spikeSort->generateNewIDs();
        electrode->spikePlot->updateUnitsFromProcessor();
        //processor->updateSinks(electrode->electrodeID,false);
    }
    else if (button == deleteAllUnits)
    {
        // delete unit
        processor->getActiveElectrode()->spikeSort->removeAllUnits();
        electrode->spikePlot->updateUnitsFromProcessor();
        processor->removeAllUnits(electrode->electrodeID);
    }

    repaint();
}





// ----------------------------------------------------------------

SpikeThresholdDisplay::SpikeThresholdDisplay(SpikeSorter* p, SpikeSorterCanvas* sdc, Viewport* v) :
    processor(p),canvas(sdc), viewport(v)
{

    totalHeight = 1000;

}

SpikeThresholdDisplay::~SpikeThresholdDisplay()
{

}

void SpikeThresholdDisplay::clear()
{
    if (spikePlots.size() > 0)
    {
        for (int i = 0; i < spikePlots.size(); i++)
        {
            spikePlots[i]->clear();
        }
    }

}


void SpikeThresholdDisplay::removePlots()
{
    spikePlots.clear();

}

SpikeHistogramPlot* SpikeThresholdDisplay::addSpikePlot(int numChannels, int electrodeID, String name_)
{

    std::cout << "Adding new spike plot." << std::endl;

    SpikeHistogramPlot* spikePlot = new SpikeHistogramPlot(processor,canvas, electrodeID, 1000 + numChannels, name_);
    spikePlots.add(spikePlot);
    addAndMakeVisible(spikePlot);

    return spikePlot;
}

void SpikeThresholdDisplay::paint(Graphics& g)
{

    g.fillAll(Colours::grey);

}

void SpikeThresholdDisplay::setPolygonMode(bool on)
{
    if (spikePlots.size() > 0)
        spikePlots[0]->setPolygonDrawingMode(on);
}

void SpikeThresholdDisplay::resized()
{
    // this is kind of a mess -- is there any way to optimize it?

    if (spikePlots.size() > 0)
    {

        int w = getWidth();
        int h = 430;//getHeight();

        spikePlots[0]->setBounds(0, 0, w, h);


        setBounds(0, 0, w, h);
    }

}

void SpikeThresholdDisplay::mouseDown(const MouseEvent& event)
{

}

void SpikeThresholdDisplay::plotSpike(SorterSpikePtr spike, int electrodeNum)
{
    spikePlots[electrodeNum]->processSpikeObject(spike);

}






// ----------------------------------------------------------------

SpikeHistogramPlot::SpikeHistogramPlot(SpikeSorter* prc,SpikeSorterCanvas* sdc, int electrodeID_, int p, String name_) :
    canvas(sdc), isSelected(false), plotType(p), electrodeID(electrodeID_),
    limitsChanged(true), processor(prc), name(name_)

{

    font = Font("Default", 15, Font::plain);

    switch (p)
    {
        case SINGLE_PLOT:
            // std::cout<<"SpikePlot as SINGLE_PLOT"<<std::endl;
            nWaveAx = 1;
            nProjAx = 1;
            nChannels = 1;
            minWidth = 600;
            aspectRatio = 0.5f;
            break;
        case STEREO_PLOT:
            //  std::cout<<"SpikePlot as STEREO_PLOT"<<std::endl;
            nWaveAx = 2;
            nProjAx = 1;
            nChannels = 2;
            minWidth = 300;
            aspectRatio = 0.5f;
            break;
        case TETRODE_PLOT:
            // std::cout<<"SpikePlot as TETRODE_PLOT"<<std::endl;
            nWaveAx = 4;
            nProjAx = 1;
            nChannels = 4;
            minWidth = 400;
            aspectRatio = 0.5f;
            break;
        //        case HIST_PLOT:
        //            nWaveAx = 1;
        //            nProjAx = 0;
        //            nHistAx = 1;
        //            break;
        default: // unsupported number of axes provided
            std::cout << "SpikePlot as UNKNOWN, defaulting to SINGLE_PLOT" << std::endl;
            nWaveAx = 1;
            nProjAx = 0;
            plotType = SINGLE_PLOT;
            nChannels = 1;
    }

    std::vector<float> scales = processor->getElectrodeVoltageScales(electrodeID);
    initAxes(scales);

    for (int i = 0; i < nChannels; i++)
    {
        UtilityButton* rangeButton = new UtilityButton(String(scales[i],0), Font("Small Text", 10, Font::plain));
        rangeButton->setRadius(3.0f);
        rangeButton->addListener(this);
        addAndMakeVisible(rangeButton);

        rangeButtons.add(rangeButton);
    }

}


void SpikeHistogramPlot::setSelectedUnitAndBox(int unitID, int boxID)
{
    const ScopedLock myScopedLock(mut);
    processor->getActiveElectrode()->spikeSort->setSelectedUnitAndBox(unitID, boxID);
}

void SpikeHistogramPlot::getSelectedUnitAndBox(int& unitID, int& boxID)
{
    const ScopedLock myScopedLock(mut);
    processor->getActiveElectrode()->spikeSort->getSelectedUnitAndBox(unitID, boxID);
}


SpikeHistogramPlot::~SpikeHistogramPlot()
{
    pAxes.clear();
    wAxes.clear();

}

void SpikeHistogramPlot::paint(Graphics& g)
{

    //const MessageManagerLock mmLock;

    g.setColour(Colours::white);
    g.drawRect(0,0,getWidth(),getHeight());

    g.setFont(font);

    g.drawText(name,10,0,200,20,Justification::left,false);

}

void SpikeHistogramPlot::setFlipSignal(bool state)
{
    for (int i = 0; i < wAxes.size(); i++)
    {
        wAxes[i]->setSignalFlip(state);
    }
}

void SpikeHistogramPlot::setPolygonDrawingMode(bool on)
{
    const ScopedLock myScopedLock(mut);
    pAxes[0]->setPolygonDrawingMode(on);
}

void SpikeHistogramPlot::updateUnitsFromProcessor()
{
    const ScopedLock myScopedLock(mut);
    boxUnits = processor->getActiveElectrode()->spikeSort->getBoxUnits();
    pcaUnits = processor->getActiveElectrode()->spikeSort->getPCAUnits();

    if (nWaveAx > 0)
    {
        wAxes[0]->updateUnits(boxUnits);
    }
    pAxes[0]->updateUnits(pcaUnits);


    int selectedUnitID, selectedBoxID;
    processor->getActiveElectrode()->spikeSort->getSelectedUnitAndBox(selectedUnitID, selectedBoxID);




}

void SpikeHistogramPlot::setPCARange(float p1min, float p2min, float p1max, float p2max)
{
    const ScopedLock myScopedLock(mut);
    pAxes[0]->setPCARange(p1min, p2min, p1max, p2max);
}

void SpikeHistogramPlot::processSpikeObject(SorterSpikePtr s)
{
    const ScopedLock myScopedLock(mut);
    if (nWaveAx > 0)
    {
        for (int i = 0; i < nWaveAx; i++)
        {
            wAxes[i]->updateSpikeData(s);
        }

        pAxes[0]->updateSpikeData(s);


    }
}

void SpikeHistogramPlot::select()
{
    isSelected = true;
}

void SpikeHistogramPlot::deselect()
{
    isSelected = false;
}

void SpikeHistogramPlot::initAxes(std::vector<float> scales)
{
    const ScopedLock myScopedLock(mut);
    initLimits();

    for (int i = 0; i < nWaveAx; i++)
    {
        WaveformAxes* wAx = new WaveformAxes(this,processor, electrodeID, i);
        wAx->setDetectorThreshold(processor->getActiveElectrode()->thresholds[i]);
        wAxes.add(wAx);
        addAndMakeVisible(wAx);
        ranges.add(scales[i]);
    }

    PCAProjectionAxes* pAx = new PCAProjectionAxes(processor);
    float p1min,p2min, p1max,  p2max;
    processor->getActiveElectrode()->spikeSort->getPCArange(p1min,p2min, p1max,  p2max);
    pAx->setPCARange(p1min,p2min, p1max,  p2max);

    pAxes.add(pAx);
    addAndMakeVisible(pAx);

    setLimitsOnAxes(); // initialize the ranges
}

void SpikeHistogramPlot::resized()
{
    const ScopedLock myScopedLock(mut);

    float width = (float)getWidth()-10;
    float height = (float) getHeight()-25;

    float axesWidth = 0;
    float axesHeight = 0;

    // to compute the axes positions we need to know how many columns of proj and wave axes should exist
    // using these two values we can calculate the positions of all of the sub axes
    int nProjCols = 0;
    int nWaveCols = 0;

    switch (plotType)
    {
        case SINGLE_PLOT:
            nProjCols = 1;
            nWaveCols = 1;
            axesWidth = width/2;
            axesHeight = height;
            break;

        case STEREO_PLOT:
            nProjCols = 1;
            nWaveCols = 2;
            axesWidth = width/2;
            axesHeight = height;
            break;
        case TETRODE_PLOT:
            nProjCols = 1;
            nWaveCols = 2;
            axesWidth = width/2;
            axesHeight = height/2;
            break;
    }

    for (int i = 0; i < nWaveAx; i++)
    {
        wAxes[i]->setBounds(5 + (i % nWaveCols) * axesWidth/nWaveCols, 20 + (i/nWaveCols) * axesHeight, axesWidth/nWaveCols, axesHeight);
        rangeButtons[i]->setBounds(8 + (i % nWaveCols) * axesWidth/nWaveCols,
                                   20 + (i/nWaveCols) * axesHeight + axesHeight - 18,
                                   35, 15);
    }
    pAxes[0]->setBounds(5 +  axesWidth, 20 + 0, width/2, height);


}



void SpikeHistogramPlot::modifyRange(std::vector<float> values)
{
    const int NUM_RANGE = 7;
    float range_array[NUM_RANGE] = {100,250,500,750,1000,1250,1500};
    String label;
    int newIndex = 0;

    for (int index = 0; index < nChannels; index++)
    {
        for (int k = 0; k < NUM_RANGE; k++)
        {
            if (std::abs(values[index] - range_array[k]) < 0.1)
            {
                newIndex = k;
                break;
            }
        }

        ranges.set(index, range_array[newIndex]);
        String label = String(range_array[newIndex],0);
        rangeButtons[index]->setLabel(label);
    }
    setLimitsOnAxes();
}


void SpikeHistogramPlot::modifyRange(int index,bool up)
{
    const int NUM_RANGE = 7;
    float range_array[NUM_RANGE] = {100,250,500,750,1000,1250,1500};
    String label;
    for (int k = 0; k < NUM_RANGE; k++)
    {
        if (std::abs(ranges[index] - range_array[k]) < 0.1)
        {
            int newIndex;
            if (up)
                newIndex  = (k + 1) % NUM_RANGE;
            else
            {
                newIndex  = (k - 1);
                if (newIndex < 0)
                    newIndex  = NUM_RANGE-1;
            }
            ranges.set(index, range_array[newIndex]);
            String label = String(range_array[newIndex],0);
            rangeButtons[index]->setLabel(label);
            setLimitsOnAxes();

            processor->setElectrodeVoltageScale(electrodeID, index, range_array[newIndex]);
            return;
        }

    }
    // we shoudl never reach here.
    jassert(false);
    return ;

}

void SpikeHistogramPlot::buttonClicked(Button* button)
{
    UtilityButton* buttonThatWasClicked = (UtilityButton*) button;

    int index = rangeButtons.indexOf(buttonThatWasClicked);
    modifyRange(index,true);

}

void SpikeHistogramPlot::setLimitsOnAxes()
{
    const ScopedLock myScopedLock(mut);
    for (int i = 0; i < nWaveAx; i++)
        wAxes[i]->setRange(ranges[i]);
}

void SpikeHistogramPlot::initLimits()
{
    for (int i = 0; i < nChannels; i++)
    {
        limits[i][0] = 1209;//-1*pow(2,11);
        limits[i][1] = 11059;//pow(2,14)*1.6;
    }

}

void SpikeHistogramPlot::getBestDimensions(int* w, int* h)
{
    switch (plotType)
    {
        case TETRODE_PLOT:
            *w = 4;
            *h = 2;
            break;
        case STEREO_PLOT:
            *w = 2;
            *h = 1;
            break;
        case SINGLE_PLOT:
            *w = 1;
            *h = 1;
            break;
        default:
            *w = 1;
            *h = 1;
            break;
    }
}

void SpikeHistogramPlot::clear()
{
    const ScopedLock myScopedLock(mut);
    std::cout << "SpikePlot::clear()" << std::endl;

    for (int i = 0; i < nWaveAx; i++)
        wAxes[i]->clear();
    for (int i = 0; i < nProjAx; i++)
        pAxes[i]->clear();


}


void SpikeHistogramPlot::setDisplayThresholdForChannel(int i, float f)
{
    const ScopedLock myScopedLock(mut);
    if (i < wAxes.size())
        wAxes[i]->setDetectorThreshold(f);

    return;
}


float SpikeHistogramPlot::getDisplayThresholdForChannel(int i)
{
    const ScopedLock myScopedLock(mut);
    float f= wAxes[i]->getDisplayThreshold();

    return f;
}

/********************************/



// --------------------------------------------------

GenericDrawAxes::GenericDrawAxes(int t)
    : gotFirstSpike(false), type(t)
{
    ylims[0] = 0;
    ylims[1] = 1;

    xlims[0] = 0;
    xlims[1] = 1;

    font = Font("Default", 12, Font::plain);

}

GenericDrawAxes::~GenericDrawAxes()
{

}

bool GenericDrawAxes::updateSpikeData(SorterSpikePtr newSpike)
{
    if (!gotFirstSpike)
    {
        gotFirstSpike = true;
    }

    s = newSpike;
    return true;
}

void GenericDrawAxes::setYLims(double ymin, double ymax)
{

    //std::cout << "setting y limits to " << ymin << " " << ymax << std::endl;
    ylims[0] = ymin;
    ylims[1] = ymax;
}
void GenericDrawAxes::getYLims(double* min, double* max)
{
    *min = ylims[0];
    *max = ylims[1];
}
void GenericDrawAxes::setXLims(double xmin, double xmax)
{
    xlims[0] = xmin;
    xlims[1] = xmax;
}
void GenericDrawAxes::getXLims(double* min, double* max)
{
    *min = xlims[0];
    *max = xlims[1];
}


void GenericDrawAxes::setType(int t)
{
    if (t < WAVE1 || t > PROJ3x4)
    {
        std::cout<<"Invalid Axes type specified";
        return;
    }
    type = t;
}

int GenericDrawAxes::getType()
{
    return type;
}

int GenericDrawAxes::roundUp(int numToRound, int multiple)
{
    if (multiple == 0)
    {
        return numToRound;
    }

    int remainder = numToRound % multiple;
    if (remainder == 0)
        return numToRound;
    return numToRound + multiple - remainder;
}


void GenericDrawAxes::makeLabel(int val, int gain, bool convert, char* s)
{
    if (convert)
    {
        double volt = ad16ToUv(val, gain)/1000.;
        if (abs(val)>1e6)
        {
            //val = val/(1e6);
            sprintf(s, "%.2fV", volt);
        }
        else if (abs(val)>1e3)
        {
            //val = val/(1e3);
            sprintf(s, "%.2fmV", volt);
        }
        else
            sprintf(s, "%.2fuV", volt);
    }
    else
    {
        sprintf(s,"%d", (int)val);
    }
}

double GenericDrawAxes::ad16ToUv(int x, int gain)
{
    int result = (double)(x * 20e6) / (double)(gain * pow(2.0,16));
    return result;
}


// --------------------------------------------------


WaveformAxes::WaveformAxes(SpikeHistogramPlot* plt, SpikeSorter* p,int electrodeID_, int _channel) : GenericDrawAxes(_channel),  electrodeID(electrodeID_),
    signalFlipped(false),
    channel(_channel),
    drawGrid(true),
    displayThresholdLevel(0.0f),
    spikesReceivedSinceLastRedraw(0),
    spikeIndex(0),
    bufferSize(5),
    range(250.0f),
    isOverThresholdSlider(false),
    isDraggingThresholdSlider(false),
    processor(p),
    spikeHistogramPlot(plt)
{
    bDragging  = false;

    isOverUnit = -1;
    isOverBox = -1;
    //	 selectedUnit = -1;
    //	 selectedBox = -1;
    addMouseListener(this, true);

    thresholdColour = Colours::red;


    font = Font("Small Text",10,Font::plain);
    int numSamples = 40;
    for (int n = 0; n < bufferSize; n++)
    {
        spikeBuffer.add(nullptr);
    }
}

void WaveformAxes::mouseWheelMove(const MouseEvent& event, const MouseWheelDetails& wheel)
{
    // weirdly enough, sometimes we get twice of this event even though a single wheel move was made...
    if (wheel.deltaY < 0)
        spikeHistogramPlot->modifyRange(channel, true);
    else
        spikeHistogramPlot->modifyRange(channel, false);

}

void WaveformAxes::setSignalFlip(bool state)
{
    signalFlipped = state;
    repaint();
}

void WaveformAxes::setRange(float r)
{

    //std::cout << "Setting range to " << r << std::endl;

    range = r;

    repaint();
}

void WaveformAxes::plotSpike(SorterSpikePtr s, Graphics& g)
{
	if (s.get() == nullptr) return;
    float h = getHeight();
	g.setColour(Colour(s->color[0], s->color[1], s->color[2]));
    //g.setColour(Colours::pink);
    //compute the spatial width for each waveform sample
    float dx = getWidth()/float(s->getChannel()->getTotalSamples());

    /*
    float align = 8 * getWidth()/float(spikeBuffer[0].nSamples);
    g.drawLine(align,
                       0,
                       align,
                       h);
    */
	
	int spikeSamples = s->getChannel()->getTotalSamples();
    // type corresponds to channel so we need to calculate the starting
    // sample based upon which channel is getting plotted
	int offset = channel*spikeSamples; //spikeBuffer[0].nSamples * type; //

    //int dSamples = 1;

    float x = 0.0f;

	for (int i = 0; i < spikeSamples - 1; i++)
	{
		//std::cout << s.data[sampIdx] << std::endl;


		float s1 = h - (h / 2 + s->getData()[offset + i] / (range)* h);
		float s2 = h - (h / 2 + s->getData()[offset + i + 1] / (range)* h);
		if (signalFlipped)
		{
			s1 = h - s1;
			s2 = h - s2;
		}
		g.drawLine(x,
			s1,
			x + dx,
			s2);

		x += dx;
	}

}

void WaveformAxes::drawThresholdSlider(Graphics& g)
{

    // draw display threshold (editable)
    g.setColour(thresholdColour);
    if (signalFlipped)
    {
        float h = getHeight()-(getHeight()*(0.5f - displayThresholdLevel/range));
        g.drawLine(0, h, getWidth(), h);
        g.drawText(String(roundFloatToInt(displayThresholdLevel)),2,h,35,10,Justification::left, false);
    }
    else
    {
        float h = getHeight()*(0.5f - displayThresholdLevel/range);
        g.drawLine(0, h, getWidth(), h);
        g.drawText(String(roundFloatToInt(displayThresholdLevel)),2,h,35,10,Justification::left, false);
    }

}


void WaveformAxes::drawWaveformGrid(Graphics& g)
{

    float h = getHeight();
    float w = getWidth();

    g.setColour(Colours::darkgrey);

    for (float y = -range/2; y < range/2; y += 25.0f)
    {
        if (y == 0)
            g.drawLine(0,h/2 + y/range*h, w, h/2+ y/range*h,2.0f);
        else
            g.drawLine(0,h/2 + y/range*h, w, h/2+ y/range*h);
    }

}


bool WaveformAxes::updateSpikeData(SorterSpikePtr s)
{
    if (!gotFirstSpike)
    {
        gotFirstSpike = true;
    }

    if (spikesReceivedSinceLastRedraw < bufferSize)
    {

        spikeIndex++;
        spikeIndex %= bufferSize;

        spikeBuffer.set(spikeIndex, s);

        spikesReceivedSinceLastRedraw++;

    }

    return true;

}

bool WaveformAxes::checkThreshold(SorterSpikePtr s)
{
    int sampIdx = s->getChannel()->getTotalSamples()*type;

	for (int i = 0; i < s->getChannel()->getTotalSamples() - 1; i++)
    {

        if (s->getData()[sampIdx] > displayThresholdLevel)
        {
            return true;
        }

        sampIdx++;
    }

    return false;

}

void WaveformAxes::clear()
{
    processor->clearRunningStatForSelectedElectrode();
    spikeBuffer.clear();
    spikeIndex = 0;
    int numSamples=40;
    for (int n = 0; n < bufferSize; n++)
    {
        spikeBuffer.add(nullptr);
    }

    repaint();
}

void WaveformAxes::mouseMove(const MouseEvent& event)
{

    // Point<int> pos = event.getPosition();

    float y = event.y;

    float h = getHeight()*(0.5f - displayThresholdLevel/range);
    if (signalFlipped)
    {
        h = getHeight() - h;
    }

    // std::cout << y << " " << h << std::endl;

    if (y > h - 10.0f && y < h + 10.0f && !isOverThresholdSlider)
    {
        thresholdColour = Colours::yellow;
        //  std::cout << "Yes." << std::endl;
        isOverThresholdSlider = true;
        // cursorType = MouseCursor::DraggingHandCursor;
    }
    else if ((y < h - 10.0f || y > h + 10.0f) && isOverThresholdSlider)
    {
        thresholdColour = Colours::red;
        isOverThresholdSlider = false;
    }
    else
    {
        // are we inside a box ?
        isOverUnit = 0;
        isOverBox = -1;
        strOverWhere = "";
        isOverUnitBox(event.x, event.y, isOverUnit, isOverBox, strOverWhere);

    }
    repaint();

}

int WaveformAxes::findUnitIndexByID(int ID)
{
    for (int k = 0; k < units.size(); k++)
        if (units[k].UnitID == ID)
            return k;
    return -1;
}

void WaveformAxes::mouseDown(const juce::MouseEvent& event)
{

    if (event.mods.isRightButtonDown())
    {
        clear();
    }

    float h = getHeight();
    float w = getWidth();
    float microsec_span = 40.0/30000.0 * 1e6;
    float microvolt_span = range/2;
    mouseDownX = event.x/w * microsec_span ;
    if (signalFlipped)
        mouseDownY=(h/2- (h-event.y))/(h/2)*microvolt_span;
    else
        mouseDownY=(h/2- event.y)/(h/2)*microvolt_span;

    if (isOverUnit > 0)
    {
        processor->getActiveElectrode()->spikeSort->setSelectedUnitAndBox(isOverUnit, isOverBox);
        int indx = findUnitIndexByID(isOverUnit);
        jassert(indx >= 0);
        mouseOffsetX = mouseDownX - units[indx].lstBoxes[isOverBox].x;
        mouseOffsetY = mouseDownY - units[indx].lstBoxes[isOverBox].y;
    }
    else
    {
        processor->getActiveElectrode()->spikeSort->setSelectedUnitAndBox(-1, -1);

    }
    //	MouseUnitOffset = ScreenToMS_uV(e.X, e.Y) - new PointD(boxOnDown.x, boxOnDown.y);

    // if (isOverThresholdSlider)
    // {
    //     cursorType = MouseCursor::DraggingHandCursor;
    // }
}


void WaveformAxes::mouseUp(const MouseEvent& event)
{
    if (bDragging)
    {
        bDragging = false;
        // send a message to processor to update its internal structure?
        Electrode* e = processor->getActiveElectrode();
        e->spikeSort->updateBoxUnits(units);
    }
    // if (isOverThresholdSlider)
    // {
    //     cursorType = MouseCursor::DraggingHandCursor;
    // }
}

void WaveformAxes::mouseDrag(const MouseEvent& event)
{
    bDragging = true;

    if (isOverUnit > 0)
    {
        // dragging a box
        // convert position to metric coordinates.
        float h = getHeight();
        float w = getWidth();
        float microsec_span = 40.0/30000.0 * 1e6;
        float microvolt_span = range/2;
        float x = event.x/w * microsec_span ;

        float y;
        if (signalFlipped)
            y=(h/2- (h-event.y))/(h/2)*microvolt_span;
        else
            y=(h/2- event.y)/(h/2)*microvolt_span;

        // update units position....

        for (int k=0; k<units.size(); k++)
        {
            if (units[k].getUnitID() == isOverUnit)
            {
                float oldx = units[k].lstBoxes[isOverBox].x;
                float oldy = units[k].lstBoxes[isOverBox].y;

                float dx = x - oldx;
                float dy = y - oldy;

                if (strOverWhere == "right")
                {
                    units[k].lstBoxes[isOverBox].w = x - oldx;
                }
                else if (strOverWhere == "left")
                {
                    units[k].lstBoxes[isOverBox].w += -dx;
                    units[k].lstBoxes[isOverBox].x = x;
                }
                else if ((!signalFlipped && strOverWhere == "top") || (signalFlipped && strOverWhere == "bottom"))
                {
                    units[k].lstBoxes[isOverBox].y += dy;
                    units[k].lstBoxes[isOverBox].h += dy;
                }
                else if ((!signalFlipped && strOverWhere == "bottom") || (signalFlipped && strOverWhere == "top"))
                {
                    units[k].lstBoxes[isOverBox].h = -dy;
                }
                else if ((!signalFlipped && strOverWhere == "bottomright") || (signalFlipped && strOverWhere == "topright"))
                {
                    units[k].lstBoxes[isOverBox].w = x - oldx;
                    units[k].lstBoxes[isOverBox].h = -dy;

                }
                else if ((!signalFlipped && strOverWhere == "bottomleft") || (signalFlipped && strOverWhere == "topleft"))
                {
                    units[k].lstBoxes[isOverBox].w += -dx;
                    units[k].lstBoxes[isOverBox].x = x;
                    units[k].lstBoxes[isOverBox].h = -dy;
                }
                else if ((!signalFlipped && strOverWhere == "topright") || (signalFlipped && strOverWhere == "bottomright"))
                {
                    units[k].lstBoxes[isOverBox].y += dy;
                    units[k].lstBoxes[isOverBox].h += dy;
                    units[k].lstBoxes[isOverBox].w = x - oldx;

                }
                else if ((!signalFlipped && strOverWhere == "topleft") || (signalFlipped && strOverWhere == "bottomleft"))
                {
                    units[k].lstBoxes[isOverBox].w += -dx;
                    units[k].lstBoxes[isOverBox].x = x;
                    units[k].lstBoxes[isOverBox].y += dy;
                    units[k].lstBoxes[isOverBox].h += dy;

                }
                else if (strOverWhere == "inside")
                {
                    units[k].lstBoxes[isOverBox].x = x-mouseOffsetX;
                    units[k].lstBoxes[isOverBox].y = y-mouseOffsetY;
                }

                if (units[k].lstBoxes[isOverBox].h < 0)
                {
                    units[k].lstBoxes[isOverBox].y -= units[k].lstBoxes[isOverBox].h;
                    units[k].lstBoxes[isOverBox].h *= -1;
                    if (strOverWhere == "top")
                        strOverWhere = "bottom";
                    else if (strOverWhere == "bottom")
                        strOverWhere = "top";
                    else if (strOverWhere == "topleft")
                        strOverWhere = "bottomleft";
                    else if (strOverWhere == "topright")
                        strOverWhere = "bottomright";
                    else if (strOverWhere == "bottomleft")
                        strOverWhere = "topleft";
                    else if (strOverWhere == "bottomright")
                        strOverWhere = "topright";
                }
                if (units[k].lstBoxes[isOverBox].w < 0)
                {
                    units[k].lstBoxes[isOverBox].x += units[k].lstBoxes[isOverBox].w;
                    units[k].lstBoxes[isOverBox].w *= -1;
                    if (strOverWhere == "left")
                        strOverWhere = "right";
                    else if (strOverWhere == "right")
                        strOverWhere = "left";
                    else if (strOverWhere == "topleft")
                        strOverWhere = "topright";
                    else if (strOverWhere == "topright")
                        strOverWhere = "topleft";
                    else if (strOverWhere == "bottomleft")
                        strOverWhere = "bottomright";
                    else if (strOverWhere == "bottomright")
                        strOverWhere = "bottomleft";
                }

            }

        }

        //void WaveformAxes::isOverUnitBox(float x, float y, int &UnitID, int &BoxID, String &where)
    }
    else  if (isOverThresholdSlider)
    {

        float thresholdSliderPosition ;
        if (signalFlipped)
            thresholdSliderPosition = (getHeight()-float(event.y)) / float(getHeight());
        else
            thresholdSliderPosition =  float(event.y) / float(getHeight());

        if (thresholdSliderPosition > 1)
            thresholdSliderPosition = 1;
        else if (thresholdSliderPosition < -1) // Modified to allow negative thresholds.
            thresholdSliderPosition =-1;


        displayThresholdLevel = (0.5f - thresholdSliderPosition) * range;
        // update processor
        processor->getActiveElectrode()->thresholds[channel] = displayThresholdLevel;
        SpikeSorterEditor* edt = (SpikeSorterEditor*) processor->getEditor();
        for (int k=0; k<processor->getActiveElectrode()->numChannels; k++)
            edt->electrodeButtons[k]->setToggleState(false, dontSendNotification);

        edt->electrodeButtons[channel]->setToggleState(true, dontSendNotification);

        edt->setThresholdValue(channel,displayThresholdLevel);
    }
    repaint();
}

// MouseCursor WaveAxes::getMouseCursor()
// {
//     MouseCursor c = MouseCursor(cursorType);

//     return c;
// }

void WaveformAxes::mouseExit(const MouseEvent& event)
{
    if (isOverThresholdSlider)
    {
        isOverThresholdSlider = false;
        thresholdColour = Colours::red;
        repaint();
    }
}

float WaveformAxes::getDisplayThreshold()
{
    return displayThresholdLevel;
}

void WaveformAxes::setDetectorThreshold(float t)
{
    displayThresholdLevel = t;
}



void WaveformAxes::isOverUnitBox(float x, float y, int& UnitID, int& BoxID, String& where)
{

    float h = getHeight();
    float w = getWidth();
    // Map box coordinates to screen coordinates.
    // Assume time span is 40 samples at 30 Khz?
    float microsec_span = 40.0/30000.0 * 1e6;
    float microvolt_span = range/2;

    // Typical spike is 40 samples, at 30kHz ~ 1.3 ms or 1300 usecs.
    for (int k = 0; k < units.size(); k++)
    {
        for (int boxiter = 0; boxiter< units[k].lstBoxes.size(); boxiter++)
        {
            Box B = units[k].lstBoxes[boxiter];
            float rectx1 = B.x / microsec_span * w;
            float recty1 = h/2 - (B.y / microvolt_span * h/2);
            float rectx2 = (B.x+B.w) / microsec_span * w;
            float recty2 = h/2 - ((B.y-B.h) / microvolt_span * h/2);

            if (signalFlipped)
            {
                recty1 = h-recty1;
                recty2 = h-recty2;
            }

            if (rectx1 > rectx2)
                swapVariables(rectx1,rectx2);
            if (recty1 > recty2)
                swapVariables(recty1,recty2);

            if (x >= rectx1 - 10 & y >= recty1 -10 & x <= rectx2 + 10 & y <= recty2+10)
            {
                //setMouseCursor(MouseCursor::DraggingHandCursor);
                UnitID = units[k].UnitID;
                BoxID = boxiter;
                if (x >= rectx1 - 10 & x <= rectx1 + 10 && y >= recty1-10 & y <= recty1+10)
                {
                    where = "topleft";
                    setMouseCursor(MouseCursor::TopLeftCornerResizeCursor);
                }
                else if (x >= rectx2 - 10 & x <= rectx2 + 10 && y >= recty1-10 & y <= recty1+10)
                {
                    where = "topright";
                    setMouseCursor(MouseCursor::TopRightCornerResizeCursor);
                }
                else if (x >= rectx1 - 10 & x <= rectx1 + 10 && y >= recty2-10 & y <= recty2+10)
                {
                    where = "bottomleft";
                    setMouseCursor(MouseCursor::BottomLeftCornerResizeCursor);
                }
                else if (x >= rectx2 - 10 & x <= rectx2 + 10 && y >= recty2-10 & y <= recty2+10)
                {
                    where = "bottomright";
                    setMouseCursor(MouseCursor::BottomRightCornerResizeCursor);
                }
                else if (x >= rectx1 - 10 & x <= rectx1 + 10)
                {
                    where = "left";
                    setMouseCursor(MouseCursor::LeftEdgeResizeCursor);
                }
                else if (x >= rectx2 - 10 & x <= rectx2 + 10)
                {
                    where = "right";
                    setMouseCursor(MouseCursor::RightEdgeResizeCursor);
                }
                else if (y >= recty1 - 10 & y <= recty1 + 10)
                {
                    setMouseCursor(MouseCursor::TopEdgeResizeCursor);
                    where = "top";
                }
                else if (y >= recty2 - 10 & y <= recty2 + 10)
                {
                    where = "bottom";
                    setMouseCursor(MouseCursor::BottomEdgeResizeCursor);
                }
                else
                {
                    setMouseCursor(MouseCursor::DraggingHandCursor);
                    where = "inside";
                }
                return;
            }
        }
    }

    setMouseCursor(MouseCursor::NormalCursor); // not inside any boxes
}

void WaveformAxes::drawBoxes(Graphics& g)
{
    // y and h are given in micro volts.
    // x and w and given in micro seconds.

    float h = getHeight();
    float w = getWidth();
    // Map box coordinates to screen coordinates.
    // Assume time span is 40 samples at 30 Khz?
    float microsec_span = 40.0/30000.0 * 1e6;
    float microvolt_span = range/2;

    int selectedUnitID, selectedBoxID;
    processor->getActiveElectrode()->spikeSort->getSelectedUnitAndBox(selectedUnitID, selectedBoxID);

    // Typical spike is 40 samples, at 30kHz ~ 1.3 ms or 1300 usecs.
    for (int k = 0; k < units.size(); k++)
    {
        g.setColour(Colour(units[k].ColorRGB[0],units[k].ColorRGB[1],units[k].ColorRGB[2]));

        for (int boxiter = 0; boxiter < units[k].lstBoxes.size(); boxiter++)
        {
            Box B = units[k].lstBoxes[boxiter];

            float thickness = 2;
            if (units[k].getUnitID() == selectedUnitID && boxiter == selectedBoxID)
                thickness = 3;
            else if (units[k].getUnitID() == isOverUnit && boxiter == isOverBox)
                thickness = 2;
            else
                thickness = 1;


            float rectx1 = B.x / microsec_span * w;
            float recty1 = (h/2 - (B.y / microvolt_span * h/2));

            float rectx2 = (B.x+B.w) / microsec_span * w;
            float recty2 = (h/2 - ((B.y-B.h) / microvolt_span * h/2));

            //std::cout << rectx1 << " " << rectx2 << " " << recty1 << " " << recty2 << std::endl;

            float drawRecty1, drawRecty2;
            if (signalFlipped)
            {
                drawRecty2 = h-recty1;
                drawRecty1 = h-recty2;
            }
            else
            {
                drawRecty1 = recty1;
                drawRecty2 = recty2;
            }
            g.drawRect(rectx1,drawRecty1,rectx2-rectx1, drawRecty2-drawRecty1,thickness);
            g.drawText(String(units[k].UnitID), rectx1,drawRecty1-15,rectx2-rectx1,15,juce::Justification::centred,false);

        }
    }
}



void WaveformAxes::updateUnits(std::vector<BoxUnit> _units)
{
    units = _units;
}

void WaveformAxes::paint(Graphics& g)
{
    g.setColour(Colours::black);
    g.fillRect(0,0,getWidth(), getHeight());

    // int chan = 0;

    if (drawGrid)
        drawWaveformGrid(g);

    double noise = processor->getSelectedElectrodeNoise();
    String d = "STD: " + String(noise, 2) + "uV";
    g.setFont(Font("Small Text", 13, Font::plain));
    g.setColour(Colours::white);

    g.drawText(d, 10, 10, 150, 20, Justification::left, false);
    // draw the grid lines for the waveforms

    // draw the threshold line and labels
    drawThresholdSlider(g);
    drawBoxes(g);
    // if no spikes have been received then don't plot anything
    if (!gotFirstSpike)
    {
        return;
    }


    for (int spikeNum = 0; spikeNum < bufferSize; spikeNum++)
    {

        if (spikeNum != spikeIndex)
        {
            g.setColour(Colours::grey);
            plotSpike(spikeBuffer[spikeNum], g);
        }

    }

    g.setColour(Colours::white);
    plotSpike(spikeBuffer[spikeIndex], g);

    spikesReceivedSinceLastRedraw = 0;

}

// --------------------------------------------------


PCAProjectionAxes::PCAProjectionAxes(SpikeSorter* p) : GenericDrawAxes(0), processor(p), imageDim(500),
    rangeX(250), rangeY(250), spikesReceivedSinceLastRedraw(0)
{
    projectionImage = Image(Image::RGB, imageDim, imageDim, true);
    bufferSize = 600;
    pcaMin[0] = pcaMin[1] = 0;
    pcaMax[0] = pcaMax[1] = 0;

    rangeSet = false;
    inPolygonDrawingMode = false;
    clear();
    updateProcessor = false;
    isOverUnit = -1;

    rangeUpButton = new UtilityButton("+", Font("Small Text", 10, Font::plain));
    rangeUpButton->setRadius(3.0f);
    rangeUpButton->addListener(this);
    addAndMakeVisible(rangeUpButton);

    rangeDownButton = new UtilityButton("-", Font("Small Text", 10, Font::plain));
    rangeDownButton->setRadius(3.0f);
    rangeDownButton->addListener(this);
    addAndMakeVisible(rangeDownButton);

    redrawSpikes = true;

}

void PCAProjectionAxes::resized()
{

    rangeDownButton->setBounds(10,10, 20, 15);
    rangeUpButton->setBounds(35,10, 20, 15);
}

void PCAProjectionAxes::setPolygonDrawingMode(bool on)
{
    if (on)
    {
        inPolygonDrawingMode = true;
        setMouseCursor(MouseCursor::CrosshairCursor);

    }
    else
    {
        inPolygonDrawingMode = false;
        setMouseCursor(MouseCursor::NormalCursor);
    }
}

void PCAProjectionAxes::updateUnits(std::vector<PCAUnit> _units)
{
    units = _units;
}

void PCAProjectionAxes::drawUnit(Graphics& g, PCAUnit unit)
{
    float w = getWidth();
    float h = getHeight();

    int selectedUnitID, selectedBoxID;
    processor->getActiveElectrode()->spikeSort->getSelectedUnitAndBox(selectedUnitID, selectedBoxID);
    g.setColour(Colour(unit.ColorRGB[0],unit.ColorRGB[1],unit.ColorRGB[2]));
    if (unit.poly.pts.size() > 2)
    {
        float thickness;
        if (unit.getUnitID() == selectedUnitID)
            thickness = 3;
        else if (unit.getUnitID() == isOverUnit)
            thickness = 2;
        else
            thickness = 1;

        double cx=0,cy=0;
        for (int k=0; k<unit.poly.pts.size()-1; k++)
        {
            // convert projection coordinates to screen coordinates.
            float x1 = (unit.poly.offset.X + unit.poly.pts[k].X - pcaMin[0]) / (pcaMax[0]-pcaMin[0]) * w;
            float y1 = (unit.poly.offset.Y + unit.poly.pts[k].Y - pcaMin[1]) / (pcaMax[1]-pcaMin[1]) * h;
            float x2 = (unit.poly.offset.X + unit.poly.pts[k+1].X - pcaMin[0]) / (pcaMax[0]-pcaMin[0]) * w;
            float y2 = (unit.poly.offset.Y + unit.poly.pts[k+1].Y - pcaMin[1]) / (pcaMax[1]-pcaMin[1]) * h;
            cx+=x1;
            cy+=y1;
            g.drawLine(x1,y1,x2,y2,thickness);
        }
        float x1 = (unit.poly.offset.X + unit.poly.pts[0].X - pcaMin[0]) / (pcaMax[0]-pcaMin[0]) * w;
        float y1 = (unit.poly.offset.Y + unit.poly.pts[0].Y - pcaMin[1]) / (pcaMax[1]-pcaMin[1]) * h;
        float x2 = (unit.poly.offset.X + unit.poly.pts[unit.poly.pts.size()-1].X - pcaMin[0]) / (pcaMax[0]-pcaMin[0]) * w;
        float y2 = (unit.poly.offset.Y + unit.poly.pts[unit.poly.pts.size()-1].Y - pcaMin[1]) / (pcaMax[1]-pcaMin[1]) * h;
        g.drawLine(x1,y1,x2,y2,thickness);

        cx+=x2;
        cy+=y2;

        g.drawText(String(unit.UnitID), (cx/unit.poly.pts.size())-10,(cy/unit.poly.pts.size())-10,20,15,juce::Justification::centred,false);
    }
}

void PCAProjectionAxes::paint(Graphics& g)
{

    spikesReceivedSinceLastRedraw = 0;

    g.drawImage(projectionImage,
                0, 0, getWidth(), getHeight(),
                0, 0, rangeX, rangeY);


    // draw pca units polygons
    for (int k=0; k<units.size(); k++)
    {
        drawUnit(g, units[k]);
    }


    if (inPolygonDrawingMode)
    {
        setMouseCursor(MouseCursor::CrosshairCursor);
        // draw polygon
        bool first = true;
        PointD prev;

        if (drawnPolygon.size() > 0)
        {
            g.setColour(Colour(drawnUnit.ColorRGB[0],drawnUnit.ColorRGB[1],drawnUnit.ColorRGB[2]));

            for (std::list<PointD>::iterator it = drawnPolygon.begin(); it != drawnPolygon.end(); it++)
            {
                if (first)
                {
                    first = false;
                }
                else
                {
                    g.drawLine((*it).X, (*it).Y, prev.X,prev.Y);
                }
                prev = *it;
            }

            g.drawLine(drawnPolygon.front().X,drawnPolygon.front().Y,drawnPolygon.back().X,drawnPolygon.back().Y);
        }
    }

    //Graphics im(projectionImage);

    if (redrawSpikes)
    {
        // recompute image
        //int w = getWidth();
        //int h = getHeight();
        projectionImage.clear(juce::Rectangle<int>(0, 0, projectionImage.getWidth(), projectionImage.getHeight()),
                              Colours::black);

        bool subsample = false;
        int dk = (subsample) ? 5 : 1;

        for (int k=0; k<bufferSize; k+=dk)
        {
            drawProjectedSpike(spikeBuffer[k]);
        }
        redrawSpikes = false;
    }

}


void PCAProjectionAxes::drawProjectedSpike(SorterSpikePtr s)
{
    if (s != nullptr && rangeSet)
    {
        Graphics g(projectionImage);

        g.setColour(Colour(s->color[0],s->color[1],s->color[2]));

        float x = (s->pcProj[0] - pcaMin[0]) / (pcaMax[0]-pcaMin[0]) * rangeX;
        float y = (s->pcProj[1] - pcaMin[1]) / (pcaMax[1]-pcaMin[1]) * rangeY;
        if (x >= 0 & y >= 0 & x <= rangeX & y <= rangeY)
            g.fillEllipse(x,y,2,2);
    }
}

void PCAProjectionAxes::redraw(bool subsample)
{
    Graphics g(projectionImage);

    // recompute image
    //int w = getWidth();
    //int h = getHeight();
    projectionImage.clear(juce::Rectangle<int>(0, 0, projectionImage.getWidth(), projectionImage.getHeight()),
                          Colours::black);

    int dk = (subsample) ? 5 : 1;

    for (int k=0; k<bufferSize; k+=dk)
    {
        drawProjectedSpike(spikeBuffer[k]);
    }

}

void PCAProjectionAxes::setPCARange(float p1min, float p2min, float p1max, float p2max)
{

    pcaMin[0] = p1min;
    pcaMin[1] = p2min;
    pcaMax[0] = p1max;
    pcaMax[1] = p2max;
    rangeSet = true;
    redrawSpikes = true;
    processor->getActiveElectrode()->spikeSort->setPCArange(p1min,p2min, p1max,  p2max);

}

bool PCAProjectionAxes::updateSpikeData(SorterSpikePtr s)
{

    if (spikesReceivedSinceLastRedraw < bufferSize)
    {

        spikeIndex++;
        spikeIndex %= bufferSize;

        spikeBuffer.set(spikeIndex, s);

        spikesReceivedSinceLastRedraw++;
        //drawProjectedSpike(newSpike);
        redrawSpikes = true;

    }
    return true;
}


void PCAProjectionAxes::clear()
{
    projectionImage.clear(juce::Rectangle<int>(0, 0, projectionImage.getWidth(), projectionImage.getHeight()),
                          Colours::black);


    spikeBuffer.clear();
    spikeIndex = 0;

    redrawSpikes = true;
    //repaint();
}


void PCAProjectionAxes::mouseDrag(const juce::MouseEvent& event)
{

    if (!inPolygonDrawingMode)
    {

        setMouseCursor(MouseCursor::DraggingHandCursor);
        int selectedUnitID, selectedBoxID;
        processor->getActiveElectrode()->spikeSort->getSelectedUnitAndBox(selectedUnitID, selectedBoxID);

        if (isOverUnit > 0 && selectedUnitID == isOverUnit)
        {
            // pan unit
            int unitindex=-1;

            for (int k=0; k<units.size(); k++)
            {
                if (units[k].getUnitID() == selectedUnitID)
                {
                    unitindex = k;
                    break;
                }
            }
            jassert(unitindex >= 0);

            int w = getWidth();
            int h = getHeight();
            float range0 = pcaMax[0]-pcaMin[0];
            float range1 = pcaMax[1]-pcaMin[1];

            float dx = float(event.x-prevx) / w*range0;
            float dy = float(event.y-prevy) / h*range1;


            units[unitindex].poly.offset.X += dx;
            units[unitindex].poly.offset.Y += dy;
            updateProcessor = true;
            // draw polygon
            prevx = event.x;
            prevy = event.y;

        }
        else
        {
            // Pan PCA space
            int w = getWidth();
            int h = getHeight();
            float range0 = pcaMax[0]-pcaMin[0];
            float range1 = pcaMax[1]-pcaMin[1];

            float dx = -float(event.x-prevx) / w*range0;
            float dy = -float(event.y-prevy) / h*range1;

            pcaMin[0]+=dx;
            pcaMin[1]+=dy;
            pcaMax[0]+=dx;
            pcaMax[1]+=dy;
            processor->getActiveElectrode()->spikeSort->setPCArange(pcaMin[0],pcaMin[1], pcaMax[0],  pcaMax[1]);

            // draw polygon
            prevx = event.x;
            prevy = event.y;

            redrawSpikes = true;
        }

    }
    else
    {
        int pixel_quantizer = 6;
        float distance = float(event.x-prevx)*float(event.x-prevx)+
                         float(event.y-prevy)*float(event.y-prevy);
        if (distance > pixel_quantizer*pixel_quantizer)  // add a point every n pixels.
        {
            drawnPolygon.push_back(PointD(event.x,event.y));
            // draw polygon
            prevx = event.x;
            prevy = event.y;

            repaint();
        }
    }

}

void PCAProjectionAxes::mouseUp(const juce::MouseEvent& event)
{
    repaint();
    //redraw(false);
    setMouseCursor(MouseCursor::NormalCursor);
    if	(updateProcessor)
    {
        processor->getActiveElectrode()->spikeSort->updatePCAUnits(units);
        updateProcessor = false;

    }

    if (inPolygonDrawingMode)
    {
        inPolygonDrawingMode = false;
        SpikeSorterEditor* edt = (SpikeSorterEditor*)processor->getEditor();
        edt->spikeSorterCanvas->addPolygonUnitButton->setToggleState(false, dontSendNotification);

        // convert pixel coordinates to pca space coordinates and update unit
        cPolygon poly;
        poly.pts.resize(drawnPolygon.size());
        int k=0;

        float w = getWidth();
        float h = getHeight();
        float range0 = pcaMax[0]-pcaMin[0];
        float range1 = pcaMax[1]-pcaMin[1];

        for (std::list<PointD>::iterator it = drawnPolygon.begin(); it != drawnPolygon.end(); it++,k++)
        {
            poly.pts[k].X = (*it).X / w * range0 + pcaMin[0];
            poly.pts[k].Y = (*it).Y / h * range1 + pcaMin[1];
        }
        drawnUnit.poly = poly;
        units.push_back(drawnUnit);
        // add a new PCA unit
        Electrode* e = processor->getActiveElectrode();
        e->spikeSort->addPCAunit(drawnUnit);


        uint8 r,g,b;
        e->spikeSort->getUnitColor(drawnUnit.getUnitID(), r,g,b);

        processor->addNewUnit(e->electrodeID, drawnUnit.getUnitID(),r,g,b);

        drawnPolygon.clear();
    }
}


void PCAProjectionAxes::mouseMove(const juce::MouseEvent& event)
{
    isOverUnit = -1;
    float w = getWidth();
    float h = getHeight();

    for (int k=0; k<units.size(); k++)
    {
        // convert projection coordinates to screen coordinates.
        float x1 = ((float)event.x/w) * (pcaMax[0]-pcaMin[0]) +  pcaMin[0];
        float y1 = ((float)event.y/h) * (pcaMax[1]-pcaMin[1]) +  pcaMin[1];
        if (units[k].isPointInsidePolygon(PointD(x1,y1)))
        {
            isOverUnit = units[k].getUnitID();
            break;
        }

    }


}


void PCAProjectionAxes::mouseDown(const juce::MouseEvent& event)
{
    prevx = event.x;
    prevy = event.y;
    if (event.mods.isRightButtonDown())
    {
        clear();
    }
    if (inPolygonDrawingMode)
    {
        drawnUnit = PCAUnit(processor->getActiveElectrode()->spikeSort->generateUnitID(),processor->getActiveElectrode()->spikeSort->generateLocalID());
        drawnPolygon.push_back(PointD(event.x,event.y));
    }
    else
    {
        if (isOverUnit > 0)
            processor->getActiveElectrode()->spikeSort->setSelectedUnitAndBox(isOverUnit, -1);
        else
            processor->getActiveElectrode()->spikeSort->setSelectedUnitAndBox(-1, -1);
    }
}


bool PCAProjectionAxes::keyPressed(const KeyPress& key)
{
    KeyPress e = KeyPress::createFromDescription("escape");

    if (key.isKeyCode(e.getKeyCode()) && inPolygonDrawingMode) // C
    {
        inPolygonDrawingMode = false;
        setMouseCursor(MouseCursor::NormalCursor);
        return true;
    }
    return false;
}

void PCAProjectionAxes::rangeDown()
{
    float range0 = pcaMax[0]-pcaMin[0];
    float range1 = pcaMax[1]-pcaMin[1];
    pcaMin[0] = pcaMin[0] - 0.1 * range0;
    pcaMax[0] = pcaMax[0] + 0.1 * range0;
    pcaMin[1] = pcaMin[1] - 0.1 * range1;
    pcaMax[1] = pcaMax[1] + 0.1 * range1;
    setPCARange(pcaMin[0], pcaMin[1], pcaMax[0], pcaMax[1]);
}

void PCAProjectionAxes::rangeUp()
{
    float range0 = pcaMax[0]-pcaMin[0];
    float range1 = pcaMax[1]-pcaMin[1];
    pcaMin[0] = pcaMin[0] + 0.1 * range0;
    pcaMax[0] = pcaMax[0] - 0.1 * range0;
    pcaMin[1] = pcaMin[1] + 0.1 * range1;
    pcaMax[1] = pcaMax[1] - 0.1 * range1;

    setPCARange(pcaMin[0], pcaMin[1], pcaMax[0], pcaMax[1]);

}

void PCAProjectionAxes::buttonClicked(Button* button)
{
    if (button == rangeDownButton)
    {
        rangeDown();
    }

    else if (button == rangeUpButton)
    {
        rangeUp();
    }

}

void PCAProjectionAxes::mouseWheelMove(const MouseEvent& event, const MouseWheelDetails& wheel)
{
    if (wheel.deltaY > 0)
        rangeDown();
    else
        rangeUp();
}