contract THE9 is ERC20, Pausable, AccessControl {
    uint256 constant DEFAULT_RELEASE_TIMESTAMP = 4102444800; // 2100년 January 1일 Friday AM 12:00:00
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant ORG_ADMIN_ROLE = keccak256("ORG_ADMIN_ROLE");

    struct BeneficiaryInfo {
        uint256 amount;
        uint256 releaseTime;
        uint256 unlockPercent;
        uint256 lockCycleDays;
        uint256 remainPercent;
        uint256 remainAmount;
    }

    mapping(address => BeneficiaryInfo) private _addressBeneficiaryInfo;
    address[] public Beneficiaries;

    constructor() ERC20("THE9", "THE9") {
        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
        _setupRole(PAUSER_ROLE, _msgSender());
        _setupRole(ORG_ADMIN_ROLE, _msgSender());
        _mint(_msgSender(), _calcDecimal(10000000000));
    }

    event CreateAmountWithLock(address beneficiary, BeneficiaryInfo beneficiaryInfo);
    event UpdateAmountWithLock(address beneficiary, uint256 amount, uint256 releaseTime, BeneficiaryInfo beneficiaryInfo);
    event TransferAmountWithLock(address beneficiary, uint256 amount, BeneficiaryInfo beneficiaryInfo);

    /* public functions */
    function pause() public onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() public onlyRole(PAUSER_ROLE) {
        _unpause();
    }
       
    function createAmountWithLock(address beneficiary, uint256 amount, uint256 firstReleaseTime, uint256 unlockPercent, uint256 lockCycleDays)
        public
        whenNotPaused
        onlyRole(ORG_ADMIN_ROLE) 
    {
        if (firstReleaseTime == 0) {
            firstReleaseTime = DEFAULT_RELEASE_TIMESTAMP;
        }
        assert(_beforeSaveAmountWithLock(beneficiary, firstReleaseTime, unlockPercent, lockCycleDays));
        
        if (_checkExists(beneficiary)) {
            updateAmountWithLock(beneficiary, amount, firstReleaseTime, unlockPercent, lockCycleDays);
        } else {
            _setInfo(
                beneficiary, 
                _calcDecimal(amount), 
                firstReleaseTime, 
                unlockPercent, 
                lockCycleDays, 
                100, 
                _calcDecimal(amount)
            );
            Beneficiaries.push(beneficiary);

            emit CreateAmountWithLock(beneficiary, _getInfo(beneficiary));
        }
    }

    function updateAmountWithLock(address beneficiary, uint256 amount, uint256 firstReleaseTime, uint256 unlockPercent, uint256 lockCycleDays)
        public
        whenNotPaused
        onlyRole(ORG_ADMIN_ROLE) 
    {
        require(_checkExists(beneficiary), "THE9: beneficiary not found");
        
        if (firstReleaseTime == 0) {
            firstReleaseTime = DEFAULT_RELEASE_TIMESTAMP;
        }
        assert(_beforeSaveAmountWithLock(beneficiary, firstReleaseTime, unlockPercent, lockCycleDays));
        
        BeneficiaryInfo memory beforeInfo = _getInfo(beneficiary);
        if(beforeInfo.remainPercent > 0 && beforeInfo.remainAmount > 0) {
            require(_getInfo(beneficiary).remainPercent >= 100, "THE9: account to which revenue was transferred");    
        }
        
        _setInfo(
            beneficiary, 
            _calcDecimal(amount), 
            firstReleaseTime, 
            unlockPercent, 
            lockCycleDays, 
            100, 
            _calcDecimal(amount)
        );
        
        emit UpdateAmountWithLock(beneficiary, amount, firstReleaseTime, beforeInfo);
    }

    function transferAmountWithLock(address beneficiary)
        public
        whenNotPaused
        onlyRole(ORG_ADMIN_ROLE) 
    {
        require(beneficiary != address(0), "THE9: beneficiary from the zero address");

        // check exists
        require(_checkExists(beneficiary), "THE9: beneficiary not found");

        // check timestamp
        BeneficiaryInfo memory beforeInfo = _getInfo(beneficiary);
        require(_isReleased(beneficiary), "THE9: It hasn't been released yet");
        require(beforeInfo.remainAmount > 0, "THE9: No remaining amount");

        // remainAmount
        uint256 remainAmount = beforeInfo.remainAmount;
        uint256 remainPercent = beforeInfo.remainPercent;
        
        // check transfer available amount
        uint256 transferableAmount = _getTransferableAmount(beneficiary);
        if (transferableAmount > beforeInfo.remainAmount) {
            transferableAmount = beforeInfo.remainAmount;
            remainPercent = 0;
        } else {
            remainPercent = SafeMath.sub(remainPercent, beforeInfo.unlockPercent);
        }

        _beforeTokenTransfer(_msgSender(), beneficiary, transferableAmount);

        transfer(beneficiary, transferableAmount);

        _setInfo(
            beneficiary, 
            beforeInfo.amount, 
            _getNextReleaseTime(beneficiary), 
            beforeInfo.unlockPercent, 
            beforeInfo.lockCycleDays, 
            remainPercent, 
            SafeMath.sub(remainAmount, transferableAmount)
        );
        
        super._afterTokenTransfer(_msgSender(), beneficiary, transferableAmount);
        emit TransferAmountWithLock(beneficiary, transferableAmount, _getInfo(beneficiary));
    }

    function getBeneficiaryInfo(address beneficiary) 
        public 
        view 
        returns(uint256, uint256, uint256, uint256, uint256, uint256, uint256) 
    {
        require(beneficiary != address(0), "THE9: beneficiary from the zero address");
        BeneficiaryInfo memory beforeInfo = _getInfo(beneficiary);
        
        // check transfer available amount
        uint256 transferableAmount = _getTransferableAmount(beneficiary);
        if (transferableAmount > beforeInfo.remainAmount) {
            transferableAmount = beforeInfo.remainAmount;
        }
        
        return (beforeInfo.amount,
            beforeInfo.releaseTime,
            beforeInfo.unlockPercent,
            beforeInfo.lockCycleDays,
            beforeInfo.remainPercent,
            beforeInfo.remainAmount,
            transferableAmount);
    }


    /* internal functions */
    function _getInfo(address beneficiary)
        internal
        view
        returns (BeneficiaryInfo memory)
    {
        return _addressBeneficiaryInfo[beneficiary];
    }
    
    function _setInfo(address beneficiary, uint256 amount, uint256 releaseTime, uint256 unlockPercent, uint256 lockCycleDays, uint256 remainPercent, uint256 remainAmount)
        internal
    {
        _addressBeneficiaryInfo[beneficiary].amount = amount;
        _addressBeneficiaryInfo[beneficiary].releaseTime = releaseTime;
        _addressBeneficiaryInfo[beneficiary].unlockPercent = unlockPercent;
        _addressBeneficiaryInfo[beneficiary].lockCycleDays = lockCycleDays;
        _addressBeneficiaryInfo[beneficiary].remainPercent = remainPercent;
        _addressBeneficiaryInfo[beneficiary].remainAmount = remainAmount;
    }
    
    function _beforeSaveAmountWithLock(address beneficiary, uint256 firstReleaseTime, uint256 unlockPercent, uint256 lockCycleDays)
        internal
        view
        returns (bool)
    {
        require(beneficiary != address(0), "THE9: beneficiary from the zero address");
        require(unlockPercent > 0, "THE9: percentage cannot be zero");
        require(unlockPercent <= 100, "THE9: percentage cannot exceed 100");
        require(lockCycleDays > 0, "THE9: lock cycle days cannot be zero");
        require(firstReleaseTime > block.timestamp, "THE9: first release time is before current time");
        
        return true;
    }
    
    function _beforeTokenTransfer(address from, address to, uint256 amount)
        internal
        whenNotPaused
        override
    {
        super._beforeTokenTransfer(from, to, amount);
    }

    function _isReleased(address beneficiary) 
        internal 
        view 
        returns(bool) 
    {
        if (_checkExists(beneficiary)) {
            return block.timestamp >= _getInfo(beneficiary).releaseTime;
        } else {
            return false;
        }
    }

    function _getNextReleaseTime(address beneficiary) 
        internal 
        view 
        returns(uint256) 
    {
        return SafeMath.add(_getInfo(beneficiary).releaseTime, SafeMath.mul(_getInfo(beneficiary).lockCycleDays, 24*60*60)); // per day
    }
    
    function _checkExists(address beneficiary) 
        internal 
        view 
        returns(bool) 
    {
        if (_getInfo(beneficiary).amount > 0) {
            return true;
        } else {
            return false;
        }
    }
    
    function _calcDecimal(uint256 amount) 
        internal 
        view
        returns(uint256)
    {
        return amount * 10 ** decimals();
    }
    
    function _getTransferableAmount(address beneficiary)
        internal
        view
        returns (uint256)
    {
        if (_isReleased(beneficiary)) {
            return SafeMath.div(SafeMath.mul(_getInfo(beneficiary).amount, _getInfo(beneficiary).unlockPercent), 100);
        } else {
            return 0;
        }
    }
}
function _getInfo(address beneficiary)
        internal
        view
        returns (BeneficiaryInfo memory)
    {
        return _addressBeneficiaryInfo[beneficiary];
    }
/* internal functions */