#include <cstddef> // for size_t type (used as loop index instead of int to avoid compile warnings)
#include "DisplayManager.h"

DisplayManager::DisplayManager(int x, int y){
    sizeX = x;
    sizeY = y;
    last_row=0;
    last_col=0;
    imagesPerRow=0;

    displayZoom = 0;
    delay = 0;
    imagesPerRow=4;

    numberModules = 0;
    valuesAllocated = false;

    // Indicate to destructor that these variables have not been allocated yet:
    intermediateImages = NULL;
    inputImage = NULL;
    templateBar = NULL;
    bars = NULL;
}

DisplayManager::DisplayManager(const DisplayManager& copy){
    cout << "Internal error: empty DisplayManager copy constructor has been called" << endl;
}

DisplayManager::~DisplayManager(void){
    // Free memory allocated in several parts of the class
    while(!displays.empty()) {
        delete displays.back();
        displays.pop_back();
    }
    
    while(!multimeters.empty()) {
        delete multimeters.back();
        multimeters.pop_back();
    }

    if(intermediateImages != NULL){
        for (int i=0;i<numberModules-1;i++)
            delete intermediateImages[i];
        delete[] intermediateImages;
    }

    if(bars != NULL){
        for (int i=0;i<numberModules-1;i++)
            delete bars[i];
        delete[] bars;
    }

    if(inputImage != NULL)
        delete inputImage;
        
    if(templateBar != NULL)
        delete templateBar;
}

void DisplayManager::setLNFile(const char *file, double ampl){
    LNFile = file;
    LNfactor = ampl;
}

void DisplayManager::reset(){
    sizeX = 1;
    sizeY = 1;
    last_row=0;
    last_col=0;
    imagesPerRow=0;

    displayZoom = 0;
    delay = 0;
    imagesPerRow=4;

    valuesAllocated = false;
}

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

bool DisplayManager::allocateValues(int number, double tstep){

    if(valuesAllocated == false){
        simStep = tstep;
        numberModules = number;

        // security check
        if (displayZoom*(double)sizeY >= CImgDisplay::screen_width()/4){
            displayZoom = CImgDisplay::screen_width()/(4.0*(double)sizeY);
            cout << "zoom has been readjusted to "<< displayZoom << endl;
        }

        for(int k=0;k<numberModules;k++){
            isShown.push_back(false);
        }

        for(int k=0;k<numberModules;k++){
            margin.push_back(0.0);
        }

        valuesAllocated = true;
    }
    return(true);
}

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


bool DisplayManager::setSizeX(int x){
    bool ret_correct;
    if (x>0){
        sizeX = x;
        ret_correct=true;
    } else
        ret_correct=false;
    return(ret_correct);
}

bool DisplayManager::setSizeY(int y){
    bool ret_correct;    
    if (y>0){
        sizeY = y;
        ret_correct=true;
    } else
        ret_correct=false;
    return(ret_correct);
}

void DisplayManager::setZoom(double zoom){
    displayZoom = zoom;
}

void DisplayManager::setDelay(int displayDelay){
    delay = displayDelay;
}

void DisplayManager::setImagesPerRow(int numberI){
    imagesPerRow = numberI;
}

void DisplayManager::setIsShown(bool value,int pos){
    isShown[pos]=value;
}

void DisplayManager::setMargin(double m, int pos){
    margin[pos]=m;
}


bool DisplayManager::setSimStep(double step_value){
    bool ret_correct;
    if(step_value > 0){
        simStep = step_value;
        cout << "Display simStep = "<< simStep << endl;
        ret_correct=true;
    } else
        ret_correct=false;
    return(ret_correct);
}

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


void DisplayManager::addMultimeterTempSpat(string multimeterID, string moduleID, int param1, int param2,bool temporalSpatial, string Show){

    multimeter* nm= new multimeter(sizeX,sizeY);
    nm->setSimStep(simStep);
    multimeters.push_back(nm);

    multimeterIDs.push_back(multimeterID);
    moduleIDs.push_back(moduleID);
    vector <int> aux;
    aux.push_back(param1);
    aux.push_back(param2);
    multimeterParam.push_back(aux);

    if(temporalSpatial)
        multimeterType.push_back(0);
    else
        multimeterType.push_back(1);

    const char * ShowChar = (Show).c_str();
    if(strcmp(ShowChar, "False") == 0)
        isShown.push_back(false);
    else
        isShown.push_back(true);

}

void DisplayManager::addMultimeterLN(string multimeterID, string moduleID, int x, int y, double segment, double interval, double start, double stop, string Show){

    multimeter* nm= new multimeter(sizeX,sizeY);
    nm->setSimStep(simStep);
    multimeters.push_back(nm);

    multimeterIDs.push_back(multimeterID);
    moduleIDs.push_back(moduleID);
    vector <int> aux;
    aux.push_back(x);
    aux.push_back(y);
    multimeterParam.push_back(aux);

    LNSegment.push_back(segment);
    LNInterval.push_back(interval);
    LNStart.push_back(start);
    LNStop.push_back(stop);

    multimeterType.push_back(2);

    const char * ShowChar = (Show).c_str();
    if(strcmp(ShowChar, "False") == 0)
        isShown.push_back(false);
    else
        isShown.push_back(true);
}

void DisplayManager::modifyLN(string moduleID, double start, double stop){

    size_t pos = 0;
    const char * str1 = moduleID.c_str();

    for(size_t k=0;k<multimeterIDs.size();k++){
        const char * str2 = (multimeterIDs[k]).c_str();
        if(strcmp(str1,str2)==0){
            pos = k;
            break;
        }
    }

    LNStart[pos] = start;
    LNStop[pos] = stop;
}

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

void DisplayManager::addModule(int pos,string ID){

    // Input
    if(displays.size() == 0){

        // black image
        double newX = (double)sizeX * displayZoom;
        double newY = (double)sizeY * displayZoom;
        CImg <double> image ((int)newY,(int)newX,1,1,0.0);

        // create input display
        if(isShown.size() > 0 && isShown[0]){
            CImgDisplay *input = new CImgDisplay(image,"Norm. input",0);
            input->move(0,0);
            displays.push_back(input);
            inputImage = new CImg <double>(sizeY,sizeX,1,1,0.0);
        }else{
            displays.push_back(new CImgDisplay());
        }

        // initialize intermediate images at the first call
        if(numberModules > 1){
            intermediateImages = new CImg<double>*[numberModules-1];
            for (int i=0;i<numberModules-1;i++)
              intermediateImages[i] = new CImg<double> (sizeY,sizeX,1,1,0.0);
        }
    }

    if(pos > 0 && isShown.size() > (size_t)pos) { // display for pos==0 (Input) is create above
        if(isShown[pos]){

            // black image
            double newX = (double)sizeX * displayZoom;
            double newY = (double)sizeY * displayZoom;
            CImg <double> image((int)newY,(int)newX,1,1,0.0);

            // Color Bar
            CImg <double> bar(50,(int)newX, 1, 1);
            cimg_forXY(bar,x,y) {
                bar(x,(int)newX-y-1,0,0)=255*((double)y/newX);
            }
            bar.map(CImg<double>::jet_LUT256());

            // Create window
            const char * WindowName = (ID).c_str();
            CImgDisplay *disp = new CImgDisplay((image,bar),WindowName,0);
            // Display
            (image,bar).display(*disp);

            // new row of the display
            int capacity = int((CImgDisplay::screen_width()-newY-100) / (newY+50));

            if (last_col<capacity && last_col < imagesPerRow){
                last_col++;
            }else{
                last_col = 1;
                last_row++;
            }

            // move display
            disp->move((int)last_col*(newY+80.0),(int)last_row*(newX+80.0));

            // Save display
            displays.push_back(disp);
        }else
            displays.push_back(new CImgDisplay());
    }
}

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


void DisplayManager::updateDisplay(CImg <double> *input, Retina &retina, int simTime, double totalSimTime, double numberTrials,double totalNumberTrials){

    double newX = (double)sizeX * displayZoom;
    double newY = (double)sizeY * displayZoom;

    double max=0.0,min=0.0;
    const unsigned char color[] = {255,255,255};


    // Display input
    if(isShown.size() > 0 && isShown[0] && input != NULL){

        CImgDisplay *d0 = displays[0];
        *inputImage = *input;

        inputImage->crop(margin[0],margin[0],0,0,sizeY-margin[0]-1,sizeX-margin[0]-1,0,inputImage->spectrum()-1,false);
        min = inputImage->min_max(max); // find maximum and minimum values in all color channels
        if(max-min > DBL_EPSILON) // normalize Input before showing it if it has different pixel values
            ((255*(*inputImage - min)/(max-min))).resize((int)newY,(int)newX).display(*d0);
        else // If we normalize we lose the offset and so all the informaton: we better don't normalize
            inputImage->resize((int)newY,(int)newX).display(*d0);
    }

    // Update windows
    if (numberModules>0 && simTime==0){
        bars = new CImg<double>*[numberModules-1];
        templateBar = new CImg <double>(50,(int)newX, 1, 1);
        for(int i=0;i<numberModules-1;i++){
            bars[i] = new CImg <double>(50,(int)newX, 1, 1);
        }
    }

    // copy interm. images
    for(int i=0;i<numberModules-1;i++){
        module* m = retina.getModule(i+1);
        CImg<double> *module_output = m->getOutput();
        if(module_output != NULL)
            *intermediateImages[i] = *module_output;
    }

    // show modules
    for(int k=0;k<numberModules-1;k++){
        if(isShown[k+1]){

            CImgDisplay *d = displays[k+1];

            // Color Bar
            *bars[k]=*templateBar;
            cimg_forXY(*(bars[k]),x,y) {
                (*bars[k])(x,(int)newX-y-1,0,0)=255*((double)y/newX);
            }
            bars[k]->map(CImg<double>::jet_LUT256());


            // find maximum and minimum values
            min = findMin(intermediateImages[k]);
            max = findMax(intermediateImages[k]);

            // draw them
            std::ostringstream strs1,strs2;

            if(abs(min)<100)
                strs1 << min;
            else
                strs1 << (int)min;

            string str1 = strs1.str();
            string min_value_text = str1.substr(0,4);

            bars[k]->draw_text(0,bars[k]->height()-20,min_value_text.c_str(),color,0,1,20);

            if(abs(max)<100)
                strs2 << max;
            else
                strs2 << (int)max;

            string str2 = strs2.str();
            string max_value_text = str2.substr(0,4);

            bars[k]->draw_text(0,10,max_value_text.c_str(),color,0,1,20);

            // show image
            intermediateImages[k]->crop(margin[k+1],margin[k+1],0,0,sizeY-margin[k+1]-1,sizeX-margin[k+1]-1,0,0,false);
            if(max-min > DBL_EPSILON) // normalize image before showing it if all its pixel do not the same value
                ((255*(*intermediateImages[k] - min)/(max-min)).map(CImg<double>::jet_LUT256()).resize((int)newY,(int)newX),*bars[k]).display(*d);
            else // Do not normalize to preserve the pixel offset information
                (intermediateImages[k]->map(CImg<double>::jet_LUT256()).resize((int)newY,(int)newX),*bars[k]).display(*d);
        }
    }

    if(input!=NULL) { // If the retina has input, update multimeters
        for(size_t i=0;i<multimeters.size();i++){
            multimeter *m = multimeters[i];
            module *n;
            const char * moduleID = (moduleIDs[i]).c_str();

            // find target module
            if(strcmp(moduleID, "Input") != 0){
                for(int j=1;j<retina.getNumberModules();j++){
                    n = retina.getModule(j);
                    if(n->checkID(moduleID)){
                        break;
                    }
                }
            }

            // temporal and LN mult.
            if(multimeterType[i]==0 || multimeterType[i]==2){
                vector <int> aux = multimeterParam[i];

                if(strcmp(moduleID, "Input") == 0){
                    m->recordValue((*input)(aux[0],aux[1],0,0));
                }else{
                    CImg<double> *module_output = n->getOutput();
                    if(module_output != NULL)
                        m->recordValue((*module_output)(aux[0],aux[1],0,0));
                }

                m->recordInput((*input)(aux[0],aux[1],0,0));
            }
            // spatial mult.
            else if(multimeterType[i]==1){
                vector <int> aux = multimeterParam[i];
                if(simTime >= aux[1] && simTime < aux[1]+simStep) { // aux[1] may not be divisible by simStep, we check that aux[1] is in the current sim. slot
                    // set position
                    int capacity = int((CImgDisplay::screen_width()-newY-100) / (newY+50));

                    if (last_col<capacity && last_col < imagesPerRow){
                        last_col++;
                    }else{
                        last_col = 1;
                        last_row++;
                    }

                    if(isShown[numberModules+i]==true){

                        if(strcmp(moduleID, "Input") == 0){

                            if(aux[0]>0)
                                m->showSpatialProfile(input,true,aux[0],multimeterIDs[i],(int)last_col*(newY+80.0),(int)last_row*(newX+80.0),-1);
                            else
                                m->showSpatialProfile(input,false,-aux[0],multimeterIDs[i],(int)last_col*(newY+80.0),(int)last_row*(newX+80.0),-1);
                        }else{
                            CImg<double> *module_output = n->getOutput();
                            if(module_output != NULL) {
                                if(aux[0]>0)
                                    m->showSpatialProfile(module_output,true,aux[0],multimeterIDs[i],(int)last_col*(newY+80.0),(int)last_row*(newX+80.0),-1);
                                else
                                    m->showSpatialProfile(module_output,false,-aux[0],multimeterIDs[i],(int)last_col*(newY+80.0),(int)last_row*(newX+80.0),-1);
                            }
                        }
                    }
                }
            }
        }
    }

    // display temporal and LN multimeters for the last simulation step
    if(valuesAllocated && (simTime==totalSimTime-simStep || input == NULL)) { // if time to show or end of input:
        int LNMultimeters = 0;
        for(size_t i=0;i<multimeters.size();i++){
            multimeter *m = multimeters[i];

            // set position
            if(multimeterType[i]==0 || multimeterType[i]==2){
                int capacity = int((CImgDisplay::screen_width()-newY-100) / (newY+50));

                if (last_col<capacity && last_col < imagesPerRow){
                    last_col++;
                }else{
                    last_col = 1;
                    last_row++;
                }
            }

            // temporal mult.
            if(multimeterType[i]==0){

                if(isShown[numberModules+i]==true){
                    if (i<multimeters.size()-1)
                        m->showTemporalProfile(multimeterIDs[i],(int)last_col*(newY+80.0),(int)last_row*(newX+80.0),delay,LNFile);
                    else
                        m->showTemporalProfile(multimeterIDs[i],(int)last_col*(newY+80.0),(int)last_row*(newX+80.0),-1,LNFile);

                }else{
                    m->showTemporalProfile(multimeterIDs[i],(int)last_col*(newY+80.0),(int)last_row*(newX+80.0),-2,LNFile);
                }

            // LN mult.
            }else if(multimeterType[i]==2){
                if(isShown[numberModules+i]==false){
                    m->showLNAnalysis(multimeterIDs[i],(int)last_col*(newY+80.0),(int)last_row*(newX+80.0),-2,LNSegment[LNMultimeters]/simStep,LNInterval[LNMultimeters]/simStep,LNStart[LNMultimeters]/simStep,LNStop[LNMultimeters]/simStep,totalNumberTrials,LNFile);
                }else{
                    if (i<multimeters.size()-1)
                        m->showLNAnalysis(multimeterIDs[i],(int)last_col*(newY+80.0),(int)last_row*(newX+80.0),delay,LNSegment[LNMultimeters]/simStep,LNInterval[LNMultimeters]/simStep,LNStart[LNMultimeters]/simStep,LNStop[LNMultimeters]/simStep,totalNumberTrials,LNFile);
                    else
                        m->showLNAnalysis(multimeterIDs[i],(int)last_col*(newY+80.0),(int)last_row*(newX+80.0),-1,LNSegment[LNMultimeters]/simStep,LNInterval[LNMultimeters]/simStep,LNStart[LNMultimeters]/simStep,LNStop[LNMultimeters]/simStep,totalNumberTrials,LNFile);
                }

                // check last trial to show average results
                if(numberTrials == totalNumberTrials-1){
                    m->getSwitchTime(retina.getWhiteNoise()->getSwitchTime());
                    if(isShown[numberModules+i]==false){
                        m->showLNAnalysisAvg((int)last_col*(newY+80.0),(int)last_row*(newX+80.0),-2,LNSegment[LNMultimeters]/simStep,LNStart[LNMultimeters]/simStep, LNStop[LNMultimeters]/simStep,totalNumberTrials,LNFile,LNfactor);
                    }else{
                        m->showLNAnalysisAvg((int)last_col*(newY+80.0),(int)last_row*(newX+80.0),0,LNSegment[LNMultimeters]/simStep,LNStart[LNMultimeters]/simStep, LNStop[LNMultimeters]/simStep,totalNumberTrials,LNFile,LNfactor);
                    }
                }
                LNMultimeters++;
            }
        }
    }
    // Show displays if there's an input display
    if(isShown.size() > 0 && isShown[0])
        displays[0]->wait(delay);
}

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


double DisplayManager::findMin(CImg<double> *input){
    double min = DBL_INF;
    cimg_forXY(*(input),x,y) {
        if ((*input)(x,y,0,0) < min)
            min =(*input)(x,y,0,0);
    }

    return min;
}

double DisplayManager::findMax(CImg<double> *input){
    double max = -DBL_INF;
    cimg_forXY(*(input),x,y) {
        if ((*input)(x,y,0,0) > max)
            max =(*input)(x,y,0,0);
    }

    return max;
}