// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

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

/**
 * @title Auction contract which allows placing bids on cascade auctions
 * @dev This contract implements a set of auctions for different artwork items.
 * Each artwork has one item up for auction at a time.
 * Bidders can place bids on an individual item of an artwork for at least MIN_BID_SPREAD more than the previous bid.
 * With each bid, the end time of the auction increases, but during the free bid period, the deadline does not increase.
 * The auction ends when the block's timestamp exceeds the deadline without any other bids.
 * Bids placed on an ended auction will close the auction and start a new one, if there are still auctions available.
 * Sales are launched for each artwork with a specified starting price and minimum bid spread.
 * Bidders who are outbid can place another bid by sending only the difference from their previous bid, their available balance will be deducted accordingly.
 * The contract owner can withdraw the profit generated by the auctions.
 */
contract AuctionOrderBook is Ownable {
    struct Auction {
        uint56 endAt;
        uint56 startedAt;
        uint128 highestBid;
        address highestBidder;
    }

    struct ArtworkAuctions {
        uint8 currentAuction;
        Auction[] auctions;
    }

    uint8 public constant ARTWORK_COUNT = 34;
    uint128 public constant NEXT_SALE_PRICE_PERCENTAGE = 85;
    mapping(uint256 => uint8) public SUPPLY;

    uint56 public immutable FREE_BID_PERIOD;
    uint128 public immutable MIN_BID_SPREAD;

    mapping(address => uint128) private availableBalances;
    uint256 private profitAmount;

    ArtworkAuctions[ARTWORK_COUNT] private artworkToAuctions;

    event Start(
        uint256 indexed artworkId,
        uint8 indexed currentAuction,
        uint128 amount,
        uint56 endAt
    );
    event Bid(
        uint256 indexed artworkId,
        uint8 indexed currentAuction,
        address indexed bidder,
        uint128 amount,
        uint56 endAt
    );
    event End(
        uint256 indexed artworkId,
        uint8 indexed currentAuction,
        address indexed bidder,
        uint128 lastBid
    );
    event Withdraw(address indexed account, uint256 amount);
    event WithdrawProfit(uint256 profitAmount);

    constructor(
        uint8[ARTWORK_COUNT] memory _supply,
        uint256 _startPrice,
        uint256 _minBidSpread,
        uint256 _freeBidPeriod
    ) {
        for (uint256 i; i < _supply.length; ++i) {
            require(_supply[i] != 0, "Cannot have zero supply");
            SUPPLY[i] = _supply[i];
        }
        MIN_BID_SPREAD = uint128(_minBidSpread);
        FREE_BID_PERIOD = uint56(_freeBidPeriod);

        for (uint256 i; i < _supply.length; ++i) {
            _launchSale(i, _startPrice, block.timestamp);
        }
    }

    /**
     * @dev Allows a user to place a bid on an item up for auction.
     * @param _artworkId The ID of the item to bid on.
     * @param _bidAmount The amount of the bid.
     */
    function bid(uint256 _artworkId, uint256 _bidAmount) external payable {
        ArtworkAuctions storage artworkAuctions = artworkToAuctions[_artworkId];

        for (
            uint256 version = artworkAuctions.currentAuction;
            version < SUPPLY[_artworkId] &&
                block.timestamp > artworkAuctions.auctions[version].endAt;
            ++version
        ) {
            _closeSale(_artworkId);
        }
        require(
            artworkAuctions.currentAuction < SUPPLY[_artworkId],
            "No auction for this artwork"
        );

        Auction memory auction = artworkAuctions.auctions[
            artworkAuctions.currentAuction
        ];

        require(
            msg.sender != auction.highestBidder,
            "You are already the best bidder"
        );

        // Must increase by at least MIN_BID_SPREAD unless it's the first bid
        require(
            uint128(_bidAmount) >=
                auction.highestBid +
                    (auction.highestBidder != address(0) ? MIN_BID_SPREAD : 0),
            "Increase at least MIN_BID_SPREAD"
        );

        uint128 availableBalance = availableBalances[msg.sender] +
            uint128(msg.value);

        require(
            availableBalance >= uint128(_bidAmount),
            "Insufficient funds sent"
        );

        unchecked {
            availableBalances[msg.sender] =
                availableBalance -
                uint128(_bidAmount);
        }

        if (auction.highestBidder == address(0)) {
            // First bidder should lead auction for 12 hours after free bid period to win
            auction.endAt = auction.startedAt + FREE_BID_PERIOD + 12 hours;
        } else {
            // Give amount back to the previous bidder
            availableBalances[auction.highestBidder] += auction.highestBid;

            // If free bid period is over, increase the deadline
            if (block.timestamp > auction.startedAt + FREE_BID_PERIOD) {
                auction.endAt = uint56(block.timestamp) + 6 hours;
            }
        }

        // Register the bid
        auction.highestBidder = msg.sender;
        auction.highestBid = uint128(_bidAmount);

        artworkAuctions.auctions[artworkAuctions.currentAuction] = auction;

        emit Bid(
            _artworkId,
            artworkAuctions.currentAuction,
            msg.sender,
            uint128(_bidAmount),
            auction.endAt
        );
    }

    /**
     * @dev Allows a user to withdraw their available balance.
     */
    function withdraw() external {
        uint128 availableBalance = availableBalances[msg.sender];
        availableBalances[msg.sender] = 0;

        (bool sent, ) = msg.sender.call{value: availableBalance}("");
        require(sent, "withdrawal failed");

        emit Withdraw(msg.sender, availableBalance);
    }

    /**
     * @dev Allows the contract owner to withdraw profit from the contract.
     */
    function withdrawProfit() external onlyOwner {
        ArtworkAuctions[ARTWORK_COUNT]
            storage artworkToAuctions_ = artworkToAuctions;

        for (uint8 artworkId; artworkId < ARTWORK_COUNT; ++artworkId) {
            ArtworkAuctions storage artworkAuctions = artworkToAuctions_[
                artworkId
            ];

            for (
                uint256 version = artworkAuctions.currentAuction;
                version < SUPPLY[artworkId] &&
                    block.timestamp > artworkAuctions.auctions[version].endAt;
                ++version
            ) {
                _closeSale(artworkId);
            }
        }

        uint256 availableProfit = profitAmount;
        profitAmount = 0;

        (bool sent, ) = msg.sender.call{value: availableProfit}("");
        require(sent, "withdrawal failed");

        emit WithdrawProfit(availableProfit);
    }

    /**
     * @dev Returns the available balance of a given user.
     * @param _user The address of the user to check.
     * @return The user's available balance.
     */
    function availableBalanceOf(address _user) external view returns (uint128) {
        return availableBalances[_user];
    }

    /**
     * @dev Returns the current auction details for a specific artwork
     * @param _artworkId The ID of the artwork whose auction details are being retrieved
     * @return The current auction details for the artwork
     */
    function currentAuctionOf(
        uint256 _artworkId
    ) external view returns (Auction memory, uint8) {
        ArtworkAuctions memory artworkAuctions = artworkToAuctions[_artworkId];
        uint8 version = artworkAuctions.currentAuction;

        require(version < SUPPLY[_artworkId], "No auction for this artwork");

        Auction memory auction = artworkAuctions.auctions[version];

        while (
            block.timestamp > auction.endAt && ++version < SUPPLY[_artworkId]
        ) {
            auction.startedAt = auction.endAt + 1;
            auction.endAt = auction.startedAt + FREE_BID_PERIOD;
            auction.highestBid =
                (auction.highestBid * NEXT_SALE_PRICE_PERCENTAGE) /
                100;
            auction.highestBidder = address(0);
        }

        require(version < SUPPLY[_artworkId], "No auction for this artwork");

        return (auction, version);
    }

    /**
     * @dev Returns the current auction details for a specific artwork & version
     * @param _artworkId id of the artwork
     * @param _version version of the artwork
     * @return The current auction details for the artwork
     */
    function auctionOf(
        uint256 _artworkId,
        uint256 _version
    ) external view returns (Auction memory) {
        require(
            _version <= artworkToAuctions[_artworkId].currentAuction,
            "No auction for this artwork"
        );

        return artworkToAuctions[_artworkId].auctions[_version];
    }

    /**
     * @dev Launches a new auction for the given artworkId, setting the initial bid to _startPrice
     * and the initial auction end time to now + FREE_BID_PERIOD.
     * @dev Assumes currentAuction has already been incremented.
     * @param _artworkId The ID of the artwork being auctioned
     * @param _startPrice The initial bid price for the auction
     */
    function _launchSale(
        uint256 _artworkId,
        uint256 _startPrice,
        uint256 _startAt
    ) private {
        Auction[] storage artworkAuctions = artworkToAuctions[_artworkId]
            .auctions;

        Auction memory auction;
        auction.startedAt = uint56(_startAt);
        auction.endAt = auction.startedAt + FREE_BID_PERIOD;
        auction.highestBid = uint128(_startPrice);
        artworkAuctions.push(auction);

        emit Start(
            _artworkId,
            uint8(artworkAuctions.length - 1),
            auction.highestBid,
            auction.endAt
        );
    }

    /**
     * @dev Closes the current auction for a given artwork and starts the next one.
     * @param _artworkId The ID of the artwork.
     */
    function _closeSale(uint256 _artworkId) private {
        ArtworkAuctions storage artworkAuctions = artworkToAuctions[_artworkId];
        Auction storage auction = artworkAuctions.auctions[
            artworkAuctions.currentAuction
        ];

        if (auction.highestBidder != address(0)) {
            profitAmount += auction.highestBid;
        }

        emit End(
            _artworkId,
            artworkAuctions.currentAuction,
            auction.highestBidder,
            auction.highestBid
        );

        // When currentAuction is equal to SUPPLY, there is no item left to auction off
        if (++artworkAuctions.currentAuction < SUPPLY[_artworkId]) {
            // The price of the next item starts at NEXT_SALE_PRICE_PERCENTAGE % of the previous item's closing price
            _launchSale(
                _artworkId,
                (auction.highestBid * NEXT_SALE_PRICE_PERCENTAGE) / 100,
                auction.endAt + 1
            );
        }
    }
}
[{"inputs":[{"internalType":"uint8[34]","name":"_supply","type":"uint8[34]"},{"internalType":"uint256","name":"_startPrice","type":"uint256"},{"internalType":"uint256","name":"_minBidSpread","type":"uint256"},{"internalType":"uint256","name":"_freeBidPeriod","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"artworkId","type":"uint256"},{"indexed":true,"internalType":"uint8","name":"currentAuction","type":"uint8"},{"indexed":true,"internalType":"address","name":"bidder","type":"address"},{"indexed":false,"internalType":"uint128","name":"amount","type":"uint128"},{"indexed":false,"internalType":"uint56","name":"endAt","type":"uint56"}],"name":"Bid","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"artworkId","type":"uint256"},{"indexed":true,"internalType":"uint8","name":"currentAuction","type":"uint8"},{"indexed":true,"internalType":"address","name":"bidder","type":"address"},{"indexed":false,"internalType":"uint128","name":"lastBid","type":"uint128"}],"name":"End","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"artworkId","type":"uint256"},{"indexed":true,"internalType":"uint8","name":"currentAuction","type":"uint8"},{"indexed":false,"internalType":"uint128","name":"amount","type":"uint128"},{"indexed":false,"internalType":"uint56","name":"endAt","type":"uint56"}],"name":"Start","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdraw","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"profitAmount","type":"uint256"}],"name":"WithdrawProfit","type":"event"},{"inputs":[],"name":"ARTWORK_COUNT","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FREE_BID_PERIOD","outputs":[{"internalType":"uint56","name":"","type":"uint56"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_BID_SPREAD","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NEXT_SALE_PRICE_PERCENTAGE","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"SUPPLY","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_artworkId","type":"uint256"},{"internalType":"uint256","name":"_version","type":"uint256"}],"name":"auctionOf","outputs":[{"components":[{"internalType":"uint56","name":"endAt","type":"uint56"},{"internalType":"uint56","name":"startedAt","type":"uint56"},{"internalType":"uint128","name":"highestBid","type":"uint128"},{"internalType":"address","name":"highestBidder","type":"address"}],"internalType":"struct AuctionOrderBook.Auction","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_user","type":"address"}],"name":"availableBalanceOf","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_artworkId","type":"uint256"},{"internalType":"uint256","name":"_bidAmount","type":"uint256"}],"name":"bid","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_artworkId","type":"uint256"}],"name":"currentAuctionOf","outputs":[{"components":[{"internalType":"uint56","name":"endAt","type":"uint56"},{"internalType":"uint56","name":"startedAt","type":"uint56"},{"internalType":"uint128","name":"highestBid","type":"uint128"},{"internalType":"address","name":"highestBidder","type":"address"}],"internalType":"struct AuctionOrderBook.Auction","name":"","type":"tuple"},{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawProfit","outputs":[],"stateMutability":"nonpayable","type":"function"}]