<?php

/**
 * Copyright (c) 2016. PlatyHouse. all rights reserved.
 */

function ptyTransformArray($inputArray, $keyName)
{
    $result = [];
    foreach ($inputArray as $value) {
        $result[] = [$keyName => $value];
    }
    return $result;
}


/**
 * 문자열일 경우 , 로 구분 처리하여 배열로 생성함
 * 배열일 경우 array_filter 를 통해 빈 항목 제거
 * @param $s
 * @return mixed
 */
function ptyGetMagicArray($s)
{
    if ($s instanceof \platyFramework\ptyArrayItemModel) {
        $s = $s->_item;
    }

    $out = $s;
    if (!is_array($out)) {
        $out = explode(",", $out);
    }
    $ins = array_filter($out);

    $out = [];
    foreach ($ins as $i => $oi) {
        $s = explode(",", $oi);

        foreach ($s as $value) {
            $out[] = $value; // 문자열을 정수로 변환하여 저장
        }
    }

    pd($out, "ptyGetMagicArray. $s");
    return $out;
}

/**
 * 필드들의 값이 비어있는지 체크합니다.
 * * 하나라도 비어 있으면 예외를 발생시킵니다.
 *
 * ### example
 * ```
 * ptyCheckEmptys(["board" => $board, "제목" => $title, "content" => $content]);
 * ```
 *
 * @param $arr
 * @return void
 */
function ptyCheckEmptys($arr)
{
    foreach ($arr as $title => $field) {
        ptyCheckEmpty($field, $title);
    }
}

function ptyCheckItem($s = null, string $title = "아이템이 없습니다")
{
    if ($s == "" || is_null($s) || $s == "undefined")
        ptyThrow($title, "ERROR_NOT_FOUND_ITEM");
}


function ptyCheckEmpty($s = null, string $parameterName = "")
{
    if ($s == "" || is_null($s) || $s == "undefined")
        ptyThrow("{$parameterName} parameter required", "ERROR_INVALID_PARAMETER");
}

function ptyCheckParamter($s = null, string $parameterName = "")
{
    if ($s == "" || is_null($s) || $s == "undefined")
        ptyThrow("{$parameterName} parameter required", "ERROR_INVALID_PARAMETER");
}

function ptyCheckNull($s = null, string $parameterName = "")
{
    if ($s === null)
        ptyThrow("{$parameterName} parameter required", "ERROR_INVALID_PARAMETER");
}

function ptyIsCli()
{
    if (php_sapi_name() != "cli")
        return false;

    return true;
}

/**
 * UserAgent 를 통해 PC, Mobile 여부 확인
 *
 * PC = true
 * Mobile = false
 *
 * @return bool
 */
function ptyIsPC()
{
    if (isset($_SERVER['HTTP_USER_AGENT'])) {
        if (stripos($_SERVER['HTTP_USER_AGENT'], 'curl') !== false) return false;
        if (stripos($_SERVER['HTTP_USER_AGENT'], 'dart') !== false) return false;
        if (stripos($_SERVER['HTTP_USER_AGENT'], 'axios') !== false) return false;
        return (bool)!preg_match("/(android|avantgo|blackberry|bolt|boost|cricket|docomo|fone|hiptop|mini|mobi|palm|phone|pie|tablet|up\.browser|up\.link|webos|wos)/i", $_SERVER["HTTP_USER_AGENT"]);
    }

    return false;
}

function ptyGetCurrentUrl($forTelegram = false)
{
    if (php_sapi_name() == "cli") {
        $cmd = $_SERVER['PWD'] . "/" . implode(" ", $_SERVER['argv']);
        return $cmd;
    } else {
        if (isset($_SERVER["HTTPS"]) && !empty($_SERVER["HTTPS"]) && ($_SERVER["HTTPS"] == 'on')) {
            $url = 'https://' . $_SERVER["SERVER_NAME"];

            if (($_SERVER["SERVER_PORT"] != 443)) {
                $url .= ":" . $_SERVER["SERVER_PORT"];
            }

        } else {
            $url = 'http://' . $_SERVER["SERVER_NAME"];
            if (($_SERVER["SERVER_PORT"] != 80)) {
                $url .= ":" . $_SERVER["SERVER_PORT"];
            }
        }

        $url .= $_SERVER["REQUEST_URI"];
    }

    if ($forTelegram)
        $url = str_replace("://", ":// ", $url);

    return $url;
}

function ptyGetCurrentUrlExceptParams()
{
    if (isset($_SERVER["HTTPS"]) && !empty($_SERVER["HTTPS"]) && ($_SERVER["HTTPS"] == 'on')) {
        $url = 'https://' . $_SERVER["SERVER_NAME"];//https url

        if (($_SERVER["SERVER_PORT"] != 443)) {
            $url .= ":" . $_SERVER["SERVER_PORT"];
        }

    } else {
        $url = 'http://' . $_SERVER["SERVER_NAME"];//http url
        if (($_SERVER["SERVER_PORT"] != 80)) {
            $url .= ":" . $_SERVER["SERVER_PORT"];
        }
    }

    $url .= $_SERVER["REQUEST_URI"];
    return $url;
}


function ptyGetColorJson($data, $indent = 0)
{
    $tab = str_repeat("    ", $indent);  // 4개의 공백으로 들여쓰기
    $result = [];

    foreach ($data as $key => $value) {
        $coloredKey = "\033[1;32m" . json_encode($key, JSON_UNESCAPED_UNICODE) . "\033[0m";  // 밝은 녹색
        $entry = $tab . $coloredKey . ": ";

        if (is_string($value)) {
            $entry .= "\033[1;33m" . json_encode($value, JSON_UNESCAPED_UNICODE) . "\033[0m";  // 밝은 청색
        } elseif (is_numeric($value)) {
            $entry .= "\033[1;35m" . json_encode($value, JSON_UNESCAPED_UNICODE) . " (numeric) \033[0m";  // 밝은 자홍색
        } elseif (is_bool($value)) {
            $entry .= "\033[1;35m" . json_encode($value, JSON_UNESCAPED_UNICODE) . " (bool) \033[0m";  // 밝은 자홍색
        } elseif (is_null($value)) {
            $entry .= "\033[1;35m" . json_encode($value, JSON_UNESCAPED_UNICODE) . " (NULL) \033[0m";  // 밝은 자홍색
        } elseif (is_array($value)) {
            $sub = ptyGetColorJson($value, $indent + 1);
            $entry .= "{\n" . $sub . "\n" . $tab . "}";
        } else {
            $entry .= "\033[1;35m" . json_encode($value, JSON_UNESCAPED_UNICODE) . " (Unknown) \033[0m";  // 밝은 자홍색
        }
        $result[] = $entry;
    }

    return implode(",\n", $result);
}

function ptyGetArrayToString($data, $indent = 0)
{
    $tab = str_repeat("    ", $indent);  // 4개의 공백으로 들여쓰기
    $result = [];

    foreach ($data as $key => $value) {
        $coloredKey = "" . json_encode($key, JSON_UNESCAPED_UNICODE) . "";  // 밝은 녹색
        $entry = $tab . $coloredKey . ": ";

        if (is_string($value)) {
            $entry .= "" . json_encode($value, JSON_UNESCAPED_UNICODE) . "";  // 밝은 청색
        } elseif (is_numeric($value)) {
            $entry .= "" . json_encode($value, JSON_UNESCAPED_UNICODE) . " (numeric)";  // 밝은 자홍색
        } elseif (is_bool($value)) {
            $entry .= "" . json_encode($value, JSON_UNESCAPED_UNICODE) . " (bool)";  // 밝은 자홍색
        } elseif (is_null($value)) {
            $entry .= "" . json_encode($value, JSON_UNESCAPED_UNICODE) . " (NULL)";  // 밝은 자홍색
        } elseif (is_array($value)) {
            $sub = ptyGetArrayToString($value, $indent + 1);
            $entry .= "{\n" . $sub . "\n" . $tab . "}";
        } else {
            $entry .= "" . json_encode($value, JSON_UNESCAPED_UNICODE) . " (Unknown)";  // 밝은 자홍색
        }
        $result[] = $entry;
    }

    return "{\n" . implode(",\n", $result) . "}";
}

function ptyGetArrayToString2($content, $indent = 0)
{
    if (is_array($content)) {

        foreach ($content as $key => $value) {
            /*
            if ($level == ptyLogLevel::WARNING || $level == ptyLogLevel::ERROR || $level == ptyLogLevel::FATAL) {
                $_k = "[ ● $key ]\n";
            } else {
                $_k = "● $key : ";
            }
            */

            $_k = "[ ● $key ]\n";

            if ($value instanceof ptyArrayItemModel) {
                $_content .= $_k . ptyGetArrayToString($value->_item) . "\n";
            } else if (is_int($key) && $value == "---") {
                $_content .= "\n-----------------------------\n\n";
            } else if (is_int($key) && $value == "\n") {
                $_content .= "\n";
            } else if (is_array($value)) {
                $_content .= $_k . ptyGetArrayToString($value) . "\n";
            } else if (is_null($value)) {
                $_content .= $_k . "$value (null)\n";
            } else {
                $_content .= $_k . formatTypeValue($value) . "\n";
            }

            // if ($level == ptyLogLevel::WARNING || $level == ptyLogLevel::ERROR || $level == ptyLogLevel::FATAL) {
            $_content .= "\n";
            // }
        }

        $content = json_encode($content, JSON_UNESCAPED_UNICODE);
    } else {
        $_content = formatTypeValue($content);
    }

    return $_content;
}

function ptyGetColorArrayHtml($data, $indent = 0)
{
    $tab = str_repeat("    ", $indent);  // 4개의 공백으로 들여쓰기
    $result = [];

    foreach ($data as $key => $value) {
        $coloredKey = "" . $key . "";  // 밝은 녹색
        $entry = "<b>[ ● " . $tab . $coloredKey . " ]</b> \n";

        if (is_numeric($value)) {
            $entry .= "" . $value . " (numeric) \n";  // 밝은 자홍색
        } elseif (is_bool($value)) {
            $entry .= "" . $value . " (bool) \n";  // 밝은 자홍색
        } elseif (is_null($value)) {
            $entry .= "" . $value . " (NULL) \n";  // 밝은 자홍색
        } elseif (is_array($value)) {
            $sub = ptyGetColorArrayHtml($value, $indent + 1);
            $entry .= "{<br>&nbsp;&nbsp;&nbsp;" . $sub . "<br>" . $tab . "} \n";
        } elseif (is_string($value)) {
            $entry .= "<div style='background-color: #393939; color: #ffffff; padding: 5px'>" . $value . " </div> (string, length : " . strlen($value) . ")\n";  // 밝은 청색

        } else {
            $entry .= "" . $value . " (Unknown) \n";  // 밝은 자홍색
        }
        $result[] = $entry;
    }

    return nl2br(implode("<br>", $result));
}


function ptyGetPrettyJson($s)
{
    $json = "";
    if (is_array($s)) {
        return "{\n" . ptyGetColorJson($s, 1) . "\n}";
    }

    return $json;
}

function ptyJsonReturn($returnValue, $arr = null)
{
    pdv("set contentType to application/json");
    header('Content-Type: application/json; charset=utf-8');

    global $platyFramework;

    $ret = array();
    $ret['return'] = $returnValue;

    // ptyArrayItemModel 형태를 리턴함
    if ($arr instanceof \platyFramework\ptyArrayItemModel) {
        $arr = $arr->_item;
    }

    if (is_array($arr)) {
        foreach ($arr as $k => $v) {
            if ($v instanceof \platyFramework\ptyArrayItemModel) {
                $arr[$k] = $v->_item;
            }

            if (is_array($v)) {
                foreach ($v as $k2 => $v2) {
                    if ($v2 instanceof \platyFramework\ptyArrayItemModel) {
                        $arr[$k][$k2] = $v2->_item;
                    }
                }

            }
        }
        $ret = array_merge($ret, $arr);
    }

    pd(ptyGetPrettyJson($ret), "ptyJsonReturn");

    $output = json_encode($ret, JSON_UNESCAPED_UNICODE);
    // $platyFramework->callAction("onControllerFinished", $output);
    echo $output;
}

function ptyJsonReturnTrueDie($arr = null)
{
    pdv("set contentType to application/json");
    header('Content-Type: application/json; charset=utf-8');
    $output = ptyJsonReturnTrue($arr);
    die ($output);
}

function ptyJsonReturnTrue($arr = null)
{
    global $platyFramework;

    // $platyFramework->response->responseType = "json";
    pdv("set contentType to application/json");
    header('Content-Type: application/json; charset=utf-8');

    $ret = array();
    $ret['return'] = 'true';

    // ptyArrayItemModel 형태를 리턴함
    if ($arr instanceof \platyFramework\ptyArrayItemModel) {
        $arr = $arr->_item;
    }

    if (is_array($arr)) {
        foreach ($arr as $k => $v) {
            if ($v instanceof \platyFramework\ptyArrayItemModel) {
                $arr[$k] = $v->_item;
            }

            if (is_array($v)) {
                foreach ($v as $k2 => $v2) {
                    if ($v2 instanceof \platyFramework\ptyArrayItemModel) {
                        $arr[$k][$k2] = $v2->_item;
                    }
                }

            }
        }
        $ret = array_merge($ret, $arr);
    }

    pd(ptyGetPrettyJson($ret), "ptyJsonReturnTrueDie");

    $output = json_encode($ret, JSON_UNESCAPED_UNICODE);
    $platyFramework->callAction("onControllerFinished", $output);
    return $output;
}

function ptyEncodedReturnTrue($arr)
{
    pdv("set contentType to application/json");
    header('Content-Type: application/json; charset=utf-8');

    $ret = array();
    $ret['return'] = 'true';
    if (is_array($arr))
        $ret = array_merge($ret, $arr);

    $json = json_encode($ret, JSON_UNESCAPED_UNICODE);
    $n = new \platyFramework\ptyencoderAes256hmacModel;
    $encoded = $n->encrypt($json, "abcdefghijklmnopqrstuvwxyz123456");

    $ret = array();
    $ret['encoded'] = $encoded;

    return $ret;
}

function ptyReturnEncoded($returnValue, $arr)
{
    $ret = array();
    $ret['return'] = $returnValue ? 'true' : 'false';
    if (is_array($arr))
        $ret = array_merge($ret, $arr);

    // if ($_REQUEST['encoded']) {
    {
        $json = json_encode($ret, JSON_UNESCAPED_UNICODE);
        $n = new \platyFramework\ptyencoderAes256hmacModel;
        $encoded = $n->encrypt($json, "abcdefghijklmnopqrstuvwxyz123456");

        $ret = array();
        $ret['encoded'] = $encoded;

        return $ret;
    }

    return $ret;
}

function ptyReturnTrue($arr = null)
{
    $ret = array();
    $ret['return'] = 'true';
    if (is_array($arr))
        $ret = array_merge($ret, $arr);

    /*
    if ($_REQUEST['encoded']) {
        $json = json_encode($ret, JSON_UNESCAPED_UNICODE);
        $n = new \platyFramework\ptyencoderAes256hmacModel;
        $encoded = $n->encrypt($json, "abcdefghijklmnopqrstuvwxyz123456");

        $ret = array();
        $ret['encoded'] = $encoded;

        return $ret;
        // die (json_encode($ret, JSON_UNESCAPED_UNICODE));
    }
    */

    return $ret;
}

function returnFalse($arr = null)
{
    $GLOBALS['returnFalse'] = $arr;
    return false;
}

function ptyReturnFalse($arr = null)
{
    $ret = array();
    $ret['return'] = 'false';
    if (is_array($arr))
        $ret = array_merge($ret, $arr);

    return $ret;
}

function ptyIsTrue($arr = null)
{
    if ($arr['return'] == 'true')
        return true;
    else
        return false;
}

function ptyJsonReturnFalseDie($arr = null)
{
    global $platyFramework;

    $ret = array();
    $ret['return'] = 'false';

    if (is_array($arr))
        $ret = array_merge($ret, $arr);

    pd(ptyGetPrettyJson($ret), "ptyJsonReturnFalseDie");

    $output = json_encode($ret, JSON_UNESCAPED_UNICODE);
    $platyFramework->callAction("onControllerFinished", $output);
    die ($output);
}

function ptyJsonReturnFalse($arr = null)
{
    global $platyFramework;

    $ret = array();
    $ret['return'] = 'false';

    if (is_array($arr))
        $ret = array_merge($ret, $arr);

    pd(ptyGetPrettyJson($ret), "ptyJsonReturnFalseDie");

    $output = json_encode($ret, JSON_UNESCAPED_UNICODE);
    return $output;
}

/**
 * parse_str 을 통해 SQL Injection 을 방지하기 위해 ptyParseStr 을 사용해야 한다
 *
 * @param $str
 * @param array|null $arr
 * @return mixed
 */
function ptyParseStr($str, ?array &$arr = null)
{
    $arr = array();
    parse_str($str, $param);
    $arr = $GLOBALS['platyFramework']->request->clean($param);
    return $param;
}

function ptyCutStr($str, $length = 20)
{
    return mb_strimwidth($str, 0, $length, '...', 'utf-8');
}


function ptyCutLength($str, $maxBytes = 65535, $encoding = 'utf-8')
{
    $ellipsis = '...';
    if (mb_strlen($str, $encoding) <= $maxBytes) {
        return $str;
    }

    return mb_substr($str, 0, $maxBytes - strlen($ellipsis), $encoding) . $ellipsis;

    /*
    $ellipsisBytes = strlen($ellipsis); // UTF-8 가정

    // 전체 UTF-8 인코딩 문자열
    $encoded = mb_convert_encoding($str, 'UTF-8', $encoding);
    $totalBytes = strlen($encoded);

    // 전체가 제한 안 넘으면 그대로 리턴
    if ($totalBytes <= $maxBytes) {
        return $str;
    }

    // 잘라야 함
    $limit = $maxBytes - $ellipsisBytes;
    $cut = mb_strcut($encoded, 0, $limit, 'UTF-8'); // 멀티바이트 안전한 바이트 자르기
    $cutStr = mb_convert_encoding($cut, $encoding, 'UTF-8'); // 원래 인코딩으로 복원 (옵션)

    return $cutStr . $ellipsis;
    */
}

function ptyCutHintStr($str, $length = 20)
{
    return mb_strimwidth($str, 0, $length, '...', 'utf-8');
}

/**
 * is_bool 및 string 을 bool 형태로 리턴 합니다.(1, true, True, TRUE => true)
 *
 * @param $str
 * @return bool
 * @author KwangHee Yoo <cpueblo@platyhouse.com>
 * @created 2023-11-14
 */
function ptyBoolVal($str)
{
    $result = false; // 기본적으로 false를 할당

    if ($str == "1") $result = true;
    if ($str == "true") $result = true;
    if ($str == "True") $result = true;
    if ($str == "TRUE") $result = true;

    if ($str == "0") $result = false;
    if ($str == "false") $result = false;
    if ($str == "False") $result = false;
    if ($str == "FALSE") $result = false;

    if (is_bool($str)) $result = $str;

    // pd("ptyBoolVal. result = " . ($result ? "true" : "false")); // 결과 출력
    return $result;
}

function ptyIntVal($str)
{
    if ($str == "true") return true;
    if ($str == "false") return false;
    if ($str == "1") return 1;
    if ($str == "0") return 0;

    if (is_bool($str) && $str == true) return 1;
    if (is_bool($str) && $str == false) return 0;

    return (int)$str;
}

function debug2($s, $title = "", $rows = 10)
{
    if ($_REQUEST['debug2'] != "1") {
        return;
    }

    if (is_array($s))
        echo print_r($s, true) . " <br>\n";
    else
        echo $s . " <br>\n";

}

function isDebug()
{
    return ($_REQUEST['debug'] ?? null) == "cpueblo" ? true : false;
}

function debug($s, $title = "", $rows = 10)
{
    if (!ispdv()) return;
//    if (($_REQUEST['debug'] ?? null) != "cpueblo") {
//        return;
//    }

    $trace = debug_backtrace();
    for ($i = 0; $i < 4; $i++) {
        $file[$i] = debug_backtrace()[$i]['file'];
        $func[$i] = debug_backtrace()[$i]['function'];
        $args[$i] = debug_backtrace()[$i]['args'];
        $args[$i] = print_r($args[$i], true);
        $args[$i] = ptyCutStr($args[$i], 60);
        $class[$i] = debug_backtrace()[$i]['class'];
        $line[$i] = debug_backtrace()[$i]['line'];
    }

//    var_dump($file); exit;

    $end = sprintf("%2.3f ms (startTime~) ", (microtime(true) - $GLOBALS['platyFramework_startTime']) * 1000);
    $end2 = sprintf("%2.3f ms (currentStep)", (microtime(true) - $GLOBALS['platyFramework_stepTime']) * 1000);

    $GLOBALS['platyFramework_stepTime'] = microtime(true);

    if ($title)
        echo "<pre><span style='color: red'>[ $title ] begins. $end $end2 </span><br>==================================<br>";
    else
        echo "<pre> $end";

    $now = DateTime::createFromFormat('U.u', microtime(true));
    // echo $now->format("m-d-Y H:i:s.u");

    // echo date("Y-m-d H:i:s.u")." ";
//    echo "[ptyDebug] <font color=blue>$file[2]:$line[2]</font> - $class[2]::$func[2](<font color=red>$args[2]</font>)<br>";
//    echo "[ptyDebug] <font color=blue>$file[1]:$line[1]</font> - $class[1]::$func[1](<font color=red>$args[1]</font>)<br>";
    // echo " $file[1]:$line[1]</font> - $class[1]::$func[1](...)\n";
    // echo date("Y-m-d H:i:s")." ";


    /*
    $tt = "";
    for ($t = 0; $t < count($trace) - 2; $t++)
        $tt .= "\t";
//    echo "[C2] ".$file[2].":{$line[2]} {$func[2]}() => \n";
//    echo "[C1] ".$file[1].":{$line[1]} {$func[1]}() => \n";
    // echo "[C0] ".$file[0].":{$line[0]} {$class[1]}::{$func[1]}() ";
//    echo "[C2] ".$file[2].":{$line[2]} {$class[3]}::{$func[3]}() \n";
//    echo "[C1] ".$file[1].":{$line[1]} {$class[2]}::{$func[2]}() \n";
    echo "$tt [C0] ".$file[0].":{$line[0]} {$class[1]}::{$func[1]}() ";
    */

    if (is_array($s))
        print_r($s);
    else
        echo $s . " <br>\n";

    if ($title)
        echo "----------------------------------</pre>";
    else
        echo "</pre>";

    return;

    // debug_print_backtrace(NULL, 1);

    if ($s->platyFramework) {
        $tmp = $s->platyFramework;
        $s->platyFramework = null;
    }
    if ($title)
        echo "\n<font color='red'>[ $title ]</font><Br><textarea rows={$rows} cols=120>\n";
    else
        echo "\n<textarea rows={$rows} cols=120>\n";

    if ($s === NULL)
        echo "[NULL]";
    else
        print_r($s);
    echo "\n</textarea><br>\n";

    if ($tmp) {
        $s->platyFramework = $tmp;
    }

}

function ptyDebug2($s, $title = "", $rows = 10)
{
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER["REMOTE_ADDR"];
    if ($_REQUEST['debug'] == "cpueblo" && $ip == "61.77.83.16") {
        ptyDebug($s, $title, $rows);
    }
}

function ptyShowCallstack($title = "")
{
    /*
    foreach (debug_backtrace() as $n => $node) {
        if ($n < 2)
            continue;

        print "<tr><td>Trace #" . $i . "</td>";
        print "<td>" . $node['file'] . "</td><td>" . $node['function'] . "(" . $node['line'] . ")</td></tr><br>\n";
        $i++;
    }
    return;
    */

    for ($i = 0; $i < count(debug_backtrace()); $i++) {
        $file[$i] = debug_backtrace()[$i]['file'];
        $func[$i] = debug_backtrace()[$i]['function'];
        $args[$i] = debug_backtrace()[$i]['args'];
//        $args[$i] = print_r($args[$i], true);
//        $args[$i] = ptyCutStr($args[$i], 60);
        $class[$i] = debug_backtrace()[$i]['class'];
        $line[$i] = debug_backtrace()[$i]['line'];
    }

//    var_dump($file); exit;

    if (ispd()) {
        $s = "";
        for ($i = 0; $i < count(debug_backtrace()); $i++) {
            $s .= "$file[$i]:$line[$i] - $class[$i]::$func[$i]($args[$i])\n";
        }

        pd($s, $title);
    } else {
        echo "<font color=777777>";
        echo "[ptyShowCallstack] =========================================<br>";
        for ($i = 0; $i < count(debug_backtrace()); $i++) {
            echo "[ptyShowCallstack] <font color=blue>$file[$i]:$line[$i]</font> - $class[$i]::$func[$i](<font color=red>$args[$i]</font>)<br>";
        }
    }

    // debug_print_backtr
}

function pdvShowCallBack($title = "")
{
    /*
    foreach (debug_backtrace() as $n => $node) {
        if ($n < 2)
            continue;

        print "<tr><td>Trace #" . $i . "</td>";
        print "<td>" . $node['file'] . "</td><td>" . $node['function'] . "(" . $node['line'] . ")</td></tr><br>\n";
        $i++;
    }
    return;
    */

    for ($i = 0; $i < count(debug_backtrace()); $i++) {
        $file[$i] = debug_backtrace()[$i]['file'];
        $func[$i] = debug_backtrace()[$i]['function'];
        $args[$i] = debug_backtrace()[$i]['args'];
//        $args[$i] = print_r($args[$i], true);
//        $args[$i] = ptyCutStr($args[$i], 60);
        $class[$i] = debug_backtrace()[$i]['class'];
        $line[$i] = debug_backtrace()[$i]['line'];
    }

//    var_dump($file); exit;

    if (ispdv()) {
        $s = "";
        for ($i = 0; $i < count(debug_backtrace()); $i++) {
            $s .= "$file[$i]:$line[$i] - $class[$i]::$func[$i]($args[$i])\n";
        }

        pdv($s, "pdvShowCallBack - " . $title);
    } else {
        echo "<font color=777777>";
        echo "[ptyShowCallstack] =========================================<br>";
        for ($i = 0; $i < count(debug_backtrace()); $i++) {
            echo "[ptyShowCallstack] <font color=blue>$file[$i]:$line[$i]</font> - $class[$i]::$func[$i](<font color=red>$args[$i]</font>)<br>";
        }
    }

    // debug_print_backtr
}


function ptyDebug($s, $title = "", $rows = 10)
{
    if ($_SERVER['HTTP_X_FORWARDED_HOST'] == "" && $_SERVER['HTTP_HOST'] == "") {
        if (is_object($s) && get_class($s) == "platyFramework\\ptyItemModel") {
            $s = print_r($s, true);
        }

        if ($title != "") {
            printf("%s.%s : [$title] %s\n",
                date("Y-m-d H:i:s"),
                substr(gettimeofday()["usec"], 0, 3),
                print_r($s, true));
        } else {
            printf("%s.%s : %s\n",
                date("Y-m-d H:i:s"),
                substr(gettimeofday()["usec"], 0, 3),
                print_r($s, true));

        }
        return;
    }

    ob_start();
    for ($i = 0; $i < 4; $i++) {
        $file[$i] = debug_backtrace()[$i]['file'];
        $func[$i] = debug_backtrace()[$i]['function'];
        $args[$i] = debug_backtrace()[$i]['args'];
        $args[$i] = print_r($args[$i], true);
        $args[$i] = ptyCutStr($args[$i], 60);
        $class[$i] = debug_backtrace()[$i]['class'];
        $line[$i] = debug_backtrace()[$i]['line'];
    }

//    var_dump($file); exit;

    echo "<font color=777777>";
    echo "[ptyDebug] =========================================<br>";
    echo "[ptyDebug] <font color=blue>$file[2]:$line[2]</font> - $class[2]::$func[2](<font color=red>$args[2]</font>)<br>";
    echo "[ptyDebug] <font color=blue>$file[1]:$line[1]</font> - $class[1]::$func[1](<font color=red>$args[1]</font>)<br>";
    echo "[ptyDebug] <font color=blue>$file[0]:$line[0]</font> - $class[0]::$func[0](<font color=red>$args[0]</font>)<br>";

    // debug_print_backtrace(NULL, 1);

    if ($s->platyFramework) {
        $tmp = $s->platyFramework;
        $s->platyFramework = null;
    }
    if ($title)
        echo "\n<font color='red'>[ $title ]</font><Br><textarea rows={$rows} cols=120>\n";
    else
        echo "\n<textarea rows={$rows} cols=120>\n";

    if ($s === NULL)
        echo "[NULL]";
    else {

        if (is_array($s))
            echo "array count = " . count($s) . "\n";
        print_r($s);
    }
    echo "\n</textarea><br>\n";

    if ($tmp) {
        $s->platyFramework = $tmp;
    }

    flush();
    ob_flush();

    //
    ob_end_flush();
}

/*
function debug($s, $title = "")
{
    if ($this->request->req->debug == "cpueblo")
        ptyDebugXmp($s, $title);
}
*/

class ptyTerminalColor
{
    // Reset
    public const reset = "\033[0m";  // Text Reset
    // Regular Colors
    public const black = "\033[0;30m";   // BLACK
    public const red = "\033[0;31m";     // RED
    public const green = "\033[0;32m";   // GREEN
    public const yellow = "\033[0;33m";  // YELLOW
    public const blue = "\033[0;34m";    // BLUE
    public const purple = "\033[0;35m";  // PURPLE
    public const cyan = "\033[0;36m";    // CYAN
    public const white = "\033[0;37m";   // WHITE
    // Bold
    public const blackBold = "\033[1;30m";  // BLACK
    public const redBold = "\033[1;31m";    // RED
    public const greenBold = "\033[1;32m";  // GREEN
    public const yellowBold = "\033[1;33m"; // YELLOW
    public const blueBold = "\033[1;34m";   // BLUE
    public const purpleBold = "\033[1;35m"; // PURPLE
    public const cyanBold = "\033[1;36m";   // CYAN
    public const whiteBold = "\033[1;37m";  // WHITE
    // Underline
    public const blackUnderlined = "\033[4;30m";  // BLACK
    public const redUnderlined = "\033[4;31m";    // RED
    public const greenUnderlined = "\033[4;32m";  // GREEN
    public const yellowUnderlined = "\033[4;33m"; // YELLOW
    public const blueUnderlined = "\033[4;34m";   // BLUE
    public const purpleUnderlined = "\033[4;35m"; // PURPLE
    public const cyanUnderlined = "\033[4;36m";   // CYAN
    public const whiteUnderlined = "\033[4;37m";  // WHITE
    // Background
    public const blackBackground = "\033[40m";  // BLACK
    public const redBackground = "\033[41m";    // RED
    public const greenBackground = "\033[42m";  // GREEN
    public const yellowBackground = "\033[43m"; // YELLOW
    public const blueBackground = "\033[44m";   // BLUE
    public const purpleBackground = "\033[45m"; // PURPLE
    public const cyanBackground = "\033[46m";   // CYAN
    public const whiteBackground = "\033[47m";  // WHITE
    // High Intensity
    public const blackBright = "\033[0;90m";  // BLACK
    public const redBright = "\033[0;91m";    // RED
    public const greenBright = "\033[0;92m";  // GREEN
    public const yellowBright = "\033[0;93m"; // YELLOW
    public const blueBright = "\033[0;94m";   // BLUE
    public const purpleBright = "\033[0;95m"; // PURPLE
    public const cyanBright = "\033[0;96m";   // CYAN
    public const whiteBright = "\033[0;97m";  // WHITE
    // Bold High Intensity
    public const blackBoldBright = "\033[1;90m"; // BLACK
    public const redBoldBright = "\033[1;91m";   // RED
    public const greenBoldBright = "\033[1;92m"; // GREEN
    public const yellowBoldBright = "\033[1;93m";// YELLOW
    public const blueBoldBright = "\033[1;94m";  // BLUE
    public const purpleBoldBright = "\033[1;95m";// PURPLE
    public const cyanBoldBright = "\033[1;96m";  // CYAN
    public const whiteBoldBright = "\033[1;97m"; // WHITE
    // High Intensity backgrounds
    public const blackBackgroundBright = "\033[0;100m";// BLACK
    public const redBackgroundBright = "\033[0;101m";// RED
    public const greenBackgroundBright = "\033[0;102m";// GREEN
    public const yellowBackgroundBright = "\033[0;103m";// YELLOW
    public const blueBackgroundBright = "\033[0;104m";// BLUE
    public const purpleBackgroundBright = "\033[0;105m"; // PURPLE
    public const cyanBackgroundBright = "\033[0;106m";  // CYAN
    public const whiteBackgroundBright = "\033[0;107m";   // WHITE
}

function ptyIsCurl()
{
    // return isset($_SERVER['HTTP_USER_AGENT']) && stripos($_SERVER['HTTP_USER_AGENT'], 'curl') !== false;
    if (isset($_SERVER['HTTP_USER_AGENT'])) {

        if (stripos($_SERVER['HTTP_USER_AGENT'], 'curl') !== false) return true;
        if (stripos($_SERVER['HTTP_USER_AGENT'], 'dart') !== false) return true;
        if (stripos($_SERVER['HTTP_USER_AGENT'], 'axios') !== false) return true;
        return false;
    }
    return true;
}

/**
 * str 이 find 로 시작하는지 체크
 *
 * @param $str
 * @param $find
 * @return bool
 * @example ptyIsStringStart("abcdefghijlk", "abcd")
 *
 */
function ptyIsStringStart($str, $find)
{
    // return stripos($str, $find) !== FALSE ? true : false;
    return strpos($str, $find) === 0;
}

function ptyGetFileExtension($fileName)
{
    $pathinfo = pathinfo($fileName);
    return $pathinfo['extension'];
}


function ptyIsImageUrl($fileName)
{
    if (strstr($fileName, "googleusercontent.com")) return true;

    $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
    $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
    return in_array($extension, $imageExtensions);
}

function ptyGetColorPrint_r($array, $level = 0)
{
    $indent = str_repeat("    ", $level);  // 들여쓰기
    $result = '';

    foreach ($array as $key => $value) {
        if (is_array($value)) {
            // 배열 키를 출력하고, 값을 재귀적으로 처리
            $result .= $indent . "\033[32m" . "[$key]" . "\033[0m => Array\n";
            $result .= $indent . "(\n";
            $result .= ptyGetColorPrint_r($value, $level + 1);
            $result .= $indent . ")\n";
        } else {
            // 키 출력 (녹색)
            $result .= $indent . "\033[32m" . "[$key]" . "\033[0m => ";
            // 값 출력, 타입에 따라 색상 지정
            if (is_string($value)) {
                // 문자열 값은 노란색
                $result .= "\033[1;33m" . json_encode($value, JSON_UNESCAPED_UNICODE) . "\033[0m\n";
            } elseif (is_numeric($value)) {
                // 숫자 값은 핑크색
                $result .= "\033[1;35m" . $value . "\033[0m\n";
            } else {
                // 그 외의 값은 기본 청색
                $result .= "\033[34m" . $value . "\033[0m\n";
            }
        }
    }
    return $result;
}


function ptyDebugXmp($s, $title = "", int $debugPos = 1, string $terminalColor = ptyTerminalColor::blueBoldBright, bool $showAllCallStack = false)
{
    // print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10));
    if ($showAllCallStack || $debugPos < 0) {
        print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10));
    }

    $callInfo = getCurrentFileAndLine($debugPos);
    $callInfo2 = getCurrentFileAndLine($debugPos + 1);
    $callInfo3 = getCurrentFileAndLine($debugPos + 3);

    if (isset($s)) {
        if (is_object($s) && $s->platyFramework) {
            // if ($s->platyFramework) {
            $tmp = $s->platyFramework;
            $s->platyFramework = null;
        }
    }

    if (is_array($s)) $title = $title . " (length: " . count($s) . ")";

    $isCurl = ptyIsCurl();

    if ($isCurl) {

//        print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10));
//        echo "debugPos = $debugPos\n";
        echo "\033[1;93m\033[44m[ $title ]\n";
        // echo "\033[0;90m\033[44m";
        echo $callInfo3['file'] . ":" . $callInfo3['line'] . " " . $callInfo3['class'] . "::" . $callInfo3['function'];
        echo " => \n";
        echo $callInfo2['file'] . ":" . $callInfo2['line'] . " " . $callInfo2['class'] . "::" . $callInfo2['function'];
        echo " => \n";
        echo $callInfo['file'] . ":" . $callInfo['line'] . " " . $callInfo['class'] . "::" . $callInfo['function'];
        echo "\033[0m\n";
        echo $terminalColor . "";
        // echo ptyGetColorPrint_r($s);
        if ($s === null)
            print_r("{NULL}");
        else if (is_bool($s)) {
            if ($s === true) echo "true (bool)";
            if ($s === false) echo "false (bool)";
        } else if (is_array($s)) {
            echo ptyGetPrettyJson($s);
            // print_r($s);
            echo "(array)";
        } else if (is_int($s)) {
            echo $s . " (int)";
        } else if (is_string($s)) {
            if ($s == "") echo "(EMPTY) (string)";
            else echo $s . " (string, length: " . strlen($s) . ")";
        } else if (is_object($s)) {
            print_r($s);
            echo "(object)";
        } else {
            print_r($s);
            echo "(unknown)";
        }

        echo ptyTerminalColor::reset . "\n\n";

        flush();
        if (ob_get_level() > 0) {
            ob_flush();
        }

    } else {
        if ($title)
            echo "<br><font color='red'>[ $title ]</font>" . " - " . $callInfo['file'] . ":" . $callInfo['line'] . "<Br><xmp>";
        else
            echo "<br><xmp>";

        if ($s === null)
            print_r("{NULL}");
        else if (is_bool($s)) {
            if ($s === true) echo "true (bool)";
            if ($s === false) echo "false (bool)";
        } else if (is_array($s)) {
            print_r($s);

            if (false)
                foreach ($s as $it) {
                    $it2 = $it;


                    if (false && $it instanceof \platyFramework\ptyArrayItemModel && is_array($it->_item)) {
                        // if ($it is \ptyArrayItemModel) {
                        echo implode(", ", array_map(
                                fn($key, $value) => "$key = $value",
                                array_keys($it->_item),
                                $it->_item
                            )) . "<br>";
                    } else {
                        print_r($it);
                        if (true)
                            foreach ($it as $index => $item) {
                                $itemString = implode(", ", array_map(
                                    fn($key, $value) => "$key = " . (is_bool($value) ? ($value ? 'true' : 'false') : (is_scalar($value) ? $value : json_encode($value))),
                                    array_keys($item),
                                    $item
                                ));
                                echo "[$index] $itemString\n";
                            }

                        exit;
                    }

                }


            echo "(array)";
        } else if (is_int($s)) {
            echo $s . " (int)";
        } else if (is_string($s)) {
            if ($s == "") echo "(EMPTY) (string)";
            else echo $s . " (string)";
        } else if (is_object($s)) {
            print_r($s);
            echo "(object)";
        } else {
            print_r($s);
            echo "(unknown)";
        }
        echo "</xmp>";
    }

    if (isset($tmp)) {
        $s->platyFramework = $tmp;
    }
}

class ptyDebugLevel
{
    public const VIEW = 'view';
    public const DB = 'db';
    public const SYSTEM = 'system';
    public const APP = 'app';
}


function getCurrentFileAndLine($n)
{
    if ($n < 0) return null;

    $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $n + 1);
    // $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS);
    if (isset($trace[$n])) {
        return array(
            'file' => $trace[$n]['file'],
            'line' => $trace[$n]['line'],
            'class' => $trace[$n]['class'] ?? "(UNKNOWN)",
            'function' => $trace[$n]['function']
        );
    } else {
        return getCurrentFileAndLine($n - 1);
    }
}


/**
 * 디버깅 표시
 *
 * 현재 파라미터를 노출할때 : pd(var_export(get_defined_vars(), true), "defined_vars");
 *
 * @param $s
 * @param $title
 * @param string $terminalColor
 * @return bool
 */
function pd($s = null, string $title = "", int $pos = 2, string $terminalColor = \ptyTerminalColor::blueBoldBright, bool $showAllCallStack = false): bool
{
    if (ispdv() || ispd()) {
        // error_reporting(E_ALL);
        ptyDebugXmp($s, $title, $pos, $terminalColor, showAllCallStack: $showAllCallStack);
        return true;
    }
    return false;
}

/**
 * 디버깅을 강제로 표기
 * @param $s
 * @param string $title
 * @param int $pos
 * @param string $terminalColor
 * @return bool
 */
function pdf($s = null, string $title = "", int $pos = 3, string $terminalColor = \ptyTerminalColor::blueBoldBright, $showArray = false): bool
{
    if (!isset($GLOBALS['ispd'])) {
        $GLOBALS['ispd'] = 1;

        if ($showArray && is_array($s)) {
            foreach ($s as $key => $value) {
                pd($value, "[$key] " . $title, $pos, $terminalColor);
            }
        } else {
            pd($s, $title, $pos, $terminalColor);
        }

        // pd($s, $title, $pos, $terminalColor);
        $GLOBALS['ispd'] = 0;
        return true;
    }

    $ispd = $GLOBALS['ispd'];
    $GLOBALS['ispd'] = 1;

    if ($showArray && is_array($s)) {
        foreach ($s as $key => $value) {
            pd($value, "[$key] " . $title, $pos, $terminalColor);
        }
    } else {
        pd($s, $title, $pos, $terminalColor);
    }
    $GLOBALS['ispd'] = $ispd;

//    pd()
//    ptyDebugXmp($s, $title, $pos, $terminalColor);

    return true;
}

function pd3($s = null, $title = "", $rowsPerPage = 150, $useColors = false, $useFormatting = true)
{
    if (ispdv() || ispd()) {
        $out = "";
        $rowCount = 0; // 행 카운터

        // 색상 정의
        $colors = [
            'header' => $useColors ? "\033[1;34m" : "", // 밝은 파란색
            'index' => $useColors ? "\033[1;32m" : "", // 밝은 녹색
            'reset' => $useColors ? "\033[0m" : "",    // 색상 초기화
        ];

        // [index]의 최대 길이 계산
        $maxIndexLength = 0;
        foreach ($s as $index => $obj) {
            $indexLength = strlen("{$index}");
            if ($indexLength > $maxIndexLength) {
                $maxIndexLength = $indexLength;
            }
        }
        $maxIndexLength++;

        // 모든 데이터에서 각 키의 최대 너비 계산
        $fieldWidths = [];
        foreach ($s as $obj) {
            if ($obj instanceof \platyFramework\ptyArrayItemModel) {
                $item = $obj->_item; // 객체의 _item 속성
            } else {
                $item = $obj;
            }

            foreach ($item as $key => $value) {
                $length = max(strlen($key), strlen($useFormatting ? formatTypeValue($value) : (string)$value));
                if (!isset($fieldWidths[$key]) || $length > $fieldWidths[$key]) {
                    $fieldWidths[$key] = $length;
                }
            }
        }

        // 헤더 생성 함수
        $createHeader = function () use ($fieldWidths, $maxIndexLength, $colors) {
            $header = $colors['header'] . str_pad("IDX", $maxIndexLength) . " | ";
            foreach ($fieldWidths as $key => $width) {
                $header .= str_pad($key, $width) . " | ";
            }
            $header .= $colors['reset'] . PHP_EOL;
            $header .= str_repeat("-", strlen(strip_tags($header))) . PHP_EOL;
            return $header;
        };

        // 첫 번째 헤더 추가
        $out .= $createHeader();

        // 데이터 출력 (index 포함)
        foreach ($s as $index => $obj) {
            $rowCount++;

            if ($obj instanceof \platyFramework\ptyArrayItemModel) {
                $item = $obj->_item; // 객체의 _item 속성
            } else {
                $item = $obj;
            }

            // $rowsPerPage 행마다 헤더 다시 출력
            if ($rowsPerPage > 0 && $rowCount % $rowsPerPage === 0) {
                $out .= $createHeader();
            }

            // 데이터 행 작성
            $row = $colors['index'] . str_pad("{$index}", $maxIndexLength) . " | " . $colors['reset'];
            foreach ($fieldWidths as $key => $width) {
                $value = $item[$key] ?? ""; // 키가 없는 경우 빈 값
                $row .= str_pad($useFormatting ? formatTypeValue($value) : (string)$value, $width) . " | ";
            }
            $out .= $row . PHP_EOL;
        }

        // PDF 생성 함수 호출
        pd($out, $title);
    }
}

function pdf3($s = null, $title = "", $rowsPerPage = 150, $useColors = false, $useFormatting = true)
{
    $out = "";
    $rowCount = 0; // 행 카운터

    // 색상 정의
    $colors = [
        'header' => $useColors ? "\033[1;34m" : "", // 밝은 파란색
        'index' => $useColors ? "\033[1;32m" : "", // 밝은 녹색
        'reset' => $useColors ? "\033[0m" : "",    // 색상 초기화
    ];

    // [index]의 최대 길이 계산
    $maxIndexLength = 0;
    foreach ($s as $index => $obj) {
        $indexLength = strlen("{$index}");
        if ($indexLength > $maxIndexLength) {
            $maxIndexLength = $indexLength;
        }
    }
    $maxIndexLength++;

    // 모든 데이터에서 각 키의 최대 너비 계산
    $fieldWidths = [];
    foreach ($s as $obj) {
        if ($obj instanceof \platyFramework\ptyArrayItemModel) {
            $item = $obj->_item; // 객체의 _item 속성
        } else {
            $item = $obj;
        }

        foreach ($item as $key => $value) {
            $length = max(strlen($key), strlen($useFormatting ? formatTypeValue($value) : (string)$value));
            if (!isset($fieldWidths[$key]) || $length > $fieldWidths[$key]) {
                $fieldWidths[$key] = $length;
            }
        }
    }

    // 헤더 생성 함수
    $createHeader = function () use ($fieldWidths, $maxIndexLength, $colors) {
        $header = $colors['header'] . str_pad("IDX", $maxIndexLength) . " | ";
        foreach ($fieldWidths as $key => $width) {
            $header .= str_pad($key, $width) . " | ";
        }
        $header .= $colors['reset'] . PHP_EOL;
        $header .= str_repeat("-", strlen(strip_tags($header))) . PHP_EOL;
        return $header;
    };

    // 첫 번째 헤더 추가
    $out .= $createHeader();

    // 데이터 출력 (index 포함)
    foreach ($s as $index => $obj) {
        $rowCount++;

        if ($obj instanceof \platyFramework\ptyArrayItemModel) {
            $item = $obj->_item; // 객체의 _item 속성
        } else {
            $item = $obj;
        }

        // $rowsPerPage 행마다 헤더 다시 출력
        if ($rowsPerPage > 0 && $rowCount % $rowsPerPage === 0) {
            $out .= $createHeader();
        }

        // 데이터 행 작성
        $row = $colors['index'] . str_pad("{$index}", $maxIndexLength) . " | " . $colors['reset'];
        foreach ($fieldWidths as $key => $width) {
            $value = $item[$key] ?? ""; // 키가 없는 경우 빈 값
            $row .= str_pad($useFormatting ? formatTypeValue($value) : (string)$value, $width) . " | ";
        }
        $out .= $row . PHP_EOL;
    }

    // PDF 생성 함수 호출
    pdf($out, $title);

}

/**
 * 데이터 타입에 따라 값을 포맷팅합니다.
 */
function formatTypeValue($value)
{
    if (is_int($value)) {
        return "{$value} (int)";
    } elseif (is_float($value)) {
        return "{$value}(float)";
    } elseif (is_bool($value)) {
        return "{$value} (bool)";
    } elseif (is_null($value)) {
        return "(NULL)";
    } elseif (is_string($value)) {
        return "{$value}";
    }
    // string이나 기타 값은 그대로 반환
    return "{$value}";
}


function pdv($s = null, string $title = "", int $pos = 2, string $terminalColor = \ptyTerminalColor::greenBoldBright): bool
{
    if (ispdv()) {
        ptyDebugXmp($s, $title, $pos, $terminalColor);
        return true;
    }
    return false;
}

function setpd(bool $b = true)
{
    $GLOBALS['ispd'] = $b;
}

function ptyIsDebugIP()
{
    global $platyFramework;

    if (!isset($platyFramework->request))
        return false;

    $ip = $platyFramework->request->ip;

    // IP 제한
    if (!strstr($ip, "100.80") && !strstr($ip, "100.100.") && $ip != "125.132.167.178" && $ip != "15.164.5.119" && $ip != "58.120.80.104" && $ip != "43.202.45.88" /* dev.follaw.co.kr */) {
        return false;
    }

    return true;
}

/**
 * cli 환경에서는 export PLATYFRAMEWORK_CLI_PD=1 를 실행합니다.
 * @return bool
 */
function ispd()
{
    if (ispdv()) return true;

    // export PLATYFRAMEWORK_CLI_PD=1
    // if (php_sapi_name() == "cli" && $GLOBALS["directPlatyFramework"] == 0) {
    if (php_sapi_name() == "cli" && getenv("PLATYFRAMEWORK_CLI_PD") == "1")
        return true;

    if (isset($GLOBALS['ispd']) && $GLOBALS['ispd'] == true)
        return true;

    if (!ptyIsDebugIP())
        return false;

    // if (isset($_REQUEST['pd']) && $_REQUEST['pd'] == "1") {
    if (isset($_REQUEST['pd']) && $_REQUEST['pd'] != "") {
        return $_REQUEST['pd'];
    }
    return false;
}

function setpdv(bool $b)
{
    $GLOBALS['ispdv'] = $b;
}

/**
 *  cli 환경에서는 export PLATYFRAMEWORK_CLI_PDV=1 를 실행합니다.
 *
 * @return bool
 */
function ispdv()
{
    // if (php_sapi_name() == "cli" && $GLOBALS["directPlatyFramework"] == 0) {
    if (php_sapi_name() == "cli" && getenv("PLATYFRAMEWORK_CLI_PDV") == "1")
        return true;

    if (isset($GLOBALS['ispdv']) && $GLOBALS['ispdv'] == true)
        return true;

    $ip = isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])
        ? $_SERVER['HTTP_X_FORWARDED_FOR']
        : (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '');

    // IP 제한
    if (!strstr($ip, "100.80.") && $ip != "125.132.167.178" && $ip != "15.164.5.119" && $ip != "58.120.80.104" && $ip != "43.202.45.88" /* dev.follaw.co.kr */) {
        return false;
    }

    if (isset($_REQUEST['pdv']) && $_REQUEST['pdv'] == "1") {
        return true;
    }
    return false;
}

function pdfe($s = "", $title = "", $showArray = false)
{
    pdf($s, $title, pos: 4, showArray: $showArray);
    exit;
}

function pd_exit()
{
    if (ispd()) {
        echo "exited";
        exit();
    }
}

function ptyDebugXmpOut($s, $title = "")
{
    $o = "";
    // debug_print_backtrace();

    if ($s->platyFramework) {
        $tmp = $s->platyFramework;
        $s->platyFramework = null;
    }
    if ($title)
        $o .= "<br><font color='red'>[ $title ]</font><Br><xmp>";
    else
        $o .= "<br><xmp>";

    $o .= print_r($s, true);
    $o .= "</xmp><br>";

    if ($tmp) {
        $s->platyFramework = $tmp;
    }

    return $o;
}


/**
 * 배열의 Value 를 Key 와 매치시킵니다
 * @param $array
 * @return array
 */
function ptyArrayValueToKey($array)
{
    $ret = array();
    foreach ($array as $k => $v) {
        $ret[$v] = $v;
    }
    return $ret;
}

function ptyGetPrevCallFunctionName()
{
    $callstacks = debug_backtrace();
    return $callstacks[2]['function'];
}

function ptyGetCallStack()
{
    $bt = debug_backtrace();
    for ($i = 0; $i <= count($bt) - 1; $i++) {
        if (!isset($bt[$i]["file"]))
            $out .= "[PHP core called function]";
        else
            $out .= "#{$i} " . $bt[$i]["file"] . "";

        if (isset($bt[$i]["line"]))
            $out .= ":" . $bt[$i]["line"] . " ";
        $out .= " " . $bt[$i]["function"] . "(";

        if ($bt[$i]["args"]) {
            $out .= "";
            for ($j = 0; $j <= count($bt[$i]["args"]) - 1; $j++) {
                if (is_array($bt[$i]["args"][$j])) {
                    $out .= str_replace("\n", "", print_r($bt[$i]["args"][$j], true));
                } else
                    $out .= $bt[$i]["args"][$j];

                if ($j != count($bt[$i]["args"]) - 1)
                    $out .= ", ";
            }
        }
        $out .= ")\n";
    }

    return $out;
}

function ptyGetSimpleCallStack()
{
    $bt = debug_backtrace();
    // print_r($bt);
    for ($i = 0; $i <= count($bt) - 1; $i++) {
        if (!isset($bt[$i]["file"]))
            $out .= "[PHP core called function]";
        else
            $out .= "#{$i} " . $bt[$i]["file"] . "";

        if (isset($bt[$i]["line"]))
            $out .= ":" . $bt[$i]["line"] . " ";
        $out .= " " . $bt[$i]["function"] . "(";

        /*
        if ($bt[$i]["args"]) {
            $out .= "";
            for ($j = 0; $j <= count($bt[$i]["args"]) - 1; $j++) {
                if (is_array($bt[$i]["args"][$j])) {
                    $out .= str_replace("\n", "", print_r($bt[$i]["args"][$j], true));
                } else
                    $out .= $bt[$i]["args"][$j];

                if ($j != count($bt[$i]["args"]) - 1)
                    $out .= ", ";
            }
        }
        */
        $out .= ")\n";
    }

    return $out;
}


function ptyDebugBackTrace()
{
    $out = "";
    /*
    $out = "";
        $trace = debug_backtrace();
        $caller = array_shift($trace);
        $function_name = $caller['function'];
        error_log(sprintf('%s: Called from %s:%s', $function_name, $caller['file'], $caller['line']));
        foreach ($trace as $entry_id => $entry) {
            $entry['file'] = $entry['file'] ? : '-';
            $entry['line'] = $entry['line'] ? : '-';
            if (empty($entry['class'])) {
                $out .= sprintf("%s %3s. %s() %s:%s\n", $function_name, $entry_id + 1, $entry['function'], $entry['file'], $entry['line']);
            } else {
                $out .= sprintf("%s %3s. %s->%s() %s:%s\n", $function_name, $entry_id + 1, $entry['class'], $entry['function'], $entry['file'], $entry['line']);
            }
        }
    */

    $bt = debug_backtrace();
    for ($i = 0; $i <= count($bt) - 1; $i++) {
        if (!isset($bt[$i]["file"]))
            $out .= "[PHP core called function]\n";
        else
            $out .= "File: " . $bt[$i]["file"] . "\n";

        if (isset($bt[$i]["line"]))
            $out .= "&nbsp;&nbsp;&nbsp;&nbsp;line " . $bt[$i]["line"] . "\n";
        $out .= "&nbsp;&nbsp;&nbsp;&nbsp;function called: " . $bt[$i]["function"];

        if ($bt[$i]["args"]) {
            $out .= "\n&nbsp;&nbsp;&nbsp;&nbsp;args: ";
            for ($j = 0; $j <= count($bt[$i]["args"]) - 1; $j++) {
                if (is_array($bt[$i]["args"][$j])) {
                    $out .= print_r($bt[$i]["args"][$j], true);
                } else
                    $out .= $bt[$i]["args"][$j];

                if ($j != count($bt[$i]["args"]) - 1)
                    $out .= ", ";
            }
        }
        $out .= "\n\n";
    }


    /*
    $file_paths = debug_backtrace();
    $out = "";

    foreach($file_paths AS $file_path) {
        foreach($file_path AS $key => $var) {
            if($key == 'args') {
                foreach($var AS $key_arg => $var_arg) {
                    $out .= $key_arg . ': ' . $var_arg . "";
                }
            } else {
                $out .= $key . ': ' . $var . "\n";
            }
        }
    }
    */

    ptyDebug($out);
}

/**
 *
 * trim 함수, 배열과 string 을 동시 처리
 *
 * @param $data
 * @return mixed
 * @author cpueblo <cpueblo@platyhouse.com>
 */
function ptyTrim($data)
{
    $ret = $data;
    if (is_array($data)) {
        foreach ($data as $k => $v) {
            $ret[$k] = trim($v);
        }
    } else
        trim($ret);

    return $ret;
}

function ptyGetPhoneNumberRule($phoneNumber)
{
    $phoneNumber = trim($phoneNumber);
    $phoneNumber = str_replace("-", "", $phoneNumber);

    if (!ctype_digit($phoneNumber)) {
        ptyThrow("전화번호 형식이 잘못되었습니다. 숫자만 포함해야 합니다.", "ERROR_INVALID_PHONE_NUMBER");
    }

    $chk = preg_match("/^01[0-9]{8,9}$/", $phoneNumber);
    if (!$chk)
        ptyThrow("전화번호 형식을 010-1234-1234 형태로 입력해 주세요", "ERROR_INVALID_PHONE_NUMBER");

    return $phoneNumber;
}

function ptyCheckPasswordRule($password)
{
    $check = array();
    if (preg_match('/[a-z]/', $password)) $check['lower'] = true; // 소문자 있는지…
    if (preg_match('/[A-Z]/', $password)) $check['upper'] = true; // 대문자 있는지…
    if (preg_match('/[0-9]/', $password)) $check['number'] = true; // 숫자 있는지…

    $special = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~';
    if (preg_match('/[' . preg_quote($special, '/') . ']/', $password)) $check['special'] = true; // 특수문자 있는지…

    return $check;
}

/**
 * 현재 URL 에 의해 controller, class, function 을 분리합니다
 *
 * @param $url
 * @param $controller
 * @param $class
 * @param $function
 */
function ptyExtractControllerName($url, &$controller, &$class, &$function)
{
    $info = parse_url($url);
    $url = $info['path'];
    $url = str_replace("/pc/", "", $url);
    $url = str_replace("/m/", "", $url);
    $urls = explode("/", $url);

    /*
    [0] => event
    [1] => index
    [2] =>
    */
    if (count($urls) == 2 || count($urls) == 3 || count($urls) == 4) {
        $controller = $urls[0];
        $class = $urls[1];
        $function = $urls[2];

        if ($controller == "") $controller = "index";
        if ($class == "") $class = "index";
        if ($function == "") $function = "index";
    }
}

function ptyRecordElapsedTime($title = "")
{
    global $_ptyShowElapsedTime_lastTime;
    global $_ptyShowElapsedTime_step;
    global $_ptyShowElapsedTime_items;
    global $_ptyShowElapsedTime_firstTime;

    if (!isset($_ptyShowElapsedTime_lastTime)) {
        ptyInitElapsedTime();
    }

    $currentTime = microtime(true);
    $elapsed_time = sprintf("%2.3f", $currentTime - $_ptyShowElapsedTime_lastTime);
    $totalElapsed = sprintf("%2.3f", $currentTime - $_ptyShowElapsedTime_firstTime);

    $_ptyShowElapsedTime_items[] = "[{$_ptyShowElapsedTime_step}] $title (시간 = $elapsed_time, 누적 시간 = $totalElapsed)";

    $_ptyShowElapsedTime_lastTime = $currentTime;
    $_ptyShowElapsedTime_step++;
}

function ptyInitElapsedTime()
{
    global $_ptyShowElapsedTime_lastTime;
    global $_ptyShowElapsedTime_firstTime;
    global $_ptyShowElapsedTime_step;

    $_ptyShowElapsedTime_lastTime = microtime(true);
    $_ptyShowElapsedTime_firstTime = microtime(true);
    $_ptyShowElapsedTime_step = 0;
}

/**
 * Y-m-d H:i:s 를 UTC 타임값으로 변환
 * @param $local_time
 * @return string
 * @throws Exception
 */
function ptyGetUTCDateTime($local_time = null)
{
    // 시스템의 기본 타임존을 가져옴
    $timezone = date_default_timezone_get();

    // 주어진 시간이 null이면 현재 시간을 사용
    if ($local_time === null) {
        $local_time = date('Y-m-d H:i:s');
    }

    // 주어진 시간과 시스템의 기본 타임존을 기반으로 DateTime 객체 생성
    $date = new DateTime($local_time, new DateTimeZone($timezone));

    // UTC 타임존으로 변환
    $date->setTimezone(new DateTimeZone('UTC'));

    // 변환된 시간을 형식화된 시간 문자열로 반환
    return $date->format('Y-m-dTH:i:s');
}

/**
 * ```
 * $log = ptyGetLogMsg("변호사 서비스 결제 완료",
 *
 * [
 * "회원ID" => $this->userId,
 * "상담 예약 시간" => $this->dateTime,
 * "예약 ID" => $this->bookingId,
 * "예약 레코드 ID" => $this->bookingRecordId,
 * "직전 통화 시간" => sprintf("%s 초", $this->totalPaidSeconds),
 * "이번 통화 시간" => sprintf("%d 초", $callRecordItem->outdur),
 * "기본요금" => sprintf("%s 원", number_format($this->defaultPrice)),
 * "기본시간(초)" => sprintf("%s 초", number_format($this->defaultPriceSeconds)),
 * "분당 요금" => sprintf("%s 원", number_format($this->pricePerMinute)),
 * "할인율" => sprintf("%s %%", $this->priceSale),
 *
 * "이번 결제 금액" => sprintf("%s 원", number_format($totalPrice)),
 * "총 결제 금액" => sprintf("%s 원", number_format($this->totalPaidPrice + $totalPrice)),
 * "총 통화 시간" => sprintf("%d 초", $sec),
 * "결제 유형 " => $payType,
 * ]
 * );
 * ```
 * @param $title
 * @param $data
 * @return string
 */
function ptyGetLogMsg(string $title = "", array $data = [])
{
    if ($title != "") $log = "[ $title ]\n\n";
    foreach ($data as $key => $value) {
        // $log .= "[ $key ]\n$value\n\n";
        $log .= "$key: $value\n";
    }

    return $log;
}


/**
 * minutes 가 지났는지 체크
 *
 * @param $datetime
 * @param $minutes
 * @param $passMinute 지난 분
 * @return bool true: 시간이 지났음, false : 시간이 지나지 않음
 */
function ptyHasPassedMinutes(string $datetime, int $minutes, int &$passMinute)
{
    try {
        // 현재 시간 가져오기
        $now = new DateTime();

        // 입력된 날짜를 DateTime 객체로 변환
        $date = new DateTime($datetime);

        // 현재 시간과 입력된 시간의 차이를 계산
        $diff = $now->getTimestamp() - $date->getTimestamp();
        $passMinute = floor($diff / 60); // 초 단위 차이를 분 단위로 변환

        // n분 이상 지났는지 확인
        return $passMinute >= $minutes;

    } catch (Exception $e) {
        // 예외 발생 시 false 반환, $passMinute은 -1로 설정
        $passMinute = -1;
        return false;
    }
}

function ptyGetDateTimeFromUTC($utc_time = null)
{
    if ($utc_time === null) {
        $utc_time = date('Y-m-dTH:i:s');
    }


// DateTime 객체로 변환
    $date = new DateTime($utc_time, new DateTimeZone('UTC'));

// 한국 시간대로 변환
    $date->setTimezone(new DateTimeZone('Asia/Seoul'));

// 유닉스 타임스탬프로 변환
    $unix_timestamp = $date->getTimestamp();

    return $date->format('Y-m-d H:i:s');
}


function ptyGetMiddleStr($Src, $StartStr, $EndStr)
{
    $Pos = strpos($Src, $StartStr);
    if ($Pos === FALSE)
        return "";

    $NewStr = substr($Src, $Pos + strlen($StartStr), 102400);
    $Pos2 = strpos($NewStr, $EndStr);

    $NewStr = substr($NewStr, 0, $Pos2);
    return $NewStr;
}

/**
 * 배열에서 특정 문자열을 가진 키값을 특정 문자열을 제외하고 리턴합니다
 * ex) ["hello_checked", "nice_checked"] => ["hello", "nice"]
 *
 * @param $arrays
 * @param $keyName
 * @return array
 */
function ptyExtractWithKey($arrays, $keyName)
{
    $keys = array();
    foreach ($arrays as $k => $v) {
        $n = stripos($k, $keyName);
        if (strstr($k, $keyName)) {
            if ($n == strlen($k) - strlen($keyName)) {
                $realKeyName = substr($k, 0, -strlen($keyName));
                $keys[$realKeyName] = $v;
            }
        }
    }
    return $keys;
}

/**
 * arrays 에서 key, value 를 추출합니다.
 * ex) ptyGetValues($surveyItems, "id", "regDateTime", "작성일 : ");
 * => array ([77] => "작성일 : 2018...", [88] => "테스트")
 *
 * @param platyFramework\ptyArrayItemModel[] $arrays
 * @param $keyName
 * @param $valueName
 * @return array
 */

/*
function ptyGetKeyValuesFromItems($arrays, $keyName, $valueName, $additional = "")
{
    $ret = array();
    foreach ($arrays as $a) {
        $ret[$a->$keyName] = $additional . $a->$valueName;
    }
    return $ret;
}
*/

function ptyGetValues(array $array, mixed $keys)
{
    $rets = [];
    foreach ($array as $it) {
        $ret = array();
        foreach ($keys as $k => $v) {
            $ret[$v] = $it->_item[$k];
        }
        $rets[] = $ret;
    }
    return $rets;
}

/**
 * option 항목을 노출합니다.
 * ex)
 * ptyShowOption($item->group, ["" => "선택", "1군" => "1",  "2군" => "2", "3군" => "3", "4군" => "4", "5군" => "5"]);
 * @param $value
 * @param $keys
 */
function ptyShowSelectOption($selectedValues, $keys)
{
    if (is_array($selectedValues)) {
        foreach ($keys as $key => $value) {
            $selected = in_array($key, $selectedValues) ? "selected" : "";
            echo "<option {$selected} value='$key'>$value</option>\n";
        }
    } else {
        foreach ($keys as $key => $value) {
            $selected = $selectedValues == $key ? "selected" : "";
            echo "<option {$selected} value='$key'>$value</option>\n";
        }
    }
}

function ptySystem_cache($cmd)
{
    $tempFile = sys_get_temp_dir() . "/pty_system_cache_" . hash("sha256", $cmd);
    if (file_exists($tempFile) && filesize($tempFile) > 0) {
        return file_get_contents($tempFile);
    }

    $out = system($cmd, $returnVal);
    file_put_contents($tempFile, $out);
    return $out;
}

/**
 * exec 명령어 및 캐시 기능이 가능합니다
 *
 * @param $cmd
 * @param float|int $cacheTime
 * @return false|string
 * @copyright 2020-04-06
 */
function ptyExec_cache($cmd, $cacheTime = 5 * 60)
{
    $cacheFile = sys_get_temp_dir() . "/pty_exec_cache_" . hash("sha256", $cmd);
    $filemtime = @filemtime($cacheFile);
    if ($cacheTime > 0 && file_exists($cacheFile) && time() - $filemtime < $cacheTime) {
        return file_get_contents($cacheFile);
    }

    exec($cmd, $out);
    $out = implode($out, "\n");
    file_put_contents($cacheFile, $out);
    return $out;
}

/**
 * file_get_contents 를 캐쉬를 적용하여 읽어옵니다
 *
 * @param $url
 * @param float|int $cacheTime
 * @return bool|string
 */
function ptyGetUrlContent($url, $cacheTime = 5 * 60)
{
    $fileName = sys_get_temp_dir() . "/ptyGetUrlContent_" . hash("sha256", $url);
    $filemtime = @filemtime($fileName);

    pd($url, "ptyGetUrlContent() url, cacheTime = $cacheTime");


//    echo "url = $url\n";
//    echo "tmpfileName = $fileName\n";
    if ($cacheTime > 0 && file_exists($fileName) && time() - $filemtime < $cacheTime) {
        return file_get_contents($fileName);
    }

    // $out = file_get_contents($url);
    $out = ptyCurlGet($url);
    file_put_contents($fileName, $out);
    return $out;
}

function ptyCurlGet($url, $header = array())
{
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($curl, CURLOPT_HEADER, 0);
    curl_setopt($curl, CURLOPT_TIMEOUT, 60);
    curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:7.0.1) Gecko/20100101 Firefox/7.0.1');

    if ($header !== 0) {
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
    }

    curl_setopt($curl, CURLOPT_POST, 0);
    $response = curl_exec($curl);
    curl_close($curl);
    return $response;
}

function ptyCurlPost($url, $header = array(), $post = array(), $cacheTime = 0)
{
    if ($cacheTime > 0) {
        $hash = hash("sha256", $url . print_r($post, true));
        $fileName = PLATYFRAMEWORK_DATA_DIR . "/tmp/ptyCurlPost_cache_$hash.dat";

        if (file_exists($fileName)) {
            $filemtime = @filemtime($fileName);
            if (time() - $filemtime < $cacheTime) {
                return file_get_contents($fileName);
            }
        }
    }

    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($curl, CURLOPT_HEADER, 0);
    curl_setopt($curl, CURLOPT_TIMEOUT, 60);
    curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:7.0.1) Gecko/20100101 Firefox/7.0.1');

    if ($header !== 0) {
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
    }

    curl_setopt($curl, CURLOPT_POST, 1);

    if ($post !== 0) {
        curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
    }

    pd($header, "header");
    pd($post, "post");

    $response = curl_exec($curl);
    curl_close($curl);

    if ($cacheTime > 0) {
        $hash = hash("sha256", $url . print_r($post, true));
        $fileName = PLATYFRAMEWORK_DATA_DIR . "/tmp/ptyCurlPost_cache_$hash.dat";
        file_put_contents($fileName, $response);
    }

    pd($response, "response");

    return $response;
}

function ptyFile_get_contents_with_lock($file)
{
    $fp = fopen($file, 'r');
    if (!$fp) {
        echo "ptyFile_get_contents_with_lock() file open fail. file = $file \n";
        return "";
    }

    if (flock($fp, LOCK_SH)) { // 공유 잠금 (읽기용)
        // echo "ptyFile_get_contents_with_lock() filesize = ".filesize($file)."\n";
        $contents = fread($fp, filesize($file));
        // echo "ptyFile_get_contents_with_lock() contents = ".$contents."\n";
        flock($fp, LOCK_UN); // 잠금 해제
    } else {
        // echo "ptyFile_get_contents_with_lock() file lock failed. file = $file \n";
        throw new Exception("잠금 실패");
    }

    return $contents;
}

function ptyFile_put_contents_with_lock($file, $contents)
{
    $fp = fopen($file, 'c'); // c: 파일 없으면 생성, 있으면 내용 유지

    if (flock($fp, LOCK_EX)) { // 배타 잠금
        ftruncate($fp, 0);

        // echo "ptyFile_put_contents_with_lock() file = $file\n";
        // echo "ptyFile_put_contents_with_lock() contents = $contents\n";
        fwrite($fp, $contents);
        fflush($fp); // 버퍼 플러시
        flock($fp, LOCK_UN); // 잠금 해제
        chmod($file, 0777);
    } else {
        // echo "ptyFile_put_contents_with_lock() lock failed.\n";
        throw new Exception("잠금 실패");
    }
}

function ptyFile_get_contents_cache_onlyFileName($url)
{
    $tempFile = sys_get_temp_dir() . "/file_get_contents_cache_onlyFileName_" . hash("sha256", $url);
    if (file_exists($tempFile) && filesize($tempFile) > 0) {
        return $tempFile;
        // return file_get_contents($tempFile);
    }

    $out = file_get_contents($url);
    file_put_contents($tempFile, $out);
    return $tempFile;
}

/**
 * @param $message
 * @param string $code
 * @throws \platyFramework\ptyException
 */
function ptyThrow($message, $code = "", bool $sendTelegram = true)
{
    /*
    if (ispd()) {
        ptyShowCallstack("ptyThrow() '$message', code = $code");
        exit;
    }
    */

    /*
    // 로그 기록
    if (class_exists("platyFramework\\ptylogsItemsModel")) {
        $ptylogsItemsModel = \platyFramework\ptylogsItemsModel::build();
        $ptylogsItemsModel->addLogs("common", "ptyThrow", "", "", $message);
    }
    error_log("ptyThrow() message = {$message}, code = {$code}");
    */

    throw new \platyFramework\ptyException($message, $code, sendTelegram: $sendTelegram);
}

function ptyArrayInsert(&$arr, $value, $index = -1)
{
    if ($index == -1) {
        $arr[] = $value;
        return;
    }
    $lengh = count($arr);
    if ($index < 0 || $index > $lengh)
        return;

    for ($i = $lengh; $i > $index; $i--) {
        $arr[$i] = $arr[$i - 1];
    }

    $arr[$index] = $value;
}

function ptyCheckCronRunningDuplicate()
{
    global $platyFramework;

    $path = PLATYFRAMEWORK_DATA_DIR . "/cron_running_duplicate/";
    if (!file_exists($path))
        mkdir($path);

    $f = $path . "/_running_{$platyFramework->request->controllerName}_{$platyFramework->request->className}_{$platyFramework->request->functionName}";
    if (file_exists($f)) {

        //
        $json = json_decode(file_get_contents($f), true);

        if ($json['pid'] == "") {
            unlink($f);
        } // 실제 프로세스가 떠 있는가?
        else if (file_exists("/proc/{$json['pid']}")) {
            msg("process is already running.\nfile = $f\npid = {$json['pid']}");
            die();
        }

        // mail("cpueblo@platyhouse.com", "process duplicated - {$platyFramework->request->controllerName}_{$platyFramework->request->className}_{$platyFramework->request->functionName}", $f);
    }

    $pid = getmypid();
    if (!file_put_contents($f,
        json_encode(
            ["startDate" => date("Y-m-d H:i:s"), "pid" => $pid],
            JSON_UNESCAPED_UNICODE
        )
    )) {
        msg("cannot write. file = $f");
        exit;
    }
}

function ptyEndCronRunningDuplicate()
{
    global $platyFramework;
    $path = PLATYFRAMEWORK_DATA_DIR . "/cron_running_duplicate/";
    $f = $path . "/_running_{$platyFramework->request->controllerName}_{$platyFramework->request->className}_{$platyFramework->request->functionName}";
    unlink($f);
}


/**
 * 메세지 로그를 기록합니다
 *
 * @param $msg
 */
function msg($msg, $title = "")
{
    global $platyFramework;

    if ($title != "")
        $title = "[{$title}] ";

    $m = print_r($msg, true);

    if (php_sapi_name() == "cli")
        echo date("Y-m-d H:i:s.U") . " {$title}{$m}\n";
    else
        echo date("Y-m-d H:i:s.U") . " " . $m . "</br>";


    {
        $path = PLATYFRAMEWORK_DATA_DIR . "/logs";
        if (!file_exists($path))
            mkdir($path);
    }

    // $fileName = PLATYFRAMEWORK_DATA_DIR . "/logs/{$platyFramework->request->controllerName}_{$platyFramework->request->className}_{$platyFramework->request->functionName}_pid_".getmygid().".log";
    $fileName = PLATYFRAMEWORK_DATA_DIR . "/logs/{$platyFramework->request->controllerName}_{$platyFramework->request->className}_{$platyFramework->request->functionName}.log";

    $m = getmypid() . " " . date("Y-m-d H:i:s.U") . " " . $m . "\n";


//    $ctime = filectime($fileName);
//    if ($ctime - time() > )

    file_put_contents($fileName, $m, FILE_APPEND | LOCK_EX);
}

/**
 * comma 와 파라미터로 합쳐진 문자열을 배열로 리턴합니다
 *
 * 아래의 문자열
 * recruit_id=84&car_id=43&carNo=3호0001&driverName=유광희2&driverPhoneNumber=010-2982-5875&driverId=1008078,recruit_id=85&car_id=41&carNo=1호0001&driverName=유광희2&driverPhoneNumber=010-2982-5875&driverId=1008078
 * 을 아래처럼 변경해 줍니다
 * (
 * [0] => Array
 * (
 * [recruit_id] => 84
 * [car_id] => 43
 * [carNo] => 3호0001
 * [driverName] => 유광희2
 * [driverPhoneNumber] => 010-xxxx-5875
 * [driverId] => 1008078
 * )
 *
 * [1] => Array
 * (
 * [recruit_id] => 85
 * [car_id] => 41
 * [carNo] => 1호0001
 * [driverName] => 유광희2
 * [driverPhoneNumber] => 010-xxxx-5875
 * [driverId] => 1008078
 * )
 * )
 *
 * @param $str
 * @return array
 */
function ptyExplodeCommaParam($str)
{
    $strs = explode(",", $str);
    $rs = array();
    foreach ($strs as $s) {
        parse_str($s, $r);
        if (count($r))
            $rs[] = $r;
    }
    if (count($rs))
        return $rs;
    else
        return [];
}

/**
 * , 로 되어진 항목을 배열로 리턴합니다
 * @param $str
 * @return array
 */
function ptyExplode($delimiter = ",", $str = "", $str2 = "")
{
    $in = "";
    if ($str != "" && $str2 == "") $in = $str;
    if ($str == "" && $str2 != "") $in = $str2;

    if (strstr($in, $delimiter))
        $ids = explode($delimiter, $in);
    else
        $ids = [trim($in)];

    foreach ($ids as $i => $id) {
        $ids[$i] = trim($ids[$i]);
    }

    return $ids;
}

function ptyMultiExplode($delimiters, $string)
{

    $ready = str_replace($delimiters, $delimiters[0], $string);
    $launch = explode($delimiters[0], $ready);
    return array_filter($launch);
}


function ptyGetCurlJson($method, $url, $header, $data)
{
    if ($method == 'post') {
        $method_type = 1; // 1 = POST
    } else {
        $method_type = 0; // 0 = GET
    }

    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($curl, CURLOPT_HEADER, 0);

    if ($header !== 0) {
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
    }

    curl_setopt($curl, CURLOPT_POST, $method_type);

    if ($data !== 0) {
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    }

    $response = curl_exec($curl);
    $json = json_decode($response, true);
    curl_close($curl);

    return $json;
}

function getBaseDomain($dom)
{
    $matches = array();
    preg_match('/[^\.]+\.([^\.]{4}|[^\.]{3}|(co|or|pe|ne|re|go|hs|ms|es|kg|sc|ac)\.[^\.]{2}|[^\.]{2})$/i', $dom, $matches);
    return $matches[0];
}

function ptyImplodeApostrophe($s)
{
    return "'" . implode("', '", $s) . "'";
}

function ptyInImplodeApostrophe($s)
{
    return " IN ('" . implode("', '", $s) . "')";
}

function aesEcbEncrypt(string $data, string $key)
{
    if (strlen($key) !== 16) {
        throw new \Exception("Key must be 16 bytes for AES-128-ECB");
    }

    // AES-128-ECB로 암호화
    $encrypted = openssl_encrypt($data, 'aes-128-ecb', $key, OPENSSL_RAW_DATA);

    // Base64로 인코딩하여 반환
    return base64_encode($encrypted);
}

/**
 * 1. + 를 - 로 변경
 * 2. / 를 _ 로 변경
 * 3. 마지막 = 을 제거
 *
 * @param string $data
 * @return string
 */
function ptyBase64UrlEncode(string $data): string
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function ptyBase64UrlDecode(string $data): string
{
    $padding = strlen($data) % 4;
    if ($padding > 0) {
        $data .= str_repeat('=', 4 - $padding);
    }
    return base64_decode(strtr($data, '-_', '+/'));
}


/**
 * AES-128-CBC 로 encrypt
 *
 * @param string $data
 * @param string $key
 *
 */
function ptyEncryptAes128CBC(string $data, string $key, bool $isRandomIv = true, ?string $iv = null): string
{
    // 128비트(16바이트) 키를 확인합니다.
    if (strlen($key) !== 16) ptyThrow("ptyEncryptAes128CBC() Key must be 16 bytes for AES-128-CBC");

    if (is_null($iv)) {
        if ($isRandomIv) {
            // 초기화 벡터(IV)를 생성합니다.
            $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-128-cbc'));
        } else {
            // 고정된 초기화 벡터(IV)를 사용
            $iv = substr($key, 0, openssl_cipher_iv_length('aes-128-cbc'));
        }
    }

    // AES-128-CBC로 데이터 암호화
    $encrypted = openssl_encrypt($data, 'aes-128-cbc', $key, 0, $iv);

    // 암호화된 데이터와 IV를 결합하여 인코딩
    $combined = $encrypted . "::" . $iv;

    $output = ptyBase64UrlEncode($combined);

    // pd($output, "ptyEncryptAes128CBC.output");

    return $output;
}


/**
 * AES-128-CBC 로 decrypt
 *
 * @param string $encrypted_data
 * @param string $key
 * @return false|string
 * @throws Exception
 */
function ptyDecryptAes128CBC(string $s, string $key)
{
    // 128비트(16바이트) 키를 확인합니다.
    if (strlen($key) !== 16) {
        throw new \Exception("Key must be 16 bytes for AES-128-CBC");
    }

    // Base64로 인코딩된 데이터를 디코딩합니다.
    list($encrypted_data, $iv) = explode('::', ptyBase64UrlDecode($s), 2);

    // AES-128-CBC로 데이터 복호화
    $output = openssl_decrypt($encrypted_data, 'aes-128-cbc', $key, 0, $iv);

    // -, 와 . 에 대한 보안 처리건
    if ($output === false) {
        $encrypted_data = str_replace("-", "_", $s);
        $encrypted_data = str_replace(".", "+", $encrypted_data);

        // Base64로 인코딩된 데이터를 디코딩합니다.
        list($encrypted_data, $iv) = explode('::', ptyBase64UrlDecode($encrypted_data), 2);

        // AES-128-CBC로 데이터 복호화
        $output = openssl_decrypt($encrypted_data, 'aes-128-cbc', $key, 0, $iv);
    }

    return $output;
}

/**
 * 동적인 길이로 GUID 를 생성함
 *
 * @param $length
 * @return array|string|string[]|null  382B-5A5F-C574 의 형태
 */
function ptyGenerateMediumGUID($length = 12)
{
    $guid = strtoupper(substr(bin2hex(openssl_random_pseudo_bytes($length / 2)), 0, $length));

    // 길이에 따라 패딩 처리
    $paddedGuid = str_pad($guid, ceil(strlen($guid) / 4) * 4, '0');

    // 4, 4, 4 단위로 자르고 '-' 추가
    return preg_replace('/(.{4})(.{4})(.{4})/', '$1-$2-$3', $paddedGuid);
}

function ptyGenerateSmallGUID($length = 12)
{
    // 알파벳(a~z, A~Z) 문자 목록
    // $characters = 'abcdefghjklmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ';
    $characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
    $charactersLength = strlen($characters);
    $randomString = '';

    // 지정된 길이만큼 랜덤 문자열 생성
    for ($i = 0; $i < $length; $i++) {
        $randomString .= $characters[rand(0, $charactersLength - 1)];
    }

    return $randomString;
}

/**
 * 6F5090B5-3EBB-4098-9CEC-A175C978CF06
 * @return string
 */
function ptyGenerateGUID()
{
    if (function_exists('com_create_guid') === true)
        return strtoupper(trim(com_create_guid(), '{}'));

    $data = openssl_random_pseudo_bytes(16);
    $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
    $data[8] = chr(ord($data[8]) & 0x3f | 0x80);
    $guid = strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)));
    return $guid;
}

function ptyGetPhoneNumberStyle(string $phoneNumber = "")
{
    // +82로 시작하는 국제번호 처리
    if (strpos($phoneNumber, '+82') === 0) {
        $phoneNumber = '0' . substr($phoneNumber, 3); // +82 제거하고 앞에 0 추가
    }

    // 숫자만 남기기
    $phoneNumber = preg_replace('/\D/', '', $phoneNumber);

    // 전화번호 형식 확인 및 변환
    if (strlen($phoneNumber) === 11) {
        // 11자리 번호: 010-XXXX-YYYY
        return preg_replace('/^(\d{3})(\d{4})(\d{4})$/', '$1-$2-$3', $phoneNumber);
    } elseif (strlen($phoneNumber) === 10) {
        // 10자리 번호: 02-XXX-YYYY 또는 031-XXX-$phoneNumber
        return preg_replace('/^(\d{2,3})(\d{3})(\d{4})$/', '$1-$2-$3', $phone);
    }

    // 변환할 수 없는 경우 원래 번호 반환
    return $phoneNumber;
}

/**
 * "2024년 11월 21일(목) 오후 3시 44분" 형태로 변환
 *
 * @param $dateTime
 * @return string
 */
function ptyGetKorStyleDateTime($time)
{
    // 입력값이 숫자인지 확인 (타임스탬프 처리)
    if (is_numeric($time)) {
        $dt = new \DateTime();
        $dt->setTimestamp((int)$time); // 타임스탬프를 DateTime으로 설정
    } else {
        $dt = new \DateTime($time); // 문자열 날짜는 그대로 처리
    }

    // 요일 배열 정의
    $daysOfWeek = ['일', '월', '화', '수', '목', '금', '토'];
    $dayOfWeek = $daysOfWeek[(int)$dt->format('w')]; // 숫자 요일을 한글 요일로 변환

    // 시간대 설정
    $hour = (int)$dt->format('H');
    $ampm = $hour >= 12 ? '오후' : '오전';
    $formattedHour = $hour > 12 ? $hour - 12 : $hour; // 12시간제로 변환

    return $dt->format('Y') . '년 ' .
        $dt->format('m') . '월 ' .
        $dt->format('d') . '일(' . $dayOfWeek . ') ' .
        $ampm . ' ' . $formattedHour . ':' .
        $dt->format('i');
}


function ptyGetRemainDateHourMinSecond(string $dateTime, string $now = "")
{
    if ($now == "") $now = date("Y-m-d H:i:s");

    // 현재 시간과 입력받은 시간 생성
    $currentDateTime = new DateTime($now);
    $targetDateTime = new DateTime($dateTime);

    // 시간 차이 계산
    $interval = $currentDateTime->diff($targetDateTime);

    // 시간 차이를 초 단위로 계산
    $secondsDifference = $targetDateTime->getTimestamp() - $currentDateTime->getTimestamp();

    // 남은 시간이 과거인 경우 처리
    if ($secondsDifference < 0) {
        return $targetDateTime->format("Y-m-d H:i:s");
    }

    // 1년 이상 남은 경우
    if ($interval->y > 0) {
        return sprintf(
            "%d년 %d개월 %d일 %d시간 %d분",
            $interval->y,
            $interval->m,
            $interval->d,
            $interval->h,
            $interval->i
        );
    }

    // 1개월 이상 남은 경우
    if ($interval->m > 0) {
        return sprintf(
            "%d개월 %d일 %d시간 %d분",
            $interval->m,
            $interval->d,
            $interval->h,
            $interval->i
        );
    }

    // 24시간 이후 남은 경우
    if ($secondsDifference > 24 * 60 * 60) {
        return sprintf("%d일 %d시간 %d분", $interval->d, $interval->h, $interval->i);
    }

    // 24시간 이내 남은 경우
    return sprintf("%d시간 %d분", $interval->h, $interval->i);
}


/**
 * n시간 n분 n초 를 리턴
 *
 * @param $s
 * @return string
 */
function ptyGetElapseHourMinSecond($s)
{
// 시간, 분, 초 계산
    $hours = floor($s / 3600); // 1시간 = 3600초
    $minutes = floor(($s % 3600) / 60); // 남은 초에서 분 계산
    $seconds = $s % 60; // 나머지 초 계산

    // 결과 문자열 생성
    $result = "";

    if ($hours > 0) {
        $result .= "{$hours}시간 ";
    }

    if ($minutes > 0) {
        $result .= "{$minutes}분 ";
    }

    if ($seconds > 0) {
        $result .= "{$seconds}초";
    }

    if ($result == "") return "0분";

    return trim($result); // 불필요한 공백 제거
}

/**
 * 두 시간을 비교하여
 * 오늘일 경우 : n시간 n 분 남았습니다. 표기
 * 다른 날일 경우 : n일 후 n일 오전 n시 형태
 *
 *
 * @param $timeA
 * @param $timeB
 * @return string
 */
function ptyGetTimeDifference($timeA, $timeB): string
{
    $timestampA = is_numeric($timeA) ? (int)$timeA : strtotime($timeA);
    $timestampB = is_numeric($timeB) ? (int)$timeB : strtotime($timeB);

    // 시간값 비교 (B가 더 미래의 시간인지 확인)
    if ($timestampB < $timestampA) {
        return "-";
    }

    // 현재 A와 B의 날짜 (Y-m-d)를 비교하여 같은 날인지 확인
    $dateA = date('Y-m-d', $timestampA);
    $dateB = date('Y-m-d', $timestampB);

    if ($dateA === $dateB) {
        // 오늘 안의 차이는 "hh시간 mm분 ss초" 형태로 반환
        return ptyGetElapseHourMinSecond($timestampB - $timestampA);
    } else {
        // 날짜가 다를 경우 "n일 후인 m월 d일 오후 h시" 형태로 반환
        $daysLater = floor(($timestampB - $timestampA) / 86400); // 1일 = 86400초
        $formattedDate = date('d일 A g시', $timestampB); // 'A g시'는 오전/오후 + 12시간제
        $formattedDate = str_replace(['AM', 'PM'], ['오전', '오후'], $formattedDate); // AM/PM 변환

        return "{$daysLater}일 후 {$formattedDate}";
    }
}


function ptyGetWeekLabel(string $now = "")
{
    if ($now == "") $now = date("Y-m-d H:i:s");

    // 날짜를 타임스탬프로 변환하고 요일 숫자 가져오기 (1~7)
    $dayOfWeek = date('N', strtotime($now));

    // 요일 이름 배열
    $dayNames = ['월', '화', '수', '목', '금', '토', '일'];

    // 숫자에 해당하는 요일 이름 반환
    return $dayNames[$dayOfWeek - 1];
}

function ptyGetNow()
{
    return date('Y-m-d H:i:s');
}

function ptyGetDate()
{
    return date('Y-m-d');
}

/**
 * 현재 시간을 Ymd_His_ms 로 리턴
 * @return string
 */
function ptyGetNowMs()
{
    // 현재 시간
    $now = microtime(true);

    // 날짜와 시간 포맷
    $datetime = date('Ymd_His', $now);

    // 밀리세컨드 추가
    $milliseconds = sprintf('%03d', ($now - floor($now)) * 1000);

    // 최종 결과
    $result = $datetime . '_' . $milliseconds;
    return $result;
}

/**
 * 현재 시간을 time()_ms 로 리턴
 * @return string
 */
function ptyGetNowTimeMs()
{
    // 현재 시간
    $now = microtime(true);

    // 밀리세컨드 추가
    $milliseconds = sprintf('%03d', ($now - floor($now)) * 1000);

    // 최종 결과
    $result = time() . '_' . $milliseconds;
    return $result;
}

/**
 * 오전, 오후, 저녁을 리턴
 * @return string
 */
function ptyGetTimePeriod(string $now = "")
{
    if ($now == "") $now = date("Y-m-d H:i:s");
    $hour = (int)date('H', strtotime($now));

    if ($hour >= 0 && $hour < 12) {
        return '오전';
    } elseif ($hour >= 12 && $hour <= 17) {
        return '오후';
    } else {
        return '저녁';
    }
}

/**
 * 2024-12-01 15:00:00, 10 을 입력시 10분을 더함
 *
 * @param $datetime
 * @param $minute
 * @return string
 * @throws Exception
 */
function ptySumDateMinute(string $datetime, int $minute)
{
    $date = new DateTime($datetime);
    $date->add(new DateInterval('PT' . $minute . 'M'));
    return $date->format('Y-m-d H:i:s');
}

function ptySumDate(string $datetime, int $day)
{
    $date = new DateTime($datetime);
    $date->add(new DateInterval('P' . $day . 'D'));
    return $date->format('Y-m-d H:i:s');
}

function ptyGetTimeDifferenceMinSeconds($timeA, $timeB): string
{
    $timestampA = is_numeric($timeA) ? (int)$timeA : strtotime($timeA);
    $timestampB = is_numeric($timeB) ? (int)$timeB : strtotime($timeB);

    // 시간값 비교 (B가 더 미래의 시간인지 확인)
    if ($timestampB < $timestampA) {
        return "-";
    }

    // 차이를 초 단위로 계산
    $differenceInSeconds = $timestampB - $timestampA;

    // 분과 초 계산
    $minutes = floor($differenceInSeconds / 60); // 전체 초에서 분 계산
    $seconds = $differenceInSeconds % 60;       // 남은 초 계산

    return "{$minutes}분 {$seconds}초";
}

/**
 * 현재 시간과 비교하여 년전, 일전, 시간전, 분전, 초전을 표기
 *
 * @param $datetime
 * @return false|string
 */
function ptyGetPastDateTime($datetime = '', ?int $curTime = null)
{
    if ($datetime == "")
        return "";

    if (empty($datetime)) {
        return "";
    }

    if (empty($datetime)) {
        return false;
    }

    if ($curTime == null)
        $diff = time() - strtotime($datetime);
    else
        $diff = $curTime - strtotime($datetime);


    $s = 60; //1분 = 60초
    $h = $s * 60; //1시간 = 60분
    $d = $h * 24; //1일 = 24시간
    $y = $d * 10; //1년 = 1일 * 10일

    if ($diff < $s) {
        if ((int)$diff < 0)
            $result = "";
        else
            $result = $diff . '초전';
    } elseif ($h > $diff && $diff >= $s) {
        $result = round($diff / $s) . '분전';
    } elseif ($d > $diff && $diff >= $h) {
        $result = round($diff / $h) . '시간전';
    } elseif ($y > $diff && $diff >= $d) {
        $result = round($diff / $d) . '일전';
    } else {
        $result = date('Y.m.d.', strtotime($datetime));
    }

    if ($result == "0초전")
        $result = "방금";

    return $result;
}

/**
 * 숫자 (초) 를 특정 시간 형태로 변경
 *
 * @param int $sec
 * @param $type
 * A: 90초
 * B: 1분 30초
 * C: 1분 (60초 미만 내림)
 * C2: 1 (60초 미만 내림, 분 빠짐)
 * D: 1분 30초 (90초)
 * @return string
 */
function ptyGetKoreanSecond(int $sec, $type = "A")
{
    if ($type == "A") {
        return $sec . "초";
    }

    if ($type == "B") {
        $min = floor($sec / 60); // 분 단위
        $remainSec = $sec % 60;  // 남은 초
        if ($min > 0 && $remainSec > 0) {
            return $min . "분 " . $remainSec . "초";
        } elseif ($min > 0) {
            return $min . "분";
        } else {
            return $remainSec . "초";
        }
    }

    if ($type == "C") {
        return floor($sec / 60) . "분"; // 60초 미만은 내림 처리
    }

    if ($type == "C2") {
        return floor($sec / 60) . ""; // 60초 미만은 내림 처리
    }

    if ($type == "D") {
        return ptyGetKoreanSecond($sec, type: "B") . " (" . ptyGetKoreanSecond($sec, type: "A") . ")";
    }

    if ($type == "E") {
        $seconds = $sec;

        $years = floor($seconds / (365 * 24 * 60 * 60));
        $seconds %= (365 * 24 * 60 * 60);

        $months = floor($seconds / (30 * 24 * 60 * 60)); // 평균 30일 기준
        $seconds %= (30 * 24 * 60 * 60);

        $days = floor($seconds / (24 * 60 * 60));
        $seconds %= (24 * 60 * 60);

        $hours = floor($seconds / (60 * 60));
        $seconds %= (60 * 60);

        $minutes = floor($seconds / 60);
        $seconds %= 60;

        // 결과를 담을 배열
        $result = [];

        if ($years > 0) $result[] = "{$years}년";
        if ($months > 0) $result[] = "{$months}개월";
        if ($days > 0) $result[] = "{$days}일";
        if ($hours > 0) $result[] = "{$hours}시간";
        if ($minutes > 0) $result[] = "{$minutes}분";
        if ($seconds > 0 || empty($result)) $result[] = "{$seconds}초"; // 0초인 경우를 방지

        return implode(" ", $result) . " ($sec 초)";
    }

    if ($type == "E2") {
        $seconds = $sec;

        $years = floor($seconds / (365 * 24 * 60 * 60));
        $seconds %= (365 * 24 * 60 * 60);

        $months = floor($seconds / (30 * 24 * 60 * 60)); // 평균 30일 기준
        $seconds %= (30 * 24 * 60 * 60);

        $days = floor($seconds / (24 * 60 * 60));
        $seconds %= (24 * 60 * 60);

        $hours = floor($seconds / (60 * 60));
        $seconds %= (60 * 60);

        $minutes = floor($seconds / 60);
        $seconds %= 60;

        // 결과를 담을 배열
        $result = [];

        if ($years > 0) $result[] = "{$years}년";
        if ($months > 0) $result[] = "{$months}개월";
        if ($days > 0) $result[] = "{$days}일";
        if ($hours > 0) $result[] = "{$hours}시간";
        if ($minutes > 0) $result[] = "{$minutes}분";
        if ($seconds > 0 || empty($result)) $result[] = "{$seconds}초"; // 0초인 경우를 방지

        return implode(" ", $result);
    }
}

/**
 * 2024-01-01 12:30 형태를 특정 시간 형태로 변경
 *
 * @param $dateString
 * @param $type
 * A: 1980년 3월 2일 (금) 12시 30분
 * B: 1980년 3월 2일 (금) 오후 10:30
 *
 * @return array|string|string[]
 * @throws Exception
 */
function ptyGetKoreanDateTime($dateString, $showShortWeek = false, $type = "A")
{
    if ($dateString == "") return "";

    if (true) {
        // 입력값이 숫자인지 확인 (타임스탬프 처리)
        if (is_numeric($dateString)) {
            $dt = new \DateTime();
            $dt->setTimestamp((int)$dateString); // 타임스탬프를 DateTime으로 설정
        } else {
            $dt = new \DateTime($dateString); // 문자열 날짜는 그대로 처리
        }

        // 요일 배열 정의
        $daysOfWeek = ['일', '월', '화', '수', '목', '금', '토'];
        $dayOfWeek = $daysOfWeek[(int)$dt->format('w')]; // 숫자 요일을 한글 요일로 변환

        // 시간대 설정
        $hour = (int)$dt->format('H');
        $amPm = $hour >= 12 ? '오후' : '오전';
        $hour_12 = $hour > 12 ? $hour - 12 : $hour; // 12시간제로 변환

        // B: 1980년 3월 2일 (금) 오후 10:30
        if ($type == "B") {
            return $dt->format("Y년 n월 j일($dayOfWeek) $amPm $hour_12:i");
        }
    }

    // DateTime 객체 생성
    $date = new DateTime($dateString);

    if ($showShortWeek == false) {
        // 원하는 형식으로 변환
        $formattedDate = $date->format('Y년 n월 j일 l G시 i분');

        // 요일을 한글로 변환
        $daysOfWeek = [
            'Monday' => '월요일',
            'Tuesday' => '화요일',
            'Wednesday' => '수요일',
            'Thursday' => '목요일',
            'Friday' => '금요일',
            'Saturday' => '토요일',
            'Sunday' => '일요일',
        ];
        $formattedDate = str_replace(array_keys($daysOfWeek), array_values($daysOfWeek), $formattedDate);
    } else {
        // 원하는 형식으로 변환
        $formattedDate = $date->format('Y년 n월 j일(l) G시 i분');

        // 요일을 한글로 변환
        $daysOfWeek = [
            'Monday' => '월',
            'Tuesday' => '화',
            'Wednesday' => '수',
            'Thursday' => '목',
            'Friday' => '금',
            'Saturday' => '토',
            'Sunday' => '일',
        ];
        $formattedDate = str_replace(array_keys($daysOfWeek), array_values($daysOfWeek), $formattedDate);

    }

    return $formattedDate;
}


/**
 * file_get_contents 의 동기 모드를 sleep 이용한 비동기 모드로 실행
 * @param $url
 * @param $sleep
 * @return void
 */
function ptySendAsyncRequest(string $url, int $sleep = 0)
{
    global $ptySendAsyncRequestCount;

    $cmd = "nohup bash -c 'sleep $sleep; curl -s \"$url\" > /dev/null 2>&1' > /dev/null 2>&1 &";
    $ptySendAsyncRequestCount++;
    if ($ptySendAsyncRequestCount > 20) {
        ptyShowCallstack();
        die ("error ptySendAsyncRequestCount");
    }

    exec($cmd);
}

function ptyAsyncFileGetContents(string $url, int $sleep = 0)
{
    ptySendAsyncRequest(url: $url, sleep: $sleep);
}

/**
 * 퍼센트 곱셈을 위해 10 이 입력되면 0.9 를 리턴합니다.
 *
 * 500 * ptyPerMultiper(10) = 450
 * @param $percent
 * @return float|int
 */
function ptyPerMultiplier($percent)
{
    return (100 - $percent) / 100;
}

function ptyGetUserAgentBot(?string $userAgent = null)
{
    if (is_null($userAgent) || $userAgent == "")
        return null;

    // 봇 키워드와 서비스 이름 매핑
    $botList = [
        'oai-searchbot' => 'OpenAI SearchBot',
        'chatgpt-user' => 'OpenAI GPTUser',
        'googlebot' => 'Google',
        'adsbot-google' => 'Google Ads',
        'mediapartners-google' => 'Google AdSense',
        'bingbot' => 'Bing',
        'bingpreview' => 'Bing Preview',
        'baiduspider' => 'Baidu',
        'yandexbot' => 'Yandex',
        'duckduckbot' => 'DuckDuckGo',
        'slurp' => 'Yahoo',
        'facebookexternalhit' => 'Facebook',
        'facebot' => 'Facebook',
        'twitterbot' => 'Twitter',
        'linkedinbot' => 'LinkedIn',
        'embedly' => 'Embedly',
        'quora link preview' => 'Quora',
        'showyoubot' => 'ShowYou',
        'outbrain' => 'Outbrain',
        'pinterest' => 'Pinterest',
        'slackbot' => 'Slack',
        'vkshare' => 'VKontakte',
        'rogerbot' => 'Moz',
        'ahrefsbot' => 'Ahrefs',
        'mj12bot' => 'Majestic',
        'semrushbot' => 'SEMRush',
        'dotbot' => 'Moz Dotbot',
        'screaming frog' => 'Screaming Frog',
        'uptimebot' => 'Uptime Robot',
        'w3c_validator' => 'W3C Validator',
        'https://naver.me/spd' => 'NAVER'
    ];

    $userAgentLower = strtolower($userAgent);

    foreach ($botList as $keyword => $botName) {
        if (strpos($userAgentLower, $keyword) !== false) {
            return $botName; // 어떤 봇인지 반환
        }
    }

    return null; // 봇이 아님
}

function ptyGetPrettyText(mixed $s)
{

    if (is_string($s)) {
        $json = json_decode($s, true);

        if ($json !== FALSE) {
            // return nl2br(print_r($json, true));
            return ptyGetColorArrayHtml($json);
        }
    }

    return $s;
}

function ptySessionUnlink()
{
    $session_file = session_save_path() . '/sess_' . session_id();

    session_destroy();
    if (file_exists($session_file)) {
        unlink($session_file); // 직접 삭제
    }
}

/**
 * 문자열이 패턴에 맞는지 검사하는 함수
 * n: 한자리 숫자 (0~9)
 * N: 숫자(0~9) 여러자리 (1자리 이상)
 * s: 영문자 1개 또는 한글 1개
 * S: 임의의 문자열(1자 이상)
 * @param string $str 검사할 문자열
 * @param string $pattern 검사 패턴
 * @return bool
 */
function ptyCheckPattern($str, $pattern)
{
    $reg = '';
    $len = strlen($pattern);

    for ($i = 0; $i < $len; $i++) {
        $ch = $pattern[$i];
        if ($ch === 'n') {
            $reg .= '[0-9]';
        } else if ($ch === 'N') {
            $reg .= '[0-9]+';
        } else if ($ch === 's') {
            // 영문 1글자 또는 한글 1글자
            // 한글: \x{AC00}-\x{D7AF} (완성형), \x{3130}-\x{318F} (자음/모음)
            $reg .= '(?:[A-Za-z]|[\x{AC00}-\x{D7AF}\x{3130}-\x{318F}])';
        } else if ($ch === 'S') {
            // 임의의 문자열(1글자 이상)
            $reg .= '.+';
        } else {
            // 정규식 특수문자 escape
            if (preg_match('/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/', $ch)) {
                $reg .= '\\' . $ch;
            } else {
                $reg .= $ch;
            }
        }
    }
    $reg = '/^' . $reg . '$/u'; // u 플래그(UTF-8 한글 지원)
    // 패턴에 N 또는 S가 있으면 길이는 정규식으로만 체크
    if (strpos($pattern, 'N') !== false || strpos($pattern, 'S') !== false) {
        return preg_match($reg, $str) === 1;
    } else {
        // N, S 없으면 길이까지 정확히 일치
        return (preg_match($reg, $str) === 1) && (mb_strlen($str, 'UTF-8') === mb_strlen($pattern, 'UTF-8'));
    }
}

function ptyEcho(...$args)
{
    global $_ptyEcho_pid;
    global $_ptyEcho_uid;
    global $_ptyEcho_userInfo;

    if (!$_ptyEcho_pid) {
        $_ptyEcho_pid = getmypid();
        $_ptyEcho_uid = getmyuid();
        $_ptyEcho_userInfo = posix_getpwuid($_ptyEcho_uid);
    }

    $out = [];

    echo "\r";

    if (count($args) == 1) {
        $out [] = $args[0];
    } else {
        // 두 번째 이후 인자가 있다면 print_r 로 출력
        for ($i = 0; $i < count($args); $i++) {
            if ($i % 2 == 1) $out [] = " = ";
            $out [] = print_r($args[$i], true);
            if ($i % 2 == 1) $out [] = ", ";
        }
        array_pop($out);
    }

    echo ptyTerminalColor::redBoldBright;
    {
        $microtime = microtime(true);
        $datetime = date("Y-m-d H:i:s", (int)$microtime);
        $milliseconds = (int)(($microtime - floor($microtime)) * 1000);
        echo sprintf("%s.%03d ", $datetime, $milliseconds);
    }

    echo ptyTerminalColor::red;
    echo "[$_ptyEcho_userInfo[name] ($_ptyEcho_uid) / $_ptyEcho_pid] ";
    echo ptyTerminalColor::yellowBoldBright;
    echo implode("", $out);
    echo " " . ptyTerminalColor::reset . "\n";
}

function ptyError($message)
{
    die ($message);
}

/**
 * 배열의 모든 리프 노드 값을 수집하여 결합
 *
 * @param array $data 입력 배열
 * @param string $separator 구분자
 * @return string 결합된 문자열
 */
function ptyRecursiveImplode(string $separator = "\n", string|array $data = [], bool $trim = true): string
{
    if (!is_array($data)) {
        return $trim ? trim($data) : $data;
    }

    $result = [];

    array_walk_recursive($data, function ($value) use (&$result, $trim) {
        $str = (string)$value;
        if ($trim) {
            $str = trim($str);
            // 빈 문자열 제외 (선택사항)
            if ($str !== '') {
                $result[] = $str;
            }
        } else {
            $result[] = $str;
        }
    });

    return implode($separator, $result);
}

// 사용
// $content = flattenAndImplode($data['항내용']);

function ptyShowThrowable(\Throwable|\Exception $e, ?platyFramework\ptyCliLog $log = null)
{
    if (!$log) {
        require_once("../cli/ptyCliColor.php");
        require_once("../cli/ptyCliLog.php");
        $log = new ptyCliLog(prefix: "App", color: ptyCliLog::COLOR_RED);
    }

    $log->error("Error Type: " . get_class($e));
    $log->error("Error Message: " . $e->getMessage());
    $log->error("Error Code: " . $e->getCode());
    $log->error("Error File: " . $e->getFile());
    $log->error("Error Line: " . $e->getLine());
    $log->error("Stack Trace:\n" . $e->getTraceAsString());

    // 이전 예외가 있으면 (체인된 예외)
    if ($e->getPrevious()) {
        $log->error("Previous Exception: " . $e->getPrevious()->getMessage());
    }
}

function ptyRemoveSpaces($s)
{
    $s = str_replace("  ", " ", $s);
    $s = str_replace("\n\n", "\n", $s);
    return $s;
}

?>
