<?php

class RouterosAPI
{
  public int $ErrorNo;           // Variable for storing connection error number, if any
  public string $ErrorStr;          // Variable for storing connection error text, if any
  public int $Attempts;       // Connection attempt count
  public bool $Connected;  // Connection state
  public int $Delay;          // Delay between connection attempts in seconds
  public int $Port;        // Port to connect to
  public int $Timeout;        // Connection attempt timeout and data read timeout
  public $Socket;             // Variable for storing socket resource
  public bool $Debug;
  public bool $SSL; // If SSL API connection is used. You need also change port to 8729

  function __construct()
  {
    $this->Attempts = 5;
    $this->Connected = false;
    $this->Delay = 3;
    $this->Port = 8728;
    $this->Timeout = 3;
    $this->Debug = false;
    $this->SSL = false;
    $this->ErrorNo = 0;
    $this->ErrorStr = "";
  }

  function EncodeLength(int $Length): string
  {
    if ($Length < 0x80)
    {
      $Length = chr($Length);
    } else if ($Length < 0x4000)
    {
      $Length |= 0x8000;
      $Length = chr(($Length >> 8) & 0xFF).chr($Length & 0xFF);
    } else if ($Length < 0x200000)
    {
      $Length |= 0xC00000;
      $Length = chr(($Length >> 16) & 0xFF).chr(($Length >> 8) & 0xFF).chr($Length & 0xFF);
    } else if ($Length < 0x10000000)
    {
      $Length |= 0xE0000000;
      $Length = chr(($Length >> 24) & 0xFF).chr(($Length >> 16) & 0xFF).chr(($Length >> 8) & 0xFF).chr($Length & 0xFF);
    } else if ($Length >= 0x10000000)
      $Length = chr(0xF0).chr(($Length >> 24) & 0xFF).chr(($Length >> 16) & 0xFF).chr(($Length >> 8) & 0xFF).chr($Length & 0xFF);
    return $Length;
  }

  function ConnectOnce(string $IP, string $Login, string $Password): void
  {
    if ($this->Connected) $this->Disconnect();
    if ($this->SSL)
    {
      $IP = 'ssl://'.$IP;
    }
    try
    {
      $LastErrorReporting = error_reporting();
      error_reporting(0);

      $this->Socket = @fsockopen($IP, $this->Port, $this->ErrorNo, $this->ErrorStr, $this->Timeout);
      if ($this->Socket)
      {
        socket_set_timeout($this->Socket, $this->Timeout);
        $this->Write('/login', false);
        $this->Write('=name=' . $Login, false);
        $this->Write('=password='.$Password);
        $Response = $this->Read(false);
        if ((count($Response) > 0) and ($Response[0] == '!done')) $this->Connected = true;
        if (!$this->Connected) fclose($this->Socket);
      }
    } finally
    {
      error_reporting($LastErrorReporting);
    }
  }

  function Connect(string $IP, string $Login, string $Password): bool
  {
    for ($Attempt = 1; $Attempt <= $this->Attempts; $Attempt++)
    {
      $this->ConnectOnce($IP, $Login, $Password);
      if ($this->Connected) break;
      sleep($this->Delay);
    }
    return $this->Connected;
  }

  function Disconnect(): void
  {
    if ($this->Connected)
    {
      fclose($this->Socket);
      $this->Connected = false;
    }
  }

  function ParseResponse(array $Response): array
  {
    if (is_array($Response))
    {
      $Parsed      = array();
      $Current     = null;
      $SingleValue = null;
      $count       = 0;
      foreach ($Response as $x)
      {
        if (in_array($x, array(
            '!fatal',
            '!re',
            '!trap'
        )))
        {
          if ($x == '!re')
          {
            $Current =& $Parsed[];
          } else
            $Current =& $Parsed[$x][];
        } else if ($x != '!done')
        {
          if (preg_match_all('/[^=]+/i', $x, $Matches))
          {
            if ($Matches[0][0] == 'ret') {
              $SingleValue = $Matches[0][1];
            }
            $Current[$Matches[0][0]] = (isset($Matches[0][1]) ? $Matches[0][1] : '');
          }
        }
      }
      if (empty($Parsed) && !is_null($SingleValue))
      {
        $Parsed = $SingleValue;
      }
      return $Parsed;
    } else
      return array();
  }

  function ArrayChangeKeyName(array &$array): array
  {
    if (is_array($array))
    {
      foreach ($array as $k => $v)
      {
        $tmp = str_replace("-", "_", $k);
        $tmp = str_replace("/", "_", $tmp);
        if ($tmp)
        {
          $array_new[$tmp] = $v;
        } else
        {
          $array_new[$k] = $v;
        }
      }
      return $array_new;
    } else
    {
      return $array;
    }
  }

  function Read(bool $Parse = true): array
  {
    $Line = '';
    $Response = array();
    while (true)
    {
      // Read the first byte of input which gives us some or all of the length
      // of the remaining reply.
      $Byte = ord(fread($this->Socket, 1));
      $Length = 0;
      // If the first bit is set then we need to remove the first four bits, shift left 8
      // and then read another byte in.
      // We repeat this for the second and third bits.
      // If the fourth bit is set, we need to remove anything left in the first byte
      // and then read in yet another byte.
      if ($Byte & 0x80)
      {
        if (($Byte & 0xc0) == 0x80)
        {
          $Length = (($Byte & 63) << 8) + ord(fread($this->Socket, 1));
        } else
        {
          if (($Byte & 0xe0) == 0xc0)
          {
            $Length = (($Byte & 31) << 8) + ord(fread($this->Socket, 1));
            $Length = ($Length << 8) + ord(fread($this->Socket, 1));
          } else
          {
            if (($Byte & 0xf0) == 0xe0)
            {
              $Length = (($Byte & 15) << 8) + ord(fread($this->Socket, 1));
              $Length = ($Length << 8) + ord(fread($this->Socket, 1));
              $Length = ($Length << 8) + ord(fread($this->Socket, 1));
            } else
            {
              $Length = ord(fread($this->Socket, 1));
              $Length = ($Length << 8) + ord(fread($this->Socket, 1));
              $Length = ($Length << 8) + ord(fread($this->Socket, 1));
              $Length = ($Length << 8) + ord(fread($this->Socket, 1));
            }
          }
        }
      } else
      {
        $Length = $Byte;
      }
      // If we have got more characters to read, read them in.
      if ($Length > 0)
      {
        $Line = '';
        $RetLen = 0;
        while ($RetLen < $Length)
        {
          $ToRead = $Length - $RetLen;
          $Line .= fread($this->Socket, $ToRead);
          $RetLen = strlen($Line);
        }
        $Response[] = $Line;
      }
      if ($this->Debug) echo($Line);
      // If we get a !done, make a note of it.
      if ($Line == "!done") $ReceivedDone = true;
        else $ReceivedDone = false;
      $Status = socket_get_status($this->Socket);
      if ((!$this->Connected && !$Status['unread_bytes']) ||
          ($this->Connected && !$Status['unread_bytes'] && $ReceivedDone))
        break;
    }
    if ($Parse) $Response = $this->ParseResponse($Response);
    return $Response;
  }

  function Write(string $Command, bool $Param2 = true): bool
  {
    if ($Command)
    {
      $Data = explode("\n", $Command);
      foreach ($Data as $Com)
      {
        $Com = trim($Com);
        fwrite($this->Socket, $this->EncodeLength(strlen($Com)).$Com);
      }
      if (gettype($Param2) == 'integer')
      {
        fwrite($this->Socket, $this->EncodeLength(strlen('.tag='.$Param2)).'.tag='.$Param2.chr(0));
      } else if (gettype($Param2) == 'boolean')
        fwrite($this->Socket, ($Param2 ? chr(0) : ''));
      return true;
    } else
      return false;
  }

  function Comm(string $Com, array $Arr = array()): array
  {
    $Count = count($Arr);
    $this->write($Com, !$Arr);
    $i = 0;
    foreach ($Arr as $k => $v)
    {
      switch ($k[0])
      {
        case "?":
          $el = "$k=$v";
          break;
        case "~":
          $el = "$k~$v";
          break;
        default:
          $el = "=$k=$v";
          break;
      }
      $Last = ($i++ == $Count - 1);
      $this->Write($el, $Last);
    }
    return $this->Read();
  }
}
