<?php

/**
 *
 * Core Comsnd Interface
 *
 *  https://www.voip-info.org/asterisk-manager-example-php/
 */

namespace FreePBX\modules\Sccp_manager;

class aminterface
{

    var $_socket;
    var $_error;
    var $_config;
    var $_test;
    private $_connect_state;
    private $_lastActionClass;
    private $_lastActionId;
    private $_lastRequestedResponseHandler;
    private $_ProcessingMessage;
    private $_DumpMessage;
    private $debug_level = 1;
    private $_incomingRawMessage;
    private $eventListEndEvent;

    public function load_subspace($parent_class = null)
    {
        $driverNamespace = "\\FreePBX\\Modules\\Sccp_manager\\aminterface";

        $drivers = array('Message' => 'Message.class.php',
                  'Response' => 'Response.class.php',
                  'Event' => 'Event.class.php'
                );
        foreach ($drivers as $key => $value) {
            $class = $driverNamespace . "\\" . $key;
            $driver = __DIR__ . "/amInterfaceClasses/" . $value;
            if (!class_exists($class, false)) {
                if (file_exists($driver)) {
                    include(__DIR__ . "/amInterfaceClasses/" . $value);
                } else {
                    throw new \Exception("Class required but file not found " . $driver);
                }
            }
        }
    }

    public function __construct($parent_class = null)
    {
        global $amp_conf;
        $this->paren_class = $parent_class;
        $this->_socket = false;
        $this->_connect_state = false;
        $this->_error = array();
        $this->_config = array('host' => 'localhost',
                        'user' => '',
                        'pass' => '',
                        'port' => '5038',
                        'tsoket' => 'tcp://',
                        'timeout' => 30,
                        'enabled' => true
                      );
        $this->_eventListeners = array();
        $this->_incomingMsgObjectList = array();
        $this->_lastActionId = false;
        $this->_incomingRawMessage = array();
        $this->eventListEndEvent = '';

        $fld_conf = array('user' => 'AMPMGRUSER', 'pass' => 'AMPMGRPASS');
        if (isset($amp_conf['AMPMGRUSER'])) {
            foreach ($fld_conf as $key => $value) {
                if (isset($amp_conf[$value])) {
                    $this->_config[$key] = $amp_conf[$value];
                }
            }
        }
        if ($this->_config['enabled']) {
            $this->load_subspace();
        }

        if ($this->_config['enabled']) {
            // Ami is not hard disabled in __construct line 63.
            if ($this->open()) {
                // Can open a connection. Now check compatibility with chan-sccp.
                // will return true if compatible.
                if (!$this->get_compatible_sccp(true)[1]) {
                    // Close the open socket as will not use
                    $this->close();
                }
            }
        }
    }

    public function info()
    {
        $Ver = '13.0.4';
        if ($this->_config['enabled']){
            return array('Version' => $Ver,
                'about' => 'AMI data ver: ' . $Ver, 'test' => get_declared_classes());
        } else {
            return array('Version' => $Ver,
                'about' => 'Disabled AMI  ver: ' . $Ver);
        }
    }

    /*
     * Opens a socket connection to ami.
     */
    public function open()
    {
        $cString = $this->_config['tsoket'] . $this->_config['host'] . ':' . $this->_config['port'];
        $this->_context = stream_context_create();
        $errno = 0;
        $errstr = '';
        $this->_ProcessingMessage = '';
        $this->_socket = @stream_socket_client(
            $cString,
            $errno,
            $errstr,
            $this->_config['timeout'],
            STREAM_CLIENT_CONNECT,
            $this->_context
        );
        if ($this->_socket === false) {
            $this->_errorException('Error connecting to ami: ' . $errstr . $cString);
            return false;
        }
        $msg = new aminterface\LoginAction($this->_config['user'], $this->_config['pass']);
        $response = $this->send($msg);

        if ($response != false) {
            if (!$response->isSuccess()) {
                $this->_errorException('Could not connect: ' . $response->getMessage());
                return false;
            } else {
                @stream_set_blocking($this->_socket, 0);
                $this->_connect_state = true;
                $this->_ProcessingMessage = '';
            }
        }
        return true;
    }

    /*
     * Closes the connection to ami.
     */
    public function close()
    {
        $this->_connect_state = false;
        $this->_ProcessingMessage = '';
        @stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR);
    }
    /*
    * Send action message to ami, and wait for Response
    */
    public function send($message)
    {
        $_incomingRawMessage = array();
        $messageToSend = $message->serialize();
        $length = strlen($messageToSend);
        $this->_DumpMessage = '';
        $this->_lastActionId = $message->getActionID();
        $this->_lastRequestedResponseHandler = $message->getResponseHandler();
        $this->_lastActionClass = $message;
        $this->_incomingRawMessage[$this->_lastActionId] = '';
        $this->eventListIsCompleted = array();
        if (@fwrite($this->_socket, $messageToSend) < $length) {
            $this->_errorException('Could not send message');
            return false;
        }
        // Have sent a message and now have to wait for and read the reply
        // The below infinite loop waits for $this->completed to be true.
        // The loop calls readBuffer, which calls GetMessages, which calls Process
        // This loop then continues until we have _thisComplete as an object variable
        $this->eventListIsCompleted[$this->_lastActionId] = false;
        while (true) {
            stream_set_timeout($this->_socket, 1);
            $this->readBuffer();
            $info = stream_get_meta_data($this->_socket);
            if ($info['timed_out'] == true) {
                $this->_errorException("Read waittime: " . ($this->socket_param['timeout']) . " exceeded (timeout).\n");
                return false;
            }
            if ($this->eventListIsCompleted[$this->_lastActionId]) {
                $response = $this->_incomingMsgObjectList[$this->_lastActionId];
                // need to test that the list was successfully completed here
                $allReceived = $response->getClosingEvent()
                                ->listCorrectlyReceived($this->_incomingRawMessage[$this->_lastActionId],
                                $response->getCountOfEvents());
                // now tidy up removing any temp variables or objects
                $response->removeClosingEvent();
                unset($_incomingRawMessage[$this->_lastActionId]);
                unset($this->_incomingMsgObjectList[$this->_lastActionId]);
                unset($this->_lastActionId);
                if ($allReceived) {
                    return $response;
                }
                // Something is missing from the events list received via AMI, or
                // the control parameter at the end of the list has changed.
                // This will cause an exception as returning a boolean instead of a Response
                // Maybe should handle better, but
                // need to break out of the loop as nothing more coming.
                try {
                    throw new \invalidArgumentException("Counts do not match on returned AMI Result");
                } catch ( \invalidArgumentException $e) {
                    echo substr(strrchr(get_class($response), '\\'), 1), " ", $e->getMessage(), "\n";
                }
                return $response;
            }
        }
    }

    protected function readBuffer ()
    {
        $read = @fread($this->_socket, 65535);
        // AMI never returns EOF
        if ($read === false ) {
            $this->_errorException('Error reading');
        }
        // Do not return empty Messages
        while ($read == "" ) {
            $read = @fread($this->_socket, 65535);
        }
        // Add read to the rest of buffer from previous read
        $this->_ProcessingMessage .= $read;
        $this->getMessages();
    }

    protected function getMessages()
    {
        $msgs = array();
        // Extract any complete messages and leave remainder for next read
        while (($marker = strpos($this->_ProcessingMessage, aminterface\Message::EOM))) {
            $msg = substr($this->_ProcessingMessage, 0, $marker);
            $this->_ProcessingMessage = substr(
                $this->_ProcessingMessage,
                $marker + strlen(aminterface\Message::EOM)
            );
            $msgs[] = $msg;
        }
        $this->process($msgs);
    }

    public function process(array $msgs)
    {
        foreach ($msgs as $aMsg) {
            // 2 types of message; Response or Event. Response only incudes data
            // for JSON response and Command response. All other responses expect
            // data in an event list - these events need to be attached to the response.
            $resPos = strpos($aMsg, 'Response: ');   // Have a Response message. This may not be 0.
            $evePos = strpos($aMsg, 'Event: ');   // Have an Event Message. This should always be 0.
            // Add the incoming message to a string that can be checked
            // against the completed message event when it is received.
            $this->_incomingRawMessage[$this->_lastActionId] .= "\r\n\r\n" . $aMsg;
            if (($resPos !== false) && (($resPos < $evePos) || $evePos === false)) {
                $response = $this->_responseObjFromMsg($aMsg); // resp Ok
                $this->eventListEndEvent = $response->getKey('eventlistendevent');
                $this->_incomingMsgObjectList[$this->_lastActionId] = $response;
                $this->eventListIsCompleted[$this->_lastActionId] = $response->isComplete();
            } elseif ($evePos === 0) {      // Event must be at the start of the msg.
                $event = $this->_eventObjFromMsg($aMsg); // Event  Ok
                $this->eventListIsCompleted[$this->_lastActionId] = $event->isComplete();
                $this->_incomingMsgObjectList[$this->_lastActionId]->addEvent($event);
            } else {
                // broken ami most probably through changes in chan_sccp_b.
                // AMI is sending a message which is neither a response nor an event.
                $this->_msgToDebug(1, 'resp broken ami');
                $bMsg = 'Event: ResponseEvent' . "\r\n";
                $bMsg .= 'ActionId: ' . $this->_lastActionId . "\r\n" . $aMsg;
                $event = $this->_eventObjFromMsg($bMsg);
                $this->_incomingMsgObjectList[$this->_lastActionId]->addEvent($event);
            }
        }
    }

    private function _msgToDebug($level, $msg)
    {
        if ($level > $this->debug_level) {
            return;
        }
        print_r('<br> level: '.$level.' ');
        print_r($msg);
        print_r('<br>');
    }

    private function _responseObjFromMsg($message)
    {
        $_className = false;

        $responseClass = '\\FreePBX\\modules\\Sccp_manager\\aminterface\\Generic_Response';
        if ($this->_lastRequestedResponseHandler != false) {
            $_className = '\\FreePBX\\modules\\Sccp_manager\\aminterface\\' . $this->_lastRequestedResponseHandler . '_Response';
        }
        if ($_className) {
            if (class_exists($_className, true)) {
                $responseClass = $_className;
            } elseif ($responseHandler != false) {
                $this->_errorException('Response Class ' . $_className . '  requested via responseHandler, could not be found');
            }
        }
        $response = new $responseClass($message);
        $actionId = $response->getActionID();
        if ($actionId === null) {
            $response->setActionId($this->_lastActionId);
        }
        return $response;
    }
    public function _eventObjFromMsg($message)
    {
        $eventType = explode(aminterface\Message::EOL,$message,2);
        $name = trim(explode(':',$eventType[0],2)[1]);
        $className = '\\FreePBX\\modules\\Sccp_manager\\aminterface\\' . $name . '_Event';
        if (class_exists($className, true) === false) {
            $className = '\\FreePBX\\modules\\Sccp_manager\\aminterface\\UnknownEvent';
        }
        return new $className($message);
    }

    protected function dispatch($message)
    {
        print_r("<br>------------dispatch----------<br>");
        print_r($message);
        return false;
        die();
        foreach ($this->_eventListeners as $data) {
            $listener = $data[0];
            $predicate = $data[1];
            print_r($data);

            if (is_callable($predicate) && !call_user_func($predicate, $message)) {
                continue;
            }
            if ($listener instanceof \Closure) {
                $listener($message);
            } elseif (is_array($listener)) {
                $listener[0]->$listener[1]($message);
            } else {
                $listener->handle($message);
            }
        }
        print_r("<br>------------E dispatch----------<br>");
    }
//-------------------Adaptive Function ------------------------------------------------------------

    function core_list_hints()
    {
        $result = array();
        if ($this->_connect_state) {
            $_action = new \FreePBX\modules\Sccp_manager\aminterface\ExtensionStateListAction();
            $_response = $this->send($_action);
            $_res = $_response->getResult();
            foreach ($_res as $key => $value) {
                foreach ($value as $key2 => $value2) {
                    $result[$key2] = '@' . $key2;
                }
            }
        }
        return $result;
    }

    function core_list_all_hints()
    {
        $result = array();
        if ($this->_connect_state) {
            $_action = new \FreePBX\modules\Sccp_manager\aminterface\ExtensionStateListAction();
            $_res = $this->send($_action)->getResult();
            foreach ($_res as $key => $value) {
                foreach ($value as $key2 => $value2) {
                    $result[$key.'@'.$key2] = $key.'@'.$key2;
                }
            }
        }
        return $result;
    }
// --------------------- SCCP Comands
    function sccp_list_keysets()
    {
        $result = array();
        if ($this->_connect_state) {
            $_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPShowSoftkeySetsAction();
            $_res = $this->send($_action)->getResult();
            foreach ($_res as $key => $value) {
                $result[$key] = $key;
            }
        }
        return $result;
    }
    function sccp_get_active_device()
    {
        $result = array();
        if ($this->_connect_state) {
            $_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPShowDevicesAction();
            $result = (array)$this->send($_action)->getResult();
        }
        return $result;
    }
    function sccp_getdevice_info($devicename)
    {
        $result = array();
        if ($this->_connect_state) {
            $_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPShowDeviceAction($devicename);
            $result = $this->send($_action)->getResult();
            $result['MAC_Address'] = $result['macaddress'];
        }
        return $result;
    }
    function sccpDeviceReset($devicename, $action = '')
    {
        if ($this->_connect_state) {
            if ($action == 'tokenack') {
                $_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPTokenAckAction($devicename);
            } else {
                $_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPDeviceRestartAction($devicename, $action);
            }
            $_response = $this->send($_action);
            $result['data'] = 'Device: '.$devicename.' Result: '.$_response->getMessage();
            $result['Response']=$_response->getKey('Response');
        }
        return $result;
    }

//------------------- Core Comands ----
    function core_sccp_reload()
    {
        $result = array();
        if ($this->_connect_state) {
            $_action = new \FreePBX\modules\Sccp_manager\aminterface\ReloadAction('chan_sccp');
            $result = ['Response' => $this->send($_action)->getMessage(), 'data' => ''];
        }
        return $result;
    }
    function getSCCPConfigMetaData($segment = '') {
        if ($this->_connect_state) {
            $_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPConfigMetaDataAction($segment);
            $metadata = $this->send($_action)->getResult();
        }
        return $metadata;
    }

    function getSCCPVersion()
    {
        //Initialise result array
        $result = array( 'RevisionHash' => '', 'vCode' => 0, 'RevisionNum' => 0, 'buildInfo' => '', 'Version' => 0);
        $metadata = $this->getSCCPConfigMetaData();

        if (isset($metadata['Version'])) {
            $result['Version'] = $metadata['Version'];
            $version_parts = explode('.', $metadata['Version']);
            if ($version_parts[0] === 4) {
                switch ($version_parts[1]) {
                    case 1:
                        $result['vCode'] = 410;
                        break;
                    case 2:
                        $result['vCode'] = 420;
                        break;
                    case 3. . .5:
                        $result['vCode'] = 430;
                        if($version_parts[2] == 3){
                            $result['vCode'] = 433;
                        }
                        break;
                    default:
                        $result['vCode'] = 400;
                        break;
                }
            }
            if (array_key_exists("RevisionHash", $metadata)) {
                $result['RevisionHash'] = $metadata["RevisionHash"];
            }
            if (isset($metadata['RevisionNum'])) {
                if ($metadata['RevisionNum'] >= 11063) { // new method, RevisionNum is incremental
                    $result['vCode'] = 433;
                }
                $result['RevisionNum'] = $metadata["RevisionNum"];
            }
            if (isset($metadata['ConfigureEnabled'])) {
                $result['buildInfo'] = $metadata['ConfigureEnabled'];
            }
        }
        return $result;
    }

    function getRealTimeStatus()
    {
        // Initialise array with default values to eliminate testing later
        $result = array();
        $cmd_res = array();
        $cmd_res = ['sccp' => ['message' => 'default value', 'realm' => '', 'status' => 'ERROR']];
        if ($this->_connect_state) {
            $_action = new \FreePBX\modules\Sccp_manager\aminterface\CommandAction('realtime mysql status');
            $result = $this->send($_action)->getResult();
         }
         if (is_array($result['Output'])) {
             foreach ($result['Output'] as $aline) {
                 if (strlen($aline) > 3) {
                     $temp_strings = explode(' ', $aline);
                     $cmd_res_key = $temp_strings[0];
                     foreach ($temp_strings as $test_string) {
                          if (strpos($test_string, '@')) {
                            $this_realm = $test_string;
                            break;
                          }
                     }
                     $cmd_res[$cmd_res_key] = array('message' => $aline, 'realm' => $this_realm, 'status' => strpos($aline, 'connected') ? 'OK' : 'ERROR');
                 }
            }
        }
        return $cmd_res;
    }

    public function get_compatible_sccp($revNumComp=false) {
        // only called with args from installer to get revision and compatibility
        $res = $this->getSCCPVersion();
        if ($res['RevisionNum'] < 11063) {
            $this->useAmiInterface = false;
        }
        if ($revNumComp) {
            return array($res['vCode'], true);
        }
        return $res['vCode'];
    }
}
