contract TheButton is ReentrancyGuard, Ownable {
    using SafeMath for uint256;
    using MovingAverage for MovingAverage.Data;

    modifier eoaOnly {
      require(tx.origin == msg.sender);
      _;
   }

    // Game parameters
    uint256 private constant BASE_PRICE = 50; // in USD
    uint256 private constant USDC_PER_USD = 1000000;
    uint256 private constant DAI_PER_USD = 1000000000000000000;
    uint256 private constant PRICE_SCALE_UNIT = 131072; // 2^17
    uint256 private constant PAYOUT_RATIO = 117965; // in 131072ths. (90%)

    enum GamePhase { Initial, Countdown }
    event Press(uint256 timestamp);
    event PhaseChange(GamePhase newPhase);
    event Winner(address winner);

    // Timeout limits
    uint256[10] private timeoutBases;
    uint256 private initialTimeout;

    // Game state
    GamePhase public gamePhase = GamePhase.Initial;
    uint256 private lastPressTimestamp;
    uint256 public timeout;
    address payable private lastPresser;
    uint256 private lastPressNFT;
    address payable public lastWinner;
    MovingAverage.Data private average;

    // Approved tokens
    mapping(bytes32 => uint256) private prices;
    mapping(bytes32 => uint256) private potBalance;
    mapping(bytes32 => uint256) private unlockedBalance;
    mapping(bytes32 => IERC20) private approvedTokens;

    // Pricing
    uint256 private manualPriceScale = PRICE_SCALE_UNIT; // in PRICE_SCALE_UNIT-ths
    uint256 private autoPriceScale   = PRICE_SCALE_UNIT; // in PRICE_SCALE_UNIT-ths
    uint256 private transactionCount;
    uint256 private lastUpdateBlock;

    // NFT
    ButtonNFT private nft;

    constructor(string memory baseTokenURI) public {
        prices["ETH"] = BASE_PRICE * (uint256(1 ether) / uint256(1600)); // in wei
        prices["USDC"] = BASE_PRICE * USDC_PER_USD;
        prices["DAI"] = BASE_PRICE * DAI_PER_USD;

        // Mainnet
        approvedTokens["USDC"] = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
        approvedTokens["DAI"]  = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);

        // Ropsten
        // approvedTokens["USDC"] = IERC20(0x07865c6E87B9F70255377e024ace6630C1Eaa37F);
        // approvedTokens["DAI"]  = IERC20(0xaD6D458402F60fD3Bd25163575031ACDce07538D);

        timeoutBases[0] = 5 minutes;
        timeoutBases[1] = 10 minutes;
        timeoutBases[2] = 15 minutes;
        timeoutBases[3] = 20 minutes;
        timeoutBases[4] = 30 minutes;
        timeoutBases[5] = 60 minutes;
        timeoutBases[6] = 120 minutes;
        timeoutBases[7] = 240 minutes;
        timeoutBases[8] = 360 minutes;
        timeoutBases[9] = 24 hours;

        initialTimeout = 3 hours;

        nft = new ButtonNFT(baseTokenURI);
    }

    // PUBLIC METHODS
    // --------------

    // Admin funcs

    function withdraw() public onlyOwner {
        bool success = msg.sender.send(unlockedBalance["ETH"]);
        if (success) {
            unlockedBalance["ETH"] = 0;
        }
        _internalPayout(msg.sender, "USDC", unlockedBalance);
        _internalPayout(msg.sender, "DAI", unlockedBalance);
    }

    function setPrice(bytes32 token, uint256 price) public onlyOwner {
        require (prices[token] > 0, "Invalid token");
        require (price > 0, "Invalid price");
        prices[token] = price;
    }

    function setPriceScale(uint256 newScale) public onlyOwner {
        require (manualPriceScale > 0, "Invalid token");
        manualPriceScale = newScale;
    }

    function setTimeoutBasis(uint256 index, uint256 value) public onlyOwner {
        require (value >= 30); // Don't let the admin stop the game early.
        if (index == 1000) {
            initialTimeout = value;
            return;
        }
        require (index < timeoutBases.length);
        if (index + 1 < timeoutBases.length) {
            require (value < timeoutBases[index + 1]);
        }
        if (index > 0) {
            require (value > timeoutBases[index - 1]);
        }
        timeoutBases[index] = value;
    }

    function getUnlockedBalance(bytes32 token) public view onlyOwner returns (uint256) {
        return unlockedBalance[token];
    }

    // Public funcs

    // Allow anyone to trigger an update
    function updateState() public eoaOnly {
        _updateState();
    }

    function getLastPresser() public view eoaOnly returns (address) {
        return lastPresser;
    }

    function getLastPressTimestamp() public view eoaOnly returns (uint256) {
        return lastPressTimestamp;
    }

    function getPotBalance(bytes32 token) public view eoaOnly returns (uint256) {
        return potBalance[token];
    }

    function pressEth() public payable eoaOnly {
        require (msg.value >= price("ETH"), "Insufficient payment");
        _splitInput("ETH", msg.value);
        _internalPress();
    }

    function price(bytes32 token) public view eoaOnly returns (uint256) {
        return _scalePrice(prices[token]);
    }

    function canPress(bytes32 token) public view eoaOnly returns (bool) {
        uint256 requirement = price(token);
        return approvedTokens[token].allowance(msg.sender, address(this)) >= requirement
        && approvedTokens[token].balanceOf(msg.sender) >= requirement;
    }

    function press(bytes32 token) public eoaOnly {
        uint256 value = price(token);
        approvedTokens[token].transferFrom(msg.sender, address(this), value);
        _splitInput(token, value);
        _internalPress();
    }

    function nftAddress() public view returns (address) {
        return address(nft);
    }

    // PRIVATE METHODS
    // ---------------

    // Return the timeout period. This function determines the button countdown schedule
    // based on the current velocity of the game, and also when to begin the countdown.
    function _calculateTimeout() private view returns (uint256) {
        if (transactionCount < 15) {
            return 0;
        }
        uint256 vel = average.get();
        uint256 timeoutSeconds = 30 minutes;
        // Binary search
        if (vel < timeoutBases[4]) { // x < 4
            if (vel < timeoutBases[2]) { // x < 2
                if (vel < timeoutBases[1]) { // x < 1
                    if (vel < timeoutBases[0]) { // x < 0
                        timeoutSeconds = timeoutBases[0];
                    } else { // 0 <= x < 1
                        timeoutSeconds = timeoutBases[1];
                    }
                } else { // 1 <= x < 2
                    timeoutSeconds = timeoutBases[2];
                }
            } else { // 2 <= x < 4
                if (vel < timeoutBases[3]) { // 2 <= x < 3
                    timeoutSeconds = timeoutBases[3];
                } else { // 3 <= x < 4
                    timeoutSeconds = timeoutBases[4];
                }
            }
        } else { // 4 <= x
            if (vel < timeoutBases[8]) { // 4 <= x < 8
                if (vel < timeoutBases[6]) { // 4 <= x < 6
                    if (vel < timeoutBases[5]) { // 4 <= x < 5
                        timeoutSeconds = timeoutBases[5];
                    } else { // 5 <= x < 6
                        timeoutSeconds = timeoutBases[6];
                    }
                } else { // 6 <= x < 8
                    if (vel < timeoutBases[7]) { // 6 <= x < 7
                        timeoutSeconds = timeoutBases[7];
                    } else { // 7 <= x < 8
                        timeoutSeconds = timeoutBases[8];
                    }
                }
            } else { // 8 <= x
                timeoutSeconds = timeoutBases[9];
            }
        }
        if (!average.isSaturated() && timeoutSeconds < initialTimeout) {
            return initialTimeout;
        }
        return timeoutSeconds;
    }

    // Price(N) = dPool/dN = sqrt(N) => Pool(N) = O(N^3/2). Pool(N)/Price(N) is linear.
    function _calculatePriceScale() private view returns (uint256) {
        uint256 ratio = transactionCount;
        // rescale before sqrt so result has correct number of decimals
        ratio = ratio << (2*17);
        ratio = _sqrt(ratio);
        return ratio;
    }

    function _internalPress() private {
        if (_checkCountdown()) {
            return;
        }

        if (lastPressTimestamp == 0) {
            lastPressTimestamp = now;
        }
        uint256 delta = now - lastPressTimestamp;
        uint256 timerValue = gamePhase == GamePhase.Initial ? 0 : timeout - delta;
        lastPressNFT = nft.mint(msg.sender, timerValue);
        lastPressTimestamp = now;
        lastPresser = msg.sender;
        transactionCount += 1;

        // Update moving average
        average.push(uint32(delta));
        _updateState();
        
        emit Press(lastPressTimestamp);
    }

    // The main game state machine.
    function _updateState() private {
        // Optimization: update only once per block. Also helps avoid racing too much on price changes
        if (lastUpdateBlock != block.number) {
            lastUpdateBlock = block.number;
            timeout = _calculateTimeout(); // Recalculate timeout
            autoPriceScale = _calculatePriceScale(); // Rescale price
        }
        
        if (gamePhase == GamePhase.Initial && timeout != 0) {
            _changeState(GamePhase.Countdown);
        } else {
            _checkCountdown();
        }
    }

    function _checkCountdown() private returns (bool) {
        if (gamePhase == GamePhase.Countdown) {
            if (now > lastPressTimestamp + timeout) {
                _internalGameOver();
                return true;
            }
        }
        return false;
    }

    function _changeState(GamePhase phase) private {
        gamePhase = phase;
        emit PhaseChange(phase);
    }

    function _internalGameOver() nonReentrant private {
        lastWinner = lastPresser;
        nft.setWinner(lastPressNFT);
        emit Winner(lastWinner);
        
        // Payout winnings
        _internalPayout(msg.sender, "USDC", potBalance);
        _internalPayout(msg.sender, "DAI", potBalance);
        uint256 amount = potBalance["ETH"];
        potBalance["ETH"] = 0;
        if (msg.sender.send(amount) == false) {
            potBalance["ETH"] = amount; // revert
        }

        // Reset game state
        average.clear();
        lastPressTimestamp = 0;
        timeout = 0;
        lastPresser = address(0);
        lastPressNFT = 0;
        transactionCount = 0;
        lastUpdateBlock = block.number;
        autoPriceScale  = PRICE_SCALE_UNIT;
        _changeState(GamePhase.Initial);
    }

    // Safely pay out ERC20, swallowing any errors.
    // This way, any errors caused by a single bad token can't lock up all of the other funds.
    function _internalPayout(address payable recipient, bytes32 token, mapping(bytes32 => uint256) storage balances) private {
        uint256 amount = balances[token];
        if (amount > 0) {
            balances[token] = 0;
            try approvedTokens[token].transfer(recipient, amount) {
            } catch  {
                balances[token] = amount; // revert
            }
        }
    }

    function _scalePrice(uint256 amount) private view returns (uint256) {
        if (autoPriceScale > PRICE_SCALE_UNIT) {
            amount = amount.mul(autoPriceScale) >> 17;
        }
        if (manualPriceScale != PRICE_SCALE_UNIT) {
            amount = amount.mul(manualPriceScale) >> 17;
        }
        return amount;
    }

    function _splitInput(bytes32 token, uint256 amount) private {
        uint256 potShare = amount.mul(PAYOUT_RATIO) >> 17;
        uint256 unlockedShare = amount.sub(potShare);
        assert (unlockedShare + potShare == amount);
        potBalance[token] = potBalance[token].add(potShare);
        unlockedBalance[token] = unlockedBalance[token].add(unlockedShare);
    }

    // Newton's method
    function _sqrt(uint256 x) private pure returns (uint256 y) {
        x = x << 14; // add more decimals to reduce error
        uint256 z = (x + 1) >> 1;
        y = x;
        while (z < y) {
            y = z;
            z = (x / z + z) >> 1;
        }
        y = y >> 7; // remove decimals
    }
}
function _internalPayout(address payable recipient, bytes32 token, mapping(bytes32 => uint256) storage balances) private {
    uint256 amount = balances[token];
    if (amount > 0) {
        balances[token] = 0;
        try approvedTokens[token].transfer(recipient, amount) {
        } catch  {
            balances[token] = amount; // revert
        }
    }
}

// Safely pay out ERC20, swallowing any errors.
// This way, any errors caused by a single bad token can't lock up all of the other funds.