<?php

class Session
{
  private static          $_singletonInstance = null;
  private                 $_localSessionData = [];
  protected               $_stationViewCounter = [];
  public    string        $_lastError = '';
  protected bool          $_loggedIn;
  protected User          $_user;
  protected UserSettings  $_userSettings;

  public function __construct()
  {
    // PHP Session Support Enabled?
    $this->_init();

    // Check for a persistent authentication token
    if (!$this->_loggedIn) $this->checkPersistentAuthToken();

    // Override mobile user setting with the value in the latest request URL
    if (isset($_REQUEST['isMobile'])) $this->isMobile = intval($_REQUEST['isMobile']);
  }


  public function __destruct()
  {
    session_write_close();
    $_SESSION = array();

    $this->_localSessionData = array();
  }


  /**
   * Sets the $value for a $key
   *
   * @param string $key (Name of the paramter to set)
   * @param mixed $value (Value of the paramter to set)
   */
  public function __set($key, $value)
  {
    $this->_localSessionData[$key] = $value;
  }

  /**
   * Returns the $value for a $key
   *
   * @param string $key (Name of the paramter to retreive)
   * @return mixed $value (Value associated with $key)
   */
  public function __get($key)
  {
    return $this->_localSessionData[$key] ?? null;
  }


  /**
   * Logs the user out of the current session and resets all session variables.
   */
  public function __isset($key)
  {
    return isset($this->_localSessionData[$key]);
  }


  /**
   * Initialize the current session with defaults
   * This is called for every page request
   * sets _lastError to an empty string.
   * @param boolean $reInit (true = Reinit current session)
   */
  private function _init($reInit = false)
  {
    if ((session_id() == false && session_start()) || $reInit)
    {
      // Local session storage
      if (!isset($_SESSION['store'])) $_SESSION['store'] = [];
      $this->_localSessionData = &$_SESSION['store'];

      // Session user
      if (!isset($_SESSION['user'])) $_SESSION['user'] = new User(0);
      $this->_user = &$_SESSION['user'];

      // User logged in?
      $this->_loggedIn = $this->_user->isExistingObject();

      // Session user settings
      if (!isset($_SESSION['user_settings'])) $_SESSION['user_settings'] = new UserSettings(0);
      $this->_userSettings = &$_SESSION['user_settings'];

      // Station view counter deduplication
      if (!isset($_SESSION['station_view_counter'])) $_SESSION['station_view_counter'] = [];
      $this->_stationViewCounter = &$_SESSION['station_view_counter'];

      // Initital session start time (only set once)
      if (!isset($this->start_time)) $this->start_time = time();

      // Flush session hit counter store thats used to de-dupe station views
      if (!isset($this->view_counter_history)) $this->view_counter_history = [];
      $expire = time() - (60 * (getWebsiteConfig("view_counter_hysteresis") ?? 60));
      foreach($this->view_counter_history AS $k => $v)
      {
        if ($v < $expire) unset($this->view_counter_history[$k]);
      }

      // Mobile User?  Override setting with the value in the latest request URL
      if (isset($_REQUEST['isMobile'])) $this->isMobile = intval($_REQUEST['isMobile']);
      if (is_null($this->isMobile)) $this->isMobile = false;

      // Set the session defaults to the user preferences (if logged in)
      if (is_null($this->imperialUnits) && $this->_loggedIn && $this->_userSettings->isExistingObject())
      {
        $this->imperialUnits = $this->_userSettings->useImperialUnits;
      }

      // Set the session defaults to the user preferences (if logged in)
      if (is_null($this->useMilTime) && $this->_loggedIn && $this->_userSettings->isExistingObject())
      {
        $this->useMilTime = $this->_userSettings->useMilTime;
      }

      // Set defaults if not overridden by user preferences (check browser locale)
      if (!isset($this->imperialUnits) || is_null($this->imperialUnits)) $this->isImperialUnitUser();

      // Map settings use a default location, if the user has set one, use that, otherwise get from config
      if ($this->_userSettings->isExistingObject() && $this->_userSettings->loginApplyMapCenterZoom)
      {
        $this->defaultMapCenterLatitude = $this->_userSettings->defaultMapCenterLatitude;
        $this->defaultMapCenterLongitude = $this->_userSettings->defaultMapCenterLongitude;
        $this->defaultMapZoom = $this->_userSettings->defaultMapZoom;
      }
      else
      {
        $this->defaultMapCenterLatitude = getWebsiteConfig('default_latitude');
        $this->defaultMapCenterLongitude = getWebsiteConfig('default_longitude');
        $this->defaultMapZoom = getWebsiteConfig('default_zoom');
      }

      // This is a failsafe for ensuring that basic map settings are set.
      if (!isset($this->showAprsPositions)) $this->showAprsPositions = true;
      if (!isset($this->showCwopPositions)) $this->showCwopPositions = true;
      if (!isset($this->showOgnPositions)) $this->showOgnPositions = true;
      if (!isset($this->showCbAprsPositions)) $this->showCbAprsPositions = true;
      if (!isset($this->showInternetMarkers)) $this->showInternetMarkers = true;
      if (!isset($this->showStationaryMarkers)) $this->showStationaryMarkers = true;
      if (!isset($this->showOgflymMarkers)) $this->showOgflymMarkers = true;
      if (!isset($this->showUnknownMarkers)) $this->showUnknownMarkers = true;
      if (!isset($this->showMaidenheadGrid)) $this->showMaidenheadGrid = false;
      if (!isset($this->showDayNightOverlay)) $this->showDayNightOverlay = false;
      if (!isset($this->dayNightRefreshInterval)) $this->dayNightRefreshInterval = 5000;
      if (!isset($this->tailTimeLength)) $this->tailTimeLength = 60;

      if (!isset($this->showStationMarkers)) $this->showStationMarkers = true;
      if (!isset($this->showObjectMarkers)) $this->showObjectMarkers = true;
      if (!isset($this->showItemMarkers)) $this->showItemMarkers = true;
    }
  }


  /**
   * Returnes an initialized Session
   *
   * @return Session
   */
  public static function getInstance()
  {
      if (self::$_singletonInstance === null) {
          self::$_singletonInstance = new Session();
      }

      return self::$_singletonInstance;
  }


  /**
   * Returnes the user record associated with the initialized Session
   *
   * @return User (User object, or an unititlized user object if none loaded)
   */
  public function getUser()
  {
    if ($this->_user) return $this->_user;
    return new User(0);
  }


  /**
   * Returns the user settings object associated with the initialized Session
   *
   * @param boolean $reload (Reload settings from the database)
   * @return UserSettings (User settings object, or an unititlized user object if none loaded)
   */
  public function getUserSettings($reload = false)
  {
    if ($reload)
      $this->_userSettings = UserSettingsRepository::getInstance()->getObjectByUserId($this->_user->_id);

    if (!$this->_userSettings->isExistingObject())
    {
      $this->_userSettings->mpShowPath = 1;
      $this->_userSettings->mpShowCourseSpeed = 1;
      $this->_userSettings->mpShowMice = 1;
      $this->_userSettings->mpShowComment = 1;
      $this->_userSettings->mpShowPhgRng = 1;
      $this->_userSettings->mpShowDistance = 1;
      $this->_userSettings->mpShowWeather = 1;
      $this->_userSettings->mpUseLargeIcon = 1;
      $this->_userSettings->useMaxModal = 0;
    }
    return $this->_userSettings;
  }


  /**
   * Returns all Session Values
   * @return array All currently saves session settings
   */
  public function getAll()
  {
    return $this->_localSessionData ?? [];
  }


  /**
   * Process a user login in the active session
   * This will attempt to load the user record and validate the password.
   *
   * @param string $username (Username of the user)
   * @param string $password (Password of the user)
   * @return boolean (True if the login was successful, false if not)
   */
  public function loginUser($username, $password)
  {
    // Remove whitespace
    $username = trim($username);

    // Only process login if not already logged in
    if (!$this->_loggedIn)
    {
      // Test the username and set it if passes (no )
      if (strlen($username) <= 4 || preg_match('/[^a-z_\-0-9]/i', $username) == 0)
      {
        // Username is good, attempt to pull the record
        $user = UserRepository::getInstance()->getObjectByUsername($username);
        if ($user->isExistingObject())
        {
          if ($user->verifyPassword($password))
          {
            $this->_loggedIn = true;
            $this->_user = $user;

            // Get the user User Settings
            $this->_userSettings = UserSettingsRepository::getInstance()->getObjectByUserId($this->_user->_id);
            return true;
          }
          else
          {
            $this->_lastError = "Password specified was not correct.";
          }
        }
        else
        {
          $this->_lastError = "The username could not found.";
        }
      }
      else
      {
        $this->_lastError = "Username is not valid for this site.";
      }
    }
    else
    {
      $this->_lastError = "Already logged in.";
    }
  }


  /**
   * Logs the user out of the current session and resets all session variables.
   */
  public function logOutUser()
  {
    unset($this->_user);
    unset($this->_userSettings);
    $this->_loggedIn = false;

    // Delete cookies
    $cookie_prefix = getWebsiteConfig('cookie_prefix');
    $cookie_domain = getWebsiteConfig('cookie_domain');
    $cookie_secure = getWebsiteConfig('cookie_secure') == 1 ? true : false;
    $cookie_httponly = getWebsiteConfig('cookie_httponly') == 1 ? true : false;
    setcookie("{$cookie_prefix}_login", '', time()-3600, '/', $cookie_domain, $cookie_secure, $cookie_httponly);
    setcookie("{$cookie_prefix}_identifier", '', time()-3600, '/', $cookie_domain, $cookie_secure, $cookie_httponly);
    setcookie("{$cookie_prefix}_selector", '', time()-3600, '/', $cookie_domain, $cookie_secure, $cookie_httponly);

    // Delete the cookie entries before re-initalizing session
    unset($_COOKIE['aprs_login']);
    unset($_COOKIE['aprs_identifier']);
    unset($_COOKIE['aprs_selector']);

    $_SESSION = [];

    $this->_init(true);
  }


  /**
   * Sets a persistent authentication token and cookies ("rememeber me")
   * @return bool (True if the token was set, otherwise false)
   */
  public function setPersistentAuthToken()
  {
    // No point in doing anything if not logged in
    if ($this->_loggedIn)
    {
      $token = $this->_user->generatePersistentAuthToken();
      if (is_array($token) && $token['id'])
      {
        // Set Cookie expiration for 1 month
        $cookie_prefix = getWebsiteConfig('cookie_prefix');
        $cookie_domain = getWebsiteConfig('cookie_domain');
        $cookie_secure = getWebsiteConfig('cookie_secure') == 1 ? true : false;
        $cookie_httponly = getWebsiteConfig('cookie_httponly') == 1 ? true : false;
        setcookie("{$cookie_prefix}_login", $this->_user->userName, $token['expiration'], '/', $cookie_domain, $cookie_secure, $cookie_httponly);
        setcookie("{$cookie_prefix}_identifier", $token['identifier'], $token['expiration'], '/', $cookie_domain, $cookie_secure, $cookie_httponly);
        setcookie("{$cookie_prefix}_selector", $token['selector'], $token['expiration'], '/', $cookie_domain, $cookie_secure, $cookie_httponly);
        return true;
      }
      else
      {
        $this->_lastError = "An error occured creating persistent login token.";
      }
    }
    else
    {
      $this->_lastError = "Not logged in.";
    }

    return false;
  }


  /**
   * Checks the persistent authentication token and legs the user in if set
   * @return bool (True if the token was set, otherwise false)
   */
  public function checkPersistentAuthToken()
  {
    // No point in doing anything if logged in
    if (!$this->_loggedIn && isset($_COOKIE['aprs_login']))
    {
      // Attempt to find the user record
      $username = trim($_COOKIE['aprs_login']);
      $user = UserRepository::getInstance()->getObjectByUsername($username);
      if ($user->isExistingObject())
      {
        if ($user->validatePersistentAuthToken($_COOKIE['aprs_selector'] ?? '', $_COOKIE['aprs_identifier'] ?? ''))
        {
          // Get the user User Settings
          $this->_userSettings = UserSettingsRepository::getInstance()->getObjectByUserId($user->_id);
          $this->_loggedIn = true;
          $this->_user = $user;
          return true;
        }
      }
    }
  }


  /**
   * Checks the station view cache to see if a station has been viewed recenely
   */
  public function incrementStationCounter($station)
  {
    if (getWebsiteConfig("view_counter_increment_enabled") == '0') return;
    if ($station->isExistingObject() && !array_key_exists($station->id, $this->_stationViewCounter) && !$this->isBot())
    {
      // Check the deduplication/spam table
      $pdo = PDOMysqlConnection::getInstance();

      $expire = time() - (60 * (getWebsiteConfig("view_counter_hysteresis") ?? 60));
      $sql = "SELECT * FROM station_view_count_tracker WHERE station_id = ? AND expire_timestamp > ? AND (user_id = ? OR ip_address = INET_ATON(?)) LIMIT 1";
      $stmt = $pdo->prepareAndExec($sql, [$station->id, $expire, $this->getUser()->id, $this->getClientIp()]);
      if ($record = $stmt->fetch(PDO::FETCH_ASSOC))
      {
        // Already recorded a hit, skip
        return;
      }
      $this->_stationViewCounter[$station->id] = time();

      // Add to the the deduplication/spam table
      $expire = time() + (60 * (getWebsiteConfig("view_counter_hysteresis") ?? 60));
      $sql = "INSERT INTO station_view_count_tracker (user_id, ip_address, station_id, expire_timestamp) VALUES(?, INET_ATON(?), ?, ?) LIMIT 1";
      $stmt = $pdo->prepareAndExec($sql, [$this->getUser()->id ?? 0, $this->getClientIp(), $station->id, $expire]);
    }
  }

  /**
   * Records the a station browse (view) event
   */
  public function recordStationBrowseHistory($station_id)
  {
    if (!isInt($station_id) || (isset($this->lastViewedStationId) && $station_id == $this->lastViewedStationId) || !$this->isLoggedIn()) return false;

    // Only record history for valid users
    if ($this->getUser()->isExistingObject())
    {
      $ret = $this->getUser()->recordStationBrowseHistory($station_id);
      $this->_lastError = $this->getUser()->getLastError();
      $this->lastViewedStationId = $station_id;
      return $ret;
    }
  }

  /**
   * Logs the user out of the current session and resets all session variables.
   */
  public function isLoggedIn()
  {
    return $this->_loggedIn;
  }


  public function getURL($baseUrl, array $queryArray = [])
  {
    $path = array_slice(explode('/', $baseUrl), 1);
    $section = array_shift($path);
    $view = str_replace('.php', '', array_shift($path));
    $newUrl = $baseUrl;

    if ($section == "views")
    {
      $newUrl = '/v/' . $view;
    }

    if ($section == "account")
    {
      $newUrl = '/a/' . $view;
    }

    else if (isset($queryArray['id']) && $section == 'station')
    {
      $newUrl = '/' . $section . '/' . $queryArray['id'] . ($section != $view ? '/' . $view : '');
      unset($queryArray['id']);
    }

    else if (isset($queryArray['s']) && $section == 'station')
    {
      $newUrl = '/' . $section . '/' . $queryArray['s'] . ($section != $view ? '/' . $view : '');
      unset($queryArray['s']);
    }

    // Make the URL
    return $newUrl . $this->getQuerySting($queryArray, '?');
  }


  public function getQuerySting(array $queryArray = [], $append = '&')
  {
    // Make the URL
    return (sizeof($queryArray) ? $append . http_build_query($queryArray) : '');
  }


  public function isImperialUnitUser()
  {
    // See if the current value is being overridden
    if (isset($_GET['imperialUnits']) && $_GET['imperialUnits'])
    {
      $this->imperialUnits = 1;
    }
    else if (isset($_GET['imperialUnits']) && !$_GET['imperialUnits'])
    {
      $this->imperialUnits = 0;
    }

    // Return stored (or just set) session value
    if (isset($this->imperialUnits)) return $this->imperialUnits;


    // Initialize the default for the users location
    if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
    {
      // USA
      if (substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 5) == 'en-US'

      // Myanmar / Burma
      ||  substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2) == 'my'

      // Liberia
      ||  substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 5) == 'en-LR')
      {
        $this->imperialUnits = 1;
      }
      else
      {
        $this->imperialUnits = 0;
      }
    }
  }


  /**
   * Returns the client IP address
   *
   * @return string IP address of the client
   */
  public function getClientIp()
  {
    static $proxies;

    if (is_null($proxies))
    {
      $proxies = explode(',', getWebsiteConfig("reverse_proxy_ips") ?? '');
    }

    if (!is_array($proxies) || sizeof($proxies) == 0)
    {
      return $_SERVER['REMOTE_ADDR'] ?? $_SERVER['HTTP_CLIENT_IP'] ?? '';
    }

    return (in_array($_SERVER['REMOTE_ADDR'], $proxies) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'])
      ?? $_SERVER['REMOTE_ADDR']
      ?? $_SERVER['HTTP_CLIENT_IP']
      ?? '';
  }


  /**
   * Detects whether the user agent is believed to be a robot or spider
   *
   * @return string IP address of the client
   */
  public function isBot()
  {
    if ( !empty($_SERVER['HTTP_USER_AGENT']) and preg_match('/abacho|accona|AddThis|AdsBot|ahoy|AhrefsBot|AISearchBot|alexa|altavista|anthill|appie|applebot|arale|araneo|AraybOt|ariadne|arks|aspseek|ATN_Worldwide|Atomz|baiduspider|baidu|bbot|bingbot|bing|Bjaaland|BlackWidow|BotLink|bot|boxseabot|bspider|calif|CCBot|ChinaClaw|christcrawler|CMC\/0\.01|combine|confuzzledbot|contaxe|CoolBot|cosmos|crawler|crawlpaper|crawl|curl|cusco|cyberspyder|cydralspider|dataprovider|digger|DIIbot|DotBot|downloadexpress|DragonBot|DuckDuckBot|dwcp|EasouSpider|ebiness|ecollector|elfinbot|esculapio|ESI|esther|eStyle|Ezooms|facebookexternalhit|facebook|facebot|fastcrawler|FatBot|FDSE|FELIX IDE|fetch|fido|find|Firefly|fouineur|Freecrawl|froogle|gammaSpider|gazz|gcreep|geona|Getterrobo-Plus|get|girafabot|golem|googlebot|\-google|grabber|GrabNet|griffon|Gromit|gulliver|gulper|hambot|havIndex|hotwired|htdig|HTTrack|ia_archiver|iajabot|IDBot|Informant|InfoSeek|InfoSpiders|INGRID\/0\.1|inktomi|inspectorwww|Internet Cruiser Robot|irobot|Iron33|JBot|jcrawler|Jeeves|jobo|KDD\-Explorer|KIT\-Fireball|ko_yappo_robot|label\-grabber|larbin|legs|libwww-perl|linkedin|Linkidator|linkwalker|Lockon|logo_gif_crawler|Lycos|m2e|majesticsEO|marvin|mattie|mediafox|mediapartners|MerzScope|MindCrawler|MJ12bot|mod_pagespeed|moget|Motor|msnbot|muncher|muninn|MuscatFerret|MwdSearch|NationalDirectory|naverbot|NEC\-MeshExplorer|NetcraftSurveyAgent|NetScoop|NetSeer|newscan\-online|nil|none|Nutch|ObjectsSearch|Occam|openstat.ru\/Bot|packrat|pageboy|ParaSite|patric|pegasus|perlcrawler|phpdig|piltdownman|Pimptrain|pingdom|pinterest|pjspider|PlumtreeWebAccessor|PortalBSpider|psbot|rambler|Raven|RHCS|RixBot|roadrunner|Robbie|robi|RoboCrawl|robofox|Scooter|Scrubby|Search\-AU|searchprocess|search|SemrushBot|Senrigan|seznambot|Shagseeker|sharp\-info\-agent|sift|SimBot|Site Valet|SiteSucker|skymob|SLCrawler\/2\.0|slurp|snooper|solbot|speedy|spider_monkey|SpiderBot\/1\.0|spiderline|spider|suke|tach_bw|TechBOT|TechnoratiSnoop|templeton|teoma|titin|topiclink|twitterbot|twitter|UdmSearch|Ukonline|UnwindFetchor|URL_Spider_SQL|urlck|urlresolver|Valkyrie libwww\-perl|verticrawl|Victoria|void\-bot|Voyager|VWbot_K|wapspider|WebBandit\/1\.0|webcatcher|WebCopier|WebFindBot|WebLeacher|WebMechanic|WebMoose|webquest|webreaper|webspider|webs|WebWalker|WebZip|wget|whowhere|winona|wlm|WOLP|woriobot|WWWC|XGET|xing|yahoo|YandexBot|YandexMobileBot|yandex|yeti|Zeus/i', $_SERVER['HTTP_USER_AGENT']))
    {
      return true; // 'Above given bots detected'
    }
    return false;
  }


  /**
   * Returns the last error message set during a session action then
   * sets _lastError to an empty string.
   *
   * @return string (Text of the last recorded error)
   */
  public function getLastError()
  {
    $last_error = $this->_lastError;
    $this->_lastError = '';
    return $last_error;
  }
}
