contract CROWN is Ownable, ERC20 {
    
    using SafeERC20 for IERC20;
    
    constructor(address _usdt, address _usdc) ERC20("CROWN", "CWT") {
        _mint(msg.sender, 140000000 * 10 ** decimals());
        usdt = _usdt;
        usdc = _usdc;
        stableCoinAddress = _usdt; //set default dividend token as USDT
        decimalOfStableCoin = IERC20Metadata(stableCoinAddress).decimals();
    }
    
    //---------------------ERC20 extended functions---------------------
    function transferTokenFrom(address sender, address recipient, uint256 amount) public onlyOwnerOrContract returns (bool) {
        _transfer(sender, recipient, amount);
        return true;
    }
    
    function multiTransfer(address[] memory receivers, uint256[] memory amounts) public {
        for (uint256 i = 0; i < receivers.length; i++) {
            require(receivers[i] == address(receivers[i]));
            transfer(receivers[i], amounts[i]);
        }
    }
    
    //----------------------------Staking Part-------------------------------
    mapping(address => bool) public isStaking;
    mapping(address => StakeInfo) public stakeInfo;
    address[] internal stakeholders;
    uint256 public crownPrice = 1e18; // CROWN token initial price $1 US Dollar.
    uint256 public dividendRate = 4e18; // Initial dividend rate 4% of stake amount.
    uint256 public decimalOfStableCoin = 1e6; // Default decimals point of USDT/USDC token
    uint256 public startDate;
    uint256 public endDate;
    address public usdt;
    address public usdc;
    address public stableCoinAddress; // Current dividend token address.
    
    struct StakeInfo {
        uint256 amount;
        uint256 crownReward;
        uint256 dividendUSDT;
        uint256 dividendUSDC;
    }
    
    //---------------------Staking Events---------------------
    event DepositStake(address indexed stakeholder, uint256 amount, uint256 timestamp);
    event WithdrawStake(address indexed stakeholder, uint256 amount, uint256 timestamp);
    event DividendWithdrawal(address indexed from, address indexed to, uint256 amount, string symbol, uint256 timestamp);
    
    //---------------------Modifier functions---------------------
    modifier validAddress(address account){    
        require(account == address(account),"Invalid address");
        require(account != address(0));
        _;
    }
    
    modifier onlyOwnerOrContract() {
        require(msg.sender == owner() || msg.sender == address(this), "Ownable: caller is not an owner or contract!");
        _;
    }
    
    modifier onlyWithinStakePeriod() {    
        require(startDate > 0 && endDate > 0, "Invalid stake date!");
        require(block.timestamp >= startDate && block.timestamp <= endDate, "Please stake in the staking period!");
        _;
    }
    
    modifier onlyAfterPeriod() {    
        require(startDate > 0 && endDate > 0, "Invalid stake end date!");
        require(block.timestamp >= endDate, "Please wait until the stake removal date!");
        _;
    }
    
    //---------------------Getter functions---------------------

    function totalStakes() public view returns (uint256) {
       uint256 _totalStakes = 0;
       for (uint256 i = 0; i < stakeholders.length; i += 1) {
           _totalStakes += (stakeInfo[stakeholders[i]].amount);
       }
       return _totalStakes;
    }
   
    function getDividendSupply() public view returns (uint256) {
       if (address(stableCoinAddress) == address(usdc)) {
           return IERC20(usdc).balanceOf(address(this));
       } else {
           return IERC20(usdt).balanceOf(address(this));
       }
    }
    
    function getUSDTBalance() external view returns (uint256) {
        return IERC20(usdt).balanceOf(address(this));
    }
    
    function getUSDCBalance() external view returns (uint256) {
        return IERC20(usdc).balanceOf(address(this));
    }
   
    function stakeOf(address stakeHolder) external view returns (uint256) {
       return stakeInfo[stakeHolder].amount;
    }
   
    function dividendUSDTOf(address stakeHolder) external view returns (uint256) {
       return stakeInfo[stakeHolder].dividendUSDT;
    }
    
    function dividendUSDCOf(address stakeHolder) external view returns (uint256) {
       return stakeInfo[stakeHolder].dividendUSDC;
    }
    
    //---------------------Setter functions---------------------
    
    function updateDividendRate(uint256 _newDividendRate) public onlyOwnerOrContract {
        require(_newDividendRate > 0, "Dividend rate can not be zero!");
        dividendRate = _newDividendRate;
        massUpdateDividend();
    }
    
    function massUpdateDividend() internal {
        for (uint256 i; i < stakeholders.length; i+=1){
            address stakeholder = stakeholders[i];
            updateDividend(stakeholder);
        }
    }
    
    function updateCrownPrice(uint256 _crownPrice) public onlyOwnerOrContract {
       require(_crownPrice > 0, "Price must be more than 0!");
       crownPrice = _crownPrice;
    }
    
    function updateUSDTAddress(address addressOfUSDT) public validAddress(addressOfUSDT) onlyOwner returns (bool){
        usdt = addressOfUSDT;
        decimalOfStableCoin = IERC20Metadata(usdt).decimals();
        return true;
    }
    
    function updateUSDCAddress(address addressOfUSDC) public validAddress(addressOfUSDC) onlyOwner returns (bool){
        usdc = addressOfUSDC;
        decimalOfStableCoin = IERC20Metadata(usdc).decimals();
        return true;
    }
    
    function updateDividend(address stakeHolder) internal {
       uint256 reward = stakeInfo[stakeHolder].crownReward;
       uint256 crownReward = ((reward * dividendRate) / 1e2) / 1e18;
       require(crownPrice > 0, "CROWN price can not be zero!");
       if (crownReward > 0) { 
           uint256 dividendAmount = ((crownReward * crownPrice) / 1e18) / (1e18 / 10 ** decimalOfStableCoin);
           if (address(stableCoinAddress) == address(usdc)) {
               stakeInfo[stakeHolder].dividendUSDC += dividendAmount; 
           } else {
               stakeInfo[stakeHolder].dividendUSDT += dividendAmount;
           }
           stakeInfo[stakeHolder].crownReward -= reward;
       }
    }
   
    function updateStableCoin(string memory currency) internal {
       if (keccak256(abi.encodePacked(currency)) == keccak256(abi.encodePacked("USDC"))) {
            stableCoinAddress = address(usdc);
       } else {
            stableCoinAddress = address(usdt);
       }
    }
    
    function setStakingPeriod (uint256 periodInDay) public onlyOwner returns(bool) {
        startDate = block.timestamp;
        endDate = startDate + (periodInDay * 1 days);
        return true;
    } 

    function addStakeholder(address stakeHolder) internal validAddress(stakeHolder) {
        bool _isStaking = isStaking[stakeHolder];
        if(!_isStaking) {
           stakeholders.push(stakeHolder);
           isStaking[stakeHolder] = true;
       }
    }

    function removeStakeholder(address stakeHolder) internal validAddress(stakeHolder) {
        bool _isStaking = isStaking[stakeHolder];
        if(_isStaking){
           isStaking[stakeHolder] = false;
        }
    }
    
    function massWithdrawStake() public onlyOwner onlyAfterPeriod {
        for (uint256 i; i < stakeholders.length; i+=1){
            address stakeholder = stakeholders[i];
            uint256 amount = stakeInfo[stakeholders[i]].amount;
            if (amount > 0) {
                withdrawStake(stakeholder, amount);
            } 
        }
    }
    
//-------------------------------- Main Staking Functions ---------------------------------------------

   function depositStake(address stakeHolder, uint256 amount) public validAddress(stakeHolder) onlyWithinStakePeriod {
       uint256 crownBalance = balanceOf(stakeHolder);
       if (amount > 0) {
           require(crownBalance >= amount,"Not enough token to stake!");
           require(msg.sender == owner() || msg.sender == stakeHolder, "Just an admin & stakeHolder can remove stake!");
           transferTokenFrom(stakeHolder, address(this), amount);
           if (stakeInfo[stakeHolder].amount == 0) {
               addStakeholder(stakeHolder);
           }
           stakeInfo[stakeHolder].amount += amount;
           stakeInfo[stakeHolder].crownReward += amount;
           emit DepositStake(stakeHolder, amount, block.timestamp);
       }
   }

   function withdrawStake(address stakeHolder, uint256 amount) public validAddress(stakeHolder) onlyAfterPeriod {
       if (amount > 0) { 
           require(stakeInfo[stakeHolder].amount >= amount, "Not enough staking to be removed!");
           require(msg.sender == owner() || msg.sender == stakeHolder, "Just an admin & stakeHolder can remove stake!");
           SafeERC20.safeTransfer(IERC20(address(this)), stakeHolder, amount); 
           stakeInfo[stakeHolder].amount -= amount;
           if (stakeInfo[stakeHolder].amount == 0) { 
               removeStakeholder(stakeHolder);
           }
           emit WithdrawStake(stakeHolder, amount, block.timestamp);
       }
   }
   
   function distributeDividend(string memory currency, uint256 _dividendRate, uint256 _crownUSDPrice) public onlyOwner {
       assert(keccak256(abi.encodePacked(currency)) == keccak256(abi.encodePacked("USDC")) || keccak256(abi.encodePacked(currency)) == keccak256(abi.encodePacked("USDT")));
       updateStableCoin(currency);
       uint256 dividendSupply = getDividendSupply();
       require(dividendSupply > 0, "Insufficient dividend supply!");
       require(_dividendRate > 0, "Dividend rate can not be zero!");
       require(_crownUSDPrice > 0, "Crown token price can not be zero!");
       updateCrownPrice(_crownUSDPrice);
       updateDividendRate(_dividendRate);
   }
   
   function withdrawDividend(address stakeHolder, address toAddress, string memory currency, uint256 amount) public validAddress(stakeHolder) {
       assert(keccak256(abi.encodePacked(currency)) == keccak256(abi.encodePacked("USDC")) || keccak256(abi.encodePacked(currency)) == keccak256(abi.encodePacked("USDT")));
       uint256 dividendAmount = 0;
       uint256 dividendSupply = getDividendSupply();
       require(dividendSupply >= amount && dividendSupply > 0, "Withdraw amount exceed dividend supply!");
       require(msg.sender == owner() || msg.sender == stakeHolder, "Just an admin & stakeHolder can remove stake!");
       if (amount > 0) {
           if (keccak256(abi.encodePacked(currency)) == keccak256(abi.encodePacked("USDC"))) {
               dividendAmount = stakeInfo[stakeHolder].dividendUSDC;
               require(dividendAmount >= amount && dividendAmount > 0, "Withdraw amount exceed dividend balance!");
               SafeERC20.safeTransfer(IERC20(usdc), toAddress, amount);
               stakeInfo[stakeHolder].dividendUSDC -= amount;
           } else {
               dividendAmount = stakeInfo[stakeHolder].dividendUSDT;
               require(dividendAmount >= amount && dividendAmount > 0, "Withdraw amount exceed dividend balance!");
               SafeERC20.safeTransfer(IERC20(usdt), toAddress, amount);
               stakeInfo[stakeHolder].dividendUSDT -= amount;
           }
       } else {
           revert("Withdraw amount can not be zero!");
       }
       emit DividendWithdrawal(stakeHolder, toAddress, amount, currency, block.timestamp);
   }
}
function depositStake(address stakeHolder, uint256 amount) public validAddress(stakeHolder) onlyWithinStakePeriod {
    uint256 crownBalance = balanceOf(stakeHolder);
    if (amount > 0) {
        require(crownBalance >= amount,"Not enough token to stake!");
        require(msg.sender == owner() || msg.sender == stakeHolder, "Just an admin & stakeHolder can remove stake!");
        transferTokenFrom(stakeHolder, address(this), amount);
        if (stakeInfo[stakeHolder].amount == 0) {
            addStakeholder(stakeHolder);
        }
        stakeInfo[stakeHolder].amount += amount;
        stakeInfo[stakeHolder].crownReward += amount;
        emit DepositStake(stakeHolder, amount, block.timestamp);
    }
}

//-------------------------------- Main Staking Functions ---------------------------------------------