// Contract: HumansOfTheMetaverseToken
// Target function: function watchTransfer(address _from, address _to, uint256 _tokenId) external
(address => YielderSettings) yielders;

    mapping(address => BoosterSettings) boosters;

    address[] public boostersAddresses; // boosters should be iterable

    mapping(address => mapping(address => EnumerableSet.UintSet)) tokensOwnerShip; // map econtract addrss => map owner address => yieldingToken

    uint256 public allowedPublicTokensMinted = 31207865 ether; // max total supply * 0.6
    uint256 public foundersAndOthersAllowedMinting = 20805244 ether;
    uint256 public foundersLinearDistributionPeriod = 365 days;

    uint256 public foundersAndOthersLastClaim;

    constructor() {
        _pause();
        foundersAndOthersLastClaim = block.timestamp;
    }

    // YIELDERS

    function setYielderSettings(
        uint256 _defaultYieldRate,
        uint256 _startTime,
        uint256 _endTime,
        uint256 _timeRate,
        address _yielderAddress
    ) external onlyOwner {
        YielderSettings storage yielderSettings =  yielders[_yielderAddress];

        yielderSettings._defaultYieldRate = _defaultYieldRate;
        yielderSettings._startTime = _startTime;
        yielderSettings._endTime = _endTime;
        yielderSettings._timeRate = _timeRate;
    }

    function getYielderSettings(address _address) internal view returns (YielderSettings storage) {
        YielderSettings storage yielderSettings = yielders[_address];
        require(yielderSettings._startTime != uint256(0), "There is no yielder with provided address");

        return yielderSettings;
    }

    function setTokenYielderMapping(
        address _yielderAddress,
        uint256[] calldata _tokenIds,
        uint256[] calldata _yieldingCores
    ) external onlyOwner {
        require(_tokenIds.length == _yieldingCores.length, "Provided arrays should have the same length");

        YielderSettings storage yielderSettings = getYielderSettings(_yielderAddress);

        for(uint256 i = 0; i < _tokenIds.length; ++i) {
            yielderSettings._tokenYieldingCoresMapping[_tokenIds[i]] = _yieldingCores[i];
        }
    }

    function setYieldingAmountMapping(
        address _yielderAddress,
        uint256[] calldata _yieldingCores,
        uint256[] calldata _amounts
    ) external onlyOwner {
        require(_amounts.length == _yieldingCores.length, "Provided arrays should have the same length");

        YielderSettings storage yielderSettings = getYielderSettings(_yielderAddress);

        for(uint256 i = 0; i < _yieldingCores.length; ++i) {
            yielderSettings._yieldingCoresAmountMapping[_yieldingCores[i]] = _amounts[i] * (10 ** 18); // cast to ether
        }
    }

    function setEndDateForYielder(uint256 _endTime, address _contract) external onlyOwner {
        YielderSettings storage yielderSettings = getYielderSettings(_contract);
        yielderSettings._endTime = _endTime;
    }

    function setStartDateForYielder(uint256 _startTime, address _contract) external onlyOwner {
        YielderSettings storage yielderSettings = getYielderSettings(_contract);
        yielderSettings._startTime = _startTime;
    }

    function setDefaultYieldRateForYielder(uint256 _defaultYieldRate, address _contract) external onlyOwner {
        YielderSettings storage yielderSettings = getYielderSettings(_contract);
        yielderSettings._defaultYieldRate = _defaultYieldRate * (10 ** 18);
    }

    function setTimeRateForYielder(uint256 _timeRate, address _contract) external onlyOwner {
        YielderSettings storage yielderSettings = getYielderSettings(_contract);
        yielderSettings._timeRate = _timeRate;
    }

    // BOOSTERS

    function setBoosterConfiguration(
        address _appliesFor,
        bool _status,
        address _boosterAddress
    ) external onlyOwner {
        boostersAddresses.push(_boosterAddress);
        BoosterSettings storage boosterSettings = boosters[_boosterAddress];
        boosterSettings._appliesFor=  _appliesFor;
        boosterSettings._status = _status;
    }

    function getBoosterSettings(address _address) internal view returns (BoosterSettings storage) {
        BoosterSettings storage boosterSettings = boosters[_address];
        require(boosterSettings._appliesFor != address(0), "There is no yielder with provided address");

        return boosterSettings;
    }

    function setBoosterCores(address _boosterAddress, uint256[] calldata _yieldingCoresIds) external onlyOwner {
        BoosterSettings storage boosterSettings = getBoosterSettings(_boosterAddress);
        for (uint256 i = 0; i < _yieldingCoresIds.length; ++i) {
            boosterSettings._yieldingCores.add(_yieldingCoresIds[i]);
        }
    }

    function setBoosterStatus(address _boosterAddress, bool _status) external onlyOwner {
        BoosterSettings storage boosterSettings = getBoosterSettings(_boosterAddress);
        boosterSettings._status = _status;
    }

    function setBoosterAppliesFor(address _boosterAddress, address _appliesFor) external onlyOwner{
        BoosterSettings storage boosterSettings = getBoosterSettings(_boosterAddress);
        boosterSettings._appliesFor = _appliesFor;
    }

    function replaceBoosterCores(address _boosterAddress, uint256[] calldata _yieldingCoresIds) external onlyOwner {
        BoosterSettings storage boosterSettings = getBoosterSettings(_boosterAddress);

        for (uint256 i = 0; i < boosterSettings._yieldingCores.length(); ++i) {
            boosterSettings._yieldingCores.remove(boosterSettings._yieldingCores.at(i));
        }

        for (uint256 i = 0; i < _yieldingCoresIds.length; ++i) {
            boosterSettings._yieldingCores.add(_yieldingCoresIds[i]);
        }
    }


    // TOKEN

    function claimRewards(
        address _contractAddress,
        uint256[] calldata _tokenIds
    ) external whenNotPaused nonReentrant() returns (uint256) {
        YielderSettings storage yielderSettings = getYielderSettings(_contractAddress);

        for(uint256 i = 0; i < _tokenIds.length; i++) {
            uint256 _tokenId = _tokenIds[i];
            processTokenOwnerShip(_contractAddress, _tokenId);
        }
        uint256 currentTime = block.timestamp;

        uint256 totalUnclaimedRewards = computeUnclaimedRewardsAndUpdate(yielderSettings, _contractAddress, _tokenIds, currentTime);

        claimTokens(totalUnclaimedRewards);

        for(uint256 i = 0; i < _tokenIds.length; i++) {
            uint256 _tokenId = _tokenIds[i];
            if (currentTime > yielderSettings._endTime) {
                yielderSettings._lastClaim[_tokenId] = yielderSettings._endTime;
            } else {
                yielderSettings._lastClaim[_tokenId] = currentTime;
            }
        }

        return totalUnclaimedRewards;
    }

    function checkClaimableAmount(address _contractAddress, uint256[] calldata _tokenIds) view external whenNotPaused returns(uint256) {
        YielderSettings storage yielderSettings = getYielderSettings(_contractAddress);

        uint256 totalUnclaimedRewards = computeUnclaimedRewardsSafe(yielderSettings, _contractAddress, _tokenIds, block.timestamp);

        return totalUnclaimedRewards;
    }

    function computeUnclaimedRewardsAndUpdate(
        YielderSettings storage _yielderSettings,
        address _yielderAddress,
        uint256[] calldata _tokenIds,
        uint256 _currentTime
    ) internal returns (uint256) {
        uint256 totalReward = 0;

        totalReward += computeBaseAccumulatedRewards(_yielderSettings, _tokenIds, _currentTime);
        totalReward += computeBoostersAccumulatedRewardsAndUpdate(_yielderAddress, _yielderSettings, _tokenIds, _currentTime);

        return totalReward;
    }

    function computeUnclaimedRewardsSafe(
        YielderSettings storage _yielderSettings,
        address _yielderAddress,
        uint256[] calldata _tokenIds,
        uint256 _currentTime
    ) internal view returns (uint256) {
        uint256 totalReward = 0;

        totalReward += computeBaseAccumulatedRewards(_yielderSettings, _tokenIds, _currentTime);
        totalReward += computeBoostersAccumulatedRewardsSafe(_yielderAddress, _yielderSettings, _tokenIds, _currentTime);

        return totalReward;
    }

    function computeBaseAccumulatedRewards(
        YielderSettings storage _yielderSettings,
        uint256[] calldata _tokenIds,
        uint256 _currentTime
    ) internal view returns (uint256) {
        uint256 baseAccumulatedRewards = 0;

        for (uint256 i = 0; i < _tokenIds.length; ++i) {
            uint256 lastClaimDate = getLastClaimForYielder(_yielderSettings, _tokenIds[i]);

            if (lastClaimDate != _yielderSettings._endTime) {
                uint256 secondsElapsed = _currentTime - lastClaimDate;
                if (_yielderSettings._defaultYieldRate != uint256(0)) {
                    baseAccumulatedRewards += secondsElapsed * _yielderSettings._defaultYieldRate / _yielderSettings._timeRate;
                } else {
                    baseAccumulatedRewards +=
                    secondsElapsed * _yielderSettings._yieldingCoresAmountMapping[_yielderSettings._tokenYieldingCoresMapping[_tokenIds[i]]] / _yielderSettings._timeRate;
                }
            }
        }

        return baseAccumulatedRewards;
    }

    function computeBoostersAccumulatedRewardsAndUpdate(
        address _yielderAddress,
        YielderSettings storage _yielderSettings,
        uint256[] calldata _tokenIds,
        uint256 _currentTime
    ) internal returns (uint256) {

        uint256 boosterAccumulatedRewards = 0;

        for (uint256 boosterIndex = 0; boosterIndex < boostersAddresses.length; ++boosterIndex) {
            BoosterSettings storage boosterSettings = getBoosterSettings(boostersAddresses[boosterIndex]);
            uint256 toBeSentArraysIndex = 0;
            uint256[] memory accumulatedRewardsForBooster = new uint256[](_tokenIds.length);
            uint256[] memory validTokensCandidates = new uint256[](_tokenIds.length);

            if (boosterSettings._appliesFor == _yielderAddress && boosterSettings._status) {
                for (uint256 i = 0; i < _tokenIds.length; ++i) {
                    uint256 boosterStartDate = getLastClaimForBooster(boosterSettings, _tokenIds[i]);
                    if (
                        (
                        boosterSettings._yieldingCores.length() == 0
                        || boosterSettings._yieldingCores.contains(_yielderSettings._tokenYieldingCoresMapping[_tokenIds[i]])
                        ) && getLastClaimForYielder(_yielderSettings, _tokenIds[i]) != _yielderSettings._endTime
                        && boosterStartDate != uint256(0)
                    ) {
                        uint256 secondsElapsed = _currentTime - boosterStartDate;

                        if (_yielderSettings._defaultYieldRate != uint256(0)) {
                            accumulatedRewardsForBooster[toBeSentArraysIndex] =
                            secondsElapsed * _yielderSettings._defaultYieldRate / _yielderSettings._timeRate;
                        } else {
                            uint256 tokenYieldingCoresMapping = _yielderSettings._tokenYieldingCoresMapping[_tokenIds[i]];
                            uint256 yieldingCoresAmountMapping = _yielderSettings._yieldingCoresAmountMapping[tokenYieldingCoresMapping];
                            accumulatedRewardsForBooster[toBeSentArraysIndex] =
                            secondsElapsed * yieldingCoresAmountMapping / _yielderSettings._timeRate;
                        }
                        validTokensCandidates[toBeSentArraysIndex] = _tokenIds[i];
                        toBeSentArraysIndex++;
                    }
                }
                if (boosterSettings._yieldingCores.length() != 0) {
                    uint256[] memory yieldingCores = new uint256[](validTokensCandidates.length);

                    for (uint256 i = 0; i < validTokensCandidates.length; ++i) {
                        yieldingCores[i] = _yielderSettings._tokenYieldingCoresMapping[validTokensCandidates[i]];
                    }

                    boosterAccumulatedRewards +=
                    IBooster(boostersAddresses[boosterIndex]).computeAmounts(accumulatedRewardsForBooster, yieldingCores, validTokensCandidates);
                } else {
                    uint256 summedBoosterAccumulatedRewards = 0;
                    for (uint256 i = 0; i < validTokensCandidates.length; ++i) {
                        summedBoosterAccumulatedRewards += accumulatedRewardsForBooster[i];
                    }
                    boosterAccumulatedRewards += IBooster(boostersAddresses[boosterIndex]).computeAmount(summedBoosterAccumulatedRewards);
                }
                for (uint256 i = 0; i < validTokensCandidates.length; ++i) {
                    if (boosterSettings._boosterStartDates[validTokensCandidates[i]] != uint256(0)) {
                        boosterSettings._boosterStartDates[validTokensCandidates[i]] = _currentTime;
                    }
                }
            }
        }

        return boosterAccumulatedRewards;
    }

    function computeBoostersAccumulatedRewardsSafe(
        address _yielderAddress,
        YielderSettings storage _yielderSettings,
        uint256[] calldata _tokenIds,
        uint256 _currentTime
    ) internal view returns (uint256) {

        uint256 boosterAccumulatedRewards = 0;

        for (uint256 boosterIndex = 0; boosterIndex < boostersAddresses.length; ++boosterIndex) {
            BoosterSettings storage boosterSettings = getBoosterSettings(boostersAddresses[boosterIndex]);
            uint256 toBeSentArraysIndex = 0;
            uint256[] memory accumulatedRewardsForBooster = new uint256[](_tokenIds.length);
            uint256[] memory validTokensCandidates = new uint256[](_tokenIds.length);

            if (boosterSettings._appliesFor == _yielderAddress && boosterSettings._status) {
                for (uint256 i = 0; i < _tokenIds.length; ++i) {
                    uint256 boosterStartDate = getLastClaimForBooster(boosterSettings, _tokenIds[i]);
                    if (
                        (
                        boosterSettings._yieldingCores.length() == 0
                        || boosterSettings._yieldingCores.contains(_yielderSettings._tokenYieldingCoresMapping[_tokenIds[i]])
                        ) && getLastClaimForYielder(_yielderSettings, _tokenIds[i]) != _yielderSettings._endTime
                        && boosterStartDate != uint256(0)
                    ) {

                        uint256 secondsElapsed = _currentTime - boosterStartDate;

                        if (_yielderSettings._defaultYieldRate != uint256(0)) {
                            accumulatedRewardsForBooster[toBeSentArraysIndex] =
                            secondsElapsed * _yielderSettings._defaultYieldRate / _yielderSettings._timeRate;
                        } else {
                            uint256 tokenYieldingCoresMapping = _yielderSettings._tokenYieldingCoresMapping[_tokenIds[i]];
                            uint256 yieldingCoresAmountMapping = _yielderSettings._yieldingCoresAmountMapping[tokenYieldingCoresMapping];
                            accumulatedRewardsForBooster[toBeSentArraysIndex] =
                            secondsElapsed * yieldingCoresAmountMapping / _yielderSettings._timeRate;
                        }
                        validTokensCandidates[toBeSentArraysIndex] = _tokenIds[i];
                        toBeSentArraysIndex++;
                    }
                }
                if (boosterSettings._yieldingCores.length() != 0) {
                    uint256[] memory yieldingCores = new uint256[](validTokensCandidates.length);

                    for (uint256 i = 0; i < validTokensCandidates.length; ++i) {
                        yieldingCores[i] = _yielderSettings._tokenYieldingCoresMapping[validTokensCandidates[i]];
                    }

                    boosterAccumulatedRewards +=
                    IBooster(boostersAddresses[boosterIndex]).computeAmounts(accumulatedRewardsForBooster, yieldingCores, validTokensCandidates);

                } else {
                    uint256 summedBoosterAccumulatedRewards = 0;
                    for (uint256 i = 0; i < validTokensCandidates.length; ++i) {
                        summedBoosterAccumulatedRewards += accumulatedRewardsForBooster[i];
                    }
                    boosterAccumulatedRewards += IBooster(boostersAddresses[boosterIndex]).computeAmount(summedBoosterAccumulatedRewards);
                }
            }
        }

        return boosterAccumulatedRewards;
    }

    function getLastClaimForYielder(YielderSettings storage _yielderSettings, uint256 _tokenId) internal view returns (uint256) {
        uint256 lastClaimDate =  _yielderSettings._lastClaim[_tokenId];
        if (lastClaimDate == uint256(0)) {
            lastClaimDate = _yielderSettings._startTime;
        }

        return lastClaimDate;
    }

    function getLastClaim(address _yielderAddress, uint256 _tokenId) external whenNotPaused view returns (uint256) {
        YielderSettings storage yielderSettings = getYielderSettings(_yielderAddress);

        return getLastClaimForYielder(yielderSettings, _tokenId);
    }

    function getLastClaimForBooster(BoosterSettings storage _boosterSettings, uint256 _tokenId) view internal returns (uint256) {
        uint256 lastClaimDate = _boosterSettings._boosterStartDates[_tokenId];

        return lastClaimDate;
    }