//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

//////////////////////////////////////////////////////////////////
//     __  ___         __  _                                    //
//    /  |/  /__ _____/ /_(_)__ ____  ___                       //
//   / /|_/ / _ `/ __/ __/ / _ `/ _ \(_-<                       //
//  /_/  /_/\_,_/_/  \__/_/\_,_/_//_/___/                       //
//                                                              //
//    _____                 _ __    ___                  __     //
//   / ___/__  __ _  __ _  (_) /_  / _ )___ ____ ___ ___/ /     //
//  / /__/ _ \/  ' \/  ' \/ / __/ / _  / _ `(_-</ -_) _  /      //
//  \___/\___/_/_/_/_/_/_/_/\__/ /____/\_,_/___/\__/\_,_/       //
//                                                              //
//     ___       ________                                       //
//    / _ \___ _/ _/ _/ /__                                     //
//   / , _/ _ `/ _/ _/ / -_)                                    //
//  /_/|_|\_,_/_//_//_/\__/                                     //
//                                                              //
//  by: 0xInuarashi.eth                                         //
////////////////////////////////////////////////////////////////// 

abstract contract Ownable {
    address public owner; 
    constructor() { owner = msg.sender; }
    modifier onlyOwner { require(owner == msg.sender, "Not Owner!"); _; }
    function transferOwnership(address new_) external onlyOwner { owner = new_; }
}

interface iCS {

    struct Character {
        // general info
        uint8 race_;
        uint8 renderType_;

        // equipment
        uint16 transponderId_;
        uint16 spaceCapsuleId_;

        // stats
        uint8 augments_;
        uint16 basePoints_;
        uint16 totalEquipmentBonus_;
    }

    function characters(uint256 tokenId_) external view returns (Character memory);
}

interface iMES {
    function transferFrom(address from_, address to_, uint256 amount_) external;
    function burn(address from_, uint256 amount_) external;
}

interface IERC721 {
    function balanceOf(address owner_) external view returns (uint256); 
}

contract MESCommitBasedRaffle is Ownable {

    // M a r t i a n  E s s e n c e 
    iMES public MES = iMES(0x3C2Eb40D25a4b2B5A068a959a40d57D63Dc98B95);

    // M a r t i a n s 
    IERC721 public Martians = IERC721(0x075854b315F2cd7eC490853Bc5589B09E546449f);

    // M a r t i a n s  C h a r a c t e r  S t o r a g e 
    iCS public CS = iCS(0xC7C40032E952F52F1ce7472913CDd8EeC89521c4);

    // For a commit-based raffle, we need to store somewhere the index of users as well
    // as user-data.

    // Then, we can pull a seed with provably-fair randomness and then use
    // that randomness as inertia for a compute-intensive on-chain result.

    // Gas cost is conscious in this contract.

    // First, we define an interia.
    bytes32 public raffleInertia;
    
    // We then have a inertia pull method.
    function pullRaffleInertia() external onlyOwner {
        // Pulling is a 1-time function.
        require(raffleInertia == 0,
            "Inertia already set");

        // A pseudo-random generated bytes32. For provably-fair, change this.
        raffleInertia = keccak256(abi.encodePacked(
            block.timestamp, block.difficulty, blockhash(block.number)));
    }

    // Now, we need to set some commit-based raffle logics.
    struct CommitData {
        address entrant; // 20 | 12
        uint64 entries; // 8 | 4
    }

    // 2 SSTORE are necessary to do this on-chain with flexibility.
    CommitData[] public commitData;
    function viewCommitData() external view returns (CommitData[] memory) {
        return commitData;
    }

    // Then, we set some raffle logics
    // uint256 public weiPerEntry = 1 ether;

    // Benchmark: 58,304 GAS Units
    // commitBasedRaffleOffChainERC20.sol: 43,431 GAS Units 

    // Numbers without any require statements and reading additional SSTORES...
    // 67,424 GAS Units per TX using ERC20I with full approval (no approve SSTORE update)
    // 70,677 GAS Units per TX using ERC20I with partial approval (approve SSTORE update)

    // Numbers with require statement... (2120 additional GAS)
    // 69,544 GAS Units per TX using ERC20I with full approval (no approve SSTORE update)
    // 72,797 GAS Units per TX using ERC20I with partial approval (approve SSTORE update)

    function raffleCommit(uint256 amount_, uint256 tokenId_) external {
        require(raffleInertia == 0, 
            "Raffle Inertia set! Raffles closed!");

        // We hardcode weiPerEntry for gas savings
        uint256 _entries = amount_ / 5 ether;

        // Instantiate the multipler into local memory
        uint256 _multiplier = 1;

        // Here, we check for tokenId for augmentation
        // If the tokenId is not 0, means there is an input.
        if (tokenId_ != 0) {
            iCS.Character memory _Character = CS.characters(tokenId_);
            
            // If the token is fully augmented
            if (_Character.augments_ == 10) {
                // Multiply their entries by 3
                _multiplier = 3;
            }
        } 

        // If the tokenId_ is 0, there is no target character. See if they are a holder.
        else {
            // If they have a balance
            if (Martians.balanceOf(msg.sender) > 0) {
                // Multiply their entries by 2
                _multiplier = 2;
            }
        }

        // Recalculate the entries with the multipler
        _entries = _entries * _multiplier;

        require(_entries > 0,
            "Invalid amount of entries!");

        // MES.transferFrom(msg.sender, address(this), amount_);

        // We replace a standard ERC20 transferFrom to a custom burn to save gas
        MES.burn(msg.sender, amount_);

        commitData.push( 
            CommitData(
                msg.sender, 
                uint64(_entries)
        ));
    }

    // Now, we need to create some pulling logic. Gas intensive view-function.
    function _getTotalEntries() public view returns (uint256) {
        // Find the total entries
        uint256 _totalEntries;
        for (uint256 i = 0; i < commitData.length; i++) {
            _totalEntries += commitData[i].entries;
        }
        return _totalEntries;
    }

    // Now, we need to create some pulling logic. Gas intensive view-function.
    function _getTotalEntriesOfAddress(address address_) public view returns (uint256) {
        // Find the total entries
        uint256 _totalEntries;
        for (uint256 i = 0; i < commitData.length; i++) {
            if (address_ == commitData[i].entrant) {
                _totalEntries += commitData[i].entries;
            }
        }
        return _totalEntries;
    }

    function _getInertiaOfIndex(uint256 index_, uint256 modulo_) public 
    view returns (uint256) {
        require(raffleInertia != 0, 
            "raffleInertia not set yet!");
        
        return uint256(keccak256(abi.encodePacked(
            raffleInertia, index_
        ))) % modulo_;
    }

    function _getRaffleWinner(uint256 index_, uint256 modulo_) public 
    view returns (address) {
        // Find the total entries
        uint256 _modulo = modulo_ == 0 ? (_getTotalEntries() + 1) : modulo_;

        // Now, pull an inertia with the amount of entries
        uint256 _winningEntry = _getInertiaOfIndex(index_, _modulo); 

        // Now, we find the winner of the index.
        uint256 _currentEntry;
        address _winner;
        uint256 i;
        while (_winner == address(0)) {
            uint256 _previousEntries = _currentEntry;
            _currentEntry += commitData[i].entries;
            if (_previousEntries < _winningEntry 
                && _currentEntry >= _winningEntry) { 
                _winner = commitData[i].entrant;
            }
            i++;
        }
        return _winner;
    }

    // Pagination for Read Limit Safety
    function getRaffleWinnerFromInertia(uint256 inertia_) public
    view returns (address) {
        // Now, we find the winner of the index.
        uint256 _currentEntry;
        address _winner;
        uint256 i;
        while (_winner == address(0)) {
            uint256 _previousEntries = _currentEntry;
            _currentEntry += commitData[i].entries;
            if (_previousEntries < inertia_ 
                && _currentEntry >= inertia_) { 
                _winner = commitData[i].entrant;
            }
            i++;
        }
        return _winner;   
    }

    function viewRaffleResultsPaginated(uint256 start_, uint256 end_) external
    view returns (address[] memory) {
        uint256 _iterations = end_ - start_ + 1;
        uint256 _totalEntries = _getTotalEntries() + 1;
        address[] memory _winners = new address[](_iterations);
        uint256 _index;

        for (uint256 i = start_; i <= end_; i++) {
            _winners[_index++] = _getRaffleWinner(i, _totalEntries);
        }

        return _winners;
    }
    function viewInertiaResultsPaginated(uint256 start_, uint256 end_) external
    view returns (uint256[] memory) {
        uint256 _iterations = end_ - start_ + 1;
        uint256 _totalEntries = _getTotalEntries() + 1;
        uint256[] memory _winners = new uint256[](_iterations);
        uint256 _index;

        for (uint256 i = start_; i <= end_; i++) {
            _winners[_index++] = _getInertiaOfIndex(i, _totalEntries);
        }

        return _winners;
    }

    // Tested on Etherscan @ 2022-05-10 => 
    //  10000 Items in Array - 150 Results [/]
    //  20000 Items in Array - 75 Results [/]
    function viewRaffleResults(uint256 amountOfWinners_) external 
    view returns (address[] memory) {
        uint256 _totalEntries = _getTotalEntries() + 1;
        address[] memory _winners = new address[](amountOfWinners_);
        for (uint256 i = 0; i < amountOfWinners_; i++) {
            _winners[i] = _getRaffleWinner(i, _totalEntries);
        }
        return _winners;
    }
    function viewInertiaResults(uint256 amountOfWinners_) external 
    view returns (uint256[] memory) {
        uint256 _totalEntries = _getTotalEntries() + 1;
        uint256[] memory _winners = new uint256[](amountOfWinners_);
        for (uint256 i = 0; i < amountOfWinners_; i++) {
            _winners[i] = _getInertiaOfIndex(i, _totalEntries);
        }
        return _winners;
    }
}