<?php


namespace lbuchs\WebAuthn\CBOR;
use lbuchs\WebAuthn\WebAuthnException;
use lbuchs\WebAuthn\Binary\ByteBuffer;

/**
 * Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/CborDecoder.php
 * Copyright © 2018 Thomas Bleeker - MIT licensed
 * Modified by Lukas Buchs
 * Thanks Thomas for your work!
 */
class CborDecoder {
    const CBOR_MAJOR_UNSIGNED_INT = 0;
    const CBOR_MAJOR_TEXT_STRING = 3;
    const CBOR_MAJOR_FLOAT_SIMPLE = 7;
    const CBOR_MAJOR_NEGATIVE_INT = 1;
    const CBOR_MAJOR_ARRAY = 4;
    const CBOR_MAJOR_TAG = 6;
    const CBOR_MAJOR_MAP = 5;
    const CBOR_MAJOR_BYTE_STRING = 2;

    /**
     * @param ByteBuffer|string $bufOrBin
     * @return mixed
     * @throws WebAuthnException
     */
    public static function decode($bufOrBin) {
        $buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin);

        $offset = 0;
        $result = self::_parseItem($buf, $offset);
        if ($offset !== $buf->getLength()) {
            throw new WebAuthnException('Unused bytes after data item.', WebAuthnException::CBOR);
        }
        return $result;
    }

    /**
     * @param ByteBuffer|string $bufOrBin
     * @param int $startOffset
     * @param int|null $endOffset
     * @return mixed
     */
    public static function decodeInPlace($bufOrBin, $startOffset, &$endOffset = null) {
        $buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin);

        $offset = $startOffset;
        $data = self::_parseItem($buf, $offset);
        $endOffset = $offset;
        return $data;
    }

    // ---------------------
    // protected
    // ---------------------

    /**
     * @param ByteBuffer $buf
     * @param int $offset
     * @return mixed
     */
    protected static function _parseItem(ByteBuffer $buf, &$offset) {
        $first = $buf->getByteVal($offset++);
        $type = $first >> 5;
        $val = $first & 0b11111;

        if ($type === self::CBOR_MAJOR_FLOAT_SIMPLE) {
            return self::_parseFloatSimple($val, $buf, $offset);
        }

        $val = self::_parseExtraLength($val, $buf, $offset);

        return self::_parseItemData($type, $val, $buf, $offset);
    }

    protected static function _parseFloatSimple($val, ByteBuffer $buf, &$offset) {
        switch ($val) {
            case 24:
                $val = $buf->getByteVal($offset);
                $offset++;
                return self::_parseSimple($val);

            case 25:
                $floatValue = $buf->getHalfFloatVal($offset);
                $offset += 2;
                return $floatValue;

            case 26:
                $floatValue = $buf->getFloatVal($offset);
                $offset += 4;
                return $floatValue;

            case 27:
                $floatValue = $buf->getDoubleVal($offset);
                $offset += 8;
                return $floatValue;

            case 28:
            case 29:
            case 30:
                throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR);

            case 31:
                throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR);
        }

        return self::_parseSimple($val);
    }

    /**
     * @param int $val
     * @return mixed
     * @throws WebAuthnException
     */
    protected static function _parseSimple($val) {
        if ($val === 20) {
            return false;
        }
        if ($val === 21) {
            return true;
        }
        if ($val === 22) {
            return null;
        }
        throw new WebAuthnException(sprintf('Unsupported simple value %d.', $val), WebAuthnException::CBOR);
    }

    protected static function _parseExtraLength($val, ByteBuffer $buf, &$offset) {
        switch ($val) {
            case 24:
                $val = $buf->getByteVal($offset);
                $offset++;
                break;

            case 25:
                $val = $buf->getUint16Val($offset);
                $offset += 2;
                break;

            case 26:
                $val = $buf->getUint32Val($offset);
                $offset += 4;
                break;

            case 27:
                $val = $buf->getUint64Val($offset);
                $offset += 8;
                break;

            case 28:
            case 29:
            case 30:
                throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR);

            case 31:
                throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR);
        }

        return $val;
    }

    protected static function _parseItemData($type, $val, ByteBuffer $buf, &$offset) {
        switch ($type) {
            case self::CBOR_MAJOR_UNSIGNED_INT: // uint
                return $val;

            case self::CBOR_MAJOR_NEGATIVE_INT:
                return -1 - $val;

            case self::CBOR_MAJOR_BYTE_STRING:
                $data = $buf->getBytes($offset, $val);
                $offset += $val;
                return new ByteBuffer($data); // bytes

            case self::CBOR_MAJOR_TEXT_STRING:
                $data = $buf->getBytes($offset, $val);
                $offset += $val;
                return $data; // UTF-8

            case self::CBOR_MAJOR_ARRAY:
                return self::_parseArray($buf, $offset, $val);

            case self::CBOR_MAJOR_MAP:
                return self::_parseMap($buf, $offset, $val);

            case self::CBOR_MAJOR_TAG:
                return self::_parseItem($buf, $offset); // 1 embedded data item
        }

        // This should never be reached
        throw new WebAuthnException(sprintf('Unknown major type %d.', $type), WebAuthnException::CBOR);
    }

    protected static function _parseMap(ByteBuffer $buf, &$offset, $count) {
        $map = array();

        for ($i = 0; $i < $count; $i++) {
            $mapKey = self::_parseItem($buf, $offset);
            $mapVal = self::_parseItem($buf, $offset);

            if (!\is_int($mapKey) && !\is_string($mapKey)) {
                throw new WebAuthnException('Can only use strings or integers as map keys', WebAuthnException::CBOR);
            }

            $map[$mapKey] = $mapVal; // todo dup
        }
        return $map;
    }

    protected static function _parseArray(ByteBuffer $buf, &$offset, $count) {
        $arr = array();
        for ($i = 0; $i < $count; $i++) {
            $arr[] = self::_parseItem($buf, $offset);
        }

        return $arr;
    }
}
