<?php

/**
 * Represent a precipitation value as a string.
 *
 * @param float|string|null $val The precipitation value, which can be a float, string, or null
 * @param int $precision The number of decimal places to round to (default is 2)
 * @return string A string representation of the precipitation value, with special handling for null and empty
 */
function precip2str($val, $precision=2) {
    // null returns missing value
    if (is_null($val) || $val === "") {
        return "M";
    }
    // These are already in good shape
    if ($val === "T") {
        return $val;
    }
    // Ensure we can convert this value to float, if it is a string currently
    if (is_string($val)) {
        try {
            $val = floatval($val);
        } catch (Exception $e) {
            return "M";
        }
    }
    // Handle IEM nomeclature for 0.0001, which is a tough 32bit case, sigh
    if ($val > 0 && $val < 0.005) {
        return "T";
    }

    $fmt = "%.{$precision}f";
    return sprintf($fmt, $val);
}

/**
 * Helper for the case of snow, which should be 1 decimal
 * @param float|string|null $val The snow value, which can be a float, string, or null
 * @return string A string representation of the snow value, with special handling for null and empty
 */
function snow2str($val) {
    return precip2str($val, 1);
}


/**
 * Common logic for converting a hacky 12 char time string in UTC
 *
 * @param string $dstr The date string in the format YYYYMMDDHHMM
 * @param int $minute_modulo The minute must be a multiple of this value (default is 5)
 * @return DateTimeImmutable The corresponding DateTimeImmutable object in UTC
 * @throws Exception If the date string is invalid or represents a future time
 */
function dstr2dt($dstr, $minute_modulo = 5) {
    if (is_null($dstr)) {
        http_response_code(422);
        die("Invalid date format");
    }
    // Validate dstr format (12 digits: YYYYMMDDHHMM)
    if (!preg_match('/^\d{12}$/', $dstr)) {
        http_response_code(422);
        die("Invalid date format");
    }
    // The bang ensures all values are zeroed out
    $dt = DateTimeImmutable::createFromFormat(
        "!YmdHi",
        $dstr,
        new DateTimeZone("UTC")
    );
    if ($dt === false) {
        http_response_code(422);
        die("Invalid date format");
    }
    $minute = (int)$dt->format("i");

    // Ensure the minute is modulo the specified value
    if ($minute % $minute_modulo != 0) {
        http_response_code(422);
        die("Minute must be a multiple of $minute_modulo");
    }

    $now = new DateTimeImmutable("now", new DateTimeZone("UTC"));
    if ($dt > $now) {
        http_response_code(422);
        die("Request from the future?");
    }
    return $dt;
}

/**
 * Convert a vague 3 character WFO identifier to a 4 character one
 * @param string $wfo3 the 3 character WFO identifier
 * @return string the 4 character WFO identifier
 */
function rectify_wfo($wfo3){
    $xref = Array(
        "AFC" => "PAFC",
        "AJK" => "PAJK",
        "AFG" => "PAFG",
        "HFO" => "PHFO",
        "GUM" => "PGUM",
        "SJU" => "TJSJ",
        "JSJ" => "TJSJ",
    );
    if (array_key_exists($wfo3, $xref)){
        return $xref[$wfo3];
    };
    return sprintf("K%s", $wfo3);
}

/**
 * Figure out the vague 3 character ID :/
 * @param string $wfo the 4 character WFO identifier
 * @return string the 3 character WFO identifier
 */
function unrectify_wfo($wfo){
    if (strlen($wfo) == 4){
        $wfo = substr($wfo, 1, 3);
    }
    if ($wfo == "SJU"){
        return "JSJ";
    }
    return $wfo;
}


function getClientIp() {
    if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ipArray = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        // Likely want the last IP in the list
        $ip = trim(end($ipArray));
    } else {
        $ip = $_SERVER['REMOTE_ADDR'];
    }
    return $ip;
}

/**
 * Generate HTML for a list of tags with links to their respective pages
 * @param array $tokens An array of tag strings to be displayed
 * @return string HTML string containing the formatted tags with links
 */
function printTags($tokens)
{
    if (sizeof($tokens) == 0 || $tokens[0] == "") {
        return "";
    }
    $s = "<br /><span style=\"font-size: smaller; float: left;\">Tags: &nbsp; ";
    foreach ($tokens as $k => $v) {
        $s .= sprintf(
            "<a href=\"/onsite/features/tags/%s.html\">%s</a> &nbsp; ",
            $v,
            $v
        );
    }
    $s .= "</span>";
    return $s;
}


/**
 * Workaround round() quirks
 * @param mixed $val The value to be rounded
 * @param int $prec The precision to round to
 * @return mixed The rounded value or the original value if it's null or a string
 */
function myround($val, $prec){
    if (is_null($val)) return "";
    if (is_string($val)) return $val;
    return round($val, $prec);
}

/**
 * Helper to make a service request and enforce a JSON response.
 * NOTE: Failures are fatal.
 * @param string $url The URL to make the request to, if this starts with just
 *   a '/', the internal base URL will be prepended.
 * @param array $args The arguments to be passed to the service
 * @return array The decoded JSON response if all goes well.
 */
function require_json_response($url, $args) {
    // Everything is method get at the moment
    $cgi = http_build_query($args);
    if (strpos($url, "/") === 0) {
        $url = IEMConfig::INTERNAL_BASEURL . $url;
    }
    $uri = "{$url}?{$cgi}";
    // Try twice to get the content
    $jobj = FALSE;
    for ($i = 0; $i < 2; $i++) {
        // Use curl to get the data with a 15 second timeout
        $ch = curl_init($uri);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_TIMEOUT, 15);
        $res = curl_exec($ch);
        $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($res === FALSE || $http_status != 200) {
            openlog("iem", LOG_PID | LOG_PERROR, LOG_LOCAL1);
            syslog(LOG_WARNING, "iemws fail[$i] $http_status ".
                "from:" . $_SERVER["REQUEST_URI"] .
                ' remote: ' . getClientIp() .
                ' to: ' . $uri);
            closelog();
            sleep(3);
            continue;
        }
        try {
            $jobj = json_decode($res, $assoc = TRUE);
            break;
        } catch (Exception $e) {
            // Log
            openlog("iem", LOG_PID | LOG_PERROR, LOG_LOCAL1);
            syslog(
                LOG_WARNING,
                "iemws jsonfail  from:" . $_SERVER["REQUEST_URI"] .
                    ' remote: ' . getClientIp() .
                    ' msg: ' . $e .
                    ' to: ' . $uri
            );
            closelog();
        }
    }
    if ($jobj === FALSE) {
        header("Content-type: text/plain");
        http_response_code(503);
        echo "Backend web service failure, please try again later.";
        exit();
    }
    return $jobj;
}

/**
 * Helper to make an iem-web-services call.
 * NOTE: Failures are fatal and will return a 503 to the client.
 * @param string $endpoint The endpoint of the webservice
 * @param array $args The arguments to be passed to the webservice
 * @return array The decoded JSON response if all goes well.
 */
function iemws_json($endpoint, $args)
{
     $uri = "http://iem-web-services.agron.iastate.edu:8080/{$endpoint}";
     return require_json_response($uri, $args);
}

// Make sure a page is HTTPS when called
function force_https()
{
    global $EXTERNAL_BASEURL;
    // Require that EXTERNAL_BASEURL is https, otherwise we have infinite loop
    if (strpos($EXTERNAL_BASEURL, "https://") !== 0) {
        return;
    }
    if (empty($_SERVER["HTTPS"]) || $_SERVER["HTTPS"] !== "on") {
        // Ensure we collapse folks that have bookmarks to aliases
        header("Location: {$EXTERNAL_BASEURL}" . $_SERVER["REQUEST_URI"]);
        exit();
    }
}

//________________________________________________________
/**
 * Sort a multi-dimensional array by a specific second-level index
 * @param array $multiArray The array to be sorted
 * @param string|int $secondIndex The second-level index to sort by
 * @param string $sorder The sort order, either "asc" for ascending or "desc" for descending
 * @return array The sorted array
 */
function aSortBySecondIndex($multiArray, $secondIndex, $sorder = "asc")
{
    reset($multiArray);
    if (sizeof($multiArray) == 0) {
        return array();
    }
    $indexMap = array();
    foreach ($multiArray as $firstIndex => $value) {
        if (array_key_exists($secondIndex, $value)) {
            $val = $value[$secondIndex];
        } else {
            $val = null;
        }
        $indexMap[$firstIndex] = $val;
    }
    if ($sorder == "asc") {
        asort($indexMap);
    } else {
        arsort($indexMap);
    }
    $sortedArray = array();
    foreach ($indexMap as $firstIndex => $unused) {
        $sortedArray[$firstIndex] = $multiArray[$firstIndex];
    }
    return $sortedArray;
}

/**
 * Convert Celsius to Fahrenheit
 * @param float|string $myC The temperature in Celsius
 * @return float|string The temperature in Fahrenheit or an empty string if input is empty
 */
function c2f($myC)
{
    if ($myC == "") {
        return "";
    }
    return round(((9.00 / 5.00) * $myC + 32.00), 2);
} // End of function c2f()

/**
 * Convert Fahrenheit to Celsius
 * @param float|string $tmpf The temperature in Fahrenheit
 * @return float|null The temperature in Celsius or null if input is null
 */
function f2c($tmpf)
{
    if (is_null($tmpf)) return null;
    return round(((5.00 / 9.00) * ((float)$tmpf - 32.00)), 2);
} // End of function f2c()


/**
 * Calculate the dew point temperature in Fahrenheit
 * @param float $tmpf The temperature in Fahrenheit
 * @param float $relh The relative humidity in percentage
 * @return float The dew point temperature in Fahrenheit
 */
function dwpf($tmpf, $relh)
{
    $tmpk = 273.15 + (5.00 / 9.00 * ($tmpf - 32.00));
    $dwpk = $tmpk / (1 + 0.000425 * $tmpk * - (log10($relh / 100)));
    return ($dwpk - 273.15) * 9.00 / 5.00 + 32;
}


/**
 * Calculate relative humidity
 *  /home/nawips/nawips56.e.1/gempak/source/gemlib/pr/prvapr.f
 *  /home/nawips/nawips56.e.1/gempak/source/gemlib/pr/prrelh.f
 *  @param float $tmpc The temperature in Celsius
 *  @param float $dwpc The dew point temperature in Celsius
 *  @return float|null The relative humidity in percentage
 */
function relh($tmpc, $dwpc)
{
    if (is_null($tmpc) || is_null($dwpc)) return null;
    $e  = 6.112 * exp((17.67 * (float)$dwpc) / ((float)$dwpc + 243.5));
    $es  = 6.112 * exp((17.67 * (float)$tmpc) / ((float)$tmpc + 243.5));
    $relh = ($e / $es) * 100.00;
    return round($relh, 0);
}

/**
 * Convert a wind direction in degrees to a textual representation (e.g., N, NE, E, etc.)
 * @param int|string $dir The wind direction in degrees
 * @return string The textual representation of the wind direction
 */
function drct2txt($dir)
{
    $dir = intval($dir);
    $directions = [
        "N" => [350, 360, 0, 13],
        "NNE" => [13, 35],
        "NE" => [35, 57],
        "ENE" => [57, 80],
        "E" => [80, 102],
        "ESE" => [102, 127],
        "SE" => [127, 143],
        "SSE" => [143, 166],
        "S" => [166, 190],
        "SSW" => [190, 215],
        "SW" => [215, 237],
        "WSW" => [237, 260],
        "W" => [260, 281],
        "WNW" => [281, 304],
        "NW" => [304, 324],
        "NNW" => [324, 350]
    ];

    foreach ($directions as $text => $ranges) {
        if (count($ranges) == 4) {
            if (($dir >= $ranges[0] && $dir <= $ranges[1]) || ($dir >= $ranges[2] && $dir < $ranges[3])) {
                return $text;
            }
        } else {
            if ($dir >= $ranges[0] && $dir < $ranges[1]) {
                return $text;
            }
        }
    }

    return "";
}
