<?php

namespace Clevercherry\Backslash4;

use Faker\Core\Number;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\DB;
use Ramsey\Uuid\Type\Integer;
use SebastianBergmann\Type\NullType;

class Backslash4
{
  public function __construct()
  {

  }

  public static function getVersion(): string
  {
    return "4.0.69";
  }

  public static function getVersionName(): string
  {
    return "Ordos";

    /**
     * Planet Names (from Movies)
     * ------------
     * Alderaan   4.0.2
     * Bespin     4.0.3
     * Coruscant  4.0.4
     * Dagobah    4.0.5
     * Endor      4.0.6
     * Ferrix     4.0.8
     * Geonosis   4.0.8
     * Geonosis   4.0.9  --  tagged package shows as 408
     * Hoth       4.0.10-dev
     * Ilum       4.0.10
     * Jedha      4.0.11 --  media manager search fix, CMD-K insert link in nicEdit
     * Kashyyyk   4.0.12-dev - 4.0.12
     * Kashyyyk   4.0.12-dev - 4.0.13
     * Lothal     4.0.14 --  sitemap_index and multiple sitemap files
     * Mandalore  4.0.15 --  Media manager initial load uses non-AJAX links on pagination
     * Naboo      4.0.16 --  seo amends local business and article type added
     * Onderon    4.0.17-dev -- mislabeled tag
     * Pasaana    4.0.18
     * Quermia    4.0.19 --  syntax on schema
     * Ryloth     4.0.20 --  adding functionality to select multiple modules to have duplicate functionality
     * Scarif     4.0.21
     * Tatooine   4.0.22 --  more flexible CMS routing akin to front end routing overrides
     *                   --  more flexible dashboard menu
     *                   --  compatible with Laravel 11
     * Utapau     4.0.23 --  fix Admins not saving, and new change admin password on bs command line
     * Vardos     4.0.24 --  bug in amend for admins not saving when creating new record in some models
     * Wobani     4.0.25 --  bug in amend for admins not passsing updated $item to postStore() function
     * Xendek0    4.0.26 --  special type of pages, set pagination to get path from existing ajax override
     *                       function not a static path
     * Yavin      4.0.27 --  maximum number in the duplicates box
     * Zygerria   4.0.28 --  Robots Txt Amend
     * Andoria    4.0.29 --  functions with improper optional parameters for php8.3 fixed
     * Bajor      4.0.30 --  amend extra pick development dropdown logic in duplicate modal
     * Cardassia  4.0.31 --  fix mediamanager pagination bug when page is initailly loaded
     * Denobula   4.0.32 --  search in media popup (requires an npm run build to rebuild CMS javascript)
     *                       ** npm run build
     * Edos       4.0.33 --  add small text, subscript and superscript to the niceedit along with icons
     * Ferenginar 4.0.34 --  file-upload CMS field type (input type="hidden" not allowing JS to read the value,
     *                       changed to input type="text" style="display:none")
     * Genesis    4.0.35 --  fix media upload overwriting same named files in s3
     * Hysperia   4.0.36 --  cms security 404 in BS4
     *                       add response message and redirect options to default product controller
     * Iconia     4.0.37 --  first API functions and json responses from standard store edit routes
     * J'naii     4.0.38 --  nicEdit events and element labeling
     *                       2024-Nov-12  - niceEdit v24.11.12 (backslash4 v4.0.38)
     *                                      added event after initialisation document.dispatchEvent(
     *                                      new CustomEvent('nicedit_initialised', {
     *                                      added data-name to the buttons so they can be identified and
     *                                      turned on/off by javascript
     * Khitomer   4.0.39 --  custom model row Indenting - public function getRowIndent($indent=0)
     * Locarno    4.0.40 --  added @stack('admin.head.start'), @stack('admin.head.end'), @stack('admin.body.start'),
     *                       @stack('admin.body.end') to allow out of order rendering in cms
     * Minos      4.0.41 --  2024-12-03 - added in bs4StoreResponse($model, $item) and bs4UpdateResponse($model, $item)
     *                                    to allow overridden store() and update() controller functions to return same
     *                                    responses (may of been deleted in previous merge conflict)
     *                       2024-12-05 - added overwrite on upload switch to media manager, activated in
     *                                    bs4 config 'media' =>[ 'overwrite-switch' => 1 ]
     * Nibia      4.0.42     2024-12-05 - after uploading an image refresh instances of <img src="xxxx with
     *                                    ?refresh=random to try forcing a refresh of just those images
     *                                    added CDN cache warning about uploading/overwriting images, but
     *                                    above fix seems to work anyway
     * Ocampa     4.0.43     2014-12-10 - allow multiple google maps on page by having uniquely named init functions
     *                                    and those functions being queued into an array, and google JS being loaded 
     *                                    once only which calls a global init function, which calls each individual
     *                                    init funtion from the array
     * Plymeria   4.0.44     2024-12-17 - test for function scopeActive in Controller before using it.
     *                       2024-12-19 - add stack and includeFirst views in the CMS header
     * Qo'nos     4.0.45     2025-01-02 - add new icons
     * Risa       4.0.46     2025-01-05 - add new icons and fix bug where theme switch not working
     * Selay      4.0.47     2025-01-10 - added in missing optional parameter for shipping,
     *                                    setShipping($carrier=null, $shipping=0.0, $cart = null)
     *                                    as middle parameter can't be required when first is optional DEV-3168
     *                                    This is causing uncessary entries in PHP logs on all sites using the BS4
     *                                    cartControl
     *                       2025-01-16 - Removed additional colon in the robots.txt file. Caused issues reading
     *                                    the sitemap file when running lighthouse tests 
     * Trill    4.0.48       2025-02-07 - login video from digital ocean, and from a config setting
     *                                    (copied from mm200 branch)
     *                       2025-03-07 - added <x-backslash4::bs-header.journey-tracker /> for tracking a user
     *                                    journey and injecting those fields into forms
     *                       2025-03-10 - tagged 4.0.48
     * Urna     4.0.49       2025-03-13 - amended journey-tracker to call php in User model so it can be overidden 
     *                                    per application
     *                                  - added better javascript customisation to journey-tracker
     *                                  - added default journey-tracker getFieldType in autogeneric
     * Vulcan   4.0.50       2025-03-13 - changes were done in App\Model\User, moved new functions to BS4 Trait 
     *                                    journeyTracker not all app models inherit from BS4\User model, so 
     *                                    option to inherit from BS4\User or include trait or re-implement custom
     *                                    function instead of the trait
     * Wysanti  4.0.51       2025-04-17 - amend file upload button javascript to cope with data[filename] = obj;
     *                                    no longer working uses data.push(obj); but now items are no longer 
     *                                    indexed by filename so delete buttton code amended too
     * Xahea    4.0.52       2025-05-06 - added in function to add a migration for Network Event Settings
     *                                    added in functions to read settings for Network Event Settings
     *                                    and Dashboard Settings for Network API
     *                       2025-05-16 - settings for the network events and initial empty javascript deployed
     *                                    to app with backslash 46 and 47
     *                       2025-05-20 - added in use App\Models\Setting; to the seo blade file
     * Yuri     4.0.53       2025-06-11 - added $fillable fields to Admin model, override and/or use trait 
     *                                    ColumnFillable in you app/Models/Admin
     * Zimja    4.0.54       2025-06-18 - amend file upload button javascript to cope with objecting to data.push
     *                                    so tries both data[filename] = obj; and data.push(obj); in a try catch
     *                                    and fixed [] version so uses an existing variable filename, 
     *                                    not altfilename <-- which was the real problem
     *                                    tidied up longest comments above
     * Arrakis  4.0.55       2025-06-18 - same foreach breaks deleting the file tags -fixed
     * Boro     4.0.56       2025-06-27 - Amend AWS 's3' for Amazon compatibility
     * Corrino  4.0.57       2025-06-27 - added herein jsonDecodeWithDefault(...)
     *                                  - minor fixes in MediaManager model, inc. using jsonDecodeWithDefault()
     *                                  - coding standards in MediaManager
     *                                  - git info in php artisan about (from mediamanager200)
     * Dur      4.0.58       2025-07-01 - jsonDecodeWithDefault remove returned types
     * Ecaz     4.0.59       2025-07-08 - Network Event Messaging support to CCHUB
     * Fenring  4.0.60       2025-07-10 - fix the 's3' upload visibility for Amazon & DigitalOcean,
     *                                    upload the files and set visibility afterwards
     * Ginaz    4.0.61       2025-07-17 - 'bs4_network_api_host_url' live default is 
     *                                    'cchub.clevercherry.com/api/v1/network-event' not .net
     *                                    added caching on and in mediamanager getFilesInDirectory()
     *                                    to improve repeated loading of large file directories
     *                       2025-08-19 - Disable PHPDEBUGBAR in Admin/DashboardController getEditBlock($slug)
     *                                    was injecting extra HTML/SCRIPT tags that was throwing off the
     *                                    DOM parsing, unable to properly insert sub-blocks
     * Harkonnen 4.0.62      2025-08-21 - clear files caching when a new upload is actioned
     * Ipyr      4.0.63      2025-10-06 - Laravel 12 compatibility  "doctrine/dbal": "^3.6 || ^3.10"
     * Junction  4.0.64      2025-10-06 - Better Laravel 12, "doctrine/dbal": "^3.6 || ^4.0"
     * Kaitain   4.0.65      2015-10-06 - Remove short syntax :$x => :x="$x" (Laravel 12)
     *                                    SEO <x-bs-header.seo fixes
     * Lampadas  4.0.66      2015-11-25 - add $slugfield in model for front end controller, default is 'slug',
     *                                    with absolute fallback to id (overcome error occuring in PHQ3 tables)
     *                       2025-12-05 - added warning to image upload box
     *                                    If your image exceeds 2560 pixels wide and fails,
     *                                    please use your art software to reduce the maximum dimensions of your image
     *                                    and re-try.
     * Mutelli   4.0.67      2025-12-17 - dropzone was splitting media type on commas - now commas or spaces
     *                                    in function bs4CheckFileType
     *                       2025-12-23 - extra @stack admin.bodyxxx markers for @push
     *                       2026-01-23 - searchModel trait malformed function definitions, declaring default null
     *                                    parameter but not technically allowing it amended definitions to be ?string
     *                                    one function with same error in this Backslash4 class,
     *                                    quick scan through other files for same error look OK
     * Novebruns 4.0.68      2026-02-04   searchModel - fix parameters for getResults() function
     *                       2026-04-02   names not rendering in BS4 top-left burger menu - fixed
     * Ordos     4.0.69      2026-05-22   improve paginators to remember sorting and query parameters
     *                                    when definitions mix between POST and GET requests
     * 
     * Future Names:
     * Names (from Dune Franchise [Planets and Houses])
     * ------------------------------------------------
     * Poritrin
     * Richese
     * Synchrony
     * Taligari
     * Uklarda
     * Vernius
     * Wallach
     * Xuttah
     * Yardin
     * Zanovar
     * 
     * Farscape Characters and Planets
     * -------------------------------
     * Aeryn
     * Bialar
     * Braca
     * Chiana
     * Crais
     * Crichton
     * D'Argo
     * Delvia
     * DRDs
     * Eidolon
     * Grayza
     * Harvey
     * Jool
     * Jothee
     * Liantac
     * LoMo
     * Moya
     * Noranti
     * Pilot
     * Prybella
     * Qujaga
     * Rygel
     * Scarran
     * Scorpius
     * Sebacean
     * Sikozu
     * Staleek
     * Stark
     * Talyn
     * Xhalax
     * Zhaan
     */
  }

  /**
   * Will return or create a cached version of the requested view
   * accepts an array of parameters that can be passed down
   *
   * @param string $viewPath - full path of the view from the resources directory
   * @param array $params - an optional array of parameters in the format ['name' => 'value']
   * @param int $expire - an optional integer for cache lifetime (in seconds), will default to 1 hour
   * @param string $prefix - an optional prefix for the cache key - particularly useful for shared
   * views, such as product overviews / galleries where an id could be passed
   *
   * @return string - fully rendered view
   */
  public static function getCachedView(string $viewPath, array $params = [], int $expire = 3600, string $prefix = 'view'): string
  {
    $cacheKey = $prefix . '.' . $viewPath;

    if (Cache::has($cacheKey)) {
      return Cache::get($cacheKey);
    }

    return Cache::remember($cacheKey, $expire, function () use ($viewPath, $params) {
      return view($viewPath)->with($params)->render();
    });
  }

  /**
   * Explode a string on multiple delimiters, trimming each entry and removing empty entries,
   * explode("\n\r, ;", $string) could be used for a list of emails
   * @param  string | array $delimiters    array of characters or string of characters on which the
   *                                       string will be exploded
   * @param  string         $string        the string to be exploded into an array
   * 
   * @return array[string]                 an array of strings exploded from the source string, trimmed
   *                                       to remove whitespace, and without empty entries
   */
  public static function explode(string|array $delimiters, string $string)
  {
    if (gettype($delimiters) == 'string')
      $delimiters = str_split($delimiters);

    // get the first delimiter, first item in the array, and remove it from the array
    $first = array_shift($delimiters);
    // replace all other delimiters with the first delimiter
    $string = str_replace($delimiters, $first, $string);
    // remove double occurances of the first delimiter (the loop trim !empty covers this too, so it's a bit 
    // boot and braces)
    $string = str_replace($first . $first, $first, $string);

    // explode on the first delimiter
    $array = explode($first, $string);

    // build an array of non-empty items
    $trimmedArray = [];
    foreach ($array as $item) {
      $trimmedItem = trim($item);
      if (!empty($trimmedItem)) {
        $trimmedArray[] = $trimmedItem;
      }
    }

    return $trimmedArray;
  }

  /**
   * gebnerate a short uuid for idetify unique items, like dialogs in code by adding a suffic to IDs
   * @param  Integer  $len    Length of string required (min 2, max 256)
   * 
   * @return String           A Random unique as possible string
   */
  public static function generateShortUuid($len = 8): string
  {
    $hex = md5("90210_backslasg_4_8642_your_random_salt_here_31415" . uniqid("", true));
    $pack = pack('H*', $hex);

    // max 22 chars
    $uid = base64_encode($pack);
    
    // remove invalid chars, leaving only mixed case and numbers
    $uid = str_replace(['+', '/', '='], '', $uid);

    // prevent silliness
    $len = min(256, max(2, $len));

    while (strlen($uid) < $len) {
      // append until length achieved
      $uid .= self::generateShortUuid(20);
    }

    return substr($uid, 0, $len);
  }

  /**
   * return true or false by filtering the value
   * @param  mixed $val            the value to be converted to boolean
   * @param  bool  $return_null    a boolean indicating whether to return null on failure,
   *                               or just return false on failure
   * 
   * @return bool | null           the resultant boolean value (or null)
   */
  public static function is_true($val, $return_null = false): bool|null
  {
    $boolval = (is_string($val) ? filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) : (bool) $val);
    return (($boolval === null && !$return_null) ? false : $boolval);
  }

  /**
   * convert an Integer to Roman Numerals
   * @param  int $number a positive non-zero number
   * 
   * @return String      Romana Numeral representation of $number
   */
  public static function integerToRoman($number): string
  {
    // Convert the integer into an integer (just to make sure)
    $integer = intval($number);
    $result = '';

    // Create a lookup array that contains all of the Roman numerals.
    $lookup = array(
      'M' => 1000,
      'CM' => 900,
      'D' => 500,
      'CD' => 400,
      'C' => 100,
      'XC' => 90,
      'L' => 50,
      'XL' => 40,
      'X' => 10,
      'IX' => 9,
      'V' => 5,
      'IV' => 4,
      'I' => 1
    );

    foreach ($lookup as $roman => $value) {
      // Determine the number of matches
      $matches = intval($integer / $value);

      // Add the same number of characters to the string
      $result .= str_repeat($roman, $matches);

      // Set the integer to be the remainder of the integer and the value
      $integer = $integer % $value;
    }
    // The Roman numeral should be built, return it
    return $result;
  }

  public static $counties_eng = [
    'Bedfordshire',
    'Berkshire',
    'Bristol',
    'Buckinghamshire',
    'Cambridgeshire',
    'Cheshire',
    'City of London',
    'Cornwall',
    'Cumbria',
    'Derbyshire',
    'Devon',
    'Dorset',
    'County Durham',
    'East Riding of Yorkshire',
    'East Sussex',
    'Essex',
    'Gloucestershire',
    'Greater London',
    'Greater Manchester',
    'Hampshire',
    'Herefordshire',
    'Hertfordshire',
    'Isle of Wight',
    'Kent',
    'Lancashire',
    'Leicestershire',
    'Lincolnshire',
    'Merseyside',
    'Norfolk',
    'North Yorkshire',
    'Northamptonshire',
    'Northumberland',
    'Nottinghamshire',
    'Oxfordshire',
    'Rutland',
    'Shropshire',
    'Somerset',
    'South Yorkshire',
    'Staffordshire',
    'Suffolk',
    'Surrey',
    'Tyne and Wear',
    'Warwickshire',
    'West Midlands',
    'West Sussex',
    'West Yorkshire',
    'Wiltshire',
    'Worcestershire'
  ];

  public static $counties_cymru = [
    'Isle of Anglesey (Ynys Môn)',
    'Blaenau Gwent',
    'Bridgend (Pen-y-bont ar Ogwr)',
    'Cardiff (Caerdydd)',
    'Caerphilly (Caerffili)',
    'Carmarthenshire (Sir Gaerfyrddin)',
    'Ceredigion',
    'Conwy',
    'Denbighshire (Sir Ddinbych)',
    'Flintshire (Sir y Fflint)',
    'Gwynedd',
    'Merthyr Tydfil (Merthyr Tudful)',
    'Monmouthshire (Sir Fynwy)',
    'Neath Port Talbot (Castell-nedd Port Talbot)',
    'Newport (Casnewydd)',
    'Pembrokeshire (Sir Benfro)',
    'Powys',
    'Rhondda Cynon Taff',
    'Swansea (Abertawe)',
    'Torfaen (Tor-faen)',
    'Vale of Glamorgan (​​Bro Morgannwg)',
    'Wrexham (Wrecsam)'
  ];

  public static $counties_scot = [
    'Aberdeenshire (or the County of Aberdeen)',
    'Angus (or Forfarshire or the County of Forfar)',
    'Argyll (or Argyllshire)',
    'Ayrshire (or the County of Ayr)',
    'Banffshire (or the County of Banff)',
    'Berwickshire (or the County of Berwick)',
    'Buteshire (or the County of Bute)',
    'Caithness',
    'Clackmannanshire (or the County of Clackmannan)',
    'Dumfriesshire',
    'Dunbartonshire',
    'East Lothian',
    'Fife',
    'Inverness-shire',
    'Kincardineshire,',
    'Kinross-shire',
    'Kirkcudbrightshire',
    'Lanarkshire',
    'Midlothian',
    'Moray',
    'Nairnshire',
    'Orkney',
    'Peebleshire',
    'Perthshire',
    'Renfrewshire',
    'Ross and Cromarty',
    'Roxburghshire',
    'Selkirkshire',
    'Shetland',
    'Stirlingshire',
    'Sutherland',
    'West Lothian',
    'Wigtownshire'
  ];

  public static $counties_ni = ['Antrim', 'Armagh', 'Down', 'Fermanagh', 'Londonderry', 'Tyrone'];


  public static function getCounties($eng = true, $wales = true, $scot = true, $ni = true, $sort = true)
  {
    $counties = [];
    if ($eng) {
      $counties = self::$counties_eng;
    }
    if ($wales) {
      $counties = array_merge($counties, self::$counties_cymru);
    }
    if ($scot) {
      $counties = array_merge($counties, self::$counties_scot);
    }
    if ($ni) {
      $counties = array_merge($counties, self::$counties_ni);
    }

    if ($sort)
      sort($counties);

    return $counties;
  }

  /**
   * Safe Json decode with a default
   * 
   */
  public static function jsonDecodeWithDefault($json, $as_array = false, $default = [])
  {
    try {
      $res = json_decode($json, $as_array);
    } catch (\Throwable $th) {
      //does not decode so return $default
      $res = $default;
    }

    return $res;
  }

  /**
   * Get the google Longitude and Latitude
   */
  public static function getGoogleLatLng(string $address, string $apiKey, bool $returnFullInfo = false)
  {
    $url = "https://maps.google.com/maps/api/geocode/json?sensor=false&address="
         . urlencode($address) . '&key=' . $apiKey;

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $responseJson = curl_exec($ch);
    curl_close($ch);

    $response = self::jsonDecodeWithDefault($responseJson, false, (object)[]);

    if ($returnFullInfo)
      return $response;
    //else

    if ($response->status == 'OK') {

      return $response->results[0]->geometry->location;

      /*
      $latitude = $response->results[0]->geometry->location->lat;
      $longitude = $response->results[0]->geometry->location->lng;
      */
    }
    //else

    return false;
  }


  /**
   * Country Codes -- updated 2023-04-05
   * Index ISO-3166-2 codes [ 'ISO-3166-2', 'ITU Dialling code', 'short name', 'longname']
   */
  public static $country_codes = [
    'AF' => ['AF', "93", "Afghanistan", "Afghanistan"],
    'AL' => ['AL', "355", "Albania", "Albania"],
    'DZ' => ['DZ', "213", "Algeria", "Algeria"],
    'AS' => ['AS', "1 684", "American Samoa", "American Samoa"],
    'AD' => ['AD', "376", "Andorra", "Andorra"],
    'AO' => ['AO', "244", "Angola", "Angola"],
    'AI' => ['AI', "1 264", "Anguilla", "Anguilla"],
    'AG' => ['AG', "1 268", "Antigua and Barbuda", "Antigua and Barbuda"],
    'AR' => ['AR', "54", "Argentina", "Argentina"],
    'AM' => ['AM', "374", "Armenia", "Armenia"],
    'AW' => ['AW', "297", "Aruba", "Aruba"],
    'AU' => ['AU', "61", "Australia", "Australia"],
    'AT' => ['AT', "43", "Austria", "Austria"],
    'AZ' => ['AZ', "994", "Azerbaijan", "Azerbaijan"],

    'BS' => ['BS', "1 242", "Bahamas", "Bahamas"],
    'BH' => ['BH', "973", "Bahrain", "Bahrain"],
    'BD' => ['BD', "880", "Bangladesh", "Bangladesh"],
    'BB' => ['BB', "1 246", "Barbados", "Barbados"],
    'BY' => ['BY', "375", "Belarus", "Belarus"],
    'BE' => ['BE', "32", "Belgium", "Belgium"],
    'BZ' => ['BZ', "501", "Belize", "Belize"],
    'BJ' => ['BJ', "229", "Benin", "Benin"],
    'BM' => ['BM', "1 441", "Bermuda", "Bermuda"],
    'BT' => ['BT', "975", "Bhutan", "Bhutan"],
    'BO' => ['BO', "591", "Bolivia", "Bolivia (Plurinational State of)"],
    'BQ' => ['BQ', "599", "Bonaire, Sint Eustatius and Saba", "Bonaire, Sint Eustatius and Saba"],
    'BA' => ['BA', "387", "Bosnia and Herzegovina", "Bosnia and Herzegovina"],
    'BW' => ['BW', "267", "Botswana", "Botswana"],
    'BR' => ['BR', "55", "Brazil", "Brazil"],
    'IO' => ['IO', "246", "British (IOC)", "British Indian Ocean Territory"],
    'VG' => ['VG', "1 284", "British Virgin Islands", "Virgin Islands (British)"],
    'BN' => ['BN', "673", "Brunei Darussalam", "Brunei Darussalam"],
    'BG' => ['BG', "359", "Bulgaria", "Bulgaria"],
    'BF' => ['BF', "226", "Burkina Faso", "Burkina Faso"],
    'BI' => ['BI', "257", "Burundi", "Burundi"],

    'CV' => ['CV', "238", "Cabo Verde", "Cabo Verde"],
    'KH' => ['KH', "855", "Cambodia", "Cambodia"],
    'CM' => ['CM', "237", "Cameroon", "Cameroon"],
    'CA' => ['CA', "1", "Canada", "Canada"],
    'KY' => ['KY', "1 345", "Cayman Islands", "Cayman Islands"],
    'CF' => ['CF', "236", "Central African Rep.", "Central African Republic"],
    'TD' => ['TD', "235", "Chad", "Chad"],
    'CL' => ['CL', "56", "Chile", "Chile"],
    'CN' => ['CN', "86", "China", "China"],
    'CO' => ['CO', "57", "Colombia", "Colombia"],
    'KM' => ['KM', "269", "Comoros", "Comoros"],
    'CG' => ['CG', "242", "Congo (Rep. of the)", "Congo"],
    'CK' => ['CK', "682", "Cook Islands", "Cook Islands"],
    'CR' => ['CR', "506", "Costa Rica", "Costa Rica"],
    'CI' => ['CI', "225", "Côte d'Ivoire", "Côte d'Ivoire"],
    'HR' => ['HR', "385", "Croatia", "Croatia"],
    'CU' => ['CU', "53", "Cuba", "Cuba"],
    'CW' => ['CW', "599", "Curaçao", "Curaçao"],
    'CY' => ['CY', "357", "Cyprus", "Cyprus"],
    'CZ' => ['CZ', "420", "Czech Rep.", "Czechia"],

    'KP' => ['KP', "850", "Dem. People's Rep. of Korea", "Korea (the Democratic People's Republic of)"],
    'CD' => ['CD', "243", "Dem. Rep. of the Congo", "Congo (the Democratic Republic of the)"],
    'DK' => ['DK', "45", "Denmark", "Denmark"],
    'DG' => ['DG', "246", "Diego Garcia", "Diego Garcia"],
    'DJ' => ['DJ', "253", "Djibouti", "Djibouti"],
    'DM' => ['DM', "1 767", "Dominica", "Dominica"],
    'DO' => ['DO', "1 809, 1 829, 1 849", "Dominican Rep.", "Dominican Republic, The"],

    'EC' => ['EC', "593", "Ecuador", "Ecuador"],
    'EG' => ['EG', "20", "Egypt", "Egypt"],
    'SV' => ['SV', "503", "El Salvador", "El Salvador"],
    'GQ' => ['GQ', "240", "Equatorial Guinea", "Equatorial Guinea"],
    'ER' => ['ER', "291", "Eritrea", "Eritrea"],
    'EE' => ['EE', "372", "Estonia", "Estonia"],
    'SZ' => ['SZ', "268", "Eswatini", "Eswatini"],
    'ET' => ['ET', "251", "Ethiopia", "Ethiopia"],

    'FK' => ['FK', "500", "Falkland Islands (Malvinas)", "Falkland Islands, The [Malvinas]"],
    'FO' => ['FO', "298", "Faroe Islands", "Faroe Islands"],
    'FJ' => ['FJ', "679", "Fiji", "Fiji"],
    'FI' => ['FI', "358", "Finland", "Finland"],
    'FR' => ['FR', "33", "France", "France"],

    'GF' => ['GF', "594", "French Guiana", "French Guiana"],
    'PF' => ['PF', "689", "French Polynesia", "French Polynesia"],

    'GA' => ['GA', "241", "Gabon", "Gabon"],
    'GM' => ['GM', "220", "Gambia", "Gambi, The"],
    'GE' => ['GE', "995", "Georgia", "Georgia"],
    'DE' => ['DE', "49", "Germany", "Germany"],
    'GH' => ['GH', "233", "Ghana", "Ghana"],
    'GI' => ['GI', "350", "Gibraltar", "Gibraltar"],
    'GR' => ['GR', "30", "Greece", "Greece"],
    'GL' => ['GL', "299", "Greenland", "Greenland"],
    'GD' => ['GD', "1 473", "Grenada", "Grenada"],
    'GP' => ['GP', "590", "Guadeloupe", "Guadeloupe"],
    'GU' => ['GU', "1 671", "Guam", "Guam"],
    'GT' => ['GT', "502", "Guatemala", "Guatemala"],
    'GN' => ['GN', "224", "Guinea", "Guinea"],
    'GW' => ['GW', "245", "Guinea-Bissau", "Guinea-Bissau"],
    'GY' => ['GY', "592", "Guyana", "Guyana"],

    'HT' => ['HT', "509", "Haiti", "Haiti"],
    'HN' => ['HN', "504", "Honduras", "Honduras"],
    'HK' => ['HK', "852", "Hong Kong, China", "Hong Kong"],
    'HU' => ['HU', "36", "Hungary", "Hungary"],

    'IS' => ['IS', "354", "Iceland", "Iceland"],
    'IN' => ['IN', "91", "India", "India"],
    'ID' => ['ID', "62", "Indonesia", "Indonesia"],
    'IR' => ['IR', "98", "Iran (Islamic Republic of)", "Iran (Islamic Republic of)"],
    'IQ' => ['IQ', "964", "Iraq", "Iraq"],
    'IE' => ['IE', "353", "Ireland", "Ireland"],
    'IL' => ['IL', "972", "Israel", "Israel"],
    'IT' => ['IT', "39", "Italy", "Italy"],

    'JM' => ['JM', "1 876, 1 658", "Jamaica", "Jamaica"],
    'JP' => ['JP', "81", "Japan", "Japan"],
    'JO' => ['JO', "962", "Jordan", "Jordan"],

    'KZ' => ['KZ', "7", "Kazakhstan", "Kazakhstan"],
    'KE' => ['KE', "254", "Kenya", "Kenya"],
    'KI' => ['KI', "686", "Kiribati", "Kiribati"],
    'KR' => ['KR', "82", "Korea (Rep. of)", "Korea (the Republic of)"],

    'KW' => ['KW', "965", "Kuwait", "Kuwait"],
    'KG' => ['KG', "996", "Kyrgyzstan", "Kyrgyzstan"],

    'LA' => ['LA', "856", "Lao P.D.R.", "Lao People's Democratic Republic"],
    'LV' => ['LV', "371", "Latvia", "Latvia"],
    'LB' => ['LB', "961", "Lebanon", "Lebanon"],
    'LS' => ['LS', "266", "Lesotho", "Lesotho"],
    'LR' => ['LR', "231", "Liberia", "Liberia"],
    'LY' => ['LY', "218", "Libya", "Libya"],
    'LI' => ['LI', "423", "Liechtenstein", "Liechtenstein"],
    'LT' => ['LT', "370", "Lithuania", "Lithuania"],
    'LU' => ['LU', "352", "Luxembourg", "Luxembourg"],

    'MO' => ['MO', "853", "Macao, China", "Macao"],
    'MG' => ['MG', "261", "Madagascar", "Madagascar"],
    'MW' => ['MW', "265", "Malawi", "Malawi"],
    'MY' => ['MY', "60", "Malaysia", "Malaysia"],
    'MV' => ['MV', "960", "Maldives", "Maldives"],
    'ML' => ['ML', "223", "Mali", "Mali"],
    'MT' => ['MT', "356", "Malta", "Malta"],
    'MH' => ['MH', "692", "Marshall Islands", "Marshall Islands"],
    'MQ' => ['MQ', "596", "Martinique", "Martinique"],
    'MR' => ['MR', "222", "Mauritania", "Mauritania"],
    'MU' => ['MU', "230", "Mauritius", "Mauritius"],
    'MX' => ['MX', "52", "Mexico", "Mexico"],
    'FM' => ['FM', "691", "Micronesia", "Micronesia (Federated States of)"],
    'MD' => ['MD', "373", "Moldova", "Moldova (the Republic of)"],
    'MC' => ['MC', "377", "Monaco", "Monaco"],
    'MN' => ['MN', "976", "Mongolia", "Mongolia"],
    'ME' => ['ME', "382", "Montenegro", "Montenegro"],
    'MS' => ['MS', "1 664", "Montserrat", "Montserrat"],
    'MA' => ['MA', "212", "Morocco", "Morocco"],
    'MZ' => ['MZ', "258", "Mozambique", "Mozambique"],
    'MM' => ['MM', "95", "Myanmar", "Myanmar"],

    'NA' => ['NA', "264", "Namibia", "Namibia"],
    'NR' => ['NR', "674", "Nauru", "Nauru"],
    'NP' => ['NP', "977", "Nepal", "Nepal"],
    'NL' => ['NL', "31", "Netherlands", "Netherlands (Kingdom of the)"],
    'NC' => ['NC', "687", "New Caledonia", "New Caledonia"],
    'NZ' => ['NZ', "64", "New Zealand", "New Zealand"],
    'NI' => ['NI', "505", "Nicaragua", "Nicaragua"],
    'NE' => ['NE', "227", "Niger", "Niger"],
    'NG' => ['NG', "234", "Nigeria", "Nigeria"],
    'NU' => ['NU', "683", "Niue", "Niue"],
    'NF' => ['NF', "672", "Norfolk Island", "Norfolk Island"],
    'MK' => ['MK', "389", "North Macedonia", "North Macedonia"],
    'MP' => ['MP', "1 670", "Northern Mariana Islands", "Northern Mariana Islands"],
    'NO' => ['NO', "47", "Norway", "Norway"],

    'OM' => ['OM', "968", "Oman", "Oman"],

    'PK' => ['PK', "92", "Pakistan", "Pakistan"],
    'PW' => ['PW', "680", "Palau", "Palau"],
    'PA' => ['PA', "507", "Panama", "Panama"],
    'PG' => ['PG', "675", "Papua New Guinea", "Papua New Guinea"],
    'PY' => ['PY', "595", "Paraguay", "Paraguay"],
    'PE' => ['PE', "51", "Peru", "Peru"],
    'PH' => ['PH', "63", "Philippines", "Philippines"],
    'PL' => ['PL', "48", "Poland", "Poland"],
    'PT' => ['PT', "351", "Portugal", "Portugal"],
    'PR' => ['PR', "1 787, 1 939", "Puerto Rico", "Puerto Rico"],

    'QA' => ['QA', "974", "Qatar", "Qatar"],

    'RO' => ['RO', "40", "Romania", "Romania"],
    'RU' => ['RU', "7", "Russian Federation", "Russian Federation"],
    'RW' => ['RW', "250", "Rwanda", "Rwanda"],

    'SH' => ['SH', "290, 247", "Saint Helena, Ascension and Tristan da Cunha", "Saint Helena, Ascension and Tristan da Cunha"],
    'KN' => ['KN', "1 869", "Saint Kitts and Nevis", "Saint Kitts and Nevis"],
    'LC' => ['LC', "1 758", "Saint Lucia", "Saint Lucia"],
    'PM' => ['PM', "508", "Saint Pierre and Miquelon", "Saint Pierre and Miquelon"],
    'VC' => ['VC', "1 784", "Saint Vincent and the Grenadines", "Saint Vincent and the Grenadines"],
    'WS' => ['WS', "685", "Samoa", "Samoa"],
    'SM' => ['SM', "378", "San Marino", "San Marino"],
    'ST' => ['ST', "239", "Sao Tome and Principe", "Sao Tome and Principe"],
    'SA' => ['SA', "966", "Saudi Arabia", "Saudi Arabia"],
    'SN' => ['SN', "221", "Senegal", "Senegal"],
    'RS' => ['RS', "381", "Serbia", "Serbia"],
    'SC' => ['SC', "248", "Seychelles", "Seychelles"],
    'SL' => ['SL', "232", "Sierra Leone", "Sierra Leone"],
    'SG' => ['SG', "65", "Singapore", "Singapore"],
    'SX' => ['SX', "1 721", "Sint Maarten (Dutch)", "Sint Maarten (Dutch part)"],
    'SK' => ['SK', "421", "Slovakia", "Slovakia"],
    'SI' => ['SI', "386", "Slovenia", "Slovenia"],
    'SB' => ['SB', "677", "Solomon Islands", "Solomon Islands"],
    'SO' => ['SO', "252", "Somalia", "Somalia"],
    'ZA' => ['ZA', "27", "South Africa", "South Africa"],
    'SS' => ['SS', "211", "South Sudan", "South Sudan"],
    'ES' => ['ES', "34", "Spain", "Spain"],
    'LK' => ['LK', "94", "Sri Lanka", "Sri Lanka"],
    'SD' => ['SD', "249", "Sudan", "Sudan"],
    'SR' => ['SR', "597", "Suriname", "Suriname"],
    'SE' => ['SE', "46", "Sweden", "Sweden"],
    'CH' => ['CH', "41", "Switzerland", "Switzerland"],
    'SY' => ['SY', "963", "Syrian Arab Republic", "Syrian Arab Republic"],

    'TW' => ['TW', "886", "Taiwan, China", "Taiwan (Province of China)"],
    'TJ' => ['TJ', "992", "Tajikistan", "Tajikistan"],
    'TZ' => ['TZ', "255", "Tanzania", "Tanzania, the United Republic of"],
    'TH' => ['TH', "66", "Thailand", "Thailand"],
    'TL' => ['TL', "670", "Timor-Leste", "Timor-Leste"],
    'TG' => ['TG', "228", "Togo", "Togo"],
    'TK' => ['TK', "690", "Tokelau", "Tokelau"],
    'TO' => ['TO', "676", "Tonga", "Tonga"],
    'TT' => ['TT', "1 868", "Trinidad and Tobago", "Trinidad and Tobago"],
    'TN' => ['TN', "216", "Tunisia", "Tunisia"],
    'TR' => ['TR', "90", "Türkiye", "Türkiye"],
    'TM' => ['TM', "993", "Turkmenistan", "Turkmenistan"],
    'TC' => ['TC', "1 649", "Turks and Caicos", "Turks and Caicos Islands"],
    'TV' => ['TV', "688", "Tuvalu", "Tuvalu"],

    'UG' => ['UG', "256", "Uganda", "Uganda"],
    'UA' => ['UA', "380", "Ukraine", "Ukraine"],
    'AE' => ['AE', "971", "United Arab Emirates", "United Arab Emirates"],
    'GB' => ['GB', "44", "United Kingdom", "United Kingdom of Great Britain and Northern Ireland, The"],
    'US' => ['US', "1", "United States", "United States of America"],
    'VI' => ['VI', "1 340", "United States Virgin Islands", "Virgin Islands (U.S.)"],
    'UY' => ['UY', "598", "Uruguay", "Uruguay"],
    'UZ' => ['UZ', "998", "Uzbekistan", "Uzbekistan"],

    'VU' => ['VU', "678", "Vanuatu", "Vanuatu"],
    'VE' => ['VE', "58", "Venezuela", "Venezuela (Bolivarian Republic of)"],
    'VN' => ['VN', "84", "Viet Nam", "Viet Nam"],

    'WF' => ['WF', "681", "Wallis and Futuna", "Wallis and Futuna"],

    'YE' => ['YE', "967", "Yemen", "Yemen"],

    'ZM' => ['ZM', "260", "Zambia", "Zambia"],
    'ZW' => ['ZW', "263", "Zimbabwe", "Zimbabwe"],
  ];

  const COUNTRYCODECOLUMN_ALL = -1;
  const COUNTRYCODECOLUMN_ISO31662 = 0;
  const COUNTRYCODECOLUMN_ITU = 1;
  const COUNTRYCODECOLUMN_SHORTNAME = 2;
  const COUNTRYCODECOLUMN_FULLNAME = 3;
  const COUNTRYCODECOLUMN_SHORTANDITU = 9;

  /**
   * return above array with only the given column number (or all of them - default)
   * @param Number  $columnNumber  the column number from the full array to extract
   * @param Array   $first         elements by countrycode to put first
   * @param Array   $exclude       elements by countrycode to exclude
   * @return Array
   */
  public static function getCountryCodes($columnNumber = self::COUNTRYCODECOLUMN_ALL, $first = [], $exclude = []): array
  {
    $codes = [];
    foreach ($first as $key) {
      if (isset(self::$country_codes[$key])) {
        if ($columnNumber == self::COUNTRYCODECOLUMN_ALL)
          $codes[$key] = self::$country_codes[$key];
        else
          $codes[$key] = self::$country_codes[$key][$columnNumber];
      }
    }

    foreach (self::$country_codes as $key => $row) {
      if (!in_array($key, $exclude) && !in_array($key, $first)) {
        if ($columnNumber == self::COUNTRYCODECOLUMN_ALL)
          $codes[$key] = $row;
        elseif ($columnNumber == self::COUNTRYCODECOLUMN_SHORTANDITU)
          $codes[$key] = $row[self::COUNTRYCODECOLUMN_SHORTNAME] . ' (+' . $row[self::COUNTRYCODECOLUMN_ITU] . ')';
        else
          $codes[$key] = $row[$columnNumber];
      }
    }

    return $codes;
  }
  /**
   * return above array with only the given column number (or all of them - default)
   * but with the ITU code as the index, if the ITU code is used by more than one country add spaces to the index
   * @param Number    the column number from the full array to extract
   * @param Array   $first         elements by countrycode to put first
   * @param Array   $exclude       elements by countrycode to exclude
   * 
   * @return Array
   */
  public static function getITUCodes($columnNumber = self::COUNTRYCODECOLUMN_ALL, $first = [], $exclude = []): array
  {
    $codes = [];

    foreach ($first as $key) {
      if (isset(self::$country_codes[$key])) {
        $uniquekey = self::$country_codes[$key][self::COUNTRYCODECOLUMN_ITU];
        while (isset($array[$uniquekey]))
          $uniquekey .= ' ';

        $row = self::$country_codes[$key];
        if ($columnNumber == self::COUNTRYCODECOLUMN_ALL)
          $codes[$uniquekey] = $row;
        elseif ($columnNumber == self::COUNTRYCODECOLUMN_SHORTANDITU)
          $codes[$uniquekey] = $row[self::COUNTRYCODECOLUMN_SHORTNAME] . ' (+' . $row[self::COUNTRYCODECOLUMN_ITU] . ')';
        else
          $codes[$uniquekey] = $row[$columnNumber];
      }
    }

    foreach (self::$country_codes as $key => $row) {
      if (!in_array($key, $exclude) && !in_array($key, $first)) {
        $uniquekey = $row[1];
        while (isset($codes[$uniquekey]))
          $uniquekey .= ' ';

        if ($columnNumber == self::COUNTRYCODECOLUMN_ALL)
          $codes[$uniquekey] = $row;
        elseif ($columnNumber == self::COUNTRYCODECOLUMN_SHORTANDITU)
          $codes[$uniquekey] = $row[self::COUNTRYCODECOLUMN_SHORTNAME] . ' (+' . $row[self::COUNTRYCODECOLUMN_ITU] . ')';
        else
          $codes[$uniquekey] = $row[$columnNumber];
      }
    }

    return $codes;
  }


  /**
   * convert a number into B, KiB, MiB, GiB (2^10 steps)
   * @param  int $size   a non-negative integer
   * 
   * @return String      $size divided by repeated 2^10 with the correct SI suffix
   */
  public static function formatBytes($size): string
  {
    $suffix = "B";
    if ($size > 1024) {
      $suffix = "KiB";
      $size = round($size / 1024, 2);

      if ($size > 1024) {
        $suffix = "MiB";
        $size = round($size / 1024, 2);

        if ($size > 1024) {
          $suffix = "GiB";
          $size = round($size / 1024, 2);
        }
      }
    }
    return $size . " " . $suffix;
  }

  /**
   * get the max upload size for the server as a string with SI suffix
   * 
   * @return String      server upload size divided by repeated 2^10 with the correct SI suffix
   */
  public static function format_file_upload_max_size(): string
  {
    $size = self::file_upload_max_size();
    if ($size <= 0)
      return "Unlimited";
    //else

    return self::formatBytes(self::file_upload_max_size());
  }

  /**
   * get the max upload size for the server in bytes
   * 
   * @return int      server max upload size
   */
  public static function file_upload_max_size(): int
  {
    static $max_size = -1;

    if ($max_size < 0) {
      // Start with post_max_size.
      $post_max_size = self::parse_size(ini_get('post_max_size'));
      if ($post_max_size > 0) {
        $max_size = $post_max_size;
      }

      // If upload_max_size is less, then reduce. Except if upload_max_size is
      // zero, which indicates no limit.
      $upload_max = self::parse_size(ini_get('upload_max_filesize'));
      if ($upload_max > 0 && $upload_max < $max_size) {
        $max_size = $upload_max;
      }
    }
    return $max_size;
  }

  /**
   * copnvert a string with an si suffix to bytes
   * @param  String $size     a size with an SI suffix, prezumed to be mutiples of 2^10 (1024)
   * @param  bool   $strictSI flag indicating whether to multiply by 1024, or 1000 (deafult=false, ie. 1024)
   * 
   * @return int              the parameter passed converted to units
   */
  public static function parse_size($size, $strictSI = false): int
  {
    // Remove the non-unit characters from the size.
    $unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
    
    // Remove the non-numeric characters from the size.
    $size = preg_replace('/[^0-9\.]/', '', $size);
    
    if ($unit) {
      // Find the position of the unit in the ordered string which is the power of magnitude
      // to multiply a kilobyte by.
      return round($size * pow(($strictSI ? 1000 : 1024), stripos('bkmgtpezy', $unit[0])));

    } else {
      return round($size);
    }
  }

  //config manipulation commands
  /**
   * ----------------------------------------------------------------------
   *  CONFIG Manipulation Functions
   * ----------------------------------------------------------------------
   */

  public static function setStorageConfigValue($configname, $key, $value, $force = false): void
  {
    if ($force || config($configname . '.' . $key) != $value) {
      $config = self::getConfig($configname);

      if ($force || !isset($config[$key]) || $config[$key] != $value) {
        $config[$key] = $value;
        self::saveStorageConfig($config, $configname);

        Cache::forget($configname . '.' . $key);
      }
    }
  }

  /**
   * get the config file as an object
   * @param  String $primaryKey     top level key name for the config, default 'backslash4'
   * @param  bool   $refresh        whether to clear the config cache, default=true
   * 
   * @return Array | null           config array
   */
  public static function getConfig($primaryKey = null): array|null
  {
    if ($primaryKey == null)
      $primaryKey = 'backslash4';

    return Config::get($primaryKey);
  }

  /**
   * Save the config file back out in the correct format
   * @param  Array  $config       config array
   * @param  String $key          top level key (and filename excluding .php) (default 'backslash4')
   * 
   * @return void
   */
  public static function saveStorageConfig($config, $key = 'backslash4_app'): void
  {
    config([$key => $config]);

    File::ensureDirectoryExists(storage_path('app/config/'));
    File::put(
      storage_path('app/config/' . $key . '.php'),
      "<?php\n\n//dynamic update (BS4)"
      . "\nreturn " . self::varexport($config, true) . ";\n\n?>"
    );
  }

  /**
   * var_export() with square brackets and indented 4 spaces, just like we want for the config ini file
   * @param  String $expression
   * @param  bool $return         whether to echo or return the tided up string, default false (echo the output)
   * 
   * @return String | null
   */
  public static function varexport($expression, $return = false): string|null
  {
    $export = var_export($expression, true);
    $export = preg_replace("/^([ ]*)(.*)/m", '$1$1$2', $export);
    $array = preg_split("/\r\n|\n|\r/", $export);
    $array = preg_replace(["/\s*array\s\($/", "/\)(,)?$/", "/\s=>\s$/"], [NULL, ']$1', ' => ['], $array);
    $export = join(PHP_EOL, array_filter(["["] + $array));

    if ($return)
      return $export;
    //else

    echo $export;
    return null;
  }


  //----------------------------------------------------------------------
  // Tenant functions
  //----------------------------------------------------------------------

  /**
   * return the tenantKey or null if not intenannt mode
   * determined by the URL or the tenant-on flag if non-zero and a string
   * 
   * @return String | null     the tenantKey, or null if not in tenant mode
   */
  public static function getTenantKey(): string|null
  {
    $tenant = config('backslash4.tenants-on', 0);
    $tenantKey = null;
    if (!empty($tenant)) {
      if (strlen($tenant > 3)) {   //use the name in the config (as set temporarily by the command line)
        $tenantKey = self::tidyTenantKey($tenant);
      } else {   //get the basic url from request
        $tenantKey = self::tidyTenantKey(parse_url(url()->current(), PHP_URL_HOST));
      }

      //check for aliases
      $alt = config('backslash4.tenants-aliases.' . $tenantKey, null);
      if (!empty($alt))
        $tenantKey = self::tidyTenantKey($alt);
    }

    return $tenantKey;
  }

  /**
   * get tenant namespace for components
   * @param String      the tenantKey, if not provided it auto detected
   * 
   * @return String     the tenantKey transformed into a tenant namespace, or empty string if no tenant
   */
  public static function getTenantNameSpace($tenantKey = null): string
  {
    if (empty($tenantKey))
      $tenantKey = self::getTenantKey();

    //if still empty return empty string
    if (empty($tenantKey))
      return '';

    //else

    return 'backslash4_' . str_replace('-', '_', $tenantKey);
  }

  /**
   * tidy up a url to make a tenantKey, removing ww prefix and converting . full-stops to - dashes
   * @param  String $url  a string or URL
   * 
   * @return String       convert full-stops - dashes, slugify and remove any www prefix
   */
  public static function tidyTenantKey($url): string
  {
    $tenantKey = Str::slug(Str::replace('.', '-', $url));
    if (Str::startsWith($tenantKey, 'www-'))
      $tenantKey = substr($tenantKey, 4);

    return $tenantKey;
  }

  /**
   * get the tenantInfo config
   * @param String | null (optional)  the tenantkey, is omitted will auto-detect from URL/tenants-on
   * 
   * @return array | null
   */
  public static function getTenantInfo($tenantKey = null): array|null
  {

    if (empty($tenantKey))
      $key = self::getTenantKey();
    else
      $key = $tenantKey;

    if (!empty($key)) {
      $c = config('backslash4.tenants.' . $key);
      if (!empty($c)) {
        $c['key'] = $key;
        return $c;
      }
    }
    //else else

    return null; //default
  }

  /**
   * Get a tenant setting from a dot specified key  getTenantSetting('EmailTemplates.addBookings', 1)
   * @param  String $key        dot separated key
   * @param  Any    $defdault   optional, default return value, default null
   * @param  String $tenantKey  optional, an override tenantkey, default is current active detected from URL
   * 
   * @return Any                the value in the settings or the default
   */
  public static function getTenantSetting(string $key, $default = null, ?string $tenantKey = null)
  {
    $info = self::getTenantInfo($tenantKey);
    if (empty($info))
      return $default;

    $settings = $info['settings'];

    $keys = explode('.', $key);
    foreach ($keys as $thiskey) {
      if (empty($settings) || !array_key_exists($thiskey, $settings))
        return $default;
      //else

      $settings = $settings[$thiskey];
    }

    return $settings;
  }

  public static function getTenantPath($subkey, $tenantInfoKey = null): string|null
  {
    if (empty($tenantInfoKey))
      $tenantInfoKey = self::getTenantInfo();
    else if (is_string($tenantInfoKey))
      $tenantInfoKey = config('backslash4.tenants.' . $tenantInfoKey);

    if (!empty($tenantInfoKey))
      return self::tenant_path($tenantInfoKey['paths'][$subkey], $tenantInfoKey);
    //else

    return null;
  }

  /**
   * return the tenant path for the specified file
   * @param  String                 $filepath         the file path to be added to the path tennat path
   * @param  Array | String | null  $tenantInfoKey    the tenant Info array or the tenant key, or if null auto detected
   * 
   * @return String                                   the base path of the tenant plus the $filepath
   */
  public static function tenant_path($filepath, $tenantInfoKey = null): string
  {
    if (empty($tenantInfo))
      $tenantInfo = self::getTenantInfo();
    else if (is_string($tenantInfoKey))
      $tenantInfoKey = config('backslash4.tenants.' . $tenantInfoKey);

    return base_path($tenantInfoKey['paths']['base-path'] . '/' . $filepath);
  }

  public static function getDashboardConfigPath($tenantInfo = null): string
  {
    if (empty($tenantInfo))
      $tenantInfo = self::getTenantInfo();

    if (!empty($tenantInfo))
      return self::tenant_path($tenantInfo['paths']['config-file'], $tenantInfo);
    //else

    return config_path('backslash4.php');
  }

  public static function getDashboardConfigKey($tenantKey = null): string
  {
    if (empty($tenantKey))
      $tenantKey = self::getTenantKey();

    if (!empty($tenantKey))
      return 'tenants.' . $tenantKey;
    //else

    return 'backslash4';
  }

  public static function getDashboardConfig(): array|null
  {

    if (!empty(config('backslash4.tenants-on', 0))) {
      $c = Config::get('tenant');
      if (empty($c)) {
        $config_path = self::getDashboardConfigPath();
        if (file_exists($config_path)) {
          app('config')->set('tenant', require $config_path);
          $c = Config::get('tenant');
        }
      }

      if (!empty($c))
        return $c;
    }

    //else
    return Config::get('backslash4');
  }

  /**
   * Return the config section of the specified model
   * @param  $appModel          the name of the CMS Model Name, possibly from $model->getCMSModuleName()
   * 
   * @return array              model section of the flattened cmsMenu from the dashboard backslash4 config
   *                            file or tenants config file or an empty array if the key does not exist
   */
  public static function getDashboardModelConfig($CMSModuleName): array
  {
    return @self::getDashboardFlatCMS()[$CMSModuleName] ?? [];
  }

  /**
   * Return dashbaord cmsMenu as a flattened (no submenu/subnmenu) array for simplified parsing in routes
   * or backslash command line where the actual structure of the recursion does not matter
   * @param $config (optional)  a backslash4 config array, or null to fetch the appropriate one for the environment
   * 
   * @return array              flattened cmsMenu from the dashboard backslash4 config file ot tenants config file
   */
  public static function getDashboardFlatCMS($config = null): array
  {
    if ($config == null)
      $config = self::getDashboardConfig();

    if (empty($config))
      return [];
    // else

    $flatCmsMenu = [];

    //flatten recursive submenu items
    foreach ($config['cmsMenu'] as $key => $item) {
      $flatCmsMenu[$key] = $item;

      foreach ((@$item['subMenu'] ?? []) as $subkey => $submenu) {
        if (isset($submenu['subMenu'])) {
          $flatCmsMenu[$subkey] = $submenu;
        }
      }
    }

    return $flatCmsMenu;
  }

  /**
   * get the dashboard config setting from backslash4 config or the tenant config if tenancy is enabled
   * using a dot notated key
   *
   * @param  String $key        dot notated key to query
   * @param  Any    $default    optional default value to return if key does not exist, default null
   * @return Any                the value found
   */
  public static function getDashboardConfigSetting($key, $default = null)
  {
    $settings = self::getDashboardConfig();

    $keys = explode('.', $key);
    foreach ($keys as $thiskey) {
      if (!array_key_exists($thiskey, $settings))
        return $default;
      //else

      $settings = $settings[$thiskey];
    }

    return $settings;
  }

  /**
   * @deprecated
   */
  public static function getConnection($tenantKey = null): object
  {
    $conn = DB::getConnection();

    if (!empty($tenantKey) || !empty(config('backslash4.tenants-on', 0))) {   //if tenants-on isset
      if (empty($tenantKey))
        $tenantKey = self::getTenantKey();

      if (!empty($tenantKey)) {   //and we have a key
        $database = config('backslash4.tenants.' . $tenantKey . '.database', null);
        //config(['database.connections.mysql.database', $database]);

        if (!empty($database)) {   //and we have a database
          $conn->disconnect();
          $conn->setDatabaseName($database);
          Config::set("database.connections." . config('database.default') . ".database", $database);
          $conn->reconnect();
        }
      }
    }

    return $conn;
  }

  public static function getBuildDirectory()
  {
    //$buildpath = self::getTenantPath('public-build-path');
    //if(empty($buildpath))
    $buildpath = self::getTenantInfo()['paths']['public-build-path'];

    if (empty($buildpath))
      return '/build';
    //else

    return $buildpath;
  }

  public static function getViteScssPath()
  {
    if (!empty(config('backslash4.tenants-on', 0))) {
      $info = self::getTenantInfo();
      if (!empty($info)) {
        $path = $info['paths']['base-path']
          . '/' . $info['paths']['scss-file'];
        return $path;
      }
    }
    return 'resources/scss/app.scss';
  }

  public static function getViteJsPath()
  {
    if (!empty(config('backslash4.tenants-on', 0))) {
      $info = self::getTenantInfo();
      if (!empty($info)) {
        $path = $info['paths']['base-path']
          . '/' . $info['paths']['js-file'];
        return $path;
      }
    }
    return 'resources/js/app.js';
  }

}
