<?php
class Packet extends Model
{
    public function __construct($id)
    {
        parent::__construct($id);
    }

    /**
     * Returnes icon http path
     *
     * @param  int $scaleWidth
     * @param  int $scaleHeight
     * @return string
     */
    public function getIconFilePath($scaleWidth = null, $scaleHeight = null)
    {
        if (strlen($this->symbol) >= 1 && strlen($this->symbolTable) >= 1) {
            $symbolAsciiValue = ord(substr($this->symbol, 0, 1));
            $symbolTableAsciiValue = ord(substr($this->symbolTable, 0, 1));
        } else {
            // Default values
            $symbolAsciiValue = 125;
            $symbolTableAsciiValue = 47;
        }

        $scaleStrValue = '';
        if ($scaleWidth !== null && $scaleHeight !== null) {
            $scaleStrValue = '-scale' . $scaleWidth . 'x' . $scaleHeight;
        }

        return '/symbols/symbol-' . $symbolAsciiValue . '-' . $symbolTableAsciiValue . $scaleStrValue . '.svg';
    }

    /**
     * Get PGH range in meters
     *
     * @return float
     */
    public function getPHGRange()
    {
        if ($this->getPhg() != null) {
            $p = $this->getPhgPower();
            $h = $this->getPhgHaat(false);
            $g = $this->getPhgGain();

            $gain = pow(10, ($g/10)); //converts from DB to decimal
            $range = sqrt(2 * $h * sqrt(($p / 10) * ($gain / 2)));
            return $range / 0.000621371192; // convert to m and return
        }
        return null;
    }

    /**
     * Get PGH description
     *
     * @return String
     */
    public function getPHGDescription()
    {
        if ($this->getPhg() != null) {
            $power = $this->getPhgPower();
            $haat = $this->getPhgHaat();
            $gain = $this->getPhgGain();
            $direction = $this->getPhgDirection();
            $range = $this->getPHGRange();

            $description = '';
            if ($power !== null) {
                $description .= 'Power ' . $power . ' W';
            }

            if ($haat !== null) {

                if (strlen($description) > 0) {
                    $description .= ', ';
                }

                if (isImperialUnitUser()) {
                    $description .= 'Height ' . round(convertMeterToFeet($haat), 0) . ' ft';
                } else {
                    $description .= 'Height ' . $haat . ' m';
                }
            }

            if ($gain !== null && $direction !== null) {
                if (strlen($description) > 0) {
                    $description .= ', ';
                }
                $description .= 'Gain ' . $gain . ' dB ' . $direction;
            }

            return $description;
        }
        return null;
    }

    /**
     * Get PGH string
     *
     * @return string
     */
    public function getPhg()
    {
        if ($this->phg != null) {
            if ($this->phg == 0) {
                return null; // 0000 is considered not to be used (power == 0!)
            } else if ($this->phg < 10) {
                return '000' + strval($this->phg);
            }  else if ($this->phg < 100) {
                return '00' + strval($this->phg);
            } else if ($this->phg < 1000) {
                return '0' + strval($this->phg);
            } else {
                return strval($this->phg);
            }
        }
        return null;
    }

    /**
     *  Get PGH power
     *
     * @return int
     */
    public function getPhgPower()
    {
        if ($this->getPhg() != null) {
            return pow(intval(substr($this->getPhg(), 0, 1)), 2);
        }
        return null;
    }

    /**
     * Get PGH hight (above averange terrain)
     *
     * @param  boolean $inMeters
     * @return int
     */
    public function getPhgHaat($inMeters = true)
    {
        if ($this->getPhg() != null) {
            $value = intval(substr($this->getPhg(), 1, 1));

            $haat = 0;
            if ($value != 0) {
                $haat = 10 * pow(2, $value);
            }

            if ($inMeters) {
                return intval(round($haat * 0.3048));
            } else {
                return $haat;
            }

        }
        return null;
    }

    /**
     *  Get PGH Gain
     *
     * @return int
     */
    public function getPhgGain()
    {
        if ($this->getPhg() != null) {
            return intval(substr($this->getPhg(), 2, 1));
        }
        return null;
    }

    /**
     * Get PGH Direction
     *
     * @return String
     */
    public function getPhgDirection()
    {
        if ($this->getPhg() != null) {

            switch (substr($this->getPhg(), 3, 1)) {
            case 0:
                return 'omni';
                    break;
            case 1:
                return 'North East';
                    break;
            case 2:
                return 'East';
                    break;
            case 3:
                return 'South East';
                    break;
            case 4:
                return 'South';
                    break;
            case 5:
                return 'South West';
                    break;
            case 6:
                return 'West';
                    break;
            case 7:
                return 'North West';
                    break;
            case 8:
                return 'North';
                    break;
            case 9:
                return null;
                    break;
            }
        }
        return null;
    }

    /**
     * Get PGH Direction Degree
     *
     * @return int
     */
    public function getPhgDirectionDegree()
    {
        if ($this->getPhg() != null) {

            switch (substr($this->getPhg(), 3, 1)) {
            case 0:
                return null;
                    break;
            case 1:
                return 45;
                    break;
            case 2:
                return 90;
                    break;
            case 3:
                return 135;
                    break;
            case 4:
                return 180;
                    break;
            case 5:
                return 225;
                    break;
            case 6:
                return 270;
                    break;
            case 7:
                return 315;
                    break;
            case 8:
                return 360;
                    break;
            case 9:
                return null;
                    break;
            }
        }
        return null;
    }

    /**
     * Get RNG
     *
     * @return float
     */
    public function getRng()
    {
        if ($this->rng != null) {
            return $this->rng;
        }
        return null;
    }

    /**
     * Get packet type description
     *
     * @return Station
     */
    public function getPacketTypeName()
    {
        switch ($this->packetTypeId) {
        case 1:
            return 'Position';
                break;
        case 2:
            return 'Direction';
                break;
        case 3:
            return 'Weather';
                break;
        case 4:
            return 'Object';
                break;
        case 5:
            return 'Item';
                break;
        case 6:
            return 'Telemetry';
                break;
        case 7:
            return 'Message';
                break;
        case 8:
            return 'Query';
                break;
        case 9:
            return 'Response';
                break;
        case 10:
            return 'Status';
                break;
        case 11:
            return 'Other';
                break;
        case 12:
            return 'Unknown';
                break;
        case 13:
            return 'Invalid';
                break;
        case 14:
            return 'Capability';
                break;

        default:
            return 'Unknown';
                break;
        }
    }

    /**
     * Get releted packet weather
     * @return PacketWeather
     */
    public function getPacketWeather() {
        return PacketWeatherRepository::getInstance()->getObjectByPacketId($this->id, $this->timestamp);
    }

    /**
     * Get releted packet telemetry
     * @return PacketTelemetry
     */
    public function getPacketTelemetry() {
        return PacketTelemetryRepository::getInstance()->getObjectByPacketId($this->id, $this->timestamp);
    }

    /**
     * Returns OGN part of packet
     *
     * @return PacketOgn
     */
    public function getPacketOgn()
    {
        static $cache = array();
        $key = $this->id;
        if (!isset($cache[$key])) {
            if ($this->sourceId == 5) {
                $cache[$key] = PacketOgnRepository::getInstance()->getObjectByPacketId($this->id, $this->timestamp);
            } else {
                $cache[$key] = new PacketOgn(null);
            }
        }
        return $cache[$key];
    }

    /**
     * Get Station
     * @return Station
     */
    public function getStationObject() {
        return StationRepository::getInstance()->getObjectById($this->stationId);
    }

    /**
     * Get Sender
     * @return Sender
     */
    public function getSenderObject() {
        return SenderRepository::getInstance()->getObjectById($this->senderId);
    }

    /**
     * Get Receiver
     * @return Receiver
     */
    public function getReceiverObject() {
        return ReceiverRepository::getInstance()->getObjectById($this->receiverId);
    }

    /*
    const qAC = 'received from the client directly via a verified connection';
    const qAU = 'received from the client directly via a UDP connection';
    const qAO = 'received via a client-only port and the FROMCALL does not match the login';
    const qAS = 'received from another server or generated by this server';
    const qAR = 'received directly (via a verified connection) from an IGate using the ,I construct';
    */

    /**
     * Get packet path quality based on digipeater hops used
     * @return int $quality (0-4, 0 = unknown, 2 = 3 hops, 3 = 2 hops, 4+ = 1 hop)
     */
    public function getPathInfo()
    {
      if ($this->rawPath == '') return null;
      $path = explode(',', $this->rawPath);

      // A valid path cannot have less than 3 elements
      if (sizeof($path) < 3) return null;

      // To call
      $to_call = array_shift($path);

      // Final destination
      $last_hop = array_pop($path);
      $gateway = array_pop($path);

      // Iterate remaining path elements
      $info = ['quality' => 4, 'hops' => 1, 'type' => 'Unknown', 'igate' => false];
      foreach ($path AS $p)
      {
        // Digi hop?
        if (strpos($p, 'WIDE') === false && strpos($p, 'RELAY') === false && strpos($p, 'TRACE') === false && strpos($p, 'TCPIP') === false && strpos($p, 'TCPXX') === false && strpos($p, 'DMR*') === false && $p != $last_hop)
        {
          if ($this->source_id != SOURCE_CWOP)
          {
            $info['quality']--;
          }
          $info['type'] = "RF via Digi/iGate ($last_hop)";
          $info['hops']++;
        }
        else if (strpos($p, 'TCPIP') !== false)
        {
          $info['igate'] = true;
        }
        else if (strpos($p, 'DMR') !== false)
        {
          $info['type'] = "RF via DMR ($last_hop)";
        }
      }

      if (strpos($gateway, 'qAC') !== false) $info['type'] = 'APRS-IS Direct, Verified Login';
      else if (strpos($gateway, 'qAU') !== false) $info['type'] = 'APRS-IS IS Direct via UDP';
      else if (strpos($gateway, 'qAO') !== false) $info['type'] = 'APRS-IS Direct via Client Port';
      else if (strpos($gateway, 'qAS') !== false) $info['type'] = 'Forwarded via APRS-IS';
      else if (strpos($gateway, 'qAR') !== false && $info['type'] == 'Unknown') $info['type'] = 'RF Direct';
      else if (strpos($gateway, 'qAX') !== false) $info['type'] = 'APRS-IS Direct, Unverified Login';

      return $info;
    }


    /**
     * Get packet equipment type name based on to_call
     *
     * @return String
     */
    public function getEquipmentTypeName()
    {
        $path = explode(',', $this->rawPath ?? '');
        if (sizeof($path) > 1)
        {
          $path = explode('-', $path[0]);
          $pdo = PDOConnection::getInstance();
          $sql =
            'SELECT e.*, c.description, c.display_name
              FROM equipment e
              LEFT JOIN equipment_class c ON (c.id = e.class_id)
              WHERE e.to_call = ? OR e.to_call = ? OR e.to_call = ?
              ORDER BY e.to_call DESC LIMIT 1';
          $stmt = $pdo->prepareAndExec($sql, [$path[0], substr($path[0], 0, 5).'*', substr($path[0], 0, 4) . '**']);
          $record1 = $stmt->fetch(PDO::FETCH_ASSOC);

          // Extended Description
          if (!empty($record1) && is_array($record1) && !is_null($record1['model']))
          {
            $sql =
              'SELECT description AS extended_description
                FROM to_calls
                WHERE callsign = ? OR callsign = ? OR callsign = ?
                ORDER BY callsign DESC LIMIT 1';
            $stmt = $pdo->prepareAndExec($sql, [$path[0], substr($path[0], 0, 5).'*', substr($path[0], 0, 4) . '**']);
            $record2 = $stmt->fetch(PDO::FETCH_ASSOC);

            return is_array($record2) ? array_merge($record1, $record2) : $record1;
          }
          else if (is_array($record1)) return $record1;
        }
        return null;
    }

    public $MicEData = array();

    // Ref:  http://www.aprs.org/aprs12/mic-e-types.txt
    const radioTypes =
    [
        'knownPrefixes' => ['>', ']', '`', '\''],
        'radios' => [
            [
                'prefix' => '>',
                'suffix' => 'v',
                'name' => 'Kenwood TH-D7A Mobile'
            ],
            [
                'prefix' => ']',
                'suffix' => '',
                'name' => 'Kenwood TM-D700 Mobile'
            ],
            [
                'prefix' => ']',
                'suffix' => '=',
                'name' => 'Kenwood TM-D710 Mobile'
            ],
            [
                'prefix' => '>',
                'suffix' => '=',
                'name' => 'Kenwood TH-D72 Handheld'
            ],
            [
                'prefix' => '>',
                'suffix' => '^',
                'name' => 'Kenwood TH-D74 Handheld'
            ],
            [
                'prefix' => '>',
                'suffix' => '&',
                'name' => 'Kenwood TH-D75 Handheld'
            ],
            [
                'prefix' => '`',
                'suffix' => '_ ',
                'name' => 'SQ8L VP-Tracker'
            ],
            [
                'prefix' => '`',
                'suffix' => '_ ',
                'name' => 'Yaesu VX-8 Handheld'
            ],
            [
                'prefix' => '`',
                'suffix' => '_#',
                'name' => 'Yaesu VX-8G Handheld'
            ],
            [
                'prefix' => '`',
                'suffix' => '_$',
                'name' => 'Yaesu FT1D Handheld'
            ],
            [
                'prefix' => '`',
                'suffix' => '_(',
                'name' => 'Yaesu FT2D Handheld'
            ],
            [
                'prefix' => '`',
                'suffix' => '_0',
                'name' => 'Yaesu FT3D Handheld'
            ],
            [
                'prefix' => '`',
                'suffix' => '_3',
                'name' => 'Yaesu FT5D Handheld'
            ],
            [
                'prefix' => '`',
                'suffix' => '_)',
                'name' => 'Yaesu FTM-100D Mobile'
            ],
            [
                'prefix' => '`',
                'suffix' => '_"',
                'name' => 'Yaesu FTM-350 Mobile'
            ],
            [
                'prefix' => '`',
                'suffix' => '_2',
                'name' => 'Yaesu FTM-200DR Mobile'
            ],
            [
                'prefix' => '`',
                'suffix' => '_1',
                'name' => 'Yaesu FTM-300DR Mobile'
            ],
            [
                'prefix' => '`',
                'suffix' => '_%',
                'name' => 'Yaesu FTM-400DR Mobile'
            ],
            [
                'prefix' => '`',
                'suffix' => '_4',
                'name' => 'Yaesu FTM-500DR Mobile'
            ],
            [
                'prefix' => '\'',
                'suffix' => '|3',
                'name' => 'Byonics TinyTrack3'
            ],
            [
                'prefix' => '\'',
                'suffix' => '|4',
                'name' => 'Byonics TinyTrack4'
            ],
            [
                'prefix' => '`',
                'suffix' => '(5',
                'name' => 'Anytone D578UV'
            ],
            [
                'prefix' => '`',
                'suffix' => '(8',
                'name' => 'Anytone D878UV'
            ]
        ]
    ];


    /**
     * Parse MIC-E Packet Data from Raw
     *
     * @return boolean (true if successful, false if no MIC-E data present MICE)
     */
    public function getMicEData()
    {
      $this->MicEData = [];

      // Break the path from the body
      $parts = explode(':', $this->raw ?? '', 2);
      if (sizeof($parts) == 2)
      {
        $mice_raw_path = $parts[0];
        $body = $parts[1];

        // Mic-E?
        $micEPrefix = $body[0];
        if (($micEPrefix == '`' || $micEPrefix == '\'') && strlen($body) >= 9)
        {
          $symbol = $body[8] . $body[7];

          // Parse the MIC-E data...  This is very order specific as the comment is updated with each pass
          $pathParts = explode(',', $this->rawPath, 2);
          if (sizeof($pathParts) > 1)
          {
            // Parse MicE Desitnation Address
            if ($this->_getMicEDataFromToCall())
            {
              $this->_getLongitudeFromMicE($body);
              $this->_getCommentFromMicE($body);
              $this->_getRadioTypeFromMicE();
              $this->_getAltitudeFromMicE();
              return $this->MicEData;
            }
          }
        }
      }

      return false;
    }


    /**
    * Parse comment from MIC-E message body
    *
    * @param {string} $body (Message Body)
    * @return boolean
    * */
    private function _getCommentFromMicE($body)
    {
      $this->MicEData['comment'] = substr($body, 9);
    }


    /**
     * Parse longitude from Mic-E
     * @param {string} $body (Message Body)
     * @return {float}
     */
    private function _getLongitudeFromMicE($body)
    {
      $longitudeString = substr($body, 1, 3);

      //degrees
      $d = $this->MicEData['longitudeOffset'] + (ord($longitudeString[0]) - 28);
      if ($d >= 180 && $d <= 189)
        $d = $d - 80;
      else if ($d >= 190 && $d <= 199)
        $d = $d - 190;

      //minutes
      $m = ord($longitudeString[1]) - 28;
      if ($m >= 60)
        $m = $m - 60;

      //hundredths of minutes
      $h = ord($longitudeString[2]) - 28;

      $minutes = $m + ($h / 100);

      $this->MicEData['longitude'] = ($this->MicEData['isWest'] ? -1 : 1) * ($d + ($minutes / 60));
      return $this->MicEData['comment'] = substr($body, 9);
    }


    /**
    * Reads Data from a Mic-E Destination Address
    *
    * @return boolean
    * */
    private function _getMicEDataFromToCall()
    {
     $pathParts = explode(',', $this->rawPath, 2);
     if (sizeof($pathParts) <= 1 || (sizeof($pathParts) > 1 && strlen($pathParts[0]) != 6))
      return false;

     $this->toCall = $pathParts[0];

     $latitudeDigits = [];
     $message = [];
     $isNorth = false;
     $longOffset = 0;
     $isWest = false;

     for ($i = 1; $i <= 6; $i++)
     {
       $values = getInfoForDestinationAddressChar($this->toCall[$i - 1]);

       array_push($latitudeDigits, $values['latDigit']);

       if ($i >= 1 && $i <= 3)
       {
         array_push($message, $values['message']);
       }

       if ($i == 4)
         $isNorth = $values['isNorth'];

       if ($i == 5)
         $longOffset = $values['longOffset'];

       if ($i == 6)
         $isWest = $values['isWest'];
     }

     $degrees = 10 * $latitudeDigits[0] + $latitudeDigits[1];
     $minutes = 10 * $latitudeDigits[2] + $latitudeDigits[3] + ((10 * $latitudeDigits[4] + $latitudeDigits[5]) / 100);
     $latitude = ($isNorth ? 1 : -1) * $degrees + ($minutes / 60);

     $this->MicEData =
     [
       'latitude' => $latitude,
       'status' => getStateFromMicE($message),
       'longitudeOffset' => $longOffset,
       'isWest' => $isWest
     ];

     return true;
    }


    /**
     * Parse altitude from Mic-E
     * @return {string}
     */
    private function _getAltitudeFromMicE()
    {
      if (empty($this->MicEData['comment'])) return;

      $altitude = null;

      // Alt is in the format XXX}  where XXX = base91 encoded altitude in meters
      if (preg_match("/(...)}/", substr($this->MicEData['comment'], 0, 5), $msgString) && sizeof($msgString) && $msgString[1])
      {
          $value = 0;
          for ($i = 0; $i < strlen($msgString[1]); $i++) {
              $value = $value * 91 + ord($msgString[1][$i]) - 33;
          }
          $this->MicEData['altitude'] = $value - 10000;
      }
      if (isset($this->MicEData['altitude']) && $this->MicEData['altitude'] != null)
      {
          if ($this->altitude == null) $this->altitude = $this->MicEData['altitude'];  // Use Mic-E if no other altitude info is available
          $this->MicEData['comment'] = substr($this->MicEData['comment'], strpos($this->MicEData['comment'], '}') + 1);  //Remove altitude info from comment
      }
      return $this->MicEData['comment'];
    }


    /**
     * Parse raw comment and set radio type and return packet message as a string
     * @return {string}
     */
    private function _getRadioTypeFromMicE()
    {
      $this->MicEData['equipment'] = null;

      if (array_search(substr($this->MicEData['comment'], 0, 1), self::radioTypes['knownPrefixes']) !== false)
      {
        foreach(self::radioTypes['radios'] as $radio)
        {
          if (substr($this->MicEData['comment'], 0, 1) == $radio['prefix'] && substr($this->MicEData['comment'], 0 - strlen($radio['suffix'])) == $radio['suffix'])
          {
            $this->MicEData['equipment'] = $radio['name']; //Set radio name
            $this->MicEData['comment'] = substr($this->MicEData['comment'], strlen($radio['prefix']), strlen($this->MicEData['comment']) - strlen($radio['suffix']) - 1);  //Remove radio designators from comment
          }
        }
      }

      return $this->MicEData['equipment'];
    }


    /**
     * Returns wind direction in cardinal format (i.e. N, S, E, W)
     *
     * @return string
     */
    public function getCardinalCourse()
    {
      return $this->course != '' ? convertDegreesToCardinalDirection($this->course) : '';
    }


    /**
     * Returns the capabilities data for a station capability packet
     * i.e. VK4ZZ-4>APU25N,TCPIP*,qAC,T2PERTH:<IGATE,MSG_CNT=0,LOC_CNT=0
     *
     * @return array
     */
    public function getCapabilites()
    {
      if ($this->packetTypeId != 14 || is_null($this->raw) || empty($this->raw)) return null;

      // Parse the capability data
      $parts = explode('<', $this->raw, 2);
      $capabilities = explode(',', $parts[1]);
      $type = array_shift($capabilities);

      $capabilityData = [];
      foreach($capabilities AS $entry)
      {
        $data = explode('=', $entry);
        $capabilityData[$data[0]] = $data[1];
      }

      return
      [
          'type'  => $type,
          'data'  => $capabilityData
      ];
    }

}
