// Contract: PampStaking
// Target function: function claimHoldersDay() external
.sender == nextStakingContract);
        _;
    }
    
    modifier onlyOracle() {
        assert(msg.sender == oracle);
        _;
    }
    
    modifier onlyOwner() {
        assert(msg.sender == owner);
        _;
    }

    
    mapping (address => staker) public stakers;        // Mapping of all individuals staking/holding tokens greater than minStake
    
    mapping (address => string) public whitelist;      // Mapping of all addresses that do not burn tokens on receive and send (generally other smart contracts). Mapping of address to reason (string)
    
    mapping (address => uint256) public blacklist;     // Mapping of all addresses that receive a specific token burn when receiving. Mapping of address to percent burn (uint256)
    
    mapping (address => string) public uniwhitelist; // Mapping of all addresses that do not burn tokens when sending to or selling on Uniswap. Mapping of address to reason (string)
    

    bool public enableBurns; // Enable burning on transfer or fee on transfer
    
    bool public priceTarget1Hit;  // Price targets, defined in updateState()
    
    bool public priceTarget2Hit;
    
    address public uniswapV2Pair;      // Uniswap pair address, done for fees on Uniswap sells
    
    uint public uniswapSellerBurnPercent;        // Uniswap sells pay a fee. Should be based on negative streaks
    
    uint public transferBurnPercent;
    
    bool public enableUniswapDirectBurns;         // Enable seller fees on Uniswap
    
    uint256 public minStake;                      // Minimum amount to stake
        
    uint8 public minStakeDurationDays;            // Minimum amount of time to claim staking rewards
    
    uint8 public minPercentIncrease;              // Minimum percent increase to enable rewards for the day. 10 = 1.0%, 100 = 10.0%
    
    uint256 public inflationAdjustmentFactor;     // Factor to adjust the amount of rewards (inflation) to be given out in a single day
    
    uint256 public streak;                        // Number of days in a row that the price has increased
    
    uint public maxStreak;                          // Max number of days in a row we consider streak bonuses
        
    uint public negativeStreak;                     // Number of days in a row that the price has decreased
    
    update public lastUpdate;                      // latest price update

    uint public lastNegativeUpdate;                 // last time the price was negative (unix timestamp)
    
    CalculatorInterface public externalCalculator;    // external calculator to calculate the number of tokens given several variables (defined above). Currently unused
    
    address public nextStakingContract;                // Next staking contract deployed. Used for migrating staker state.
    
    bool public useExternalCalc;                      // self-explanatory
    
    bool public useExternalCalcIterative;
    
    bool public freeze;                               // freeze all transfers in an emergency
    
    bool public enableHoldersDay;                     // once a month, holders receive a nice bump. This is true for 24 hours, once a month only.
    
    mapping (bytes32 => bool) public holdersDayRewarded; // Mapping to test whether an individual received his Holder's Day reward
    
    event StakerRemoved(address StakerAddress);     // Staker was removed due to balance dropping below minStake
    
    event StakerAdded(address StakerAddress);       // Staker was added due to balance increasing abolve minStake
    
    event StakesUpdated(uint Amount);               // Staking rewards were claimed
    
    event HoldersDayEnabled();
    
    event HoldersDayRewarded(uint Amount);
    
    event Migration(address StakerAddress);
    
    event MassiveCelebration();                     // Happens when price targets are hit
    
    event Transfer(address indexed from, address indexed to, uint256 value);        // self-explanatory
    
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    
    uint public maxStakingDays;
    
    uint public holdersDayRewardDenominator;
    
    update[] public updates;
    
    address public liquidityStakingContract;
    
    address public oracle;
    
    PreviousContract public previousStakingContract;
    
    uint public numStakers;
    
    bool public increaseTransferFees;

    bool public checkPreviousStakingContractWhitelist;
    
    constructor () public {
        owner = msg.sender;
        token = PampToken(0xF0FAC7104aAC544e4a7CE1A55ADF2B5a25c65bD1);
        minStake = 200E18;
        inflationAdjustmentFactor = 800;
        streak = 0;
        minStakeDurationDays = 1;
        useExternalCalc = false; 
        useExternalCalcIterative = false;
        uniswapSellerBurnPercent = 8;
        enableBurns = true;
        freeze = false;
        minPercentIncrease = 10; // 1.0% min increase
        enableUniswapDirectBurns = true;
        transferBurnPercent = 8;
        priceTarget1Hit = true;
        oracle = msg.sender;
        maxStreak = 7;
        holdersDayRewardDenominator = 600;
        maxStakingDays = 100;
        increaseTransferFees = false;
        checkPreviousStakingContractWhitelist = true;
        previousStakingContract = PreviousContract(0x1d2121Efe25535850d1FDB65F930FeAB093416E0);
        uniswapV2Pair = 0x1C608235E6A946403F2a048a38550BefE41e1B85;
        liquidityStakingContract = 0x5CECDbdfB96463045b07d07aAa4fc2F1316F7e47;
    }
    
    // The owner (or price oracle) will call this function to update the price on days the coin is positive. On negative days, no update is made.
    
    function updateState(uint numerator, uint denominator, uint256 price, uint256 volume) external onlyOracle {  // when Chainlink is integrated a separate contract will call this function (oracle address will be changed as well)
    
        require(numerator > 0 && denominator > 0 && price > 0 && volume > 0, "Parameters cannot be negative or zero");
        
        if ((numerator < 2 && denominator == 100) || (numerator < 20 && denominator == 1000)) {
            require(mulDiv(1000, numerator, denominator) >= minPercentIncrease, "Increase must be at least minPercentIncrease to count");
        }
        
        uint secondsSinceLastUpdate = (block.timestamp - lastUpdate.timestamp);       // We calculate time since last price update in days.
        
        if (secondsSinceLastUpdate < 129600) { // We should only update once per day, but block timestamps can vary
            streak++;
        } else {
            streak = 1;
        }
        
        if (streak > maxStreak) {
            streak = maxStreak;
        }
        
        if (price >= 1000 && priceTarget1Hit == false) { // 1000 = $1.00
            priceTarget1Hit = true;
            streak = 50;
            emit MassiveCelebration();
            
        } else if (price >= 10000 && priceTarget2Hit == false) {   // It is written, so it shall be done
            priceTarget2Hit = true;
            streak = maxStreak;
            minStake = 100E18;        // Need $1000 to stake
            emit MassiveCelebration();
        }
        
        if(negativeStreak > 0) {
            uniswapSellerBurnPercent = uniswapSellerBurnPercent - (negativeStreak * 2);
            if(increaseTransferFees) {
                transferBurnPercent = transferBurnPercent - (negativeStreak * 2);
            }
            negativeStreak = 0;
        }
        
        
        lastUpdate = update(block.timestamp, numerator, denominator, price, volume, streak);
        
        updates.push(lastUpdate);

    }
    
    // We now update the smart contract on negative days. Currently this is only used to increase the Uniswap burn percent, but we may perform other functionality in the future.
    function updateStateNegative(int numerator, uint denominator, uint256 price, uint256 volume) external onlyOracle { 
        require(numerator < minPercentIncrease);
        
        uint secondsSinceLastUpdate = (block.timestamp - lastNegativeUpdate);       // We calculate time since last negative price update in days.
        
        if (secondsSinceLastUpdate < 129600) { // We should only update once per day, but block timestamps can vary
            negativeStreak++;
        } else {
            negativeStreak = 0;
        }
        
        streak = 1;
        
        uniswapSellerBurnPercent = uniswapSellerBurnPercent + (negativeStreak * 2);     // Negative day streaks increase burn fees
        
        if(increaseTransferFees) {
            transferBurnPercent = transferBurnPercent + (negativeStreak * 2);       // May have to contact exchanges about this
        }
        
        lastNegativeUpdate = block.timestamp;

        if(useExternalCalc) {
            externalCalculator.negativeDayCallback(numerator, denominator, price, volume);
        }
        
    }
    
    // This is used by the next staking contract to migrate staker state
    function resetStakeTimeMigrateState(address addr) external onlyNextStakingContract returns (uint256 startTimestamp, uint256 lastTimestamp) {
        startTimestamp = stakers[addr].startTimestamp;
        lastTimestamp = stakers[addr].lastTimestamp;
        stakers[addr].lastTimestamp = block.timestamp;
        stakers[addr].startTimestamp = block.timestamp;
    }
    
    function migratePreviousState() external {      // Migrate state to new contract and reset state from old contract. Also reset current state to block.timestamp if it is zero otherwise
        
        require(stakers[msg.sender].lastTimestamp == 0, "Last timestamp must be zero");
        require(stakers[msg.sender].startTimestamp == 0, "Start timestamp must be zero");
        require(!stakers[msg.sender].hasMigrated);
        
        (uint startTimestamp, uint lastTimestamp) = previousStakingContract.resetStakeTimeMigrateState(msg.sender);
        
        if(startTimestamp == 0) {
            stakers[msg.sender].startTimestamp = block.timestamp;
        } else {
            stakers[msg.sender].startTimestamp = startTimestamp;
        }
        if(lastTimestamp == 0) {
            stakers[msg.sender].lastTimestamp = block.timestamp;
        } else {
            stakers[msg.sender].lastTimestamp = lastTimestamp;
        }
        
        if(stakers[msg.sender].startTimestamp > stakers[msg.sender].lastTimestamp) {
            stakers[msg.sender].lastTimestamp = block.timestamp;
        }
        
        stakers[msg.sender].hasMigrated = true;
        
        numStakers++;
        
        emit Migration(msg.sender);
    }
    
    function updateMyStakes(address stakerAddress, uint256 balance, uint256 totalSupply) external onlyToken returns (uint256) {     // This function is called by the token contract. Holders call the function on the token contract to claim rewards.
        
        assert(balance > 0);
        
        staker memory thisStaker = stakers[stakerAddress];
        
        assert(thisStaker.lastTimestamp > 0/*,"Error: your last timestamp cannot be zero."*/); // We use asserts now so that we fail on errors due to try/catch in token contract.
        
        assert(thisStaker.startTimestamp > 0/*,"Error: your start timestamp cannot be zero."*/);
        
        assert(thisStaker.hasMigrated);     // If you didn't migrate or reset your state, you can't claim
        
        assert(block.timestamp > thisStaker.lastTimestamp/*, "Error: block timestamp is not greater than your last timestamp!"*/);
        assert(lastUpdate.timestamp > thisStaker.lastTimestamp/*, "Error: you can only update stakes once per day. You also cannot update stakes on the same day that you purchased them."*/);
        
        uint daysStaked = block.timestamp.sub(thisStaker.startTimestamp) / 86400;  // Calculate time staked in days
        
        assert(daysStaked >= minStakeDurationDays/*, "You must stake for at least minStakeDurationDays to claim rewards"*/);
        assert(balance >= minStake/*, "You must have a balance of at least minStake to claim rewards"*/);
        
        assert(thisStaker.lastTimestamp >= thisStaker.startTimestamp); // last timestamp should be greater than or equal to start timestamp
        
        uint numTokens = iterativelyCalculateOwedRewards(thisStaker.lastTimestamp, thisStaker.startTimestamp, balance, stakerAddress, totalSupply);
        
        stakers[stakerAddress].lastTimestamp = block.timestamp;        // Again, this can be gamed to some extent, but *cannot be before the last block*
        emit StakesUpdated(numTokens);
        
        return numTokens;       // Token contract will add these tokens to the balance of stakerAddress
    }
    
    struct iterativeCalculationVariables {
        uint index;
        uint bound;
        uint numTokens;
        uint calculatedTokens;
    }
    
    
    // Calculate owed rewards for several days, iterating back through the updates array. This is public so that the frontend can calculate expected rewards.
    function iterativelyCalculateOwedRewards(uint stakerLastTimestamp, uint stakerStartTimestamp, uint balance, address stakerAddress, uint totalSupply) public view returns (uint256) {
        
        if(useExternalCalcIterative) {
            return externalCalculator.iterativelyCalculateOwedRewards(stakerLastTimestamp, stakerStartTimestamp, balance, stakerAddress, totalSupply);
        }
        
        iterativeCalculationVariables memory vars;    // Necessary to fix stack too deep error
         
        vars.index = updates.length.sub(1); // Start from the latest update and work our way back
        
        if(vars.index > 60) {
            vars.bound = vars.index.sub(60);        // We bound the loop to 60 iterations (60 positive days)
        } else {
            vars.bound = vars.index.add(1);                    // No bound on the loop because the number of elements is less than 60
        }

        vars.numTokens = 0;
        
        for(bool end = false; end == false;) {
            
            update memory nextUpdate = updates[vars.index];      // Grab the last update from the array
            if(stakerLastTimestamp > nextUpdate.timestamp || stakerStartTimestamp > nextUpdate.timestamp || vars.index == vars.bound) { // If the staker's last timestamp or start timestamp is ahead of the next update, the staker is not owed the rewards from that update, and the updates array is in chronological order, so we end here, we also check for the bound
                end = true;
            } else {
                uint estimatedDaysStaked = nextUpdate.timestamp.sub(stakerStartTimestamp) / 86400; // We estimate the staker's holding time from the point of view of this current update
                
                vars.calculatedTokens = calculateNumTokens(nextUpdate.numerator, nextUpdate.denominator, nextUpdate.price, nextUpdate.volume, nextUpdate.streak, balance, estimatedDaysStaked, stakerAddress, totalSupply); // calculate the owed tokens from this update
                
                vars.numTokens = vars.numTokens.add(vars.calculatedTokens);
                
                balance = balance.add(vars.calculatedTokens); // We support compound interest
            }
            
            if (vars.index > 0) {
                vars.index = vars.index.sub(1);     // Only subtract when nonzero, so we don't underflow
            } else {
                end = true;
            }
            
        }
        return vars.numTokens;
    }

    function calculateNumTokens(uint numerator, uint denominator, uint price, uint volume, uint _streak, uint256 balance, uint256 daysStaked, address stakerAddress, uint256 totalSupply) public view returns (uint256) { // This is public so that the Pamp frontend can calculate expected rewards without any js issues
        
        if (useExternalCalc) {
            return externalCalculator.calculateNumTokens(numerator, denominator, price, volume, _streak, balance, daysStaked, stakerAddress, totalSupply); // Use external contract, if one is enabled (disabled by default, currently unused)
        }
        
        uint256 _inflationAdjustmentFactor = inflationAdjustmentFactor;
        
        if (_streak > 1) {
            _inflationAdjustmentFactor = _inflationAdjustmentFactor.sub(mulDiv(_inflationAdjustmentFactor, _streak*10, 100));       // If there is a streak, we decrease the inflationAdjustmentFactor
        }
        
        if (daysStaked > maxStakingDays) {      // If you stake for more than maxStakingDays days, you have hit the upper limit of the multiplier
            daysStaked = maxStakingDays;
        } else if (daysStaked == 0) {   // If the minimum days staked is zero, we change the number to 1 so we don't return zero below
            daysStaked = 1;
        }
        
        uint ratio = mulDiv(totalSupply, price, 1000E18).div(volume);       // Ratio of market cap (including locked team tokens) to volume
        
        if (ratio > 50) {  // Too little volume. Decrease rewards. To be honest, this number was arbitrarily chosen.
            _inflationAdjustmentFactor = _inflationAdjustmentFactor.mul(10);
        } else if (ratio > 25) { // Still not enough. Streak doesn't count.
            _inflationAdjustmentFactor = inflationAdjustmentFactor;
        }
        
        uint numTokens = mulDiv(balance, numerator * daysStaked, denominator * _inflationAdjustmentFactor);      // Function that calculates how many tokens are due. See muldiv below.
        uint tenPercent = mulDiv(balance, 1, 10);
        
        if (numTokens > tenPercent) {       // We don't allow a daily rewards of greater than ten percent of a holder's balance.
            numTokens = tenPercent;
        }
        
        return numTokens;
    }

    function getDaysStaked(address _staker) external view returns(uint) {
        return block.timestamp.sub(stakers[_staker].startTimestamp) / 86400;
    }