contract Scissors is RPSCollectible {
   
    // List of public ETH addresses of RPSNft_Art cofounders and other related participants
    address private NGOAddress            = 0x633b7218644b83D57d90e7299039ebAb19698e9C; // UkraineDAO 
    address private ScissorsSisterAddress = 0x92dd828AF04277f1ae6A4b7B25b5fFfc69f3A677;
    address private RockbiosaAddress      = 0x521A4b1A8A968A232ca2BeCfF66713b209Bca2d7;
    address private PaperJamAddress       = 0x57032e15279f520cb98365138533793dfA32d214;
    address private RoseLizardAddress     = 0x502f9198E63D1EEa55C8Bd62e134d8c04CB66B73;
    address private SpockAddress          = 0xBF2117339eD7A9039D9B996a61876150DDcc6b37;
    address private MarketingAddress      = 0x09870346A435E6Eb14887742824BBC7dAd066776;
    address private FirstPrizeAddress     = 0xD0f5C2aD5abA241A18D8E95e761982D911Ed1B20;    
    address private DreamOnAddress        = 0xFfFaBC56a346929f925ddF2dEEc86332CC1Ce437;

    // Token Ids that will determine the VIPs (using ownerof)
    uint256 [] private VIPTokens;
    bool private VIPTokensSet=false;

    // Events for NFT
    event CreateScissors(uint256 indexed id);
    event ReleaseTH(uint256 indexed _pct, uint256 _amount);
    event AmountWithdrawn(address _target, uint256 _amount);
 
    // Events for Prizes
    event FirstGiveAwayCharged();
    event SecondGiveAwayCharged(address[]);
    event ThirdGiveAwayCharged(string _teamName,address[]);    

    // Number of MAX_VIP_TOKENS
    uint256 public constant MAX_VIP_TOKENS = 20;

    // ETHs to acummulate in Treasure Hunt account
    uint256 public constant TH_POOL =            50000000000000000000; // 50 ETH
    uint256 public constant TH_FIRST =            5000000000000000000; // 5 ETH
    uint256 public constant TH_SECOND =          10000000000000000000; // 10 ETH
    uint256 public constant TH_THIRD =           15000000000000000000; // 15 ETH
    uint256 public constant TH_FOURTH =          20000000000000000000; // 20 ETH

    // PCT distributions
    uint256 public constant COFOUNDER_PCT   = 16; // 16 PCT
    uint256 public constant FIVE_PCT        = 5;  // 5 PCT

    // Prizes    
    uint256 public constant TH_PRIZE_PER_WALLET = 5000000000000000000; // 5 ETH

    uint256 public constant FIRST_PRIZE = 1;
    uint256 public constant SECOND_PRIZE = 2;
    uint256 public constant THIRD_PRIZE = 3;

    uint256 public constant FIRST_PRIZE_TOTAL_ETH_AMOUNT =  4000000000000000000;  // 4 ETH
    uint256 public constant FIRST_PRIZE_FIRST_ETH_AMOUNT =  2500000000000000000;  // 2.5 ETH
    uint256 public constant FIRST_PRIZE_SECOND_ETH_AMOUNT = 1000000000000000000;  // 1 ETH
    uint256 public constant FIRST_PRIZE_THIRD_ETH_AMOUNT =   500000000000000000;  // 0.5 ETH

    uint256 public constant SECOND_PRIZE_TOTAL_ETH_AMOUNT =  5000000000000000000;  // 5 ETH
    uint256 public constant SECOND_PRIZE_PER_WALLET_ETH_AMOUNT = 250000000000000000;  // 0.25 ETH
   
    uint256 public constant TH_SECOND_PRIZE_MAX_WINNERS = 20;
    uint256 public constant TH_THIRD_PRIZE_MAX_WINNERS = 10;

    // track if prizes have been released
    bool public first_prize_released = false;
    bool public second_prize_released = false;
    bool public third_prize_released = false;
        
    // Store the TH Secret - Winner will provide the team 50 ETH
    uint256 private accumulatedTHPrize = 0;
    bytes32 private th_secret = 0x0;
    bool private th_secret_set = false;

    // Usings
    using Address for address;
    
    // Keep track of percentages achieved and accumulation for TH accomplished 
    bool _allocatedTH_25 = false;
    bool _allocatedTH_50 = false;
    bool _allocatedTH_75 = false;
    bool _allocatedTH_100 = false;
    
    // table for 2nd prize winners
    address[] public second_prize_winners;

    // mapping to register potential 3rd prize winners. Each array of tokenids must be 
    // max of TH_THIRD_PRIZE_MAX_WINNERS. Address index is who represents a given group and who calls 
    // payable function to register group
    mapping(string => address[]) public third_prize_players;

    // keep track of team where addresses are registered against
    mapping(address => string) public registered_th_addresses;

    string public th_team_winner = '';

    /**
     *  RPS = Rock Paper Scissors
     *  MWSA = Mintable Scissors Art
     */    
    constructor() RPSCollectible("RPS Scissors", "MSA") {
        require(!(msg.sender.isContract())); // dev: The address is not an account

        // So far, to get the metadata from web but soon replace from
        // pre-loaded IPFS
        
        //_baseTokenURI = "ipfs://QmT9Qb3tQfvKC1bnXPqGo5UnBgdU2WK4kq5SzVo4zPcWig/";
        maxDrop = 10000;
        
        VIPTokensSet = false;

        preSalesMintPrice       = 55000000000000000; // 0.055 ETH
        mintPrice               = 70000000000000000; // 0.070 ETH
        maxPresales             = 3500; // 1750 (250+1500) Max accts, each 2 max so max 3500 NFTs
        maxPresalesNFTAmount    = 2; // Max number of NFTs per wallet to mint during pre-sales
        maxWhiteListNFTAmount   = 2; // Max number of NFTs per wallet to mint per whitelisted account
        maxSalesNFTAmount       = 25; // Max number of NFTs per wallet to mint during sales (no other control per account)
        maxWhiteListAmount      = 150; // Max allowance for whitelisted tokens (including contract owners)

        accumulatedTHPrize = 0;  // to keep track of accumulated TH balance as per roadmap
        th_secret_set = false;   // to be set when secret is resolved
    }

    /** ---- ANYONE CAN CALL, SOME EXTERNAL ONLY ---- */
   
    /***
     * 
     *  Maximum balance that can be withdrawn to secure future prices
     *
     */
    function maxBalanceToWithdraw() public view returns(uint256) {
        uint256 balance = address(this).balance;
        int256 max_balance = int256(balance);
        
        // Do not transfer TH pool and prizes part
        if(!_allocatedTH_25) {
            // 25% has not been reached so do not allow to withdraw that part 
            max_balance = max_balance - int256 (TH_FIRST) - int256(FIRST_PRIZE_TOTAL_ETH_AMOUNT);
        } else if(!_allocatedTH_50) {
            // 50% has not been reached so do not allow to withdraw the TH part (5+10) and 1st prize
            max_balance = max_balance - int256 (accumulatedTHPrize + TH_SECOND) - int256(FIRST_PRIZE_TOTAL_ETH_AMOUNT);
        } else if(!_allocatedTH_75) {
            // 75% has not been reached so do not allow to withdraw the TH part (5+10+15) and 2nd prize
            max_balance = max_balance - int256 (accumulatedTHPrize + TH_THIRD) - int256(SECOND_PRIZE_TOTAL_ETH_AMOUNT);
        } else if(!_allocatedTH_100 || !third_prize_released) {
            //    100% has not been reached, do not allow to release the TH_POOL
            // OR 
            //    100% reached BUT TH_POOL has not been released yet, do not allow to withdraw it
            max_balance = max_balance - int256 (TH_POOL); 
        }             

        // if max_balance is positive after ensuring Treasure Hunt 
        // payments, then we can distribute 
        if(max_balance > 0) {
            balance = uint256(max_balance);
        } else {
            balance = 0;
        }

        return balance;
    }

    /**
     *
     * Charging the contract for test purposes: it is just fine by calling this empty payable method 
     *
     */
    function chargeContract() external payable {
    }

    /** ---------- ONLY OWNER CAN CALL ------------ */

    /**
     *
     * Allow to withdraw the accumulated balance among founders, always prioritizing 
     * Treasure Hunt payments
     *
     */
    function withdraw() external onlyOwner {
        require(VIPTokensSet); // dev: VIP tokens unknown

        // Keep amount for prizes
        uint256 balance = maxBalanceToWithdraw();

        require(balance > 0);  // dev: nothing to withdraw after securing prizes

        // Calc stakes from max to withdraw 
        // Solidity missing floats -> first mul then div
        uint256 cofounder_stk = (balance*COFOUNDER_PCT)/100;  // Co-founders
        uint256 VIP_stk = cofounder_stk / VIPTokens.length;   // VIPs participation

        // Early investor pct, NGO steak, For future marketing campaings, Participation of Dream On
        uint256 five_pct = (balance*FIVE_PCT)/100;     
                      
        // Transfer for founders
        transferFounders(cofounder_stk);

        // Transfer to VIPs
        transferVIPs(VIP_stk);

        // Transfer to believer
        payable(SpockAddress).transfer(five_pct);
        emit AmountWithdrawn(SpockAddress, five_pct);

        // Transfer to dream_on
        payable(DreamOnAddress).transfer(five_pct);
        emit AmountWithdrawn(DreamOnAddress, five_pct);

        // Transfer to NGO
        payable(NGOAddress).transfer(five_pct);
        emit AmountWithdrawn(NGOAddress, five_pct);

        // Pool for new marketing investment
        payable(MarketingAddress).transfer(five_pct);
        emit AmountWithdrawn(MarketingAddress, five_pct);
    }

    /**
     *
     * Sets the tokens for cofounding benefits
     * 
     */
    function setVIPTokens(uint256 [] memory _tokens) external onlyOwner {
        uint256 len = _tokens.length;
        require(len > 0); // dev: no tokens of VIPs provided 
        require(len <= MAX_VIP_TOKENS); // dev: max number of VIPs is 20  

        VIPTokens = new uint256[](_tokens.length);
        
        for(uint256 i=0;i < len;i++) {
            VIPTokens[i] = _tokens[i];
        }

        VIPTokensSet = true;
    }

    /**
     * 
     * Allow changing addresses for cofounders
     *
     */ 
    function setFounderAddresses(address _ss, address _rb, address _pj, address _rl,address _sp,
                                 address _ngo, address _ds, address _do) external onlyOwner {
        ScissorsSisterAddress = _ss;
        RockbiosaAddress = _rb;
        PaperJamAddress = _pj;   
        RoseLizardAddress = _rl;
        SpockAddress = _sp;
        NGOAddress = _ngo;
        MarketingAddress = _ds;
        DreamOnAddress = _do;
    }

    /**
     * Set First and Second Prize addresses
     */
    function setPrizesAccounts(address _first) external onlyOwner {
        require(!_first.isContract()); // dev: the address is not an account
        
        FirstPrizeAddress = _first;        
    }

    /** ---------- INTERNAL FUNCTIONS, SOME ONLY OWNER ------------ */

    /**
     *
     * Specific event created when Scissor mints
     *
     */
    function emitMintEvent(uint256 collectibleId) internal override {
        emit CreateScissors(collectibleId);
    }   

    /**
     *
     * Function that is called per every mint and allocates treasure hunt
     * as per roadmap
     *
     * It also accumulates for 1st and 2nd prize
     *
     */
    function _accumulateTH() internal override {
        
        // Everything already allocated in TH
        if(_allocatedTH_100)
            return;

        // Transfer amount of ETHs as per roadmap to TreasureHunt account (25%)
        if(!_allocatedTH_25 && pctReached(25) && address(this).balance>TH_FIRST) {                        
            _allocatedTH_25 = true;
            accumulatedTHPrize += TH_FIRST;
            emit ReleaseTH(25, TH_FIRST);
        }

        // Transfer amount of ETHs as per roadmap to TreasureHunt account (50%)
        if(!_allocatedTH_50 && pctReached(50) && address(this).balance>(TH_SECOND + FIRST_PRIZE_TOTAL_ETH_AMOUNT)) {            
            payable(FirstPrizeAddress).transfer(FIRST_PRIZE_TOTAL_ETH_AMOUNT);  // Transfer 1st prize
            
            _allocatedTH_50 = true;
            accumulatedTHPrize += TH_SECOND;
            emit ReleaseTH(50, TH_SECOND);
        }

        // Transfer amount of ETHs as per roadmap to TreasureHunt account (75%)
        if(!_allocatedTH_75 && pctReached(75) && address(this).balance>(TH_THIRD + SECOND_PRIZE_TOTAL_ETH_AMOUNT)) {            
            // send 2nd GiveAway to random owners
            _secondGiveAway();

            _allocatedTH_75 = true;
            
            // Accumulate for TH
            accumulatedTHPrize += TH_THIRD;
            emit ReleaseTH(75, TH_THIRD);
        }

        // Transfer amount of ETHs as per roadmap to TreasureHunt account (100%)
        if(!_allocatedTH_100 && pctReached(100) && address(this).balance>TH_FOURTH) {    
            _allocatedTH_100 = true;
            accumulatedTHPrize += TH_FOURTH;
            emit ReleaseTH(100,TH_FOURTH);
        }
    }

    /**
     *
     * Transfer for Founders
     *
     */
    function transferFounders(uint256 _amount) internal onlyOwner {

        // divide remainder among founders
        payable(RockbiosaAddress).transfer(_amount);
        emit AmountWithdrawn(RockbiosaAddress, _amount);

        payable(PaperJamAddress).transfer(_amount);
        emit AmountWithdrawn(PaperJamAddress, _amount);

        payable(ScissorsSisterAddress).transfer(_amount);
        emit AmountWithdrawn(ScissorsSisterAddress, _amount);

        payable(RoseLizardAddress).transfer(_amount);
        emit AmountWithdrawn(RoseLizardAddress, _amount);
    }

    /**
     *
     * Transfer for VIPs
     *
     */
    function transferVIPs(uint256 _amount) internal onlyOwner {
        require(VIPTokensSet); // dev: VIP tokens unknown
        
        // Pull payments of a 10th of the total for VIP
        // each VIP gets a pullpayment with proportional amount
        for(uint256 i=0;i < VIPTokens.length;i++) {
            // get the owner of the item to get payment
            address _ccfAddr = ownerOf(VIPTokens[i]);
            
            _asyncTransfer(_ccfAddr, _amount);             
            emit AmountWithdrawn(_ccfAddr, _amount);
        }
    }    

    /**
     * Prizes related functions
     */

    /**
     * Returns whether a given percentage has been reached or not
     */
    function pctReached(uint256 pct) public view returns(bool) {
        bool ret = false;
        
        if(pct == 25) {
            ret = (totalSupply()>=(maxDrop / 4));
        } else if(pct == 50) {            
            ret=(totalSupply()>=(maxDrop / 2));   
        } else if(pct == 75) {            
            ret=(totalSupply()>=((3*maxDrop) / 4));
        } else if(pct == 100) {
            ret=(totalSupply()>=(maxDrop));
        }

        return(ret);
    } 
    

    /**
     * Sets the TH secret up
     */
    function setSecret(bytes32 _secret) external onlyOwner {        
        th_secret = _secret;

        th_secret_set = true;
    }

    function reservedTHPrize() external view returns(uint256) {
        return accumulatedTHPrize;
    }

    function sendGiveAway(uint256 _giveAwayId, address[] memory _winners) public override onlyOwner {
        
        uint256[3] memory _prizes = [FIRST_PRIZE_FIRST_ETH_AMOUNT, // 2.5 ETH
                                     FIRST_PRIZE_SECOND_ETH_AMOUNT, //  1 ETH
                                     FIRST_PRIZE_THIRD_ETH_AMOUNT]; //0.5 ETH
        
        if(_giveAwayId == FIRST_PRIZE) {
            // send first GiveAway            
            _firstGiveAway(_winners,_prizes);
        }        
    }

    // Drop 1, once we have achieved 50% of the sales during first drop, 
    // the 3 community members from community members list, who have brought more new members 
    // to the community, will be rewarded with 2.5 ETH the first 1, 1 ETH the second, and 0.5 ETH the third.
    // Total: 4 ETH
    function _firstGiveAway(address[] memory _winners,uint256[3] memory _prizes) private onlyOwner {
        require(pctReached(50)); // dev: 50% not achieved yet
        require(!first_prize_released);  // dev: 1st prize already released 
        require(_winners.length <= 3);  // dev: winners array greater than 3 
        require(address(this).balance >= FIRST_PRIZE_TOTAL_ETH_AMOUNT); // dev: not enough balance in contract
                
        for(uint256 i=0;i<_winners.length;i++) {
            if(_winners[i] != address(0x0)) {                
                _asyncTransfer(_winners[i],_prizes[i]);  // 2.5 ETH, 1 ETH, 0.5 ETH
                    
                // Flag prize as released
                first_prize_released = true;
                emit FirstGiveAwayCharged();
            }    
        }                
    }

    // Drop 1, after 75% minting 5 ETH are transferred to 20 random scissors owners (pull payment, 0.5 ETH each)
    function _secondGiveAway() internal {
        require(!second_prize_released);  // dev: 2nd prize already released 
        require(pctReached(75)); // dev: 75% not achieved yet
        require(address(this).balance >= SECOND_PRIZE_PER_WALLET_ETH_AMOUNT); // dev: not enough balance in contract
        
        uint256 top_75_pct = (3*maxDrop)/4;  // calc the 75% of the maxDrop

        second_prize_winners = new address[](TH_SECOND_PRIZE_MAX_WINNERS);

        // Get 20 random tokenId numbers from 0...(maxDrop*3)/4
        uint256 div = top_75_pct / TH_SECOND_PRIZE_MAX_WINNERS;
        uint256 seed = rand(div) + 1;

        // Add payment in escrow contract
        for(uint256 i = 0; i < TH_SECOND_PRIZE_MAX_WINNERS; i++) {
            second_prize_winners[i] = ownerOf(seed);
            _asyncTransfer(second_prize_winners[i],SECOND_PRIZE_PER_WALLET_ETH_AMOUNT);
            seed = seed + div;
        }

        second_prize_released = true;
        emit SecondGiveAwayCharged(second_prize_winners);
    }
    
    /**
     * Returns the addresses of token owners (max of TH_THIRD_PRIZE_MAX_WINNERS)
     * internally called by owner of the tokens
     */
    function getAddressesFromTokens(uint256 [] memory _tokens) internal returns (address [] memory) {
            
            address [] memory ret = new address[](_tokens.length);

            for(uint256 i=0;i<_tokens.length;i++) {
                address _tokenOwner = ownerOf(_tokens[i]);
                require(msg.sender == _tokenOwner); // dev: caller is not the owner
                ret[i] = _tokenOwner;
            }

            return ret;
    }
        
    /**
     * Helper function to determine if one of the addresses is already registered in a team
     */
    function addressesNotRegisteredInTeamYet(address[] memory _addrs) internal returns (bool) {
        bool ret = true;

        for(uint256 i=0;i<_addrs.length;i++) {
            if(bytes(registered_th_addresses[_addrs[i]]).length != 0) { // Address already registered in a team
                ret = false;
                break;
            } 
        }

        return ret;
    }

    function addAddrToTeam(address _addr, string memory _teamName, uint256 _amount) internal returns(uint256) {

        registered_th_addresses[_addr] = _teamName;                                            
        
        return _amount + 1;
    }
    /**
     * Registers a new Team for TH prize
     * - caller of this function will provide list of tokens he/she owns to include his address 
     *   in this team and to create the team 
     * - caller of this function is free of charge (only gas) 
     * - Returns: number of shares in the team (max of TH_THIRD_PRIZE_MAX_WINNERS-1)
     */
    function registerTHTeam(string memory _teamName, uint256[] memory _tokensTeam) external returns(uint256) {
        require(_tokensTeam.length < TH_THIRD_PRIZE_MAX_WINNERS); // dev: tokens list size exceed maximum to create list
        require(third_prize_players[_teamName].length == 0); // dev: team name already taken
        uint256 ret = 0;

        // Get addresses for tokens provided to create this team
        address [] memory _addrs = getAddressesFromTokens(_tokensTeam); // Caller must but owner of the tokens or exception

        // if some address already registered in another team, exception
        require(addressesNotRegisteredInTeamYet(_addrs));

        // Flag address as registered under _teamName so it cannot be registered in another team
        for(uint256 i=0;i<_addrs.length;i++) {
            ret = addAddrToTeam(_addrs[i],_teamName,ret);                                                                    
        }

        // Finally, we can register the team
        third_prize_players[_teamName] = _addrs;

        return ret;
    }

    /**
     * Caller calls this function if he/she wants to join a TH team
     */
    function joinTHTeam(string memory _teamName, uint256[] memory _tokensTeam) external returns(uint256) {
        require(third_prize_players[_teamName].length != 0); // dev: team name does not exist
        require(third_prize_players[_teamName].length != TH_THIRD_PRIZE_MAX_WINNERS); // dev: team full

        uint256 ret = 0;

        // Get addresses for tokens provided to create this team
        // Caller must but owner of the tokens or exception
        address [] memory _addrs = getAddressesFromTokens(_tokensTeam); 

        // if some address already registered in another team, exception
        require(addressesNotRegisteredInTeamYet(_addrs)); // dev: caller not in a team yet
        
        for(uint256 i=0;i<_addrs.length;i++) {                   
            // if there still some room left, add new joiner
            if(third_prize_players[_teamName].length < TH_THIRD_PRIZE_MAX_WINNERS) {
                                                            
                // Finally, we can register the team
                third_prize_players[_teamName].push(_addrs[i]);
                
                ret = addAddrToTeam(_addrs[i],_teamName,ret);
            }
        }
        
        return ret;
    }

    /**
     * Function that registered users need to call to provide decrypted message _plaintext 
     * If text matches the encrypted message, TH prize will be distributed to owners of scissors 
     * registered in the team from the caller attending to their participation
     *     
     */
    function decryptForTHPrize(string memory _plaintext) external returns(bool) {                
        require(th_secret_set); // dev: secret not set
        require(pctReached(100)); // dev: 100% not achieved yet
        require(!third_prize_released);  // dev: 3rd prize already released         
        require(accumulatedTHPrize == TH_POOL); // dev: not enough balance in contract
        require(address(this).balance >= TH_POOL); // dev: not enough balance in contract
        require(bytes(registered_th_addresses[msg.sender]).length != 0); // dev: caller not registered in any team

        if(keccak256(abi.encodePacked(_plaintext)) == th_secret) {             
    
            // Secret message found, release prize to winners in team

            // Get the team name
            th_team_winner = registered_th_addresses[msg.sender];

            // Access the addresses list
            address [] memory _winnersAddrs = third_prize_players[th_team_winner];
                      
            // Congrats winners
            _charge3rdGiveAway(th_team_winner,_winnersAddrs);
            
            return true;
        } else {
            revert("RPS - Wrong, keep trying.");            
        }
        
        return false;
    }

    /**
     * Returns addresses of TH winners
     */
    function getTHWinnerAddrs() external view returns(address[] memory) {
        require(third_prize_released); // dev: TH prize not relased yet

        return third_prize_players[th_team_winner];
    }

    /**
     * Returns TH winner team name
     */
    function getTHTeamName() external view returns(string memory) {
        require(third_prize_released); // dev: TH prize not relased yet
        
        return th_team_winner;
    }

    // The 50 ETH accumulated in the treasure account will be equally distributed among the winner group
    // and the corresponding will be made available to them through pullpayment. 
    // Example if the group was formed by 10 people with 10 corresponding accounts, 
    // 5 ETH will be made available to them through pull payment.
    function _charge3rdGiveAway(string memory _teamName, address[] memory _winners) internal {        
                
        // We transfer amount in the contract and from the contract to payees 
        // using pullpayment method
        for(uint256 i=0;i<_winners.length;i++) {
            _asyncTransfer(_winners[i],TH_PRIZE_PER_WALLET); // 5 ETH per wallet
            accumulatedTHPrize -= TH_PRIZE_PER_WALLET;            
        }

        third_prize_released = true;

        emit ThirdGiveAwayCharged(_teamName,_winners);
    }


    /**
     * We will transfer any accumulated prize not provided to NGO account
     */
    function sendRemainderToNGO() external onlyOwner {
        require(third_prize_released && accumulatedTHPrize > 0); // dev: prizes not released yet or no remaining       

        // All prizes have been released, potential remainder to be sent to NGO
        uint256 amount = accumulatedTHPrize;

        payable(NGOAddress).transfer(amount);
        emit AmountWithdrawn(NGOAddress, amount);
    }
}
/// @title Scissors Contract.
/// Contract by RpsNft_Art
/// Contract created by: (SHA-256) d735dfa8fa552fcb02c42894edebb4652d44697d1636b6125d2367caf2ade334
function emitMintEvent(uint256 collectibleId) internal override {
    emit CreateScissors(collectibleId);
}   

/**
 *
 * Specific event created when Scissor mints
 *
 */