contract RAID is ERC20Pausable, ERC20Detailed {
    string internal constant INVALID_TOKEN_VALUES = "Invalid token values";
    string internal constant NOT_ENOUGH_TOKENS = "Not enough tokens";
    string internal constant ALREADY_LOCKED = "Tokens already locked";
    string internal constant NOT_LOCKED = "No tokens locked";
    string internal constant AMOUNT_ZERO = "Amount can not be 0";
    string internal constant EXPIRED_ADDRESS = "Expired Address";

    uint256 public constant INITIAL_SUPPLY = 100000000000 * (10 ** uint256(18));

    mapping(address => bytes32[]) public lockReason;

    struct lockToken {
        uint256 amount;
        uint256 validity;
        bool claimed;
    }

    mapping(address => mapping(bytes32 => lockToken)) public locked;

    event Locked(address indexed _of, bytes32 indexed _reason, uint256 _amount, uint256 _validity);
    event Unlocked(address indexed _of, bytes32 indexed _reason, uint256 _amount);

    event RemoveLock(address indexed _of, bytes32 indexed _reason, uint256 _amount, uint256 _validity);
    event RemoveExpired(address indexed _of, uint256 _amount, uint256 _validity);

    constructor ()
    ERC20Detailed ("RAID", "RAID", 18)
    public {
        _mint(msg.sender, INITIAL_SUPPLY);
    }

    // ERC20 회수
    function claimToken(ERC20 token, address _to, uint256 _value) public onlyOwner returns (bool) {
        token.transfer(_to, _value);
        return true;
    }

    // 특정 Account에 지정된 기간동안 락업된 토큰을 전송
    function transferWithLock(address _to, bytes32 _reason, uint256 _amount, uint256 _time) public onlyOwner returns (bool) {
        uint256 validUntil = now.add(_time);

        require(tokensLocked(_to, _reason) == 0, ALREADY_LOCKED);
        require(_amount != 0, AMOUNT_ZERO);

        if (locked[_to][_reason].amount == 0)
            lockReason[_to].push(_reason);

        transfer(address(this), _amount);

        locked[_to][_reason] = lockToken(_amount, validUntil, false);
        
        emit Locked(_to, _reason, _amount, validUntil);
        return true;
    }

    // 락업 정보 삭제
    function removeLock(address _of, bytes32 _reason) public onlyOwner returns (bool deleted) {
        require(!locked[_of][_reason].claimed, EXPIRED_ADDRESS);
        this.transfer(_of, locked[_of][_reason].amount);
        delete locked[_of][_reason];
        
        emit RemoveLock(_of, _reason, locked[_of][_reason].amount, locked[_of][_reason].validity);
        return true;
    }

    // // 만료된 락업 정보 삭제
    function removeExpired(address _of) public onlyOwner returns (bool deleted) {
        for (uint256 i = 0; i < lockReason[_of].length; i++) {
            if (locked[_of][lockReason[_of][i]].claimed) {
                delete locked[_of][lockReason[_of][i]];
                emit RemoveExpired(_of, locked[_of][lockReason[_of][i]].amount, locked[_of][lockReason[_of][i]].validity);
                deleted = true;
            }
        }
        return deleted;
    }

    // 특정 Account의 토큰을 지정된 기간동안 락업
    function lock(bytes32 _reason, uint256 _amount, uint256 _time, address _of) public onlyOwner returns (bool) {
        uint256 validUntil = now.add(_time);

        require(_amount <= _balances[_of], NOT_ENOUGH_TOKENS);
        require(tokensLocked(_of, _reason) == 0, ALREADY_LOCKED);
        require(_amount != 0, AMOUNT_ZERO);

        if (locked[_of][_reason].amount == 0)
            lockReason[_of].push(_reason);

        _balances[address(this)] = _balances[address(this)].add(_amount);
        _balances[_of] = _balances[_of].sub(_amount);
        locked[_of][_reason] = lockToken(_amount, validUntil, false);

        emit Locked(_of, _reason, _amount, validUntil);
        return true;
    }

    // 특정 Account의 사유에 대해 락업 잔액 확인
    function tokensLocked(address _of, bytes32 _reason) public view returns (uint256 amount) {
        if (!locked[_of][_reason].claimed)
            amount = locked[_of][_reason].amount;
    }

    // 특정 Account, 사유, 시간에 대해 락업 잔액 확인
    function tokensLockedAtTime(address _of, bytes32 _reason, uint256 _time) public view returns (uint256 amount) {
        if (locked[_of][_reason].validity > _time)
            amount = locked[_of][_reason].amount;
    }

    // 특정 Account의 총 락업 잔액 확인
    function totalBalanceOf(address _of) public view returns (uint256 amount) {
        amount = balanceOf(_of);

        for (uint256 i = 0; i < lockReason[_of].length; i++) {
            amount = amount.add(tokensLocked(_of, lockReason[_of][i]));
        }   
    }    

    // 사유에 대한 락업 기간을 연장
    function extendLock(bytes32 _reason, uint256 _time, address _of) public onlyOwner returns (bool) {
        require(tokensLocked(_of, _reason) > 0, NOT_LOCKED);

        locked[_of][_reason].validity = locked[_of][_reason].validity.add(_time);

        emit Locked(_of, _reason, locked[_of][_reason].amount, locked[_of][_reason].validity);
        return true;
    }

    // 사유에 대해 소유자 수량을 보내어 락업 수량 추가
    function increaseLockAmount(bytes32 _reason, uint256 _amount, address _of) public onlyOwner returns (bool) {
        require(tokensLocked(_of, _reason) > 0, NOT_LOCKED);
        transfer(address(this), _amount);

        locked[_of][_reason].amount = locked[_of][_reason].amount.add(_amount);

        emit Locked(_of, _reason, locked[_of][_reason].amount, locked[_of][_reason].validity);
        return true;
    }

    // 특정 Account의 사유에 대해 만료된 락업 잔액 확인
    function tokensUnlockable(address _of, bytes32 _reason) public view returns (uint256 amount) {
        if (locked[_of][_reason].validity <= now && !locked[_of][_reason].claimed)
            amount = locked[_of][_reason].amount;
    }

    // 특정 Account에 락업된 토큰을 모두 언락
    function unlock(address _of) public onlyOwner returns (uint256 unlockableTokens) {
        uint256 lockedTokens;

        for (uint256 i = 0; i < lockReason[_of].length; i++) {
            lockedTokens = tokensUnlockable(_of, lockReason[_of][i]);
            if (lockedTokens > 0) {
                unlockableTokens = unlockableTokens.add(lockedTokens);
                locked[_of][lockReason[_of][i]].claimed = true;
                emit Unlocked(_of, lockReason[_of][i], lockedTokens);
            }
        }  

        if (unlockableTokens > 0)
            this.transfer(_of, unlockableTokens);
    }

    // 특정 Account에 언락 가능한 총 토큰 잔액
    function getUnlockableTokens(address _of) public view returns (uint256 unlockableTokens) {
        for (uint256 i = 0; i < lockReason[_of].length; i++) {
            unlockableTokens = unlockableTokens.add(tokensUnlockable(_of, lockReason[_of][i]));
        }  
    }
}
function getUnlockableTokens(address _of) public view returns (uint256 unlockableTokens) {
        for (uint256 i = 0; i < lockReason[_of].length; i++) {
            unlockableTokens = unlockableTokens.add(tokensUnlockable(_of, lockReason[_of][i]));
        }  
    }
// 특정 Account에 언락 가능한 총 토큰 잔액