contract WrappedV1Punk is Ownable, ERC721 {

    event ProxyRegistered(address user, address proxy);

    // Instance of cryptopunk smart contract
    ICryptoPunk private _punkContract;

    uint constant public totalPunkSupply = 10000; // supply is one more than in reality because of a hidden 0 index punk
    uint private _fees;

    // Mapping from user address to proxy address
    mapping(address => address) private _proxies;

    // get an address from a punkIndex
    mapping (uint => address) public punkIndexToAddress;

    // Add marketplace
    struct Offer {
        bool isForSale;
        uint punkIndex;
        address seller;
        uint minValue;          // in ether
        address onlySellTo;     // specify to sell only to a specific person
    }

    struct Bid {
        bool hasBid;
        uint punkIndex;
        address bidder;
        uint value;
    }

    // A record of punks that are offered for sale at a specific minimum value, and perhaps to a specific person
    mapping (uint => Offer) public punksOfferedForSale;

    // A record of the highest punk bid
    mapping (uint => Bid) public punkBids;

    mapping (address => uint) public pendingWithdrawals;

    event PunkOffered(uint indexed punkIndex, uint minValue, address indexed toAddress);
    event PunkBidEntered(uint indexed punkIndex, uint value, address indexed fromAddress);
    event PunkBidWithdrawn(uint indexed punkIndex, uint value, address indexed fromAddress);
    event PunkBought(uint indexed punkIndex, uint value, address indexed fromAddress, address indexed toAddress);
    event PunkNoLongerForSale(uint indexed punkIndex);
    event Wrapped(uint indexed punkIndex);
    event Unwrapped(uint indexed punkIndex);

    /**
     * @dev Initializes the contract settings
     */
    constructor(address punkContract)
    public
    ERC721("Wrapped V1 Cryptopunks", "WV1PUNKS")
    {
        _punkContract = ICryptoPunk(punkContract);
        _fees = 25;
    }

    /**
     * @dev Gets address of cryptopunk smart contract
     */
    function punkContract()
    public
    view
    returns (address)
    {
        return address(_punkContract);
    }

    /**
     * @dev Sets the base URI for all token
     */
    function setBaseURI(string memory baseUri)
    public
    onlyOwner
    {
        _setBaseURI(baseUri);
    }

    /**
     * @dev Registers proxy
     */
    function registerProxy()
    public
    {
        address sender = _msgSender();

        require(_proxies[sender] == address(0), "PunkWrapper: caller has registered the proxy");

        address proxy = address(new UserProxy());

        _proxies[sender] = proxy;

        emit ProxyRegistered(sender, proxy);
    }

    /**
     * @dev Gets proxy address
     */
    function proxyInfo(address user)
    public
    view
    returns (address)
    {
        return _proxies[user];
    }

    /**
     * @dev Mints a wrapped punk
     */
    function mint(uint punkIndex)
    public
    {
        address sender = _msgSender();

        UserProxy proxy = UserProxy(_proxies[sender]);

        require(proxy.transfer(address(_punkContract), punkIndex), "PunkWrapper: transfer fail");

        punkIndexToAddress[punkIndex] = sender;
        _mint(sender, punkIndex);
    }

    /**
     * @dev Burns a specific wrapped punk
     */
    function burn(uint punkIndex)
    public
    {
        address sender = _msgSender();

        require(_isApprovedOrOwner(sender, punkIndex), "PunkWrapper: caller is not owner nor approved");
        punkIndexToAddress[punkIndex] = address(0x0);

        // Check for the case where there is a bid from the new owner and refund it.
        // Any other bid can stay in place.
        Bid memory bid = punkBids[punkIndex];
        // Kill bid and refund value
        pendingWithdrawals[bid.bidder] += bid.value;
        punkBids[punkIndex] = Bid(false, punkIndex, address(0x0), 0);
        punksOfferedForSale[punkIndex] = Offer(false, punkIndex, sender, 0, address(0x0));

        _burn(punkIndex);

        // Transfers ownership of punk on original cryptopunk smart contract to caller
        _punkContract.transferPunk(sender, punkIndex);
    }

    /**
     * @dev Internal function to transfer ownership of a given punk index to another address
     */
    function _transferFrom(address from, address to, uint punkIndex)
    internal
    {
        punkIndexToAddress[punkIndex] = to;
        super._transfer(from, to, uint256(punkIndex));
    }

    function transferFrom(address from, address to, uint punkIndex) public override {
        punkIndexToAddress[punkIndex] = to;
        super.transferFrom(from, to, uint256(punkIndex));

        // Check for the case where there is a bid from the new owner and refund it.
        // Any other bid can stay in place.
        Bid memory bid = punkBids[punkIndex];
        if (bid.bidder == to) {
            // Kill bid and refund value
            pendingWithdrawals[to] += bid.value;
            punkBids[punkIndex] = Bid(false, punkIndex, address(0x0), 0);
        }
    }

    function _safeTransferFrom(address from, address to, uint punkIndex)
    internal
    {
        punkIndexToAddress[punkIndex] = to;
        super.safeTransferFrom(from, to, uint256(punkIndex));
    }

    function safeTransferFrom(address from, address to, uint punkIndex) public override {
        _safeTransferFrom(from, to, uint256(punkIndex));

        // Check for the case where there is a bid from the new owner and refund it.
        // Any other bid can stay in place.
        Bid memory bid = punkBids[punkIndex];
        if (bid.bidder == to) {
            // Kill bid and refund value
            pendingWithdrawals[to] += bid.value;
            punkBids[punkIndex] = Bid(false, punkIndex, address(0x0), 0);
        }
    }

    function punkNoLongerForSale(uint punkIndex) public {
        require(punkIndexToAddress[punkIndex] == msg.sender);
        require(punkIndex <= totalPunkSupply);
        punksOfferedForSale[punkIndex] = Offer(false, punkIndex, msg.sender, 0, address(0x0));
        emit PunkNoLongerForSale(punkIndex);
    }

    function offerPunkForSale(uint punkIndex, uint minSalePriceInWei) public {
        require(punkIndexToAddress[punkIndex] == msg.sender);
        require(punkIndex <= totalPunkSupply);
        punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, address(0x0));
        emit PunkOffered(punkIndex, minSalePriceInWei, address(0x0));
    }

    function offerPunkForSaleToAddress(uint punkIndex, uint minSalePriceInWei, address toAddress) public {
        require(punkIndexToAddress[punkIndex] == msg.sender);
        require(punkIndex <= totalPunkSupply);
        punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, toAddress);
        emit PunkOffered(punkIndex, minSalePriceInWei, toAddress);
    }

    function withdraw() public {
        uint amount = pendingWithdrawals[msg.sender];
        pendingWithdrawals[msg.sender] = 0;
        msg.sender.transfer(amount);
    }

    function enterBidForPunk(uint punkIndex) public payable {
        require(punkIndex < totalPunkSupply);
        require(punkIndexToAddress[punkIndex] != address(0x0));
        require(punkIndexToAddress[punkIndex] != msg.sender);
        require(msg.value > 0);
        Bid memory existing = punkBids[punkIndex];
        require(msg.value > existing.value);
        if (existing.value > 0) {
            // Refund the failing bid
            pendingWithdrawals[existing.bidder] += existing.value;
        }
        punkBids[punkIndex] = Bid(true, punkIndex, msg.sender, msg.value);
        emit PunkBidEntered(punkIndex, msg.value, msg.sender);
    }

    function acceptBidForPunk(uint punkIndex, uint minPrice) public {
        require(punkIndex < totalPunkSupply);
        require(punkIndexToAddress[punkIndex] == msg.sender);
        address seller = msg.sender;
        Bid memory bid = punkBids[punkIndex];
        require(bid.value > 0);
        require(minPrice > 0);
        require(bid.value >= minPrice);

        _safeTransferFrom(seller, bid.bidder, punkIndex);

        punksOfferedForSale[punkIndex] = Offer(false, punkIndex, bid.bidder, 0, address(0x0));
        uint amount = (bid.value * (1000-_fees)) / 1000;
        punkBids[punkIndex] = Bid(false, punkIndex, address(0x0), 0);
        pendingWithdrawals[seller] += amount;
        pendingWithdrawals[owner()] += (bid.value - amount);

        emit PunkBought(punkIndex, bid.value, seller, bid.bidder);
    }

    function buyPunk(uint punkIndex) external payable {
        Offer memory offer = punksOfferedForSale[punkIndex];
        if (!offer.isForSale) revert();                // punk not actually for sale
        if (offer.onlySellTo != address(0x0) && offer.onlySellTo != msg.sender) revert();  // punk not supposed to be sold to this user
        if (msg.value < offer.minValue) revert();   // Didn't send enough ETH
        if (offer.seller != punkIndexToAddress[punkIndex]) revert(); // Seller no longer owner of punk

        address seller = offer.seller;

        _transferFrom(seller, msg.sender, punkIndex);
        uint amount = (msg.value * (1000-_fees)) / 1000;
        punkNoLongerForSale(punkIndex);
        pendingWithdrawals[offer.seller] += amount;
        pendingWithdrawals[owner()] += (msg.value - amount);

        emit PunkBought(punkIndex, msg.value, offer.seller, msg.sender);

        // Check for the case where there is a bid from the new owner and refund it.
        // Any other bid can stay in place.
        Bid memory bid = punkBids[punkIndex];
        if (bid.bidder == msg.sender) {
            // Kill bid and refund value
            pendingWithdrawals[msg.sender] += bid.value;
            punkBids[punkIndex] = Bid(false, punkIndex, address(0x0), 0);
        }
    }

    function withdrawBidForPunk(uint punkIndex) public {
        require(punkIndex < totalPunkSupply);
        require(punkIndexToAddress[punkIndex] != address(0x0));
        require(punkIndexToAddress[punkIndex] != msg.sender);
        Bid memory bid = punkBids[punkIndex];
        require(bid.bidder == msg.sender);
        emit PunkBidWithdrawn(punkIndex, bid.value, msg.sender);
        uint amount = bid.value;
        punkBids[punkIndex] = Bid(false, punkIndex, address(0x0), 0);
        // Refund the bid money
        msg.sender.transfer(amount);
    }

    // aggregate getter to build better frontend
    function getPunksForAddress(address _user, uint256 userBal) external view returns(uint256[] memory) {
        uint256[] memory punks = new uint256[](userBal);
        uint256 j =0;
        uint256 i=0;
        for (i=0; i<10000; i++) {
            if ( _punkContract.punkIndexToAddress(i) == _user ) {
                punks[j] = i;
                j++;
            }
        }

        return punks;
    }

    function getWrappedPunksForAddress(address _user, uint256 userBal) external view returns(uint256[] memory) {
        uint256[] memory wrappedPunks = new uint256[](userBal);
        uint256 j =0;
        uint256 i=0;
        for (i=0; i<10000; i++) {
            if ( punkIndexToAddress[i] == _user ) {
                wrappedPunks[j] = i;
                j++;
            }
        }

        return wrappedPunks;
    }

    function getPunkOwners() external view returns (address[] memory) {
        address[] memory owners = new address[](totalPunkSupply);

        for (uint i = 0; i < totalPunkSupply; i++) {
            owners[i] = _punkContract.punkIndexToAddress(i);
        }
        return owners;
    }

    function getWrappedPunkOwners() external view returns (address[] memory) {
        address[] memory wrappedOwners = new address[](totalPunkSupply);

        for (uint i = 0; i < totalPunkSupply; i++) {
            wrappedOwners[i] = punkIndexToAddress[i];
        }
        return wrappedOwners;
    }

    function getAllPunkBids() external view returns (uint[] memory) {
        uint[] memory wrappedPunkBids = new uint[](totalPunkSupply);

        for (uint i = 0; i < totalPunkSupply; i++) {
            if( punkBids[i].hasBid )
                wrappedPunkBids[i] = punkBids[i].value;
        }

        return wrappedPunkBids;
    }

    function getAllPunkOffers() external view returns (uint[] memory) {
        uint[] memory wrappedPunkOffers = new uint[](totalPunkSupply);

        for (uint i = 0; i < totalPunkSupply; i++) {
            if( punksOfferedForSale[i].isForSale)
                wrappedPunkOffers[i] = punksOfferedForSale[i].minValue;
        }

        return wrappedPunkOffers;
    }

    function setFees(uint fees) public onlyOwner {
        _fees = fees;
    }
}
function burn(uint punkIndex)
public
{
    address sender = _msgSender();

    require(_isApprovedOrOwner(sender, punkIndex), "PunkWrapper: caller is not owner nor approved");
    punkIndexToAddress[punkIndex] = address(0x0);

    // Check for the case where there is a bid from the new owner and refund it.
    // Any other bid can stay in place.
    Bid memory bid = punkBids[punkIndex];
    // Kill bid and refund value
    pendingWithdrawals[bid.bidder] += bid.value;
    punkBids[punkIndex] = Bid(false, punkIndex, address(0x0), 0);
    punksOfferedForSale[punkIndex] = Offer(false, punkIndex, sender, 0, address(0x0));

    _burn(punkIndex);

    // Transfers ownership of punk on original cryptopunk smart contract to caller
    _punkContract.transferPunk(sender, punkIndex);
}

/**
 * @dev Burns a specific wrapped punk
 */