<?php

namespace app\models;

use Exception;
use OS\PB\AccessDirectionTypes;
use OS\PB\NetworkPort;
use OS\PB\Packet;
use OS\PB\Packet\PacketTypes;
use OS\PB\RelayStateEvent\RelayState;
use OS\PB\RequestMessage\RequestIdTypes;
use OS\PB\RequestMessage\RequestMethodType;
use OS\PB\ResponseMessage\ResponseCode;
use OS\PB\SystemRestartAction\RestartMethodType;
use OS\PB\SystemRestartEvent\RestartReasonType;
use OS\PB\WiegandLineTypes;
use UnexpectedValueException;

/**
 * Class to convert to and from a serialized binary string into a JSON object string
 * 
 * @author  Gareth Palmer <gareth@one-space.co.za>
 */

final class Proto extends \yii\base\Model {

    /**
     * The "Content-Type" that should be send when passing a Protobuf binary.
     * 
     * @var string  CONTENT_TYPE    `application/x-protobuf`
     * 
     * @access  public
     */

    public const CONTENT_TYPE = 'application/x-protobuf';

    /**
     * Convert a Protobuf Packet to an unserialized array.
     * 
     * @param   OS\PB\Packet    $packet The packet to deserialize.
     * 
     * @return  array
     * 
     * @access  public
     */

    public function protoPacketToUnserializedArray(Packet $packet): array {
        $serialized = json_decode($packet->serializeToJsonString(), true);
        $this->deserializeEncodedJson($serialized, Packet::class);
        return $serialized;
    }


    /**
     * Get en enum value from a class constant parsed string.
     * 
     * @param   string  $id     The constant name.
     * @param   string  $class  The full class name which has the constant. Should be passed as `OS\PB\MyClass::class`.
     * 
     * @return  int|string
     * 
     * @access  private
     */

    private function constEnumValue(string $id, string $class): int|string {
        $const = "\\{$class}::{$id}";
        if (!defined($const)) {
            throw new UnexpectedValueException(
                sprintf('Enum %s has no value defined for name %s', __CLASS__, $id)
            );
        }
        return constant($const);
    }


    /**
     * Decode a string if it is not numeric.
     * 
     * @param   int|string  $value  The raw value to be decoded.
     * 
     * @return  int|string  Decoded value.
     * 
     * @access  private
     */

    private function decodeString(int|string $value): int|string {
        if (!is_numeric($value)) {
            $bDecode = base64_decode($value, true);
            if ($bDecode !== false) {
                $value = $bDecode;
            }
        }
        return $value;
    }


    /**
     * Recursively decode a serialized JSON object
     * 
     * @param   array   &$serialized    The serialized array to decode.
     * @param   string  $context        The parent key which indicated the context of work.
     * 
     * @access  private
     */

    private function deserializeEncodedJson(array &$serialized, string $context): void {
        foreach ($serialized as $key => &$value) {
            if (is_array($value)) {
                $this->deserializeEncodedJson($value, $key);
            } else {
                switch ($context) {
                        // General including enums
                    case Packet::class:
                    case 'RelayState':
                    case 'SystemRestart':
                    case 'ActionReceived':
                    case 'WiegandLineActivate':
                    case 'WiegandLineBlink':
                    case 'AccessGateway':
                    case 'Request':
                    case 'Response':
                    case 'SystemHardwareInfo':
                        $value = match ($key) {
                            // Enums
                            'PacketType'             => $this->constEnumValue($value, PacketTypes::class),
                            'SourcePort'             => $this->constEnumValue($value, NetworkPort::class),
                            'State'                  => $this->constEnumValue($value, RelayState::class),
                            'RestartReason'          => $this->constEnumValue($value, RestartReasonType::class),
                            'RestartMethod'          => $this->constEnumValue($value, RestartMethodType::class),
                            'LineOutput'             => $this->constEnumValue($value, WiegandLineTypes::class),
                            'AccessGatewayDirection' => $this->constEnumValue($value, AccessDirectionTypes::class),
                            'Code'                   => $this->constEnumValue($value, ResponseCode::class),
                            'Method'                 => $this->constEnumValue($value, RequestMethodType::class),
                            'RequestId'              => $this->constEnumValue($value, RequestIdTypes::class),
                            // String isn't serialized
                            'Raw'                  => $value,
                            'GSMModemModel'        => $value,
                            'GSMModemRevision'     => $value,
                            'GSMModemIMEI'         => $value,
                            'GSMModemSerialNumber' => $value,
                            'GSMSIM1IMSI'          => $value,
                            'GSMSIM1ICCID'         => $value,
                            'GSMSIM2IMSI'          => $value,
                            'GSMSIM2ICCID'         => $value,
                                // Deserialize
                            default => $this->decodeString($value),
                        };
                        break;

                        // Access Devices
                    case 'AllowedAccessDevice':
                    case 'DeniedAccessDevice':
                    case 'MatchingAccessDevice':
                    case 'AccessDeviceUpdate':
                    case 'AccessDeviceDelete':
                        $value = match ($key) {
                            'NumberPlate' => $value,
                            default => $this->decodeString($value),
                        };
                        break;

                        // Unassociative arrays
                    case 'ConfigName':
                    case 'FailedReasons':
                        break;

                    default:
                        $value = $this->decodeString($value);
                }
            }
        }
    }


    /**
     * Convert a JSON object back to a serialized JSON array and returned
     * 
     * @param   string  $jsonData   The origonal JSON string to be converted
     * 
     * @return  array   Reserialized string
     * 
     * @access  public
     */

    public function jsonToSerializedArray(string $jsonData): array {
        $data = json_decode($jsonData, true);
        $this->reserializeDecodedJson($data, \OS\PB\Packet::class);
        return $data;
    }


    /**
     * Recode a string back to base64, as required
     * 
     * @param   int|string  $value  The value to be recoded, if not numeric
     * 
     * @return  int|string
     * 
     * @access  private
     */

    private function recodeString(int|string $value): int|string {
        if (!is_numeric($value)) {
            $bDecode = base64_encode($value);
            if ($bDecode !== false) {
                $value = $bDecode;
            }
        }
        return $value;
    }


    /**
     * Recode a 1 or 0 back to an actual bool value.
     * 
     * @param   int $value  The value to recode
     * 
     * @return  bool
     * 
     * @throws  Exception   If the param isn't a bool int 1 or 0.
     * 
     * @access  private
     */

    private function recodeBool(int $value): bool {
        if ($value !== 0 && $value !== 1) {
            throw new Exception("Invalid bool value");
        }

        return $value === 1;
    }


    /**
     * Recursively reserialize a decoded JSON data array into a serialized data array.
     * 
     * @param   array   &$deserialized  The array to reserialize.
     * @param   string  $context        The parent key which indicated the context of work.
     * 
     * @access  private
     */

    private function reserializeDecodedJson(array &$deserialized, string $context): void {
        foreach ($deserialized as $key => &$value) {
            if (is_array($value)) {
                $this->reserializeDecodedJson($value, $key);
            } else {
                switch ($context) {
                    case Packet::class:
                    case 'WiegandCode':
                    case 'DeniedUser':
                    case 'AccessUserUpdate':
                    case 'AccessUserDelete':
                    case 'AccessGateway':
                    case 'AccessDevicesErase':
                    case 'AccessUsersErase':
                    case 'AccessGroupsErase':
                    case 'BlueToothPort':
                    case 'Request':
                    case 'Response':
                    case 'SystemHardwareInfo':
                    case 'Log':
                    case 'Info':
                    case 'Warning':
                    case 'Error':
                    case 'Fatal':
                        $value = match ($key) {
                            // Bool to reserialize
                            'ParityFailed'          => $this->recodeBool($value),
                            'SingleKeyBitBurstMode' => $this->recodeBool($value),
                            'FirstExitsThenEnters'  => $this->recodeBool($value),
                            'RollOverExitToNextDay' => $this->recodeBool($value),
                            'AccessEnabled'         => $this->recodeBool($value),
                            'AreYouSure'            => $this->recodeBool($value),
                            'BeSecureService'       => $this->recodeBool($value),
                            'BLECodedPhyPresent'    => $this->recodeBool($value),

                            // String isn't serialized
                            'Raw'                  => $value,
                            'GSMModemModel'        => $value,
                            'GSMModemRevision'     => $value,
                            'GSMModemIMEI'         => $value,
                            'GSMModemSerialNumber' => $value,
                            'GSMSIM1IMSI'          => $value,
                            'GSMSIM1ICCID'         => $value,
                            'GSMSIM2IMSI'          => $value,
                            'GSMSIM2ICCID'         => $value,
                            'BootFirmwareVersion'  => $value,
                            'AppFirmwareVersion'   => $value,
                            'HardwarePlatform'     => $value,
                            'GSMModemManufacturer' => $value,
                            'Debug'                => $value,
                            'Info'                 => $value,
                            'Warning'              => $value,
                            'Error'                => $value,
                            'Fatal'                => $value,
                            'Json'                 => $value,

                            default => $this->recodeString($value),
                        };
                        break;

                        // Access Devices
                    case 'AllowedAccessDevice':
                    case 'DeniedAccessDevice':
                    case 'MatchingAccessDevice':
                    case 'AccessDeviceUpdate':
                    case 'AccessDeviceDelete':
                        $value = match ($key) {
                            'NumberPlate' => $value,
                            default => $this->recodeString($value),
                        };
                        break;

                        // Unassociative arrays
                    case 'ConfigName':
                    case 'FailedReasons':
                        break;

                    default:
                        $value = $this->recodeString($value);
                }
            }
        }
    }
}
