<?php

namespace Axioma\Server;

use Axioma\Utils as utils;

trait Protocol
{
    private function runApp(&$error = null)
    {
        if (!$this->checkExe($error)) {
            return false;
        }

        $lang = 'LANG=' . $this->appLocale() . ' ';
        $command = escapeshellarg($this->exePath()) . ' ' . escapeshellarg($this->iniPath());
        if (utils::isWindows()) {
            pclose(popen('start /B "" ' . $command, 'r'));
        } else {
            exec($lang . $command . ' > /dev/null 2>&1 &');
        }
        return true;
    }

    private function tryConnect(&$error = null)
    {
        $errno = 0;
        $errstr = '';
        $fp = @fsockopen("localhost", $this->appPort(), $errno, $errstr);
        if (!$fp) {
            if (!$this->runApp($error)) {
                $error = _('Невозможно запустить приложение сервера');
                return false;
            }
            $attemps = 3;
            while (!$fp && $attemps > 0) {
                sleep(1);
                $fp = @fsockopen("localhost", $this->appPort(), $errno, $errstr);
                $attemps--;
            }
        }
        if (!$fp) {
            $error = _('Ошибка соединения с приложением') . ': ' . $errstr . ' (' . $errno . ')';
        }
        return $fp;
    }

    private static function actionCode($action)
    {
        switch ($action) {
            case 'ping':
                return 0;
            case 'server_info':
                return 1;
            case 'borrow':
                return 2;
            case 'release':
                return 3;
            case 'available_licenses':
                return 4;
        }
        return -1;
    }

    private function apiRequestByCode($code, $request_data = null)
    {
        if (is_null($code)) {
            return array('status' => 'FAIL', 'reason' => _('Не задан обязательный параметр запроса ') . 'code');
        }
        if ($code < 0) {
            return array('status' => 'FAIL', 'reason' => _('Неизвестное значение параметра ') . "code: '$code'");
        }

        $error = '';
        $fp = $this->tryConnect($error);
        if (!$fp) {
            return array('status' => 'FAIL', 'reason' => $error);
        }

        $request[] = chr($code);
        $datasize = strlen($request_data);
        $request[] = pack('N', $datasize);            // unsigned long, 32 bit, big endian 
        if ($datasize > 0) {
            $request[] = $request_data;
        }
        fwrite($fp, implode($request), $datasize + 5);
        sleep(1);

        $response_code = ord(fread($fp, 1));
        $response_size = unpack('N', fread($fp, 4))[1];
        $response_data = $response_size > 0 ? fread($fp, $response_size) : null;
        fclose($fp);

        if ($response_code === 0) {
            return array('status' => 'FAIL',
                         'reason' => empty($response_data) ? _('Ошибка при выполнении запроса') : $response_data);
        } elseif ($response_code !== 1) {
            return array('status' => 'FAIL', 'reason' => _('Неизвестный ответ от сервера'));
        }

        $result = array('status' => 'OK');
        if (!empty($response_data)) {
            $result['data'] = $response_data;
        }
        return $result;
    }

    public function apiRequest($action, $request_data = null)
    {
        if (empty($action)) {
            return array('status' => 'FAIL', 'reason' => _('Не задан обязательный параметр запроса ') . 'action');
        }
        $request_code = $this->actionCode($action);
        if ($request_code < 0) {
            return array('status' => 'FAIL', 'reason' => _('Неизвестное значение параметра ') . "action: '$action'");
        }

        return $this->apiRequestByCode($request_code, $request_data);
    }

    public function pingApp(&$error = null)
    {
        $result = $this->apiRequest('ping');
        if ($result['status'] == 'OK') {
            return true;
        }

        $error = isset($result['reason']) ? $result['reason'] : _('Неизвестная ошибка сервера');
        return false;
    }

    public function terminateApp(&$error = null)
    {
        if (!$this->checkExe($error)) {
            return false;
        }

        $errno = 0;
        $errstr = '';
        $fp = fsockopen("localhost", $this->appPort(), $errno, $errstr);
        if (!$fp) {
            $error = _('Приложение не запущено');
            return true;
        }

        $result = $this->apiRequestByCode(127);
        if ($result['status'] == 'OK') {
            return true;
        }
        $error = isset($result['reason']) ? $result['reason'] : _('Неизвестная ошибка сервера');
        return false;
    }

    public function restartApp(&$error = null)
    {
        if (!$this->terminateApp($error)) {
            return false;
        }
        sleep(1);
        return $this->pingApp($error);
    }

    private function tryActivate($params, &$error = null)
    {
        if (!$this->checkExe($error)) {
            return null;
        }

        $lang = 'LANG=' . $this->appLocale();
        $command = array(escapeshellarg($this->exePath(true)), '--activate', '--silent');
        foreach ($params as $key => $param) {
            $param = trim(preg_replace('/\R+/', '', $param));
            $command[] = "--$key \"$param\"";
        }
        $command[] = escapeshellarg($this->iniPath(true));
        $random_path = utils::makeAbsolutePath(uniqid() . '.tmp', $this->rootDir());
        $command[] = '> ' . escapeshellarg($random_path);
        $command[] = '2>&1';
        $command = implode(' ', $command);
        if (utils::isWindows()) {
            exec($command);
        } else {
            exec($lang . ' ' . $command);
        }

        $output = file_get_contents($random_path);
        unlink($random_path);

        return $output;
    }

    public function getHWID(&$letter_text = null, &$error = null)
    {
        $error = '';
        $params = array();
        if (!$this->checkSerialNumber($error)) {
            return null;
        }
        $params['serial'] = $this->serialNumber();
        if (!$this->checkCompanyName($error)) {
            return null;
        }
        $params['company'] = $this->company();
        $output = $this->tryActivate($params, $error);
        if (!empty($error)) {
            return null;
        }

        /* Ожидаемый вывод приложения:
            Письмо должно содержать:

            Название программы: Сервер лицензий ГИС Аксиома 5.0
            Название компании: ИП Петров
            Серийный номер: AL53X-VUSJU-D4R7M-XVUJ3
            Идентификатор оборудования:
            == == == == == == == == == == == == == == == == == == == == == == == == == == ==
            <Уникальный идентификатор (HWID)>
            == == == == == == == == == == == == == == == == == == == == == == == == == == ==
        */
        $letter_start = _('Письмо должно содержать:');
        $output = explode(PHP_EOL, substr($output, strpos($output, $letter_start)));
        $letter_text = array_slice($output, 2, 7);
        return $letter_text[5];
    }

    public function activateWithCode($code, &$error = null, &$output = null)
    {
        $error = '';
        if (!isset($code)) {
            $error = _('Не задан код активации');
            return false;
        }
        $code = trim($code);
        if (empty($code)) {
            $error = _('Задан пустой код активации');
            return false;
        }

        $output = $this->tryActivate(array('code' => $code), $error);
        if (!empty($error)) {
            return false;
        }
        $output = explode(PHP_EOL, trim($output));
        $result = strpos(end($output), _('успешно')) !== false;
        if (!$result) {
            $error = end($output);
        }
        return $result;
    }
}
