<?php

// ==========
// Base Class
// ==========

require_once "vendor/autoload.php";
require_once "classes/Autoloader.php";

class Base {

    // =========
    // Constants
    // =========

    const APPLICATION_NAME = "DEC Data Repository";
    const APPLICATION_MNEMONIC = "DECDR";
    const VERSION = "2.00";
    const TIMEZONE = "America/New_York";
    // =================
    // Session Variables
    // =================

    const ENVIRONMENT = "DECDR_ENVIRONMENT";
    const ACTIVE_SESSION = "DECDR_ACTIVE_SESSION";
    const USER_ID = "DECDR_USER_ID";
    const USER_SERIAL = "DECDR_USER_SERIAL";
    const USER_NAME = "DECDR_USER_NAME";
    const USER_CLIENT_NAME = "DECDR_USER_CLIENT_NAME";
    const USER_EMAIL = "DECDR_USER_EMAIL";
    const USER_ROLE = "DECDR_USER_ROLE";
    const USER_AUTHENTICATED = "DECDR_USER_AUTHENTICATED";
    const REPORTS_GET = "DECDR_REPORTS_GET";
    const REPORTS_POST = "DECDR_REPORTS_POST";
    const THEME = "DECDR_THEME";
    const AJAX = "DECDR_AJAX";
    const TODAY = "DECDR_TODAY";
    const COPYRIGHT = "DECDR_COPYRIGHT";
    const PAGE = "DECDR_PAGE";
    const PAGINATION_PAGE = "DECDR_PAGINATION_PAGE";

    // =================
    // Class Constructor
    // =================

    public function __construct() {

        set_error_handler(array($this, "displayError"));

        date_default_timezone_set(self::TIMEZONE);
    }

    // ===============================================
    // Append a SimpleXMLElement to a SimpleXMLElememt
    // ===============================================

    public function appendSXE(SimpleXMLElement $from, SimpleXMLElement $to) {

        $toDOM = dom_import_simplexml($to);
        $fromDOM = dom_import_simplexml($from);

        $toDOM->appendChild($toDOM->ownerDocument->importNode($fromDOM, true));
    }

    // ==========================================
    // Apply an XSL Stylesheet to an XML Document
    // ==========================================

    public function applyXSL($XMLString = "", $XSLString = "", $XSLParms = "") {

        if ($XMLString instanceof SimpleXMLElement) {
            $XMLString = $XMLString->asXML();
        }

        if (empty($XSLString)) {
            trigger_error("No XSL Stylesheet provided.");
        }

        if (!is_array($XSLParms)) {
            $XSLParms = array();
        }

        $XSLParms["APPLICATION_NAME"] = self::APPLICATION_NAME;
        $XSLParms["APPLICATION_MNEMONIC"] = self::APPLICATION_MNEMONIC;
        $XSLParms["VERSION"] = self::VERSION;
        $XSLParms["THEME"] = $_SESSION[self::THEME] ?? self::THEME;
        $XSLParms["ENVIRONMENT"] = $_SESSION[self::ENVIRONMENT] ?? "Production";
        $XSLParms["USER_NAME"] = $_SESSION[self::USER_NAME] ?? "";
        $XSLParms["USER_CLIENT_NAME"] = $_SESSION[self::USER_CLIENT_NAME] ?? "";
        $XSLParms["USER_ROLE"] = $_SESSION[self::USER_ROLE] ?? "Guest";
        $XSLParms["RANDOM_NUMBER"] = rand();
        $XSLParms["TODAY_YYYYMMDD"] = date("Y-m-d");
        $XSLParms["TODAY_MMDDYYYY"] = date("m/d/Y");
        $XSLParms["TODAY_HHMM"] = date("Hi");
        $XSLParms["COPYRIGHT"] = date("Y");

        $XML = new DOMDocument();
        $XSL = new DOMDocument();
        $XSLT = new XSLTProcessor();

        if ($XMLString) {
            $XML->loadXML($XMLString);
        }

        if ($XSLString) {
            $XSL->loadXML($XSLString);
        }

        $XSLT->importStylesheet($XSL);

        if ($XSLParms) {
            $XSLT->setParameter("", $XSLParms);
        }

        $document = $XSLT->transformToXml($XML);

        return $document;
    }

    // ============================
    // Return a Database Connection
    // ============================

    public function connect() {

        $Settings = $this->getSettings();

        if ($Settings->Environment == 'Production') {
            $host = 'localhost';
        } else {
            $host = '192.168.1.190';
        }

        try {

            $database = (string) $Settings->DatabaseName;
            $user = (string) $Settings->DatabaseUser;
            $password = (string) $Settings->DatabasePassword;

            $connection = new PDO("mysql:host={$host};dbname={$database};charset=utf8", $user, $password);

            $connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
            $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch (PDOException | Exception $e) {

            trigger_error($e->getMessage());
        }

        return $connection;
    }

    // ===============
    // Debug an Object
    // ===============

    public function debug($object = "") {

        if ($_SESSION[self::AJAX]) {
            error_log(print_r($object, true));
        } else {
            echo "<pre>";
            print_r($object);
        }

        exit();
    }

    // =========================
    // Return a decrypted string
    // =========================

    protected function decrypt($encrypted = "", $key = "") {

        $key = (empty($key)) ? __CLASS__ : $key;

        $decoded = base64_decode($encrypted);
        $ivlen = openssl_cipher_iv_length($cipher = "AES-128-CBC");
        $iv = substr($decoded, 0, $ivlen);
        $hmac = substr($decoded, $ivlen, $sha2len = 32);
        $raw = substr($decoded, $ivlen + $sha2len);
        $decrypted = openssl_decrypt($raw, $cipher, $key, $options = OPENSSL_RAW_DATA, $iv);
        $calcmac = hash_hmac("sha256", $raw, $key, $as_binary = true);

        if (($hmac === false) or ($calcmac === false)) {
            return "";
        }

        return (hash_equals($hmac, $calcmac)) ? $decrypted : "";
    }

    // ======================
    // Display a Content Page
    // ======================

    public function displayContent($content = "") {

        $html = $this->applyXSL("", $this->getXSL("content"));
        $html = str_replace("$$$-CONTENT-$$$", $content, $html);

        echo $html;

        exit();
    }

    // =====================
    // Display a Notice Page
    // =====================

    public function displayNotice($messgae = "") {

        $XSLParms["MESSAGE"] = $messgae;

        $content = $this->applyXSL("", $this->getXSL("notice"), $XSLParms);

        $this->displayContent($content);

        exit();
    }

    // ======================================================
    // Custom error handler to display informative error page
    // ======================================================

    public function displayError($error_number = "", $error_message = "", $error_file = "", $error_line = "") {

        if (error_reporting() == 0) {
            return;
        }

        $error_file = explode("/", $error_file);
        $error_file = end($error_file);
        $error_message = str_replace("'", '"', $error_message);

        (new Journal())->journalEntry("{$error_file} | {$error_line} | {$error_message}");

        if (($_SESSION[self::ENVIRONMENT] ?? "Production") == "Production") {
            (new Email())->sendErrorEmail($error_message, $error_file, $error_line);
        }

        $XSLParms["FILE_NAME"] = $error_file;
        $XSLParms["LINE_NUMBER"] = $error_line;
        $XSLParms["ERROR_MESSAGE"] = $error_message;
        $XSLParms["ERROR_NUMBER"] = $error_number;

        $content = $this->applyXSL("", $this->getXSL("error"), $XSLParms);

        $this->displayContent($content);

        exit();  // Exit so that only one error is presented.
    }

    // =====================
    // Display the Home Page
    // =====================

    public function displayHome() {

        (new Subscriptions())->usersSubscriptions();
    }

    // =======================
    // Do the requested Action
    // =======================

    public function do() {

        $_SESSION[self::TODAY] = date("Y-m-d");

        // Get the requested Action

        $action = filter_input(INPUT_POST, "action") ?: "";
        $Xaction = filter_input(INPUT_GET, "Xaction") ?: "";

        $action = (empty($Xaction) ? $action : $Xaction);

        if ($Xaction == 'resetpassword') {
            $this->resetpassword();
        }

        // Login

        if (($_SESSION[self::USER_AUTHENTICATED] ?? false) === false) {

            (new Users())->login();
        }

        // Determine if user needs a password change
        if ($this->userChangePassword() === true) {
            (new Users())->changePassword();
            exit();
        }

        // Determine if this is an AJAX request.

        $_SESSION[self::AJAX] = (isset($_SERVER["HTTP_X_REQUESTED_WITH"]) and strtolower($_SERVER["HTTP_X_REQUESTED_WITH"]) === "xmlhttprequest");

        // Get the requested Action

        $action = filter_input(INPUT_POST, "action") ?: "";
        $Xaction = filter_input(INPUT_GET, "Xaction") ?: "";

        $action = (empty($Xaction) ? $action : $Xaction);

        // Sign Out if requested.

        if (strtolower(trim($action)) === "signout") {
            $this->initializeSession();
        }

        // Process the requested Action.

        list($class, $method) = array_pad(explode('.', $action), 2, "");

        if (empty($method)) {

            $method = $class;

            if (method_exists(__CLASS__, $method)) {
                $this->$method();
                return;
            }
        } else {

            if (method_exists($class, $method)) {
                (new $class())->$method();
                return;
            }
        }

        // Default to the Home Page.
        $this->displayHome();
    }

    // ==========================
    // Return an encrypted string
    // ==========================

    protected function encrypt($string = "", $key = "") {

        $key = (empty($key)) ? __CLASS__ : $key;

        $ivlen = openssl_cipher_iv_length($cipher = "AES-128-CBC");
        $iv = openssl_random_pseudo_bytes($ivlen);
        $raw = openssl_encrypt($string, $cipher, $key, $options = OPENSSL_RAW_DATA, $iv);
        $hmac = hash_hmac("sha256", $raw, $key, $as_binary = true);
        $encrypted = base64_encode($iv . $hmac . $raw);

        return $encrypted;
    }

    // ========================
    // Execute an SQL Statement
    // ========================

    public function executeSQL($SQL = "") {

        if (trim($SQL) == "") {
            return false;
        }

        return $this->connect()->query($SQL);
    }

    // ======================
    // Format an XML document
    // ======================

    public function formatXML($XML = "") {

        if (!$XML) {
            return false;
        }

        if ($XML instanceof SimpleXMLElement) {
            $XML = $XML->asXML();
        }

        $dom = new DOMDocument("1.0");
        $dom->preserveWhiteSpace = false;
        $dom->formatOutput = true;
        $dom->loadXML($XML);

        return $dom->saveXML();
    }

    // =====================================
    // Return an Application Settings Object
    // =====================================

    public function getSettings() {

        $settings = new SimpleXMLElement($this->getXML("settings"));

        return $settings;
    }

    // ==================================================================
    // Return Age in Years based on given Birthdate ("yyyy-mm-dd" format)
    // ==================================================================

    public function getAge($birthdate = "", $date = "") {

        $birthdate = filter_input(INPUT_POST, "birthdate", FILTER_UNSAFE_RAW) ?: $birthdate;
        $date = filter_input(INPUT_POST, "date", FILTER_UNSAFE_RAW) ?: $date;

        $age = 0;

        $date = (empty($date)) ? new Datetime() : new DateTime($date);
        $birthdate = (empty($birthdate)) ? new Datetime() : new DateTime($birthdate);

        $age = $birthdate->diff($date)->format("%y");

        if ($_SESSION[self::AJAX]) {
            echo json_encode($age);
        } else {
            return $age;
        }
    }

    // =========================
    // Return a Constants Object
    // =========================

    public function getConstants() {

        $Constants = new SimpleXMLElement($this->getXML("constants"));

        return $Constants;
    }

    // ==================
    // Return a Recordset
    // ==================

    public function getRecordset_orig($SQL = "") {

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

        $recordSet = $this->connect()->query($SQL);

        return $recordSet;
    }

    // =======================================
    // Return Table Data as a SimpleXMLElement
    // =======================================

    public function getTable_orig($table = "", $SQL = "") {

        if (empty($table)) {
            return new SimpleXMLElement("<unknown/>");
        }

        $SXE = new SimpleXMLElement("<{$table}/>");

        $recordset = $this->getRecordset(empty($SQL) ? "select * from {$table}" : $SQL);

        if ($recordset) {

            while ($row = $recordset->fetch(PDO::FETCH_ASSOC)) {

                $record = $SXE->addChild("record");

                foreach ($row as $name => $value) {
                    $record->$name = $value;
                }
            }
        }

        return $SXE;
    }

    // ==================
    // Return a Recordset
    // ==================

    public function getRecordset($SQL = "", $params = array()) {

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

        $statement = $this->connect()->prepare($SQL);

        foreach ($params as $key => $value) {

            $parameter = is_int($key) ? $key + 1 : ":" . ltrim($key, ":");

            if (is_int($value)) {
                $type = PDO::PARAM_INT;
            } elseif (is_bool($value)) {
                $type = PDO::PARAM_BOOL;
            } elseif (is_null($value)) {
                $type = PDO::PARAM_NULL;
            } else {
                $type = PDO::PARAM_STR;
            }

            $statement->bindValue($parameter, $value, $type);
        }

        $statement->execute();

        return $statement;
    }

    // =======================================
    // Return Table Data as a SimpleXMLElement
    // =======================================

    public function getTable($table = "", $SQL = "", $params = array()) {

        if (empty($table)) {
            return new SimpleXMLElement("<unknown/>");
        }

        $SXE = new SimpleXMLElement("<{$table}/>");

        $recordset = $this->getRecordset(empty($SQL) ? "select * from {$table}" : $SQL, $params);

        if ($recordset) {

            while ($row = $recordset->fetch(PDO::FETCH_ASSOC)) {

                $record = $SXE->addChild("record");

                foreach ($row as $name => $value) {
                    $record->$name = (string) $value;
                }
            }
        }

        return $SXE;
    }

    // =======================================
    // Return Table Data as a SimpleXMLElement
    // =======================================

    public function getTableXML($table = "", $statement = "") {

        if (empty($table)) {
            return new SimpleXMLElement("<unknown/>");
        }

        $SXE = new SimpleXMLElement("<{$table}/>");

        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {

            $record = $SXE->addChild("record");

            foreach ($row as $name => $value) {
                $record->$name = $value;
            }
        }

        return $SXE;
    }

    // =================================
    // Return the Time Elapsed since now
    // =================================

    public function getTimeElapsed($timestamp = "") {

        $now = new DateTime;
        $then = new DateTime($timestamp);
        $diff = (array) $now->diff($then);

        $diff["w"] = floor($diff["d"] / 7);
        $diff["d"] -= $diff["w"] * 7;

        $string = array(
            "y" => "year",
            "m" => "month",
            "w" => "week",
            "d" => "day",
            "h" => "hour",
            "i" => "minute",
            "s" => "second",
        );

        foreach ($string as $k => & $v) {

            if ($diff[$k]) {
                $v = $diff[$k] . " " . $v . ($diff[$k] > 1 ? "s" : "");
            } else {
                unset($string[$k]);
            }
        }

        $string = array_slice($string, 0, 1);

        return $string ? implode(", ", $string) . " ago" : "just now";
    }

    // ===================
    // Return a Time Stamp
    // ===================

    public function getTimestamp() {

        return date("Y-m-d H:i:s");
    }

    // ======================
    // Return an XML Document
    // ======================

    public function getXML($document = "") {

        if (trim($document) == "") {
            return "";
        }

        $document = "xml/" . trim($document) . ".xml";
        $XML = (file_exists($document) ? file_get_contents($document) : "");

        return $XML;
    }

    // ========================
    // Return an XSL Stylesheet
    // ========================

    public function getXSL($document = "") {

        if (trim($document) == "") {
            return "";
        }

        $document = "xsl/" . trim($document) . ".xsl";
        $XSL = (file_exists($document) ? file_get_contents($document) : "");

        return $XSL;
    }

    // =======================================
    // Return a Zip Code Detail Object (Array)
    // =======================================

    public function getZipcode($zip_code = 0) {

        $zip_code = filter_input(INPUT_POST, "zip_code", FILTER_SANITIZE_NUMBER_INT) ?: 0;

        $zip_code = (integer) substr((string) $zip_code, 0, 5);

        $zipCodeDetail = null;

        $SQL = "select * 
              from zipcodes 
             where zipcode_code = {$zip_code}";

        $zipcodes = $this->getTable("zipcodes", $SQL);

        if ($zipcodes->count() > 0) {

            $zipCodeDetail["ZipCode"] = $zip_code;
            $zipCodeDetail["City"] = (string) $zipcodes->record->zipcode_city;
            $zipCodeDetail["State"] = (string) $zipcodes->record->zipcode_state;
            $zipCodeDetail["County"] = (string) $zipcodes->record->zipcode_county;
            $zipCodeDetail["Country"] = (string) $zipcodes->record->zipcode_country;
        }

        if ($_SESSION[self::AJAX]) {
            echo json_encode($zipCodeDetail);
        } else {
            return $zipCodeDetail;
        }
    }

    // ======================
    // Initialize the Session
    // ======================

    public function initializeSession() {

        $settings = $this->getSettings();

        $_SESSION[self::ACTIVE_SESSION] = true;
        $_SESSION[self::THEME] = "light";
        $_SESSION[self::ENVIRONMENT] = (string) ($settings->Environment ?? "Production");
        $_SESSION[self::USER_AUTHENTICATED] = false;
        $_SESSION[self::USER_SERIAL] = 0;
        $_SESSION[self::USER_ID] = "";
        $_SESSION[self::USER_NAME] = "";
        $_SESSION[self::USER_CLIENT_NAME] = "";
        $_SESSION[self::USER_EMAIL] = "";
        $_SESSION[self::USER_ROLE] = "Guest";
        $_SESSION[self::REPORTS_GET] = "";
        $_SESSION[self::REPORTS_POST] = "";
        $_SESSION[self::AJAX] = false;
    }

    // ============
    // Log an Error
    // ============

    public function logError($error_message = "") {

        (new Journal())->journalEntry($error_message);

        $this->displayHome();

        exit();
    }

    // ==========================
    // Merge thisXML with thatXML
    // ==========================

    public function mergeXML($thisXML = "", $thatXML = "") {

        if ($thisXML instanceof SimpleXMLElement) {
            $thisXML = $thisXML->asXML();
        }

        if ($thatXML instanceof SimpleXMLElement) {
            $thatXML = $thatXML->asXML();
        }

        if (trim($thisXML) == "") {
            return $thatXML;
        }

        if (trim($thatXML) == "") {
            return $thisXML;
        }

        $thisDOM = new DOMDocument();
        $thisDOM->loadXML($thisXML);

        $thatDOM = new DOMDocument();
        $thatDOM->loadXML($thatXML);

        $xpath = new DOMXpath($thisDOM);
        $xpathQuery = $xpath->query("/*");

        for ($i = 0; $i < $xpathQuery->length; $i++) {
            $thatDOM->documentElement->appendChild($thatDOM->importNode($xpathQuery->item($i), true));
        }

        $thatSXE = simplexml_import_dom($thatDOM);

        return $thatSXE->asXML();
    }

    // ==============
    // Print a Report
    // ==============

    public function print() {

        $_SESSION[self::REPORTS_GET] = $_GET;
        $_SESSION[self::REPORTS_POST] = $_POST;

        $report = filter_input(INPUT_POST, "report") ?: "";

        (new $report())->print();
    }

    // =================
    // Sanitize a string
    // =================

    public function sanitizeString($string = "") {

        return trim(preg_replace("/\s+/", " ", htmlspecialchars($string, ENT_NOQUOTES | ENT_HTML5)));
    }

    // ========================
    // Toggle the current Theme
    // ========================

    public function toggleTheme() {

        $_SESSION[self::THEME] = (($_SESSION[self::THEME] ?? "light") === "dark") ? "light" : "dark";
    }

    // ===============================
    // Validate a Date (local or AJAX)
    // ===============================

    public function validateDate($date = "", $format = "Y-m-d") {

        $date = filter_input(INPUT_POST, "date") ?: $date;

        $datetime = DateTime::createFromFormat($format, $date);

        $valid = $datetime && $datetime->format($format) === $date;

        if ($_SESSION[self::AJAX]) {
            echo json_encode($valid);
        } else {
            return $valid;
        }
    }

    // ========================
    // Generate Random Password
    // ========================

    public function generateRandomPassword($length = 12) {

        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_+=';
        $password = '';

        for ($i = 0; $i < $length; $i++) {
            $index = rand(0, strlen($characters) - 1);
            $password .= $characters[$index];
        }

        return $password;
    }

    // ====================
    // User Change Password
    // ====================

    public function userChangePassword() {

        $user = (new Users())->getUserByUserName($_SESSION[self::USER_NAME]);

        if ($user->record->user_change_password == 1) {

            return true;
        }

        return false;
    }

    // ==============================
    // Alter Sting to Parahgraph Case
    // ==============================

    public function toParagraphCase($text) {

        $text = ucwords(strtolower($text));

        return $text;
    }

    // ==========================
    // Developer Testing Function
    // ==========================

    public function testing() {

        $SQL = "select count(*) as notify
                  from view_exportjobs
                 where view_exportjobs.exportjob_creator = 19
                   and view_exportjobs.exportjob_status = 'complete'
                   and view_exportjobs.exportjob_notified = 0";

        $exportjobs = $this->getTable("exportjobs", $SQL);

        $this->debug($exportjobs->asXML());
    }

    // =======================
    // Format Raw Phone Number
    // =======================

    public function formatPhoneNumber(string $phonenumber): string {

        // Remove everything except digits
        $digits = preg_replace('/\D+/', '', $phonenumber);

        // Handle leading country code (US)
        if (strlen($digits) === 11 && str_starts_with($digits, '1')) {
            $digits = substr($digits, 1);
        }

//        // Must be exactly 10 digits to format
//        if (strlen($digits) !== 10) {
//            return $phone; // return original if invalid
//        }

        return substr($digits, 0, 3) . '-' .
                substr($digits, 3, 3) . '-' .
                substr($digits, 6, 4);
    }

    // =====================
    // Forgot/Reset Password
    // =====================

    public function resetpassword() {

        include_once 'classes/Authentication.php';

        $reset = (new Authentication())->resetPassword() ? true : false;

        $XSLParms["BTN_SUBMIT"] = "main.php?action=Users.signIn&step=authenticate";
        $XSLParms["RESET_PASSWORD_SUCCESSFUL"] = "TRUE";

        echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
        die();
    }
}

?>
