<?php

class StationRepository extends ModelRepository
{

    private static $_singletonInstance = null;
    private $_config;

    public function __construct()
    {
        parent::__construct('Station');
    }

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

        return self::$_singletonInstance;
    }

    /**
     * Get object by id
     *
     * @param  int $id
     * @param  int $source_id (optional, 1 = APRS, 2 = CWOP, 3 = CBAPRS, 5 = OGN) used by query planner to optimize the query)
     * @return Station
     */
    public function getObjectById($id, $source_id = null)
    {
        if (!isInt($id) || (!is_null($source_id) && !isInt($source_id)))
        {
            return new Station(0);
        }

        static $cache = array();
        $key = $id;
        if (!isset($cache[$key]))
        {
          if (is_null($source_id))
            $cache[$key] = $this->getObjectFromSql('select * from station where id = ?', [$id]);
          else
            $cache[$key] = $this->getObjectFromSql('select * from station where id = ? AND source_id = ?', [$id, $source_id]);
        }
        return $cache[$key];
    }

    /**
     * Get object by name
     *
     * @param  string $name
     * @param  int    $source_id (optional, 1 = APRS, 2 = CWOP, 3 = CBAPRS, 5 = OGN) used by query planner to optimize the query)
     * @return Station
     */
    public function getObjectByName($name, $source_id = null)
    {
        if (!is_string($name) || (!is_null($source_id) && !isInt($source_id)))
        {
            return new Station(0);
        }

        static $cache = array();
        $key = $name;
        if (!isset($cache[$key]))
        {
          if (is_null($source_id))
            $cache[$key] = $this->getObjectFromSql('select * from station where name = ? order by latest_location_packet_timestamp desc nulls last limit 1', [$name]);
          else
            $cache[$key] = $this->getObjectFromSql('select * from station where name = ? AND source_id = ? order by latest_location_packet_timestamp desc nulls last limit 1', [$name, $source_id]);
        }
        return $cache[$key];
    }


    /**
     * Get list of objects by name (call)
     *
     * @param  string $name
     * @param  int    $source_id (optional, 1 = APRS, 2 = CWOP, 3 = CBAPRS, 5 = OGN) used by query planner to optimize the query)
     * @return Station
     */
    public function getObjectListByName($name, $station_type_id = null, $source_id = null, $min_timestamp = 0)
    {
        if (!is_string($name) || (!is_null($source_id) && !isInt($source_id)) || (!is_null($station_type_id) && !isInt($station_type_id)) || !isInt($station_type_id) || !isInt($min_timestamp))
        {
          return new Station(0);
        }

        static $cache = array();
        $key = $name;
        if (!isset($cache[$key]))
        {
          if (isInt($station_type_id) && isInt($source_id))
          {
            $cache[$key] = $this->getObjectListFromSql('select *
                      from station
                      where station_type_id = ?
                          and name = ?
                          and (source_id = ? or source_id is null)
                          and latest_confirmed_packet_timestamp > ?', [$station_type_id, $name, $source_id, $min_timestamp]);
          }
          else if (isInt($station_type_id))
          {
            $cache[$key] = $this->getObjectListFromSql('select *
                      from station
                      where station_type_id = ?
                          and name = ?
                          and latest_confirmed_packet_timestamp > ?', [$station_type_id, $name, $min_timestamp]);
          }
          else
          {
            $cache[$key] = $this->getObjectListFromSql('select *
                      from station
                      where name = ?
                          and latest_confirmed_packet_timestamp > ?', [$name, $min_timestamp]);
          }
        }
        return $cache[$key];
    }


    /**
     * Get sender station object by sender id
     *
     * @param  string $name
     * @param  int    $senderId
     * @param  int    $source_id (optional, 1 = APRS, 2 = CWOP, 3 = CBAPRS, 5 = OGN) used by query planner to optimize the query)
     * @return Station
     */
    public function getSenderStationObjectBySenderId($senderId, $source_id = null)
    {
        if (!isInt($senderId) || (!is_null($source_id) && !isInt($source_id)))
        {
            return new Station(0);
        }

        static $cache = array();
        $key = $senderId;
        if (!isset($cache[$key]))
        {
          if (is_null($source_id))
            $cache[$key] = $this->getObjectFromSql('select station.* from station, sender where station.latest_sender_id = sender.id and sender.name = station.name and sender.id = ? and station.station_type_id = 1', [$senderId]);
          else
            $cache[$key] = $this->getObjectFromSql('select station.* from station, sender where station.source_id = ? station.latest_sender_id = sender.id and sender.name = station.name and sender.id = ? and station.station_type_id = 1', [$source_id, $senderId]);
        }
        return $cache[$key];
    }

    /**
     * Get object by name and sender id
     *
     * @param  string $name
     * @param  int    $senderId
     * @param  int    $source_id (optional, 1 = APRS, 2 = CWOP, 3 = CBAPRS, 5 = OGN) used by query planner to optimize the query)
     * @return Station
     */
    public function getObjectByNameAndSenderId($name, $senderId, $source_id = null)
    {
        if (!is_string($name) || !isInt($senderId) || (!is_null($source_id) && !isInt($source_id)))
        {
            return new Station(0);
        }

        static $cache = array();
        $key = $name . ';' . $senderId;
        if (!isset($cache[$key]))
        {
          if (is_null($source_id))
            $cache[$key] = $this->getObjectFromSql('select * from station where name = ? and latest_sender_id = ?', [$name, $senderId]);
          else
            $cache[$key] = $this->getObjectFromSql('select * from station where name = ? and source_id = ? and latest_sender_id = ?', [$name, $source_id, $senderId]);
        }
        return $cache[$key];
    }


    /**
     * Get object list
     *
     * @param  int   $activeDuringLatestNumberOfSeconds
     * @param  int   $limit
     * @param  int   $offset
     * @param  mixed $filter_source - ID of the data_source (APRS, CWOP) or an array of data source IDs.
     * @return array
     */
    public function getObjectList($activeDuringLatestNumberOfSeconds = (24*60*60), $limit=50, $offset=0, $filter_source = 0)
    {
        if ($activeDuringLatestNumberOfSeconds == 0)
        {
            $activeDuringLatestNumberOfSeconds = time();
        }

        // Filter sources
        $filter_query = null;
        $filter_data = null;
        if (is_array($filter_source))
        {
          if (count($filter_source) > 0 && count($filter_source) < 4)
          {
            $filter_query = 'and source_id IN (' . implode(',', $filter_source) . ')';
          }
        }
        else if (isInt($filter_source) && $filter_source > 0)
        {
          $filter_query = 'and source_id = :fs';
          $filter_data = intval($filter_source);
        }

        $pdo = PDOConnection::getInstance();
        $stmt = $pdo->prepare(
            'select * from station
            where latest_confirmed_packet_timestamp is not null
                and latest_confirmed_packet_timestamp > :ts1
                and (source_id != 5 or latest_confirmed_packet_timestamp > :ts2)
                ' . $filter_query . '
            order by latest_confirmed_packet_timestamp desc
            limit :limit offset :offset'
        );
        $stmt->bindValue(':ts1', (time() - $activeDuringLatestNumberOfSeconds));
        $stmt->bindValue(':ts2', (time() - (60*60*24))); // OGN data should be deleted after 24h, but just to be safe we avoid including older data when searching
        $stmt->bindValue(':limit', $limit);
        $stmt->bindValue(':offset', $offset);

        if (!is_null($filter_data)) {
          $stmt->bindValue(':fs', $filter_data);
        }

        $stmt->execute();
        $records = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (is_array($records) && !empty($records)) {
            return $this->_getObjectListFromRecords($records);
        }

        // No object found, return empty array
        return [];
    }

    /**
     * Get number of stations
     *
     * @param  int   $activeDuringLatestNumberOfSeconds
     * @return int
     */
    public function getNumberOfStations($activeDuringLatestNumberOfSeconds = (24*60*60))
    {
        if ($activeDuringLatestNumberOfSeconds == 0) {
            $activeDuringLatestNumberOfSeconds = time();
        }

        $sql = 'select count(*) c from station
            where latest_confirmed_packet_timestamp is not null
                and latest_confirmed_packet_timestamp > ?
                and (source_id != 5 or latest_confirmed_packet_timestamp > ?)';
        $parameters = [(time() - $activeDuringLatestNumberOfSeconds), (time() - (60*60*24))];

        $pdo = PDOConnection::getInstance();
        $stmt = $pdo->prepareAndExec($sql, $parameters);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $sum = 0;
        foreach($rows as $row) {
            $sum += $row['c'];
        }

        return $sum;
    }

    /**
     * Get object list by query string
     *
     * @param  int   $q
     * @param  int   $activeDuringLatestNumberOfSeconds
     * @param  int   $limit
     * @param  int   $offset
     * @return array
     */
    public function getObjectListByQueryString($q, $activeDuringLatestNumberOfSeconds = (24*60*60), $limit=50, $offset=0)
    {
        if (!is_string($q) || !isInt($limit)) {
            return [];
        }

        if ($activeDuringLatestNumberOfSeconds == 0) {
            $activeDuringLatestNumberOfSeconds = time();
        }

        $pdo = PDOConnection::getInstance();
        $stmt = $pdo->prepare('
	    select s.*
            from station s
	        left outer join ogn_device d on d.device_id = s.latest_ogn_sender_address
            where s.latest_confirmed_packet_timestamp is not null
                and s.latest_confirmed_packet_timestamp > ?
                and (s.source_id != 5 or s.latest_confirmed_packet_timestamp > ?)
                and (s.name ilike ? or d.registration ilike ? or d.cn ilike ?)
            order by name
            limit ? offset ?'
        );
        $stmt->bindValue(1, (time() - $activeDuringLatestNumberOfSeconds));
        $stmt->bindValue(2, (time() - (60*60*24))); // OGN data should be deleted after 24h, but just to be safe we avoid including older data when searching
	$stmt->bindValue(3, "$q%");
	$stmt->bindValue(4, "$q%");
	$stmt->bindValue(5, "$q%");
        $stmt->bindValue(6, $limit);
        $stmt->bindValue(7, $offset);

        $stmt->execute();
        $records = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (is_array($records) && !empty($records)) {
            return $this->_getObjectListFromRecords($records);
        }

        // No object found, return empty array
        return [];
    }

    /**
     * Get number of stations by query string
     *
     * @param  int   $q
     * @param  int   $activeDuringLatestNumberOfSeconds
     * @return int
     */
    public function getNumberOfStationsByQueryString($q, $activeDuringLatestNumberOfSeconds = (24*60*60))
    {
        if (!is_string($q)) {
            return 0;
        }

        if ($activeDuringLatestNumberOfSeconds == 0) {
            $activeDuringLatestNumberOfSeconds = time();
        }

        $sql = 'select count(*) c from station
            where latest_confirmed_packet_timestamp is not null
                and latest_confirmed_packet_timestamp > ?
                and (source_id != 5 or latest_confirmed_packet_timestamp > ?)
                and name ilike ?';
        $parameters = [(time() - $activeDuringLatestNumberOfSeconds), (time() - (60*60*24)), "$q%"];

        $pdo = PDOConnection::getInstance();
        $stmt = $pdo->prepareAndExec($sql, $parameters);
        return $stmt->fetchColumn();
    }

    /**
     * Get object list by sender id (close to specified lat/lng)
     *
     * @param  int   $senderId
     * @param  int   $limit
     * @param  float $latitude
     * @param  float $longitude
     * @return array
     */
    public function getObjectListBySenderId($senderId, $limit, $latitude = null, $longitude = null)
    {
        if (!isInt($senderId) || !isFloat($latitude) || !isFloat($longitude) || !isInt($limit)) {
            return [];
        }

        $pdo = PDOConnection::getInstance();

        if ($latitude === null || $longitude === null) {
            $stmt = $pdo->prepare(
                'select * from station
                where latest_confirmed_packet_timestamp is not null
                    and latest_confirmed_packet_timestamp > ?
                    and latest_sender_id = ?
                limit ?'
            );
            $stmt->bindValue(1, (time() - 60*60*24));
            $stmt->bindValue(2, $senderId);
            $stmt->bindValue(3, $limit);
        } else {
            $stmt = $pdo->prepare(
                'select * from station
                where latest_confirmed_packet_timestamp is not null
                    and latest_confirmed_packet_timestamp > ?
                    and latest_sender_id = ?
                order by (abs(latest_confirmed_latitude - ?) + abs(latest_confirmed_longitude - ?))
                limit ?'
            );
            $stmt->bindValue(1, (time() - 60*60*24));
            $stmt->bindValue(2, $senderId);
            $stmt->bindValue(3, $latitude);
            $stmt->bindValue(4, $longitude);
            $stmt->bindValue(5, $limit);
        }
        $stmt->execute();
        $records = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (is_array($records) && !empty($records)) {
            return $this->_getObjectListFromRecords($records);
        }

        // No object found, return empty array
        return [];
    }

    /**
     * Get related object list by station id (stations with same name but different SSID)
     *
     * @param  int $stationId
     * @param  int $limit
    * @param   int $durationHours (Number of hours to consider as active, default 24)
     * @return array
     */
    public function getRelatedObjectListByStationId($stationId, $limit, $durationHours = 24)
    {
        if (!isInt($stationId) || !isInt($limit)) {
            return [];
        }

        $station = $this->getObjectById($stationId);
        $name = $station->name;
        $pos = strrpos($name, '-');
        if ($pos) {
            $call = substr($name, 0, $pos);
        } else {
            // Primary stations are supposed to use the -0 suffix.
            // Some APRS software will drop the -0 and transmit just the callsign.
            $call = $name;
        }

        if ($station->latestPacketTimestamp !== null) {
            $time = $station->latestPacketTimestamp;
        } else {
            $time = time();
        }

        $pdo = PDOConnection::getInstance();

        if ($station->latestConfirmedLatitude === null || $station->latestConfirmedLongitude === null) {
            $stmt = $pdo->prepare(
                'select * from station
                where latest_confirmed_packet_timestamp is not null
                    and latest_confirmed_packet_timestamp > ?
                    and name like ?
                limit ?'
            );
            $stmt->bindValue(1, $time - 60*60*$durationHours, PDO::PARAM_STR);
            $stmt->bindValue(2, "$call%", PDO::PARAM_STR);
            $stmt->bindValue(3, $limit);
        } else {
            $stmt = $pdo->prepare(
                'select * from station
                where latest_confirmed_packet_timestamp is not null
                    and latest_confirmed_packet_timestamp > ?
                    and name like ?
                order by (abs(latest_confirmed_latitude - ?) + abs(latest_confirmed_longitude - ?))
                limit ?'
            );
            $stmt->bindValue(1, $station->latestPacketTimestamp - 60*60*$durationHours, PDO::PARAM_STR);
            $stmt->bindValue(2, "$call%", PDO::PARAM_STR);
            $stmt->bindValue(3, $station->latestConfirmedLatitude);
            $stmt->bindValue(4, $station->latestConfirmedLongitude);
            $stmt->bindValue(5, $limit);
        }
        $stmt->execute();
        $records = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (is_array($records) && !empty($records)) {
            return $this->_getObjectListFromRecords($records);
        }

        // No object found, return empty array
        return [];
    }


    /**
     * Get the most recently used station (-SSID) from station call (no SSID)
     *
     * @param  int $stationName
     * @param  int $limit
    * @param   int $durationHours (Number of hours to consider as active, default 60 days)
     * @return array
     */
    public function getMostRecentStationByName($stationName, $limit = 1, $durationHours = 24*60)
    {
        if (!isInt($limit)) {
            return [];
        }

        $call = trim($stationName);

        $pdo = PDOConnection::getInstance();
        $stmt = $pdo->prepare(
            'select * from station
            where latest_confirmed_packet_timestamp is not null
                and latest_confirmed_packet_timestamp > ?
                and name like ?
            order by latest_confirmed_packet_timestamp desc
            limit ?'
        );
        $stmt->bindValue(1, time() - 60*60*$durationHours, PDO::PARAM_STR);
        $stmt->bindValue(2, "$call%", PDO::PARAM_STR);
        $stmt->bindValue(3, $limit);

        $stmt->execute();
        $records = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (is_array($records) && !empty($records)) {
            return $this->_getObjectListFromRecords($records);
        }

        // No object found, return empty array
        return [];
    }


    /**
     * Get object list of stations near specified position
     *
     * @param  int $latitude
     * @param  int $longitude
     * @param  int $maxDistanceInKm
     * @param  int $limit
     * @param  string $sourceFilter
     * @return array
     */
    public function getCloseByObjectListByPosition($latitude, $longitude, $maxDistanceInKm = 100, $limit = 10000, $sourceFilter = null)
    {
        if (!isFloat($latitude) || !isFloat($longitude) || !isInt($maxDistanceInKm) || !isInt($limit)) {
            return [];
        }

        if ($maxDistanceInKm <= 10) {
            // Plus 0.1 should be about 11km
            $minLatitude = $latitude - 0.1;
            $maxLatitude = $latitude + 0.1;
            $minLongitude = $longitude - 0.1;
            $maxLongitude = $longitude + 0.1;
        } else {
            // Plus 1 should be about 111km
            $minLatitude = $latitude - 1;
            $maxLatitude = $latitude + 1;
            $minLongitude = $longitude - 1;
            $maxLongitude = $longitude + 1;
        }

        $minTimestamp = time() - 60*60; // Latest 1h is pretty fast

        $sourceFilterSQL = '';
        if ($sourceFilter != null && strlen($sourceFilter) > 0)
        {
          $sourceFilterSQL = " and source_id IN($sourceFilter) ";
        }

        // The order by used here is not 100% accurate, but it's fast :-)
        $list = $this->getObjectListFromSql(
            'select * from station
            where latest_confirmed_latitude > ?
                and latest_confirmed_latitude < ?
                and latest_confirmed_longitude > ?
                and latest_confirmed_longitude < ?
                and latest_confirmed_packet_timestamp > ?
                ' . $sourceFilterSQL . '
            order by (abs(latest_confirmed_latitude - ?) + abs(latest_confirmed_longitude - ?))
            limit ?', [$minLatitude, $maxLatitude, $minLongitude, $maxLongitude, $minTimestamp, $latitude, $longitude, $limit]
        );

        $orderedList = [];
        foreach ($list as $closeByStation) {
            $distance = $closeByStation->getDistance($latitude, $longitude);
            if ($distance !== null && $distance <= ($maxDistanceInKm*1000)) {
                $key = intval($distance);
                while (isset($orderedList[$key])) {
                    $key++;
                }
                $orderedList[$key] = $closeByStation;
            }
        }
        ksort($orderedList);
        return array_values($orderedList);
    }

    /**
     * Get object list of stations near specified station
     *
     * @param  int $stationId
     * @param  int $limit
     * @param  string $filterSource
     * @return array
     */
    public function getCloseByObjectListByStationId($stationId, $limit, $filterSource)
    {
        if (!isInt($stationId) || !isInt($limit)) {
            return [];
        }

        $station = $this->getObjectById($stationId);
        if ($station->latestConfirmedLatitude === null || $station->latestConfirmedLongitude === null) {
            return [];
        }

        $closeByStationList = self::getCloseByObjectListByPosition($station->latestConfirmedLatitude, $station->latestConfirmedLongitude, 100, $limit + 1, $filterSource);
        $result = Array();
        foreach ($closeByStationList as $key => $closeByStation) {
            if ($station->getId() == $closeByStation->getId()) {
                break;
            }
        }
        if (isset($key)) {
            unset($closeByStationList[$key]);
        }
        return $closeByStationList;
    }

    /**
     * Super-basic utility function to get object list from raw SQL query
     *
     * @param  int $sqlQuery
     * @param  array $queryParams
     * @return array
     */
    public function getObjectListWithByRawSQLQuery($sqlQuery, $queryParams=[])
    {
        if (empty($sqlQuery)) {
            return [];
        }

        return $this->getObjectListFromSql($sqlQuery, $queryParams);
    }
}
