
using namespace std;

class probes
{
    public:
        probes();
        void saveProbeName(std::string address, std::string name, short enabled);
        void saveProbeMin(int arrayID);  // save minimum reading
        void saveProbeMax(int arrayID);  // save maximum reading
        void resetProbeMin(short address);  // reset minimum reading
        void resetProbeMax(short address);  // reset maximum reading
        void startupProbes();  // run at startup
        void checkProbes();  // check probes
        void compileProbesJson();  // compile data to be sent to front end
        void saveProbeCharts();  // save probe data for chart
        void calculateSlopes(short arrayID);  // calculate slope of probe 
        void startCalibration(short address, short type, std::string buffer);  // start calibration
        void calibrationRunning();  // run calibration
        void deleteCalibration(short address);  // delete calibration
        short getCalRunning(std::string address);  // get calRunning status
        double getValue(std::string address);  // get value
    private:
        int probeI2Cfile[10] = {};  // used when reading and writing to I2C bus
        short probePlugged[10] = {};  // track if probe is marked plugged in.
        double value[10] = {};  // ph
        double minProbeValue[10] = {};  // minimum reading
        double maxProbeValue[10] = {};  //  maximum reading
        short probeAddress[10] = {};  // used to store sensor ID up to 50
        short probeAddressSaved[10] = {};  // used to store sensor ID up to 50
        short calType[10] = {};  // track what calibration type is used - 1 or 2 point
        short calRunning[10] = {};  // track if calibration is running for a probe
        double buffer1[10] = {};  // buffer 1 solution value
        double buffer2[10] = {};  // buffer 2 solution value
        double cal1[10] = {};  // calibration 1 value read during calibration
        double cal2[10] = {};  // calibration 2 value read during calibration
        double mc1[10] = {};
        double bc1[10] = {};
        double tempCalData[5] = {};  // used to hold current calibration data in case calibration is cancelled half way through
        short probeCount = 0;  // tracks how many probes are seen
        short probesSaved = 0;  // tracks how many probes are saved
        short stabilizeTickCount = 0;  // used to count how stabe reading is
        short totalTickCount = 0;  // used to skip step if reading doesn't stabilize
        short calAddressArrayID = 0;  // track array location of probe being calibrated
        double pastPH = 0;  // track last pH during calibration so we know if it goes out of target range
        unsigned long long prevMillisProbe = 0;
        unsigned long long prevMillisSaveP = 0;
        char inData[10];
        char phRequest[5];  // array of instructions to send to circuit
};
probes::probes(){}

short probes::getCalRunning(std::string address) {  // get calRunning status
    short arrayID = 0;
    for(short a=0; a < probeCount; a++) {  //
        if (probeAddress[a] == std::stoi(address)) {arrayID = a; break;}
    }
    return calRunning[arrayID];
}

double probes::getValue(std::string address) {  // get calRunning status
    short arrayID = 0;
    for(short a=0; a < probeCount; a++) {  //
        if (probeAddress[a] == std::stoi(address)) {arrayID = a; break;}
    }
    return value[arrayID];
}

void probes::compileProbesJson() {  // compile data to be sent to front end
    for(short a=0; a < probeCount; a++) {  //
        outgoingDataString += "{\"id\":\"";
        outgoingDataString += to_string(probeAddress[a]);

        outgoingDataString += "\",\"v\":\"";
        std::stringstream v;
        v << std::fixed << std::setprecision(3) << value[a];  // round to 3 decimal places
        outgoingDataString += v.str();

        if (calRunning[a] == 0) {  // normal data to send
            outgoingDataString += "\",\"l\":\"";
            std::stringstream min;
            min << std::fixed << std::setprecision(2) << minProbeValue[a];  // round to 2 decimal places
            outgoingDataString += min.str();
            
            outgoingDataString += "\",\"h\":\"";
            std::stringstream max;
            max << std::fixed << std::setprecision(2) << maxProbeValue[a];  // round to 2 decimal places
            outgoingDataString += max.str();
        }
        else if ((calRunning[a] >= 4) && (calRunning[a] <= 6)) {  // data to send when each calibration point is finished
            outgoingDataString += "\",\"l\":\"";
            std::stringstream min;
            min << std::fixed << std::setprecision(2) << 20;
            outgoingDataString += min.str();
            
            outgoingDataString += "\",\"h\":\"";
            std::stringstream max;
            max << std::fixed << std::setprecision(2) << calRunning[a];  // round to 2 decimal places
            outgoingDataString += max.str();

            if (calType[a] == 1) {calRunning[a] = 0;}
            else if (calRunning[a] == 6) {calRunning[a] = 0;}
            // if (calType[a] == 1) 
        }
        else {  // data to send during calibration
            outgoingDataString += "\",\"l\":\"";
            std::stringstream min;
            min << std::fixed << std::setprecision(2) << stabilizeTickCount;
            outgoingDataString += min.str();
            
            outgoingDataString += "\",\"h\":\"";
            std::stringstream max;
            max << std::fixed << std::setprecision(2) << calRunning[a];  // round to 2 decimal places
            outgoingDataString += max.str();
        }
        outgoingDataString += "\"}";

        if (a < probeCount - 1) outgoingDataString += ",";  // add comma if another sensor is coming
    }
}

void probes::saveProbeName(std::string address, std::string name, short enabled) {  // save custom name
    // std::cout << "saveDS18B20Name    - " << std::endl;
    // std::cout << "name    - " << name << std::endl;
    // std::cout << "address - " << address << std::endl;
    // std::cout << "plugged - " << enabled << std::endl;
    connectMysql();
    std::string query = "UPDATE probes SET name = '" + name + "', plugged = '" + to_string(enabled) + "' WHERE address = '" + address + "'";
    mysql_query(conn, query.c_str()); // run query
    mysql_disconnect(); 
}

void probes::saveProbeMin(int arrayID) {  // save minimum reading
    minProbeValue[arrayID] = value[arrayID]; 
    connectMysql();
    std::string query = "UPDATE probes SET min = '" + to_string(value[arrayID]) + "' WHERE address = '" + to_string(probeAddress[arrayID]) + "'";
    mysql_query(conn, query.c_str());  // run query
    mysql_disconnect(); 
}

void probes::saveProbeMax(int arrayID) {  // save maximum reading
    maxProbeValue[arrayID] = value[arrayID]; 
    connectMysql();
    std::string query = "UPDATE probes SET max = '" + to_string(value[arrayID]) + "' WHERE address = '" + to_string(probeAddress[arrayID]) + "'";
    mysql_query(conn, query.c_str());  // run query
    mysql_disconnect(); 
}

void probes::resetProbeMin(short address) {  // reset minimum reading
    for (short a = 0; a < probeCount; a++) {
        if (probeAddress[a] == address) {saveProbeMin(a); break;}
    }
}

void probes::resetProbeMax(short address) {  // reset maximum reading
    for (short a = 0; a < probeCount; a++) {
        if (probeAddress[a] == address) {saveProbeMax(a); break;}
    }
}

void probes::startupProbes() {  // run once when Pi starts
    // if (startupComplete == 1)  // wait till sensors are read once on startup so we know what's plugged in
    // {
    std::cout << " " << std::endl;
    std::cout << "startupProbes-----------------------" << std::endl;

    std::fill_n(probeI2Cfile, 10, 0);  // reset array
    std::fill_n(probePlugged, 10, 0);  // reset array
    std::fill_n(value, 10, 0);  // reset array
    std::fill_n(minProbeValue, 10, 0);  // reset array
    std::fill_n(maxProbeValue, 10, 0);  // reset array
    std::fill_n(probeAddress, 10, 0);  // reset array
    std::fill_n(probeAddressSaved, 10, 0);  // reset array
    std::fill_n(calType, 10, 0);  // reset array
    std::fill_n(calRunning, 10, 0);  // reset array
    std::fill_n(buffer1, 10, 0);  // reset array
    std::fill_n(buffer2, 10, 0);  // reset array
    std::fill_n(cal1, 10, 0);  // reset array
    std::fill_n(cal2, 10, 0);  // reset array
    std::fill_n(mc1, 10, 0);  // reset array
    std::fill_n(bc1, 10, 0);  // reset array

    MYSQL_RES *res;
    MYSQL_ROW row;

    connectMysql();
    std::string query = "SELECT address, plugged, min, max, buffer1, buffer2, cal1, cal2, calType FROM probes";  // construct query string
    mysql_query(conn, query.c_str());  // run query
    res = mysql_use_result(conn);
    
    probesSaved = 0;
    probeCount = 0;
    while ((row = mysql_fetch_row(res)) != NULL)
    {
        probeAddressSaved[probesSaved] = atoi(row[0]);
        probePlugged[probesSaved] = atoi(row[1]);

        std::cout << "probeAddressSaved      - " << probeAddressSaved[probesSaved] << std::endl;
        if (probePlugged[probesSaved] == 1) 
        {
            probeAddress[probeCount] = probeAddressSaved[probesSaved];
            std::cout << "probeAddress             - " << probeAddress[probeCount] << std::endl;
            minProbeValue[probeCount] = atof(row[2]);
            maxProbeValue[probeCount] = atof(row[3]);
            buffer1[probeCount] = atof(row[4]);
            buffer2[probeCount] = atof(row[5]);
            cal1[probeCount] = atof(row[6]);
            cal2[probeCount] = atof(row[7]);
            calType[probeCount] = atof(row[8]);
            value[probeCount] = 0;
            calRunning[probeCount] = 0;
            mc1[probeCount] = 0;
            bc1[probeCount] = 0;
            probeCount++;
        }
        probesSaved++;
    }
    mysql_free_result(res);
    mysql_disconnect(); 

    std::cout << "probesSaved            - " << probesSaved << std::endl;
    std::cout << "probeCount             - " << probeCount << std::endl;
    // open connection for pH circuit
    char *filename = (char*)"/dev/i2c-1"; // Define the i2c file path
    for (short a=0; a < probeCount; a++)  // read each circuit
    {
        probeI2Cfile[a] = open(filename, O_RDWR); // open file for read and right
        calculateSlopes(a);
    }

    std::cout << "startupProbes Complete-----------------------" << std::endl;
    // }

}

void probes::checkProbes() {  // check probes
    if (currentMillisTime >= (prevMillisProbe + 3000))  // check sensors every second
    {
        for (short a=0; a < probeCount; a++)  // read each circuit
        {
            phRequest[0] = 0x52;    // 0x52 = R  - used to poll circuit for pH - https://www.rapidtables.com/convert/number/ascii-to-hex.html
            phRequest[1] = 0;    // 0 = end of line for circuit
            
            // // open connection for pH circuit
            if (ioctl(probeI2Cfile[a], I2C_SLAVE, probeAddress[a]) < 0)
            {
                std::cout << "Failed to acquire bus access" << std::endl;
                return;
            }
            
            write(probeI2Cfile[a], phRequest, 2); // initiate read
            delay(5);
            short length = 10;			//<<< Number of bytes to read
            if (read(probeI2Cfile[a], inData, 10) != length) {
                // std::cout << "Failed to read from the i2c bus - PROBE." << std::endl;
            }
            else 
            {
                for(short b=0; b < 10; b++) {inData[b] = inData[b+1];}  // remove first character of data
                
                try {value[a] = std::stof(inData);}  // try to extract ph from data
                catch(invalid_argument& e) {std::cout << "ERROR 102 --------------------- " << std::endl; return;}
                  
                // if ((value[a] < 0.0) || (value[a] > 14.0)) {return;}  // verify reading is valid, exit if it isn't
                
                if (calRunning[a] == 0)  // if calibration isn't running apply the calibration
                {
                    double phMv = (7 - value[a]) * 59.16;  // convert pH to mV
                    double temp_mvPerPH = 59.16;  // calibrated mV per pH with temperature compensation
                    double mVPerPH = 59.16;;  // calibrated mV per pH without temperature compensation
                    double pHc = 0;

                    switch (calType[a])
                    {
                        case 1:  // 1 point calibration being used
                            pHc = mc1[a] * phMv + bc1[a];
                            phMv = (7.0-pHc)*mVPerPH;
                        break;

                        case 2:  // 2 point calibration being used
                            pHc = mc1[a] * phMv + bc1[a];
                            mVPerPH = (cal2[a] - (cal1[a])) / 3;
                            phMv = (7.0-pHc) * mVPerPH;
                        break;
                    }
                    
                    double tempCalibrated = 25.0;
                    double tempC = 25.0;
                    
                    temp_mvPerPH = mVPerPH * (tempC + 273.15) / ((tempCalibrated) + 273.15);  // calculate mV per step with temperature
                    value[a] = (7.0 - (phMv / temp_mvPerPH));  // convert to pH

                    if (value[a] < minProbeValue[a]) {saveProbeMin(a);}
                    if (value[a] > maxProbeValue[a]) {saveProbeMax(a);}
                }
                else if ((calRunning[a] >= 2) && (calRunning[a] <= 3)) {calibrationRunning();}
            }
        }
        prevMillisProbe = currentMillisTime;
    }
}

void probes::saveProbeCharts() {  // save chart data
    if (currentMillisTime >= (prevMillisSaveP + 300000)) {  // write every 5 minutes

    // std::cout << "saveProbeCharts-----------------------" << std::endl;
        // std::stringstream logTimestamp;  
        // time_t now = time(0);
        // tm *ltm = localtime(&now);
        // std::string monF = systemSettings.format_number(1 + ltm->tm_mon, 2);
        // std::string dayF = systemSettings.format_number(ltm->tm_mday, 2);
        // std::string hourF = systemSettings.format_number(ltm->tm_hour, 2);
        // std::string minF = systemSettings.format_number(ltm->tm_min, 2);
        // std::string secF = systemSettings.format_number(ltm->tm_sec, 2);

        // logTimestamp.str(std::string());
        // logTimestamp << 1900 + ltm->tm_year  // create timestamp string for database
        //              << "/"
        //              << monF
        //              << "/"
        //              << dayF
        //              << " "
        //              << hourF
        //              << ":"
        //              << minF
        //              << ":"
        //              << secF;
                     
        
        time_t rawtime = currentMillisTime / 1000;
        struct tm ts;
        char buf[80];

        // Format time, "ddd yyyy-mm-dd hh:mm:ss zzz"
        ts = *localtime(&rawtime);
        strftime(buf, sizeof(buf), "%Y/%m/%d %H:%M:%S", &ts);
        std::string logTimestamp = buf;

        for (short a=0; a < probeCount; a++)
        {
            std::ofstream outfile;
            std::string path = "/var/www/html/chartData/p" + to_string(probeAddress[a]) + ".csv";
            std::string data = logTimestamp + ", " + to_string(value[a]) + "\n";

            outfile.open(path, std::ios_base::app); // append instead of overwrite
            outfile << data; 
            outfile.close();

            connectMysql3();
            MYSQL_RES *res;
            MYSQL_ROW row;

            // check if sensor table exists in database
            std::string query = "SHOW TABLES LIKE 'p" + to_string(probeAddress[a]) + "'";  // construct query string
            mysql_query(conn3, query.c_str());  // run query
        
            res = mysql_use_result(conn3);
            if (mysql_fetch_row(res) == NULL)
            {
                // check new table in database for probes data
                query = "CREATE TABLE p" + to_string(probeAddress[a]) + " (dataID int AUTO_INCREMENT, \
                                                            value float, \
                                                            timestamp varchar(40), \
                                                            PRIMARY KEY (dataID))";  // construct query string
                mysql_query(conn3, query.c_str());  // run query
            }
            mysql_free_result(res);
            mysql_disconnect3(); 
            
            connectMysql3();
            query = "INSERT INTO p" + to_string(probeAddress[a]) + "(value,timestamp) VALUES('" + to_string(value[a]) + "','" + logTimestamp + "')";
            mysql_query(conn3, query.c_str());  // run query
            
            /* close connection */
            mysql_disconnect3(); 
        }
        prevMillisSaveP = currentMillisTime;
    }
}

void probes::startCalibration(short address, short type, std::string buffer) {  // start calibration
    for (short i = 0; i < probeCount; i++) { 
        if (probeAddress[i] == address) {
            if (type == 3) {  // start calibration counter for buffer 1
                stabilizeTickCount = 0;
                totalTickCount = 0;
                calRunning[i] = 2;
                try {buffer1[i] = (7 - std::stof(buffer)) * 59.16;}  // latest version available
                catch(invalid_argument& e) {std::cout << "ERROR 102 --------------------- " << std::endl; buffer1[i] = 7;}
                  // convert to mV
            }
            else if (type == 4) {  // start calibration counter for buffer 2
                stabilizeTickCount = 0;
                totalTickCount = 0;
                calRunning[i] = 3;
                try {buffer2[i] = (7 - std::stof(buffer)) * 59.16;}  // latest version available
                catch(invalid_argument& e) {std::cout << "ERROR 102 --------------------- " << std::endl; buffer2[i] = 10;}
                  // convert to mV
            }
            else if (type == 5) {  // cancel calibration 
                stabilizeTickCount = 0;
                totalTickCount = 0;
                calRunning[i] = 0;
                calType[i] = tempCalData[0];
                buffer1[i] = tempCalData[1];
                buffer2[i] = tempCalData[2];
                cal1[i] = tempCalData[3];
                cal2[i] = tempCalData[4];
                calculateSlopes(calAddressArrayID);
            }
            else if (type <= 2) {  // this step sends uncalibrated data to frontend
                calAddressArrayID = i;
                calRunning[i] = 1;
                tempCalData[0] = calType[i];  // track calType in case calibration is cancelled after starting
                tempCalData[1] = buffer1[i];
                tempCalData[2] = buffer2[i];
                tempCalData[3] = cal1[i];
                tempCalData[4] = cal2[i];
                calType[i] = type;
                break;
            }
        }
    }
}

void probes::calibrationRunning() {  // calibration running
    totalTickCount++;
    
    if ((value[0] >= pastPH-0.04) && (value[0] <= pastPH+0.04)) {  // add tick if pH doesn't swing +/- 0.04
        stabilizeTickCount++;
    }
    else {stabilizeTickCount = 0; pastPH = value[0];}  // reset last reading and count if pH gets out of range

    if (totalTickCount >= 300) stabilizeTickCount = 20;  // if reading doesn't stablize within 15 minutes go on
    if (stabilizeTickCount == 20) {  // wait for 20 readings within range to pass
        if (calRunning[calAddressArrayID] == 2) {  // calibration is 1 point
            cal1[calAddressArrayID] = (7 - value[calAddressArrayID]) * 59.16;  // convert to mV

            if (calType[calAddressArrayID] == 1) {  // single point calibration finished
                buffer2[calAddressArrayID] = -177.48;
                cal2[calAddressArrayID] = -177.48;
                calRunning[calAddressArrayID] = 4;
                calculateSlopes(calAddressArrayID);
            }
            else {calRunning[calAddressArrayID] = 4;}
        }
        else if (calRunning[calAddressArrayID] == 3) {  // calibration is 2 point
            cal2[calAddressArrayID] = (7 - value[calAddressArrayID]) * 59.16;  // convert to mV
            calRunning[calAddressArrayID] = 6;
            calculateSlopes(calAddressArrayID);
        }

        connectMysql();
        std::string query = "UPDATE probes SET buffer1 = '" + to_string(buffer1[calAddressArrayID]) + "', buffer2 = '" + to_string(buffer2[calAddressArrayID]) + "', cal1 = '" + to_string(cal1[calAddressArrayID]) + "', cal2 = '" + to_string(cal2[calAddressArrayID]) + "', calType = '" + to_string(calType[calAddressArrayID]) + "' WHERE address = '" + to_string(probeAddress[calAddressArrayID]) + "'";
        mysql_query(conn, query.c_str()); // run query
        mysql_disconnect(); 
        // std::cout << "" << std::endl;
        // std::cout << "FINISHED ------------------------- " << std::endl;
        // std::cout << "buffer1[calAddressArrayID]       - " << buffer1[calAddressArrayID] << std::endl;
        // std::cout << "buffer2[calAddressArrayID]       - " << buffer2[calAddressArrayID] << std::endl;
        // std::cout << "cal1[calAddressArrayID]          - " << cal1[calAddressArrayID] << std::endl;
        // std::cout << "cal2[calAddressArrayID]          - " << cal2[calAddressArrayID] << std::endl;
        // std::cout << "calType[calAddressArrayID] 2     - " << calType[calAddressArrayID] << std::endl;
        // std::cout << "calRunning[calAddressArrayID] 2  - " << calRunning[calAddressArrayID] << std::endl;

    }
}

void probes::deleteCalibration(short address) {  // delete calibration
    for (short i = 0; i < probeCount; i++) { 
        if (probeAddress[i] == address) {  // find probe being calibrated
            buffer1[i] = 0;
            buffer2[i] = -177.48;
            cal1[i] = 0;
            cal2[i] = -177.48;
            calType[i] = 1;
            calculateSlopes(i);
            connectMysql();
            std::string query = "UPDATE probes SET buffer1 = '" + to_string(buffer1[i]) + "', buffer2 = '" + to_string(buffer2[i]) + "', cal1 = '" + to_string(cal1[i]) + "', cal2 = '" + to_string(cal2[i]) + "', calType = '" + to_string(calType[i]) + "' WHERE address = '" + to_string(address) + "'";
            mysql_query(conn, query.c_str()); // run query
            mysql_disconnect(); 
            break;
        }
    }
}

void probes::calculateSlopes(short arrayID) {  // calculate slope of probe 
    double m1; //slope for 4-7
    double b1; //offset for 4-7
    double m2; //slope for 7-10
    double b2; //offset for 7-10

    double bufferPh1 = 7.0 - (buffer1[arrayID] / 59.16); // convert mV to pH
    double bufferPh2 = 7.0 - (buffer2[arrayID] / 59.16); // convert mV to pH
    //  double bufferPh3 = 7.0 - (buffer3 / 59.16); // convert mV to pH

    m1 = (bufferPh2 - bufferPh1) / (buffer2[arrayID] - buffer1[arrayID]);
    b1 = bufferPh2 - buffer2[arrayID] * m1;

    mc1[arrayID] = (bufferPh2 - bufferPh1) / (cal2[arrayID] - cal1[arrayID]);
    bc1[arrayID] = bufferPh2 - cal2[arrayID] * mc1[arrayID]; 
}

probes probes;
