// File: /contracts/DutchAuction.sol



// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.12;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./interface/IBapTickets.sol";

/// @title Bulls and Apes Project - DutchAuction
/// @author BAP Dev Team
/// @notice DutchAuction contract for the BAP Tickets
contract DutchAuction is Ownable {
    /// @notice TicketHelper contract address
    IBapTickets public ticketHelper;

    /// @notice Auction IDs counter
    uint256 public auctionIds;

    /// @notice Signer address
    address public secret;

    /// @notice Auction struct
    struct Auction {
        uint256 startAt;
        uint256 eventId;
        uint256 startingPrice;
        uint256 endPrice;
        uint256 finalPrice;
        uint256 discountRate;
        uint256 ticketsForBid;
        uint256 biddersCount;
        bool isAuction;
    }

    /// @notice Auctions mapping
    mapping(uint256 => Auction) public auctions;

    /// @notice Tickets per Auction per User
    mapping(uint256 => mapping(address => uint256)) public ticketPerUser;
    /// @notice Bid money per Auction per User
    mapping(uint256 => mapping(address => uint256)) public bidMoneyPerUser;
    /// @notice Bidder position per Auction
    mapping(uint256 => mapping(address => uint256)) public bidderPosition;
    /// @notice Bidder Address per Auction per Position
    mapping(uint256 => mapping(uint256 => address)) public bidders;
    /// @notice Bid ETH is claimed per Auction per User
    mapping(uint256 => mapping(address => bool)) public isClaimed;
    /// @notice Ticket is minted per Auction per User
    mapping(uint256 => mapping(address => bool)) public isMinted;

    /// @notice Auction Created event
    event AuctionCreated(
        uint256 auctionId,
        uint256 eventId,
        uint256 startingPrice,
        uint256 endPrice,
        uint256 discountRate,
        uint256 ticketsForBid
    );

    /// @notice Bid Placed event
    event BidPlaced(
        uint256 auctionId,
        address bidder,
        uint256 amount,
        uint256 price
    );

    /// @notice Auction Ended event
    event AuctionEnded(uint256 auctionId, uint256 finalPrice, uint256 bids);

    /// @notice Bid Collected event
    event BidCollected(
        uint256 auctionId,
        address bidder,
        uint256 amount,
        uint256 price
    );

    /// @notice Ticket Minted event
    event TicketPrinted(
        uint256 auctionId,
        address bidder,
        uint256 amount,
        uint256 price,
        uint256 refund
    );

    /// @notice Auction constructor, deploys the contract
    /// @param ticketsHelperAddress IBapTickets contract address
    constructor(address ticketsHelperAddress, address _secret) {
        ticketHelper = IBapTickets(ticketsHelperAddress);
        secret = _secret;
    }

    /// @notice Create an auction
    /// @param eventId Event ID of the ticket to be auctioned
    /// @param startingPrice Starting price of the auction
    /// @param endPrice End price of the auction
    /// @param discountRate Discount rate of the auction
    /// @param ticketsForBid Number of tickets to be auctioned
    /// @dev creates a new auction with the given parameters and emits an AuctionCreated event
    function createAuction(
        uint256 eventId,
        uint256 startingPrice,
        uint256 endPrice,
        uint256 discountRate,
        uint256 ticketsForBid
    ) external onlyOwner {
        require(
            startingPrice > endPrice,
            "createAuction: Starting price must be greater than end price"
        );
        require(
            ticketsForBid > 0,
            "createAuction: You must bid at least one ticket"
        );        

        auctionIds++;

        auctions[auctionIds] = Auction(
            block.timestamp,
            eventId,
            startingPrice,
            endPrice,
            0,
            discountRate,
            ticketsForBid,
            0,
            true
        );      

        emit AuctionCreated(
            auctionIds,
            eventId,
            startingPrice,
            endPrice,
            discountRate,
            ticketsForBid
        );
    }

    /// @notice Get the current price of the auction
    /// @param auctionId Auction ID
    /// @return price of the auction
    /// @dev calculates the current price of the auction with 1 hour precision
    function getPrice(uint256 auctionId) public view returns (uint) {
        Auction memory auction = auctions[auctionId];
        uint timeElapsed = block.timestamp - auction.startAt;
        uint hourElapsed = timeElapsed / 3600;
        uint discount = auction.discountRate * hourElapsed;

        if (discount > auction.startingPrice - auction.endPrice) {
            return auction.endPrice;
        }

        return auction.startingPrice - discount;
    }

    /// @notice Place a bid
    /// @param auctionId Auction ID
    /// @param amount Amount of tickets to bid
    /// @param signature Signature to verify the bid
    /// @dev places a bid for the given auction with the given amount of tickets
    function placeBid(
        uint256 auctionId,
        uint256 amount,
        bytes memory signature
    ) external payable {
        require(amount > 0, "placeBid: You must bid at least one ticket");
        // check signature validity using _verifyHashSignature
        require(
            _verifyHashSignature(
                keccak256(abi.encode(auctionId, amount, msg.sender)),
                signature
            ),
            "placeBid: Invalid signature"
        );

        uint price = getPrice(auctionId);
        require(
            msg.value >= price * amount,
            "placeBid: The amount of ETH sent is less than the price of token"
        );

        Auction storage auction = auctions[auctionId];

        require(auction.isAuction, "placeBid: This auction has ended");
        require(
            auction.ticketsForBid >= amount,
            "placeBid: Not enough tickets left for this auction"
        );

        // check if the user has already bid
        if (ticketPerUser[auctionId][msg.sender] == 0) {
            bidderPosition[auctionId][msg.sender] = auction.biddersCount;
            bidders[auctionId][auction.biddersCount] = msg.sender;

            auction.biddersCount++;
        }

        ticketPerUser[auctionId][msg.sender] += amount;
        bidMoneyPerUser[auctionId][msg.sender] += msg.value;

        auction.ticketsForBid -= amount;

        if (auction.ticketsForBid == 0) {
            auction.isAuction = false;
            auction.finalPrice = price;

            emit AuctionEnded(auctionId, price, auction.biddersCount);
        }

        emit BidPlaced(auctionId, msg.sender, amount, price);
    }

    /// @notice withdraw all bids from the auction
    /// @param auctionId Auction ID
    /// @param startIndex Start index of the bidders array
    /// @param endIndex End index of the bidders array
    /// @dev withdraws all bids from the auction for the given range of bidders
    function withdraw(
        uint256 auctionId,
        uint256 startIndex,
        uint256 endIndex
    ) external onlyOwner {
        require(
            startIndex < endIndex,
            "withdraw: startIndex must be less than endIndex"
        );
        require(
            endIndex <= auctions[auctionId].biddersCount,
            "withdraw: endIndex must be less than or equal to the number of bidders"
        );
        require(
            !auctions[auctionId].isAuction,
            "withdraw: The auction has not ended"
        );

        uint256 finalPrice = auctions[auctionId].finalPrice;
        uint256 totalAmount;

        for (uint i = startIndex; i < endIndex; i++) {
            address bidder = bidders[auctionId][i];
            // check that bid is not withdrawn
            if (!isClaimed[auctionId][bidder]) {
                isClaimed[auctionId][bidder] = true;

                uint256 amount = ticketPerUser[auctionId][bidder];
                totalAmount += amount * finalPrice;

                emit BidCollected(auctionId, bidder, amount, finalPrice);
            }
        }

        if (totalAmount > 0) {
            // transfer using call
            (bool success, ) = payable(msg.sender).call{value: totalAmount}("");
            require(success, "withdraw: Transfer failed.");
        }
    }

    /// @notice mint tickets for the auction
    /// @param auctionId Auction ID
    /// @param startIndex Start index of the bidders array
    /// @param endIndex End index of the bidders array
    /// @dev mints tickets for the auction for the given range of bidders
    function mintTickets(
        uint256 auctionId,
        uint256 startIndex,
        uint256 endIndex
    ) external {
        require(
            startIndex < endIndex,
            "printTickets: startIndex must be less than endIndex"
        );
        require(
            endIndex <= auctions[auctionId].biddersCount,
            "printTickets: endIndex must be less than or equal to the number of bidders"
        );
        require(
            !auctions[auctionId].isAuction,
            "printTickets: The auction has not ended"
        );

        Auction memory auction = auctions[auctionId];

        uint256 finalPrice = auctions[auctionId].finalPrice;

        for (uint i = startIndex; i < endIndex; i++) {
            // get bidder info
            address bidder = bidders[auctionId][i];
            uint256 ticketsToClaim = ticketPerUser[auctionId][bidder];
            uint256 ethPaid = bidMoneyPerUser[auctionId][bidder];
            // check that bid is not minted
            if (!isMinted[auctionId][bidder]) {
                isMinted[auctionId][bidder] = true;

                uint256 refund = ethPaid - (ticketsToClaim * finalPrice);

                // send refund using call if any
                if (refund > 0) {
                    (bool success, ) = payable(bidder).call{value: refund}("");
                    require(success, "printTickets: Transfer failed.");
                }

                // mint tickets
                ticketHelper.airdrop(
                    auction.eventId,
                    bidder,
                    ticketsToClaim
                );

                emit TicketPrinted(auctionId, bidder, ticketsToClaim, finalPrice, refund);
            }
        }
    }

    /// @notice end the auction
    /// @param auctionId Auction ID
    /// @dev manually ends the auction and calculates the final price
    function endAuction(uint256 auctionId) external onlyOwner {
        Auction storage auction = auctions[auctionId];

        require(auction.isAuction, "endAuction: The auction has already ended");

        auction.isAuction = false;
        auction.finalPrice = getPrice(auctionId);

        emit AuctionEnded(auctionId, auction.finalPrice, auction.biddersCount);
    }

    /// @notice claim tickets and refund
    /// @param auctionId Auction ID
    /// @dev claims tickets and refund for the given auction and sender
    function claim(uint256 auctionId) external {
        Auction memory auction = auctions[auctionId];
        require(!auction.isAuction, "claim: The auction has not ended");
        require(
            !isMinted[auctionId][msg.sender],
            "claim: You've already claimed"
        );

        uint256 ticketsToClaim = ticketPerUser[auctionId][msg.sender];
        uint256 ethPaid = bidMoneyPerUser[auctionId][msg.sender];

        require(ticketsToClaim > 0, "claim: You have not bid for this auction");

        isMinted[auctionId][msg.sender] = true;

        uint256 moneyToClaim = ethPaid - ticketsToClaim * auction.finalPrice;

        if (moneyToClaim > 0) {
            (bool sent, ) = (msg.sender).call{value: moneyToClaim}("");
            require(sent, "Transaction failed");
        }

        ticketHelper.airdrop(
            auction.eventId,
            msg.sender,
            ticketPerUser[auctionId][msg.sender]
        );

        emit TicketPrinted(
            auctionId,
            msg.sender,
            ticketPerUser[auctionId][msg.sender],
            auction.finalPrice,
            moneyToClaim
        );
    }

    /// @notice sets a new ticket helper
    /// @param _ticketHelper Ticket helper address
    /// @dev sets a new ticket helper
    function setTicketHelper(address _ticketHelper) external onlyOwner {
        ticketHelper = IBapTickets(_ticketHelper);
    }

    /// @notice sets a new secret address
    /// @param _secret Secret address
    /// @dev sets a new secret address
    function setSecret(address _secret) external onlyOwner {
        secret = _secret;
    }

    function _verifyHashSignature(
        bytes32 freshHash,
        bytes memory signature
    ) internal view returns (bool) {
        bytes32 hash = keccak256(
            abi.encodePacked("\x19Ethereum Signed Message:\n32", freshHash)
        );

        bytes32 r;
        bytes32 s;
        uint8 v;

        if (signature.length != 65) {
            return false;
        }
        assembly {
            r := mload(add(signature, 32))
            s := mload(add(signature, 64))
            v := byte(0, mload(add(signature, 96)))
        }

        if (v < 27) {
            v += 27;
        }

        address signer = address(0);
        if (v == 27 || v == 28) {
            // solium-disable-next-line arg-overflow
            signer = ecrecover(hash, v, r, s);
        }
        return secret == signer;
    }
}

// File: /contracts/interface/IBapTickets.sol



// SPDX-License-Identifier: GPL-3.0
// solhint-disable-next-line
pragma solidity 0.8.12;

interface IBapTickets {
    function withdrawETH(address _address, uint256 amount) external;

    function airdrop(uint256 eventId, address account, uint256 amount) external;

    function createEvent(
        uint256 _maxSupply,
        uint8 _maxPerTx,
        uint8 _maxPerWallet,
        uint256 _ticketPrice,
        uint8 _status // 0: sale close, 1: public sale - everybody can buy, no checks, 2: only gods owner - only god owners can buy (you're currently checking), 3: only signature - only users with a valid signature
    ) external;
}

// 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;
    }
}

// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (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 Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

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

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        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);
    }
}