<?php
/**
 * BlueWeather
 *
 * PHP version 7.2
 *
 * @category Tools
 * @package  BlueWeather
 * @author   Dorian Zedler <dorian@itsblue.de>
 * @license  GPLV3 gpl.com
 * @link     itsblue.de
 */

class BlueWeather
{
    private $_config;
    private $_con;

    /**
     * Constructor
     *
     * @param mixed $_config config array containing some stuff
     *
     * @return void
     */
    public function __construct($_config)
    {
        $this->_config = $_config;
        $this->_con = mysqli_connect(
            $_config['dbhost'],
            $_config['dbuser'],
            $_config['dbpassword'],
            $_config['dbname']
        );

        if (!$this->_con) {
            echo "Error connecting to database: " . mysqli_connect_error();
            http_response_code(500);
            die();
        }

    }

    // -------------------
    // - user management -
    // -------------------

    /**
     * Function to login users
     *
     * @param string $username username
     * @param string $password passowrd
     *
     * @return string '' or session token
     */
    public function loginUser($username, $password)
    {
        $sql = "SELECT * FROM `users`
        WHERE`username`=\"".$this->_con->real_escape_string($username)."\"";
        $result = $this->_con->query($sql);

        // only one row will be returned
        $data = $result->fetch_assoc();

        if (!password_verify($password, $data['password'])) {
            return "";
        }

        //generate random token
        $length = 10;
        $str = "";
        $characters = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9'));
        $max = count($characters) - 1;
        for ($i = 0; $i < $length; $i++) {
            $rand = mt_rand(0, $max);
            $str .= $characters[$rand];
        }
        $token_hash = password_hash($str, PASSWORD_DEFAULT);

        $sql = 'INSERT INTO `sessions` (userId, session)
                VALUES ("'. $data['id'] .'", "'. $token_hash .'")';

        if (!$this->_con->query($sql)) {
            return "";
        }

        return $token_hash;
    }

    /**
     * Function to check if a session token exists and get the corresponding user
     *
     * @param string $session session token
     *
     * @return int (-1: does not exist; >0: userId)
     */
    public function checkSession($session)
    {
        $sql = "SELECT * FROM `sessions`
        WHERE`session`=\"".$this->_con->real_escape_string($session)."\"";
        $result = $this->_con->query($sql);

        if (!$result->num_rows > 0) {
            return -1;
        }

        // only one row will be returned
        $data = $result->fetch_assoc();

        return($data['userId']);
    }

    /**
     * Function to user information
     *
     * @param int $userId id of the user
     *
     * @return mixed (array with some user information, or -1 if user not found)
     */
    public function getUserInfo($userId) 
    {
        $sql = "SELECT username,realname FROM `users`
        WHERE`id`=\"".$this->_con->real_escape_string($userId)."\"";
        $result = $this->_con->query($sql);

        if (!$result->num_rows > 0) {
            return -1;
        }

        // only one row will be returned
        $data = $result->fetch_assoc();

        return($data);
    }


    // --------------------
    // - getter functions -
    // --------------------

    /**
     * Function to get all locations from the database
     * 
     * @return mixed (array with all locations)
     */
    public function getAllLocations()
    {
        // get all locations
        $sql = "SELECT * FROM `locations`";
        $result = $this->_con->query($sql);

        //loop through the returned data
        while ($row = $result->fetch_assoc()) {
            $locations[] = $row;
        }

        return $locations;
    }

    /**
     * Function to get the data of a specific location
     * 
     * @param int   $locId   id of the location to return data of
     * @param mixed $range   contains 'from' and 'to' as unix timestamps
     * @param mixed $maxVals array containing:
     *                       'count': maximum measvals to be transmitted
     *                       'mode': 
     *                       - 'newest': will return the newest <maxVals> values
     *                       - 'oldest': will return the oldest <maxVals> values
     *                       - 'avg'   : <maxVals> sub avarages will be calculated
     * 
     * @return mixed object with all information about the location (see docs)
     */
    function getLocationData($locId, $range, $maxVals)
    {

        $locId = $this->_con->real_escape_string($locId);
        $range["from"] = $this->_con->real_escape_string($range["from"]);
        $range["to"] = $this->_con->real_escape_string($range["to"]);

        $sql = "SELECT * FROM `locations`
        WHERE`id`=$locId";
        $result = $this->_con->query($sql);
    
        // only one row will be returned
        $data = $result->fetch_assoc();
    
        if (!isset($range['from']) || $range['from'] === "") {
            $range['from'] = time() - 24 * 60 * 60;
        }

        if ($range['from'] < 0) {
            $range['from'] = time() + $range['from'];
        }
    
        if (!isset($range['to']) || $range['to'] === "") {
            $range['to'] = time();
        }
    
        // get all measvalues of given location
        $sql = "SELECT M.measvalue,M.sensorid,M.timestamp FROM measvalues M
        JOIN sensors S ON M.sensorid = S.id
        JOIN locations L ON S.locationid=L.id
        WHERE L.id=$locId AND M.timestamp > " . $range['from'] . 
                        " AND M.timestamp < " . $range['to'] . 
                        " ORDER BY timestamp ASC";
    
        $result = $this->_con->query($sql);
    
        while ($row = $result->fetch_assoc()) {
            $measvalues[] = $row;
        }
    
        // get all sensors of given location
        $sql = "SELECT * FROM `sensors`
        WHERE `locationid` = $locId";
    
        $result = $this->_con->query($sql);
    
        //loop through the returned data
        while ($row = $result->fetch_assoc()) {
            unset($row['locationid']); // remove locId as it is redundant
            $sensors[] = $row;
        }
    
        // get all value types
        $sql = "SELECT * FROM `valuetypes`";
    
        $result = $this->_con->query($sql);
    
        // get all necessaray valuetypes
        while ($row = $result->fetch_assoc()) {
            foreach ($sensors as $sensor) {
                if ($sensor['valuetypeid'] == $row['id']) {
                    $valuetypes[] = $row;
                    break;
                }
            }
        }
    
        if (isset($maxVals) && isset($maxVals['count']) && isset($maxVals['mode'])) {
            // build the new measvalues array with respect to maxVals for each sensor
    
            $finalMeasvals = array();
    
            foreach ($sensors as $sensor) {
                
                $rawMeasvalsOfThisSensor = array();
                $finalMeasvalsOfThisSensor = array();
    
                // get all measvalues of the current sensor
                foreach ($measvalues as $measvalue) {
                    if ($measvalue["sensorid"] === $sensor["id"]) {
                        array_push($rawMeasvalsOfThisSensor, $measvalue);
                    }
                }

                if (count($rawMeasvalsOfThisSensor) <= $maxVals['count']) {
                    // measvls don't exceed maxVals -> nothng needs to be done
                    $finalMeasvals = array_merge(
                        $finalMeasvals, $rawMeasvalsOfThisSensor
                    );
                    continue;
                }
    
                switch($maxVals['mode']) {
                case "newest": 
                    $finalMeasvalsOfThisSensor = array_slice(
                        $rawMeasvalsOfThisSensor, 
                        count($rawMeasvalsOfThisSensor) - $maxVals['count'],
                        count($rawMeasvalsOfThisSensor)
                    );
                    break;
                case "oldest":
                    $finalMeasvalsOfThisSensor = array_slice(
                        $rawMeasvalsOfThisSensor, 0, $maxVals['count']
                    );
                    break;
                case "avg":
                    // always sum up the same amount of values to get a new array
                    // which doesn't have more than $maxVals values
                    $countOfValuesForAvarage = intval(
                        round(count($rawMeasvalsOfThisSensor) / $maxVals['count'], 0)
                    );
        
                    $takenValuesForNextSum = 0;
                    $tmpMeasvalueSum = 0;
                    $tmpTimestampSum = 0;
        
                    for ($i = 0; $i < count($rawMeasvalsOfThisSensor); $i++) {
                        // loop through all measvals of the sensor
                        $measvalue = $rawMeasvalsOfThisSensor[$i];
                        
                        if ($measvalue["sensorid"] === $sensor["id"]) {
                            $tmpMeasvalueSum += $measvalue["measvalue"];
                            $tmpTimestampSum += $measvalue["timestamp"];
                            $takenValuesForNextSum += 1;
                        }
                        if ($takenValuesForNextSum === $countOfValuesForAvarage 
                            || $i === count($rawMeasvalsOfThisSensor) - 1
                        ) {
                            array_push(
                                $finalMeasvalsOfThisSensor,
                                array(
                                    "measvalue" => round(
                                        $tmpMeasvalueSum / $takenValuesForNextSum, 2
                                    ),
                                    "timestamp" => round(
                                        $tmpTimestampSum / $takenValuesForNextSum, 0
                                    ),
                                    "sensorid" => $sensor["id"]
                                )
                            );
        
                            $takenValuesForNextSum = 0;
                            $tmpMeasvalueSum = 0;
                            $tmpTimestampSum = 0;
                        }
                    }
                    break;
                }
    
                // insert the new vals of this sensor into the global new vals
                $finalMeasvals = array_merge(
                    $finalMeasvals, $finalMeasvalsOfThisSensor
                );
            }
    
            $measvalues = $finalMeasvals;
        }
    
        // find actual range
        $min = null;
        $max = null;

        foreach ($measvalues as $value) {
            if ($value['timestamp'] < $min || !isset($min)) {
                $min = $value['timestamp'];
            } else if ($value['timestamp'] > $max || !isset($max)) {
                $max = $value['timestamp'];
            }
        }
    
        // add sensors and value types to data object
        $data['measvalues'] = $measvalues;
        $data['sensors'] = $sensors;
        $data['valuetypes'] = $valuetypes;
        $data['range'] = array('from' => $min, 'to' => $max);

        return $data;
    }

    // --------------------
    // - setter functions -
    // --------------------

    /**
     * Function to generate an api key for specific user
     * 
     * @param int $userId userId
     * 
     * @return bool
     */
    function createApiKey($userId) 
    {
        
        //generate random token
        $length = 10;
        $str = "";
        $characters = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9'));
        $max = count($characters) - 1;
        for ($i = 0; $i < $length; $i++) {
            $rand = mt_rand(0, $max);
            $str .= $characters[$rand];
        }

        $apiKey = password_hash($str, PASSWORD_DEFAULT);

        $sql = "INSERT INTO `apikeys` (userid, apikey)
                VALUES (\"$userId\", \"$apiKey\")";

        return $this->_con->query($sql);
    }
    
    /**
     * Function to validate and insert submited sensor data
     * 
     * @param mixed $sensorData object containing 
     *                          - identity (int)
     *                          - signature (string)
     *                          - data (string: JSON array[array[sensorId,measvalue,timestamp]])
     * 
     * @return array    
     *  'status':  200: was inserted
     *      400: format of request was wrong
     *      401: signture verify failed
     *      404: key identity was not found
     *      500: internal error
     *      900: format of data JSON string was invalid
     *      901: signature was fine but one or more sensors have been ignored due to some error (see 'data') all other sensors were processes successfully
     *  'data': Array[Array[sensorId, errorCode]] sensors which were ignored due to some error (401: user doesn't own sensor; 404: senso wasn't found) 
     *          only set if status is 901
     */
    function processSensorData($sensorData) 
    {
        
        // check if request structure is valid
        if (!isset($sensorData['identity']) || !isset($sensorData['signature']) || !isset($sensorData['data'])) {
            return array("status"=>400);
        }

        // get private key from database
        $thisIdentity = $this->_con->real_escape_string($sensorData['identity']);
        $thisData = $sensorData['data'];

        // get public key of user
        $sql = "SELECT * FROM `apikeys` WHERE id=$thisIdentity";
        
        $result = $this->_con->query($sql);
        if ($result->num_rows < 1) {
            // public key was not found
            return array("status"=>404);
        }

        // only one row will be returned
        $data = $result->fetch_assoc();
        $key = $data['apikey'];
        $thisUserId = $data["userid"];

        // check signature
        $signedHash = hash_hmac("sha256", $thisData, $key);

        if ($signedHash !== strtolower($sensorData['signature'])) {
            return array("status"=>401);
        }

        // signature is valid

        $measvalues = json_decode($thisData, true);

        if (!$measvalues) {
            // the format of the data JSON is invalid
            return array("status"=>901, "data"=>"malformed data JSON");
        }

        $allSensors = array();
        $validSensors = array();
        $ignoredSensors = array();

        // find all sensors
        foreach ($measvalues as $measvalue) {
            $thisSensorId = $this->_con->real_escape_string($measvalue[0]);
            if (!in_array($thisSensorId, $allSensors)) {
                array_push($allSensors, $thisSensorId);
            }
        }

        // check if all sensors are valid and belong to the user
        foreach ($allSensors as $sensor) {
            // check if sensor belongs to user
            $sql = "SELECT userid FROM `sensors` WHERE id=\"$sensor\"";
        
            $result = $this->_con->query($sql);

            if ($result->num_rows < 1) {
                // sensor was not found
                array_push($ignoredSensors, array($sensor, 404));
                continue;
            }
    
            // only one row will be returned
            $data = $result->fetch_assoc();
            
            if ($data['userid'] !== $thisUserId) {
                // sensor does not belong to user
                array_push($ignoredSensors, array($sensor, 401));
                continue;
            } 

            array_push($validSensors, $sensor);
        }

        // iterate through all measvalues 
        // and insert them into the database if the sensor is valid
        foreach ($measvalues as $measvalue) {
            if (!in_array($measvalue[0], $validSensors)) {
                // if the sensor is not valid -> continue
                continue;
            }

            $thisSensorId = $this->_con->real_escape_string($measvalue[0]);
            $thisMeasvalue = $this->_con->real_escape_string($measvalue[1]);
            $thisTimestamp = $this->_con->real_escape_string($measvalue[2]);

            $sql = "INSERT INTO `measvalues` (sensorid,measvalue,timestamp)
                VALUES (\"$thisSensorId\", \"$thisMeasvalue\", \"$thisTimestamp\")";

            $this->_con->query($sql);
        }

        if (count($ignoredSensors) > 0) {
            return array("status"=>901, "data"=>$ignoredSensors);
        }
        return array("status"=>200);
    }

    // --------------------
    // - Helper functions -
    // --------------------

    /**
     * Function to convert a string to hex
     * 
     * @param string $string the string to be converted
     * 
     * @return string
     */
    function strToHex($string)
    {
        $hex = '';
        for ($i=0; $i<strlen($string); $i++) {
            $ord = ord($string[$i]);
            $hexCode = dechex($ord);
            $hex .= substr('0'.$hexCode, -2);
        }
        return strToUpper($hex);
    }

    /**
     * Function to convert hex to string
     * 
     * @param string $hex hex to be converted
     * 
     * @return string
     */
    function hexToStr($hex)
    {
        $string='';
        for ($i=0; $i < strlen($hex)-1; $i+=2) {
            $string .= chr(hexdec($hex[$i].$hex[$i+1]));
        }
        return $string;
    }

}

?>