// File: lib/solady/src/utils/Base64.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/// @notice Library to encode strings in Base64.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/Base64.sol)
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/Base64.sol)
/// @author Modified from (https://github.com/Brechtpd/base64/blob/main/base64.sol) by Brecht Devos - <brecht@loopring.org>.
library Base64 {
    /// @dev Encodes `data` using the base64 encoding described in RFC 4648.
    /// See: https://datatracker.ietf.org/doc/html/rfc4648
    /// @param fileSafe  Whether to replace '+' with '-' and '/' with '_'.
    /// @param noPadding Whether to strip away the padding.
    function encode(bytes memory data, bool fileSafe, bool noPadding)
        internal
        pure
        returns (string memory result)
    {
        /// @solidity memory-safe-assembly
        assembly {
            let dataLength := mload(data)

            if dataLength {
                // Multiply by 4/3 rounded up.
                // The `shl(2, ...)` is equivalent to multiplying by 4.
                let encodedLength := shl(2, div(add(dataLength, 2), 3))

                // Set `result` to point to the start of the free memory.
                result := mload(0x40)

                // Store the table into the scratch space.
                // Offsetted by -1 byte so that the `mload` will load the character.
                // We will rewrite the free memory pointer at `0x40` later with
                // the allocated size.
                // The magic constant 0x0230 will translate "-_" + "+/".
                mstore(0x1f, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef")
                mstore(0x3f, sub("ghijklmnopqrstuvwxyz0123456789-_", mul(iszero(fileSafe), 0x0230)))

                // Skip the first slot, which stores the length.
                let ptr := add(result, 0x20)
                let end := add(ptr, encodedLength)

                // Run over the input, 3 bytes at a time.
                for {} 1 {} {
                    data := add(data, 3) // Advance 3 bytes.
                    let input := mload(data)

                    // Write 4 bytes. Optimized for fewer stack operations.
                    mstore8(0, mload(and(shr(18, input), 0x3F)))
                    mstore8(1, mload(and(shr(12, input), 0x3F)))
                    mstore8(2, mload(and(shr(6, input), 0x3F)))
                    mstore8(3, mload(and(input, 0x3F)))
                    mstore(ptr, mload(0x00))

                    ptr := add(ptr, 4) // Advance 4 bytes.

                    if iszero(lt(ptr, end)) { break }
                }

                // Allocate the memory for the string.
                // Add 31 and mask with `not(31)` to round the
                // free memory pointer up the next multiple of 32.
                mstore(0x40, and(add(end, 31), not(31)))

                // Equivalent to `o = [0, 2, 1][dataLength % 3]`.
                let o := div(2, mod(dataLength, 3))

                // Offset `ptr` and pad with '='. We can simply write over the end.
                mstore(sub(ptr, o), shl(240, 0x3d3d))
                // Set `o` to zero if there is padding.
                o := mul(iszero(iszero(noPadding)), o)
                // Zeroize the slot after the string.
                mstore(sub(ptr, o), 0)
                // Write the length of the string.
                mstore(result, sub(encodedLength, o))
            }
        }
    }

    /// @dev Encodes `data` using the base64 encoding described in RFC 4648.
    /// Equivalent to `encode(data, false, false)`.
    function encode(bytes memory data) internal pure returns (string memory result) {
        result = encode(data, false, false);
    }

    /// @dev Encodes `data` using the base64 encoding described in RFC 4648.
    /// Equivalent to `encode(data, fileSafe, false)`.
    function encode(bytes memory data, bool fileSafe)
        internal
        pure
        returns (string memory result)
    {
        result = encode(data, fileSafe, false);
    }

    /// @dev Encodes base64 encoded `data`.
    ///
    /// Supports:
    /// - RFC 4648 (both standard and file-safe mode).
    /// - RFC 3501 (63: ',').
    ///
    /// Does not support:
    /// - Line breaks.
    ///
    /// Note: For performance reasons,
    /// this function will NOT revert on invalid `data` inputs.
    /// Outputs for invalid inputs will simply be undefined behaviour.
    /// It is the user's responsibility to ensure that the `data`
    /// is a valid base64 encoded string.
    function decode(string memory data) internal pure returns (bytes memory result) {
        /// @solidity memory-safe-assembly
        assembly {
            let dataLength := mload(data)

            if dataLength {
                let decodedLength := mul(shr(2, dataLength), 3)

                for {} 1 {} {
                    // If padded.
                    if iszero(and(dataLength, 3)) {
                        let t := xor(mload(add(data, dataLength)), 0x3d3d)
                        // forgefmt: disable-next-item
                        decodedLength := sub(
                            decodedLength,
                            add(iszero(byte(30, t)), iszero(byte(31, t)))
                        )
                        break
                    }
                    // If non-padded.
                    decodedLength := add(decodedLength, sub(and(dataLength, 3), 1))
                    break
                }
                result := mload(0x40)

                // Write the length of the bytes.
                mstore(result, decodedLength)

                // Skip the first slot, which stores the length.
                let ptr := add(result, 0x20)
                let end := add(ptr, decodedLength)

                // Load the table into the scratch space.
                // Constants are optimized for smaller bytecode with zero gas overhead.
                // `m` also doubles as the mask of the upper 6 bits.
                let m := 0xfc000000fc00686c7074787c8084888c9094989ca0a4a8acb0b4b8bcc0c4c8cc
                mstore(0x5b, m)
                mstore(0x3b, 0x04080c1014181c2024282c3034383c4044484c5054585c6064)
                mstore(0x1a, 0xf8fcf800fcd0d4d8dce0e4e8ecf0f4)

                for {} 1 {} {
                    // Read 4 bytes.
                    data := add(data, 4)
                    let input := mload(data)

                    // Write 3 bytes.
                    // forgefmt: disable-next-item
                    mstore(ptr, or(
                        and(m, mload(byte(28, input))),
                        shr(6, or(
                            and(m, mload(byte(29, input))),
                            shr(6, or(
                                and(m, mload(byte(30, input))),
                                shr(6, mload(byte(31, input)))
                            ))
                        ))
                    ))

                    ptr := add(ptr, 3)

                    if iszero(lt(ptr, end)) { break }
                }

                // Allocate the memory for the string.
                // Add 31 and mask with `not(31)` to round the
                // free memory pointer up the next multiple of 32.
                mstore(0x40, and(add(end, 31), not(31)))
                // Zeroize the slot after the bytes.
                mstore(end, 0)
                // Restore the zero slot.
                mstore(0x60, 0)
            }
        }
    }
}

// File: src/core/AuthGuard.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./Registry.sol";

contract AuthGuard {
    Registry internal registry;

    constructor(address _registry) {
        initializeAuthGuard(_registry);
    }

    function initializeAuthGuard(address _registry) public {
        if (registry == Registry(address(0))) {
            registry = Registry(_registry);
        }
    }

    function isAuthorized(
        address _user,
        address _operator,
        bytes4 _role
    ) public view returns (bool) {
        return registry.isAuthorized(_user, _operator, _role);
    }

    function isAuthorizedById(
        uint64 _id,
        bytes4 _role,
        address _operator
    ) public view returns (bool) {
        return registry.isAuthorizedById(_id, _operator, _role);
    }

    function contractPermissions(address _contract) public view returns (bool) {
        return registry.contractPermissions(_contract);
    }

    function getIdOwner(uint64 _id) public view returns (address) {
        return registry.getIdOwner(_id);
    }

    function getStorageContract(uint64 _id) public view returns (address) {
        return registry.getStorageContract(_id);
    }

    function getPlugin(uint64 _id) public view returns (address) {
        return registry.getPlugin(_id);
    }

    function connectPluginContract(uint64 _id) internal {
        return registry.connectPluginContract(_id);
    }

    function migratePluginContract(
        uint64 _id,
        address _newPluginContract
    ) external returns (bool) {
        return registry.migratePluginContract(_id, _newPluginContract);
    }

    function registerStorageContract(
        address _owner,
        uint8 _salt
    ) internal returns (uint64) {
        return registry.registerStorageContract(_owner, _salt);
    }

    function migrateStorageContract(
        uint64 _id,
        address _newStorageContract
    ) internal returns (bool) {
        return registry.migrateStorageContract(_id, _newStorageContract);
    }

    modifier onlyAdmin() {
        require(registry.isAdmin(msg.sender), "UNAUTHORIZED");
        _;
    }

    modifier onlyOperatorByUser(address _user) {
        require(
            registry.isAdmin(msg.sender) ||
                registry.isOperator(_user, msg.sender),
            "UNAUTHORIZED"
        );
        _;
    }

    modifier onlyAuthorizedByUser(address _user, bytes4 _role) {
        require(
            registry.isAuthorized(_user, msg.sender, _role),
            "UNAUTHORIZED"
        );
        _;
    }

    modifier onlyAuthorizedById(uint64 _id, bytes4 _role) {
        require(
            registry.isAuthorizedById(_id, msg.sender, _role),
            "UNAUTHORIZED"
        );
        _;
    }

    modifier onlyWhitelistedContract() {
        require(
            registry.isAdmin(msg.sender) ||
                registry.contractPermissions(msg.sender),
            "UNAUTHORIZED"
        );
        _;
    }
}

// File: src/core/Registry.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████▓▀██████████████████████████████████████████████
// ██████████████████████████████████  ╙███████████████████████████████████████████
// ███████████████████████████████████    ╙████████████████████████████████████████
// ████████████████████████████████████      ╙▀████████████████████████████████████
// ████████████████████████████████████▌        ╙▀█████████████████████████████████
// ████████████████████████████████████▌           ╙███████████████████████████████
// ████████████████████████████████████▌            ███████████████████████████████
// ████████████████████████████████████▌         ▄█████████████████████████████████
// ████████████████████████████████████       ▄████████████████████████████████████
// ███████████████████████████████████▀   ,▄███████████████████████████████████████
// ██████████████████████████████████▀ ,▄██████████████████████████████████████████
// █████████████████████████████████▄▓█████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████

contract Registry {
    bytes4 public constant REGISTER_STORAGE_ROLE =
        bytes4(keccak256("REGISTER_STORAGE_ROLE"));
    bytes4 public constant REGISTER_PLUGIN_ROLE =
        bytes4(keccak256("REGISTER_PLUGIN_ROLE"));

    mapping(address => bool) public adminPermissions; // only admins
    mapping(address => bool) public contractPermissions; // trusted router contracts
    mapping(address => mapping(address => bool)) public operatorPermissions; // operate on behalf of a certain user
    mapping(address => mapping(address => mapping(bytes4 => bool)))
        public rolePermissions;

    uint64 immutable chainAndNetworkId;

    mapping(uint64 => address) public storageContracts;
    mapping(uint64 => address) public pluginContracts;
    mapping(address => uint64) public ownerNonces;
    mapping(uint64 => address) public idOwners;

    event AdminPermissionChanged(address indexed admin, bool isAuthorized);
    event ContractPermissionChanged(
        address indexed contractAddress,
        bool isAuthorized
    );
    event OperatorPermissionChanged(
        address indexed user,
        address indexed operator,
        bool isAuthorized
    );
    event UserRoleChanged(
        address indexed user,
        address indexed operator,
        bytes4 indexed role,
        bool isAuthorized
    );
    event IdRegistered(
        uint64 indexed id,
        address indexed owner,
        address indexed storageContract
    );
    event PluginConnected(uint64 indexed id, address indexed pluginContract);
    event StorageContractMigrated(
        uint64 indexed id,
        address indexed oldStorageContract,
        address indexed newStorageContract
    );
    event PluginContractMigrated(
        uint64 indexed id,
        address indexed oldPluginContract,
        address indexed newPluginContract
    );

    constructor(address[] memory _initialAdmins) {
        chainAndNetworkId = uint64(
            uint16(bytes2(keccak256(abi.encodePacked(block.chainid))))
        );
        for (uint64 i = 0; i < _initialAdmins.length; ++i) {
            adminPermissions[_initialAdmins[i]] = true;
            emit AdminPermissionChanged(_initialAdmins[i], true);
        }
    }

    /**
     * @notice Set admini permissions
     * @param _admin The address of the administrator
     * @param _isAuthorized A boolean to indicate if the administrator is authorized
     */
    function setAdmin(address _admin, bool _isAuthorized) public onlyAdmins {
        adminPermissions[_admin] = _isAuthorized;
        emit AdminPermissionChanged(_admin, _isAuthorized);
    }

    /**
     * @notice Set contract permissions
     * @param _contract The address of the contract
     * @param _isAuthorized A boolean to indicate if the contract is authorized
     */
    function setContractPermission(
        address _contract,
        bool _isAuthorized
    ) public onlyAdmins {
        contractPermissions[_contract] = _isAuthorized;
        emit ContractPermissionChanged(_contract, _isAuthorized);
    }

    /**
     * @notice Set operator permissions
     * @param _operator The address of the operator
     * @param _isAuthorized A boolean to indicate if the operator is authorized
     */
    function setOperatorPermission(
        address _operator,
        bool _isAuthorized
    ) public {
        operatorPermissions[msg.sender][_operator] = _isAuthorized;
        emit OperatorPermissionChanged(msg.sender, _operator, _isAuthorized);
    }

    /**
     * @notice Set user role permissions
     * @param _operator The address of the user
     * @param _role The role associated with the user
     * @param _isAuthorized A boolean to indicate if the user role is authorized
     */
    function setUserRolePermission(
        address _operator,
        bytes4 _role,
        bool _isAuthorized
    ) public {
        rolePermissions[msg.sender][_operator][_role] = _isAuthorized;
        emit UserRoleChanged(msg.sender, _operator, _role, _isAuthorized);
    }

    /**
     * @notice Set user role permissions for an operator in batch
     * @param _user The address of the user for whom the roles are being set
     * @param _operator The address of the operator being granted the roles
     * @param _roles The array of roles associated with the user and operator
     * @param _isAuthorized A boolean to indicate if the user roles are authorized
     */
    function setBatchUserRolePermissions(
        address _user,
        address _operator,
        bytes4[] memory _roles,
        bool _isAuthorized
    ) public {
        for (uint256 i = 0; i < _roles.length; ++i) {
            rolePermissions[_user][_operator][_roles[i]] = _isAuthorized;
            emit UserRoleChanged(_user, _operator, _roles[i], _isAuthorized);
        }
    }

    /**
     * @notice Check if a user is an admin
     * @param _user The address of the user
     * @return A boolean indicating if the user is an admin
     */
    function isAdmin(address _user) public view returns (bool) {
        return adminPermissions[_user];
    }

    /**
     * @notice Check if an address is an operator for a user
     * @param _user The address of the user
     * @param _operator The address of the operator
     * @return A boolean indicating if the address is an operator for the user
     */
    function isOperator(
        address _user,
        address _operator
    ) public view returns (bool) {
        return operatorPermissions[_user][_operator];
    }

    /**
     * @notice Check if an operator is authorized for a specific role
     * @param _user The address of the user
     * @param _operator The address of the operator
     * @param _role The role to check for authorization
     * @return A boolean indicating if the operator is authorized for the specified role
     */
    function isAuthorized(
        address _user,
        address _operator,
        bytes4 _role
    ) public view returns (bool) {
        if (adminPermissions[_operator] || _user == _operator) {
            return true;
        }
        if (operatorPermissions[_user][_operator]) {
            return true;
        }
        if (rolePermissions[_user][_operator][_role]) {
            return true;
        }
        if (contractPermissions[_operator]) {
            return true;
        }
        return false;
    }

    /**
     * @notice Check if an operator is authorized for a specific role by ID
     * @param _id The ID to check for authorization
     * @param _operator The address of the operator
     * @param _role The role to check for authorization
     * @return A boolean indicating if the operator is authorized for the specified role by ID
     */
    function isAuthorizedById(
        uint64 _id,
        address _operator,
        bytes4 _role
    ) public view returns (bool) {
        address _user = idOwners[_id];
        return isAuthorized(_user, _operator, _role);
    }

    /**
     * @notice Check if an operator is authorized for an array of specific roles
     * @param _user The address of the user
     * @param _operator The address of the operator
     * @param _roles The array of roles to check for authorization
     * @return A boolean indicating if the operator is authorized for all specified roles
     */
    function isAuthorizedForRoles(
        address _user,
        address _operator,
        bytes4[] memory _roles
    ) public view returns (bool) {
        if (adminPermissions[_operator] || _user == _operator) {
            return true;
        }
        if (operatorPermissions[_user][_operator]) {
            return true;
        }
        if (contractPermissions[_operator]) {
            return true;
        }
        for (uint256 i = 0; i < _roles.length; ++i) {
            if (!rolePermissions[_user][_operator][_roles[i]]) {
                return false;
            }
        }
        return false;
    }

    /**
     * @notice Get the owner of an ID
     * @param _id The ID to get the owner of
     * @return The address of the owner
     */
    function getIdOwner(uint64 _id) external view returns (address) {
        return idOwners[_id];
    }

    /**
     * @notice Get the storage contract for an ID
     * @param _id The ID to get the storage contract of
     * @return The address of the storage contract
     */
    function getStorageContract(uint64 _id) external view returns (address) {
        return storageContracts[_id];
    }

    /**
     * @notice Get the plugin for an ID
     * @param _id The ID to get the plugin of
     * @return The address of the plugin
     */
    function getPlugin(uint64 _id) external view returns (address) {
        return pluginContracts[_id];
    }

    /**
     * @notice Register a storage contract
     * @param _owner The address of the storage contract owner
     * @param _salt The salt for generating the ID
     * @return The ID of the registered storage contract
     */
    function registerStorageContract(
        address _owner,
        uint8 _salt
    )
        external
        onlyAuthorizedByUser(_owner, REGISTER_STORAGE_ROLE)
        returns (uint64)
    {
        uint64 id = calculateIdWithSalt(_owner, _salt);
        require(storageContracts[id] == address(0), "ID_ALREADY_REGISTERED");
        ownerNonces[_owner] += 1;
        storageContracts[id] = msg.sender;
        idOwners[id] = _owner;
        emit IdRegistered(id, _owner, msg.sender);
        return id;
    }

    /**
     * @notice Connect a plugin contract to an ID
     * @param _id The ID to connect the plugin contract to
     */
    function connectPluginContract(
        uint64 _id
    ) external onlyAuthorizedByUser(idOwners[_id], REGISTER_PLUGIN_ROLE) {
        require(storageContracts[_id] != address(0), "ID_NOT_REGISTERED");
        require(
            pluginContracts[_id] == address(0),
            "PLUGIN_ALREADY_REGISTERED"
        );
        pluginContracts[_id] = msg.sender;
        emit PluginConnected(_id, msg.sender);
    }

    /**
     * @notice Migrate a storage contract to a new address
     * @param _id The ID of the storage contract to migrate
     * @param _newStorageContract The address of the new storage contract
     * @return A boolean indicating if the migration was successful
     */
    function migrateStorageContract(
        uint64 _id,
        address _newStorageContract
    ) external onlyStorageContracts(_id) returns (bool) {
        address oldStorageContract = storageContracts[_id];
        require(oldStorageContract != address(0), "ID_NOT_REGISTERED");
        storageContracts[_id] = _newStorageContract;

        emit StorageContractMigrated(
            _id,
            oldStorageContract,
            _newStorageContract
        );

        return true;
    }

    /**
     * @notice Migrate a plugin contract to a new address
     * @param _id The ID of the plugin contract to migrate
     * @param _newPluginContract The address of the new plugin contract
     * @return A boolean indicating if the migration was successful
     */
    function migratePluginContract(
        uint64 _id,
        address _newPluginContract
    ) external onlyPluginContracts(_id) returns (bool) {
        address oldPluginContract = pluginContracts[_id];
        require(oldPluginContract != address(0), "ID_NOT_CONNECTED_TO_PLUGIN");
        pluginContracts[_id] = _newPluginContract;

        emit PluginContractMigrated(_id, oldPluginContract, _newPluginContract);

        return true;
    }

    /**
     * @notice Calculate the ID with the provided owner and salt
     * @param _owner The address of the owner
     * @param _salt The salt for generating the ID
     * @return The calculated ID
     */
    function calculateIdWithSalt(
        address _owner,
        uint8 _salt
    ) public view returns (uint64) {
        return
            (uint64(
                bytes8(
                    keccak256(
                        abi.encodePacked(_owner, ownerNonces[_owner], _salt)
                    )
                )
            ) << 16) | chainAndNetworkId;
    }

    /**
     * @notice Calculate the ID with the provided owner, nonce, and salt
     * @param _owner The address of the owner
     * @param _nonce The nonce for generating the ID
     * @param _salt The salt for generating the ID
     * @return The calculated ID
     */
    function calculateIdWithSaltAndNonce(
        address _owner,
        uint64 _nonce,
        uint8 _salt
    ) public view returns (uint64) {
        return
            (uint64(
                bytes8(keccak256(abi.encodePacked(_owner, _nonce, _salt)))
            ) << 16) | chainAndNetworkId;
    }

    modifier onlyAdmins() {
        require(adminPermissions[msg.sender], "NOT_AN_ADMIN");
        _;
    }

    modifier onlyOperators(address _user) {
        require(
            adminPermissions[msg.sender] ||
                operatorPermissions[_user][msg.sender],
            "NOT_AN_OPERATOR"
        );
        _;
    }

    modifier onlyStorageContracts(uint64 _id) {
        require(
            isAdmin(msg.sender) || storageContracts[_id] == msg.sender,
            "NOT_A_STORAGE_CONTRACT"
        );
        _;
    }

    modifier onlyPluginContracts(uint64 _id) {
        require(
            isAdmin(msg.sender) || pluginContracts[_id] == msg.sender,
            "NOT_A_PLUGIN_CONTRACT"
        );
        _;
    }

    modifier onlyAuthorizedByUser(address _user, bytes4 _role) {
        require(isAuthorized(_user, msg.sender, _role), "UNAUTHORIZED");
        _;
    }
}

// File: src/storage/VideoStorage.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████▓▀██████████████████████████████████████████████
// ██████████████████████████████████  ╙███████████████████████████████████████████
// ███████████████████████████████████    ╙████████████████████████████████████████
// ████████████████████████████████████      ╙▀████████████████████████████████████
// ████████████████████████████████████▌        ╙▀█████████████████████████████████
// ████████████████████████████████████▌           ╙███████████████████████████████
// ████████████████████████████████████▌            ███████████████████████████████
// ████████████████████████████████████▌         ▄█████████████████████████████████
// ████████████████████████████████████       ▄████████████████████████████████████
// ███████████████████████████████████▀   ,▄███████████████████████████████████████
// ██████████████████████████████████▀ ,▄██████████████████████████████████████████
// █████████████████████████████████▄▓█████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████
// ████████████████████████████████████████████████████████████████████████████████

import {AuthGuard} from "../core/AuthGuard.sol";
import {Base64} from "solady/utils/Base64.sol";

contract VideoStorage is AuthGuard {
    bytes4 public constant VIDEO_CREATE_ROLE =
        bytes4(keccak256("VIDEO_CREATE_ROLE"));
    bytes4 public constant VIDEO_UPDATE_ROLE =
        bytes4(keccak256("VIDEO_UPDATE_ROLE"));

    uint8 constant TAGS_LIMIT = 7;

    constructor(address _registry) AuthGuard(_registry) {}

    event VideoCreated(VideoCreatedEventParams videoCreatedEventParams);
    event TitleUpdated(uint64 indexed id, string newTitle);
    event ThumbnailUpdated(uint64 indexed id, string newThumbnailURL);
    event VideoUpdated(uint64 indexed id, string newVideoURL);
    event DescriptionUpdated(uint64 indexed id, string newDescription);
    event PackedDataUpdated(uint64 indexed id, bytes32 newPackedData);
    event SymbolUpdated(uint64 indexed id, string newSymbol);
    event FrozenStatusUpdated(uint64 indexed id, bool newFrozenStatus);
    event NSFWStatusUpdated(uint64 indexed id, bool newFrozenStatus);
    event LicenseTypeUpdated(uint64 indexed id, uint8 newLicenseType);
    event TagsUpdated(uint64 indexed id, string[] newTags);

    mapping(uint64 => VideoMetadata) public videos;

    struct VideoMetadata {
        // title is limited to 96 characters
        bytes32 title1; // first data slot
        bytes32 title2; // second data slot
        bytes32 title3; // third data slot
        // arweave id efficiently packed
        bytes32 arweaveThumbnail;
        // arweave id efficiently packed
        bytes32 arweaveVideo;
        // packed data combining symbol, created time, flags, and category
        bytes32 packedData; // Combines symbol, created, flags, and category
        // dynamic value at the end for efficiency
        string description;
    }

    struct CreateParams {
        address owner;
        string title; // can only be 96 characters
        string description;
        string thumbnailURL; // just the arweave ID nothing else (no https://arweave.net/)
        string videoURL; // just the arweave ID nothing else (no https://arweave.net/)
        string[] tags;
        string symbol; // 6 characters or less
        bool frozen;
        bool nsfw;
        uint8 licenseType; // license type
        uint8 salt;
    }

    struct VideoCreatedEventParams {
        uint64 id;
        string title;
        string description;
        string thumbnailURL;
        string videoURL;
        string[] tags;
        string symbol;
        bool frozen;
        bool nsfw;
        uint8 licenseType;
        uint64 created;
    }

    function create(
        CreateParams memory params
    )
        public
        onlyAuthorizedByUser(params.owner, VIDEO_CREATE_ROLE)
        returns (uint64)
    {
        uint64 id = registerStorageContract(params.owner, params.salt);
        require(getCreated(id) == 0, "VIDEO_ALREADY_EXISTS");
        (
            bytes32 title1,
            bytes32 title2,
            bytes32 title3
        ) = calculateTitleBytes32(bytes(params.title));

        bytes32 packedData = calculatePackedData(
            PackedDataParams({
                symbol: params.symbol,
                created: uint64(block.timestamp),
                hidden: false,
                frozen: params.frozen,
                licenseType: params.licenseType,
                nsfw: params.nsfw,
                tagHashes: calculateTagHashes(params.tags)
            })
        );

        videos[id] = VideoMetadata(
            title1,
            title2,
            title3,
            arweaveIdToBytes32(params.thumbnailURL),
            arweaveIdToBytes32(params.videoURL),
            packedData,
            params.description
        );

        emit VideoCreated(
            VideoCreatedEventParams(
                id,
                params.title,
                params.description,
                params.thumbnailURL,
                params.videoURL,
                params.tags,
                params.symbol,
                params.frozen,
                params.nsfw,
                params.licenseType,
                uint64(block.timestamp)
            )
        );

        return id;
    }

    modifier validUpdate(uint64 _id) {
        require(getCreated(_id) != 0, "VIDEO_NOT_CREATED");
        require(!isFrozen(_id), "VIDEO_DATA_FROZEN");
        _;
    }

    function updateTitle(
        uint64 _id,
        string memory _newTitle
    ) public validUpdate(_id) onlyAuthorizedById(_id, VIDEO_UPDATE_ROLE) {
        (
            bytes32 title1,
            bytes32 title2,
            bytes32 title3
        ) = calculateTitleBytes32(bytes(_newTitle));
        require(
            title1 != 0x0 || title2 != 0x0 || title3 != 0x0,
            "INVALID_TITLE"
        );
        videos[_id].title1 = title1;
        videos[_id].title2 = title2;
        videos[_id].title3 = title3;

        // Emit TitleUpdated event
        emit TitleUpdated(_id, _newTitle);
    }

    function updateThumbnail(
        uint64 _id,
        string memory _newThumbnailURL
    ) public validUpdate(_id) onlyAuthorizedById(_id, VIDEO_UPDATE_ROLE) {
        bytes memory input = Base64.decode(_newThumbnailURL);
        bytes32 _thumbnail;
        assembly {
            _thumbnail := mload(add(input, 32))
        }
        require(_thumbnail != 0x0, "INVALID_THUMBNAIL");
        videos[_id].arweaveThumbnail = _thumbnail;

        // Emit ThumbnailUpdated event
        emit ThumbnailUpdated(_id, _newThumbnailURL);
    }

    function updateVideo(
        uint64 _id,
        string memory _newVideoURL
    ) public validUpdate(_id) onlyAuthorizedById(_id, VIDEO_UPDATE_ROLE) {
        bytes memory input = Base64.decode(_newVideoURL);
        bytes32 _video;
        assembly {
            _video := mload(add(input, 32))
        }
        require(_video != 0x0, "INVALID_VIDEO");
        videos[_id].arweaveVideo = _video;

        // Emit VideoUpdated event
        emit VideoUpdated(_id, _newVideoURL);
    }

    function updateDescription(
        uint64 _id,
        string memory _newDescription
    ) public validUpdate(_id) onlyAuthorizedById(_id, VIDEO_UPDATE_ROLE) {
        require(bytes(_newDescription).length > 0, "INVALID_DESCRIPTION");
        videos[_id].description = _newDescription;

        // Emit DescriptionUpdated event
        emit DescriptionUpdated(_id, _newDescription);
    }

    function updatePackedData(
        uint64 _id,
        bytes16 _newPackedData
    ) public validUpdate(_id) onlyAuthorizedById(_id, VIDEO_UPDATE_ROLE) {
        require(_newPackedData != 0x0, "INVALID_COMBINED_FIELD");
        videos[_id].packedData = _newPackedData;

        // Emit PackedDataUpdated event
        emit PackedDataUpdated(_id, _newPackedData);
    }

    function updateSymbol(
        uint64 _id,
        string memory _newSymbol
    ) public validUpdate(_id) onlyAuthorizedById(_id, VIDEO_UPDATE_ROLE) {
        bytes32 newPackedData = calculatePackedData(
            PackedDataParams({
                symbol: _newSymbol,
                created: getCreated(_id),
                hidden: isHidden(_id),
                frozen: isFrozen(_id),
                licenseType: getLicenseType(_id),
                nsfw: isNSFW(_id),
                tagHashes: getTags(_id)
            })
        );

        videos[_id].packedData = newPackedData;

        // Emit SymbolUpdated event
        emit SymbolUpdated(_id, _newSymbol);
    }

    function updateFrozenStatus(
        uint64 _id,
        bool _newFrozenStatus
    ) public validUpdate(_id) onlyAuthorizedById(_id, VIDEO_UPDATE_ROLE) {
        bytes32 newPackedData = calculatePackedData(
            PackedDataParams({
                symbol: getSymbol(_id),
                created: getCreated(_id),
                hidden: isHidden(_id),
                frozen: _newFrozenStatus,
                licenseType: getLicenseType(_id),
                nsfw: isNSFW(_id),
                tagHashes: getTags(_id)
            })
        );

        videos[_id].packedData = newPackedData;

        // Emit FrozenStatusUpdated event
        emit FrozenStatusUpdated(_id, _newFrozenStatus);
    }

    function updateLicenseType(
        uint64 _id,
        uint8 _newLicenseType
    ) public validUpdate(_id) onlyAuthorizedById(_id, VIDEO_UPDATE_ROLE) {
        bytes32 newPackedData = calculatePackedData(
            PackedDataParams({
                symbol: getSymbol(_id),
                created: getCreated(_id),
                hidden: isHidden(_id),
                frozen: isFrozen(_id),
                licenseType: _newLicenseType,
                nsfw: isNSFW(_id),
                tagHashes: getTags(_id)
            })
        );

        videos[_id].packedData = newPackedData;

        // Emit LicenseTypeUpdated event
        emit LicenseTypeUpdated(_id, _newLicenseType);
    }

    function updateNSFWStatus(
        uint64 _id,
        bool _newNSFWStatus
    ) public validUpdate(_id) onlyAuthorizedById(_id, VIDEO_UPDATE_ROLE) {
        bytes32 newPackedData = calculatePackedData(
            PackedDataParams({
                symbol: getSymbol(_id),
                created: getCreated(_id),
                hidden: isHidden(_id),
                frozen: isFrozen(_id),
                licenseType: getLicenseType(_id),
                nsfw: _newNSFWStatus,
                tagHashes: getTags(_id)
            })
        );

        videos[_id].packedData = newPackedData;

        // Emit NSFWStatusUpdated event
        emit NSFWStatusUpdated(_id, _newNSFWStatus);
    }

    function updateTags(
        uint64 _id,
        string[] memory _newTags
    ) public validUpdate(_id) onlyAuthorizedById(_id, VIDEO_UPDATE_ROLE) {
        bytes32 newPackedData = calculatePackedData(
            PackedDataParams({
                symbol: getSymbol(_id),
                created: getCreated(_id),
                hidden: isHidden(_id),
                frozen: isFrozen(_id),
                licenseType: getLicenseType(_id),
                nsfw: isNSFW(_id),
                tagHashes: calculateTagHashes(_newTags)
            })
        );

        videos[_id].packedData = newPackedData;

        // Emit CategoriesUpdated event
        emit TagsUpdated(_id, _newTags);
    }

    function getTitle(uint64 _id) public view returns (string memory) {
        bytes memory titleBytes = new bytes(96);
        uint64 actualLength;

        for (uint64 i = 0; i < 32; i++) {
            titleBytes[i] = videos[_id].title1[i];
            titleBytes[32 + i] = videos[_id].title2[i];
            titleBytes[64 + i] = videos[_id].title3[i];
        }

        for (uint64 j = 0; j < titleBytes.length; j++) {
            if (titleBytes[j] == 0x00) {
                actualLength = j;
                break;
            }
        }

        bytes memory trimmedTitleBytes = new bytes(actualLength);
        for (uint64 k = 0; k < actualLength; k++) {
            trimmedTitleBytes[k] = titleBytes[k];
        }
        return string(trimmedTitleBytes);
    }

    function getThumbnail(uint64 _id) public view returns (string memory) {
        bytes32 input = videos[_id].arweaveThumbnail;
        bytes memory output = new bytes(32);
        assembly {
            mstore(add(output, 32), input)
        }
        return Base64.encode(output, true, true);
    }

    function getVideo(uint64 _id) public view returns (string memory) {
        bytes32 input = videos[_id].arweaveVideo;
        bytes memory output = new bytes(32);
        assembly {
            mstore(add(output, 32), input)
        }
        return Base64.encode(output, true, true);
    }

    function getDescription(uint64 _id) public view returns (string memory) {
        return videos[_id].description;
    }

    function getSymbol(uint64 _id) public view returns (string memory) {
        bytes32 packedData = videos[_id].packedData;
        uint48 symbol = uint48(uint256(packedData) >> 208);
        bytes6 symbolBytes = bytes6(symbol);

        // Count the number of non-null characters
        uint8 nonNullChars = 0;
        for (uint8 i = 0; i < 6; i++) {
            if (symbolBytes[i] != 0) {
                nonNullChars++;
            } else {
                break;
            }
        }

        // Create a new array with the appropriate size and copy the non-null characters
        bytes memory trimmedSymbol = new bytes(nonNullChars);
        for (uint8 i = 0; i < nonNullChars; i++) {
            trimmedSymbol[i] = symbolBytes[i];
        }

        return string(trimmedSymbol);
    }

    function getCreated(uint64 _id) public view returns (uint64) {
        bytes32 packedData = videos[_id].packedData;
        return uint64(uint256(packedData) >> 144);
    }

    function isHidden(uint64 _id) public view returns (bool) {
        bytes32 packedData = videos[_id].packedData;
        return (uint256(packedData) >> 96) & 1 == 1;
    }

    function isFrozen(uint64 _id) public view returns (bool) {
        bytes32 packedData = videos[_id].packedData;
        return (uint256(packedData) >> 97) & 1 == 1;
    }

    function getLicenseType(uint64 _id) public view returns (uint8) {
        bytes32 packedData = videos[_id].packedData;
        return uint8((uint256(packedData) >> 98) & 0xFF);
    }

    function isNSFW(uint64 _id) public view returns (bool) {
        bytes32 packedData = videos[_id].packedData;
        return (uint256(packedData) >> 106) & 1 == 1;
    }

    function getTags(uint64 _id) public view returns (uint16[] memory) {
        bytes32 packedData = videos[_id].packedData;
        uint112 packedTags = uint112(uint256(packedData) & 0xFFFFFFFFFFFF);
        uint8 tagCount = 0;

        // Count the number of non-zero tags
        for (uint8 i = 0; i < TAGS_LIMIT; i++) {
            uint16 tagHash = uint16(packedTags >> (i * 16));
            if (tagHash != 0) {
                tagCount++;
            }
        }

        // Create a new array with the appropriate size and fill it with the non-zero tags
        uint16[] memory tagHashes = new uint16[](tagCount);
        uint8 index = 0;
        for (uint8 i = 0; i < TAGS_LIMIT; i++) {
            uint16 tagHash = uint16(packedTags >> (i * 16));
            if (tagHash != 0) {
                tagHashes[index] = tagHash;
                index++;
            }
        }

        return tagHashes;
    }

    function calculateTitleBytes32(
        bytes memory titleBytes
    ) public pure returns (bytes32 title1, bytes32 title2, bytes32 title3) {
        require(titleBytes.length <= 96, "TITLE_TOO_LONG");

        // bytes memory titleBytes = bytes(title);
        uint32 i = 0;

        for (; i < 32 && i < titleBytes.length; i++) {
            title1 |= bytes32(
                uint(uint8(titleBytes[i])) * (2 ** (8 * (31 - i)))
            );
        }
        for (; i < 64 && i < titleBytes.length; i++) {
            title2 |= bytes32(
                uint(uint8(titleBytes[i])) * (2 ** (8 * (63 - i)))
            );
        }
        for (; i < 96 && i < titleBytes.length; i++) {
            title3 |= bytes32(
                uint(uint8(titleBytes[i])) * (2 ** (8 * (95 - i)))
            );
        }
    }

    // music     gaming
    // VVV         V
    // 1000000000100000000000000000

    // UP TO 7
    // 16bits(hash("asfaa"))
    // [0000000000000000] =>
    // [0000000000000000][0000000000000000][0000000000000000][0000000000000000][0000000000000000][0000000000000000][0000000000000000]

    struct PackedDataParams {
        string symbol;
        uint64 created;
        bool hidden;
        bool frozen;
        uint8 licenseType;
        bool nsfw;
        uint16[] tagHashes;
    }

    function calculatePackedData(
        PackedDataParams memory params
    ) public pure returns (bytes32) {
        uint48 symbol = uint48(bytes6(bytes(params.symbol)));
        uint64 created = params.created;
        uint8 hidden = params.hidden ? 1 : 0;
        uint8 frozen = params.frozen ? 1 : 0;
        uint8 licenseType = params.licenseType;
        uint8 nsfw = params.nsfw ? 1 : 0;
        uint112 tags = 0;

        for (uint8 i = 0; i < params.tagHashes.length && i < TAGS_LIMIT; i++) {
            tags |= uint112(params.tagHashes[i]) << (i * 16);
        }
        return
            (bytes32(uint(symbol)) << 208) |
            (bytes32(uint(created)) << 144) |
            (bytes32(uint(hidden)) << 96) |
            (bytes32(uint(frozen)) << 97) |
            (bytes32(uint(licenseType)) << 98) |
            (bytes32(uint(nsfw)) << 106) |
            (bytes32(uint(tags)));
    }

    function calculateTagHashes(
        string[] memory _tags
    ) public pure returns (uint16[] memory) {
        uint16[] memory tagHashes = new uint16[](_tags.length);

        for (uint8 i = 0; i < _tags.length && i < TAGS_LIMIT; i++) {
            tagHashes[i] = uint16(
                bytes2(keccak256(abi.encodePacked(_tags[i])))
            );
        }

        return tagHashes;
    }

    function arweaveIdToBytes32(
        string memory id
    ) public pure returns (bytes32) {
        bytes memory input = Base64.decode(id);
        bytes32 output;
        assembly {
            output := mload(add(input, 32))
        }
        return output;
    }
}