// File: contracts/PCBAuction.sol



// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./libs/ExcessivelySafeCall.sol";

contract PCBAuction is Ownable {
    struct Auction {
        uint128 start;
        uint128 end;
        uint96 highestBid;
        address highestBidder;
    }

    event AuctionCreated(uint256 id, uint256 start, uint256 end, uint256 minBid);
    event AuctionExtended(uint256 indexed id, uint256 newDeadline);
    event Bid(uint256 indexed id, address bidder, uint256 value);

    address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address constant CERT_DB = 0xbb87327F1Bf2c5a89fb97009d7b415EF48794f54;

    mapping(uint256 => Auction) public auctions;
    mapping(uint256 => bool) public collected;
    mapping(uint256 => bytes32) public idToJsonHash;
    mapping(uint256 => address) public grantedTo;
    mapping(uint256 => uint256) public idToWhitelistedCert;


    function bid(
        uint256 id
    ) external payable {
        Auction memory auctionPre = auctions[id];
        require(
            auctionPre.start <= block.timestamp
            && block.timestamp <= auctionPre.end
        );
        require(
            msg.value >= auctionPre.highestBid*11_000/10_000
            || (
                auctionPre.highestBidder == address(0)
                && msg.value >= auctionPre.highestBid
            )
        );

        auctions[id].highestBid = uint96(msg.value);
        auctions[id].highestBidder = msg.sender;
        emit Bid(id, msg.sender, msg.value);

        if (auctionPre.end - block.timestamp <= 5 minutes) {
            uint128 newTime = auctionPre.end + 5 minutes;
            auctions[id].end = newTime;
            emit AuctionExtended(id, newTime);
        }

        if (auctionPre.highestBidder != address(0)) {
            (bool success,) = ExcessivelySafeCall.excessivelySafeCall(
                auctionPre.highestBidder,
                30_000,
                auctionPre.highestBid,
                0,
                ""
            );
            if (!success) {
                IWETH(WETH).deposit{value: auctionPre.highestBid}();
                IWETH(WETH).transfer(auctionPre.highestBidder, auctionPre.highestBid);
            }
        }
    }

    function canMint(
        address to,
        uint256 id,
        bytes32 jsonHash
    ) external view returns (bool) {
        if (jsonHash != idToJsonHash[id]) {
            return false;
        }
        Auction memory auction = auctions[id];
        if (auction.end == 0 && _isWhitelisted(to, id)) {
            return true;
        }
        return (auction.end < block.timestamp)
            && (
                auction.highestBidder == to
                || (
                    auction.highestBidder == address(0)
                    && to == owner()
                )
            );
    }

    function createAuction(
        uint256 id,
        bytes32 jsonHash,
        uint256 start,
        uint256 end,
        uint256 startingBid
    ) external onlyOwner {
        _createAuction(id, jsonHash, start, end, startingBid);
    }

    function createAuctionBatch(
        uint256[] calldata ids,
        bytes32[] calldata hashes,
        uint256 start,
        uint256 end,
        uint256 startingBid
    ) external onlyOwner {
        require(ids.length == hashes.length);
        for (uint256 i; i < ids.length; i++) {
            _createAuction(ids[i], hashes[i], start, end, startingBid);
        }
    }

    function distributeProceeds(
        uint256[] calldata ids
    ) external onlyOwner {
        uint256 total;

        for(uint256 i; i < ids.length; i++) {
            Auction memory auction = auctions[ids[i]];
            if(
                auction.highestBidder != address(0) &&
                auction.end < block.timestamp &&
                collected[ids[i]] == false
            ) {
                total += auction.highestBid;
                collected[ids[i]] = true;
            }
        }

        (bool success,) = owner().call{value: total}("");
        require(success);
    }

    function grantMint(
        address to,
        uint256 id,
        bytes32 jsonHash
    ) external onlyOwner {
        _grantMint(to, id, jsonHash);
    }

    function grantMintBatch(
        address[] calldata grantees,
        uint256[] calldata ids,
        bytes32[] calldata hashes
    ) external onlyOwner {
        require(
            grantees.length == ids.length
            && ids.length == hashes.length
        );
        for (uint256 i; i < ids.length; i++) {
            _grantMint(grantees[i], ids[i], hashes[i]);
        }
    }

    function updateJsonHash(
        uint256 id,
        bytes32 jsonHash
    ) external onlyOwner {
        _updateJsonHash(id, jsonHash);
    }

    function updateJsonHashBatch(
        uint256[] calldata ids,
        bytes32[] calldata hashes
    ) external onlyOwner {
        require(ids.length == hashes.length);
        for (uint256 i; i < ids.length; i++) {
            _updateJsonHash(ids[i], hashes[i]);
        }
    }

    function whitelistCert(
        uint256 tokenId,
        uint256 certId,
        bytes32 jsonHash
    ) external onlyOwner {
        _whitelistCert(tokenId, certId, jsonHash);
    }

    function whitelistCertBatch(
        uint256[] calldata tokenIds,
        bytes32[] calldata hashes,
        uint256 certId
    ) external onlyOwner {
        require(tokenIds.length == hashes.length);
        for (uint256 i; i < tokenIds.length; i++) {
            _whitelistCert(tokenIds[i], certId, hashes[i]);
        }
    }

    function _createAuction(
        uint256 id,
        bytes32 jsonHash,
        uint256 start,
        uint256 end,
        uint256 startingBid
    ) internal {
        require(auctions[id].end == 0);
        require(start < end);

        auctions[id] = Auction(
            uint128(start),
            uint128(end),
            uint96(startingBid),
            address(0)
        );
        _updateJsonHash(id, jsonHash);

        emit AuctionCreated(id, start, end, startingBid);
    }

    function _grantMint(
        address to,
        uint256 id,
        bytes32 jsonHash
    ) internal {
        grantedTo[id] = to;
        _updateJsonHash(id, jsonHash);
    }

    function _updateJsonHash(
        uint256 id,
        bytes32 jsonHash
    ) internal {
        if(jsonHash != bytes32(0)) {
            idToJsonHash[id] = jsonHash;
        }
    }

    function _whitelistCert(
        uint256 tokenId,
        uint256 certId,
        bytes32 jsonHash
    ) internal {
        idToWhitelistedCert[tokenId] = certId;
        _updateJsonHash(tokenId, jsonHash);
    }

    function _isWhitelisted(
        address to,
        uint256 id
    ) internal view returns (bool) {
        if (to == owner()) {
            return true;
        }
        address grantee = grantedTo[id];
        if (grantee == address(0)) {
            uint256 certId = idToWhitelistedCert[id];
            return certId > 0 && ERC1155(CERT_DB).balanceOf(to, certId) > 0;
        }
        return grantee == to;
    }
}

interface IWETH {
    function deposit() external payable;
    function transfer(address to, uint256 amount) external;
}

interface ERC1155 {
    function balanceOf(address account, uint256 id) external view returns (uint256);
}
// File: contracts/libs/ExcessivelySafeCall.sol



// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.7.6;

library ExcessivelySafeCall {
    uint256 constant LOW_28_MASK =
        0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff;

    /// @notice Use when you _really_ really _really_ don't trust the called
    /// contract. This prevents the called contract from causing reversion of
    /// the caller in as many ways as we can.
    /// @dev The main difference between this and a solidity low-level call is
    /// that we limit the number of bytes that the callee can cause to be
    /// copied to caller memory. This prevents stupid things like malicious
    /// contracts returning 10,000,000 bytes causing a local OOG when copying
    /// to memory.
    /// @param _target The address to call
    /// @param _gas The amount of gas to forward to the remote contract
    /// @param _value The value in wei to send to the remote contract
    /// @param _maxCopy The maximum number of bytes of returndata to copy
    /// to memory.
    /// @param _calldata The data to send to the remote contract
    /// @return success and returndata, as `.call()`. Returndata is capped to
    /// `_maxCopy` bytes.
    function excessivelySafeCall(
        address _target,
        uint256 _gas,
        uint256 _value,
        uint16 _maxCopy,
        bytes memory _calldata
    ) internal returns (bool, bytes memory) {
        // set up for assembly call
        uint256 _toCopy;
        bool _success;
        bytes memory _returnData = new bytes(_maxCopy);
        // dispatch message to recipient
        // by assembly calling "handle" function
        // we call via assembly to avoid memcopying a very large returndata
        // returned by a malicious contract
        assembly {
            _success := call(
                _gas, // gas
                _target, // recipient
                _value, // ether value
                add(_calldata, 0x20), // inloc
                mload(_calldata), // inlen
                0, // outloc
                0 // outlen
            )
            // limit our copy to 256 bytes
            _toCopy := returndatasize()
            if gt(_toCopy, _maxCopy) {
                _toCopy := _maxCopy
            }
            // Store the length of the copied bytes
            mstore(_returnData, _toCopy)
            // copy the bytes from returndata[0:_toCopy]
            returndatacopy(add(_returnData, 0x20), 0, _toCopy)
        }
        return (_success, _returnData);
    }

    /// @notice Use when you _really_ really _really_ don't trust the called
    /// contract. This prevents the called contract from causing reversion of
    /// the caller in as many ways as we can.
    /// @dev The main difference between this and a solidity low-level call is
    /// that we limit the number of bytes that the callee can cause to be
    /// copied to caller memory. This prevents stupid things like malicious
    /// contracts returning 10,000,000 bytes causing a local OOG when copying
    /// to memory.
    /// @param _target The address to call
    /// @param _gas The amount of gas to forward to the remote contract
    /// @param _maxCopy The maximum number of bytes of returndata to copy
    /// to memory.
    /// @param _calldata The data to send to the remote contract
    /// @return success and returndata, as `.call()`. Returndata is capped to
    /// `_maxCopy` bytes.
    function excessivelySafeStaticCall(
        address _target,
        uint256 _gas,
        uint16 _maxCopy,
        bytes memory _calldata
    ) internal view returns (bool, bytes memory) {
        // set up for assembly call
        uint256 _toCopy;
        bool _success;
        bytes memory _returnData = new bytes(_maxCopy);
        // dispatch message to recipient
        // by assembly calling "handle" function
        // we call via assembly to avoid memcopying a very large returndata
        // returned by a malicious contract
        assembly {
            _success := staticcall(
                _gas, // gas
                _target, // recipient
                add(_calldata, 0x20), // inloc
                mload(_calldata), // inlen
                0, // outloc
                0 // outlen
            )
            // limit our copy to 256 bytes
            _toCopy := returndatasize()
            if gt(_toCopy, _maxCopy) {
                _toCopy := _maxCopy
            }
            // Store the length of the copied bytes
            mstore(_returnData, _toCopy)
            // copy the bytes from returndata[0:_toCopy]
            returndatacopy(add(_returnData, 0x20), 0, _toCopy)
        }
        return (_success, _returnData);
    }

    /**
     * @notice Swaps function selectors in encoded contract calls
     * @dev Allows reuse of encoded calldata for functions with identical
     * argument types but different names. It simply swaps out the first 4 bytes
     * for the new selector. This function modifies memory in place, and should
     * only be used with caution.
     * @param _newSelector The new 4-byte selector
     * @param _buf The encoded contract args
     */
    function swapSelector(bytes4 _newSelector, bytes memory _buf)
        internal
        pure
    {
        require(_buf.length >= 4);
        uint256 _mask = LOW_28_MASK;
        assembly {
            // load the first word of
            let _word := mload(add(_buf, 0x20))
            // mask out the top 4 bytes
            // /x
            _word := and(_word, _mask)
            _word := or(_newSelector, _word)
            mstore(add(_buf, 0x20), _word)
        }
    }
}
// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
        _;
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}