contract TokenVesting is HasAdmin {
  using SafeMath for uint256;

  /**
    * @dev a Chunk is a period of time in which the beneficiary can receive the same amount of token each time a followUpDuration passes,
    * starting from effectiveDate (in relative to startTime).
   */
  struct Chunk {
    uint32 effectiveDate; // Duration from startTime that the chunk becomes effective
    uint256 amountPerUnlock;
    uint32 followUps;
    uint32 followUpDuration;
  }

  struct BeneficiaryInfo {
    uint32 index; // Location of beneficiary in beneficiaryAddresses.
    Chunk[] chunks;
    uint256 claimedAmount;
  }

  event StartTimeSet(uint256 _startTime);
  event BeneficiaryAdded(address indexed _beneficiary, uint32 _index);
  event BeneficiaryRemoved(address indexed _beneficiary);
  event TokenClaimed(address indexed _beneficiary, uint256 _amount);

  IERC20 public token;
  uint256 public startTime;
  mapping(address => BeneficiaryInfo) public beneficiaries;

  address[] public beneficiaryAddresses;

  constructor(IERC20 _token, uint256 _startTime) public {
    token = _token;
    editStartTime(_startTime);
    // Add a dummy address so all beneficiary index will be positive.
    beneficiaryAddresses.push(address(0));
  }

  function editStartTime(uint256 _newTime) public onlyAdmin {
    startTime = _newTime;
    emit StartTimeSet(startTime);
  }

  /**
   * @dev Function for admin to add another beneficiary, initiate with chunks.
  */
  function addBeneficiary(address _beneficiary, Chunk[] calldata _chunks) external onlyAdmin {
    require(beneficiaries[_beneficiary].index == 0, "TokenVesting: Beneficiary already existed");

    uint32 _index = uint32(beneficiaryAddresses.length);
    beneficiaries[_beneficiary].index = _index;
    addChunks(_beneficiary, _chunks);
    beneficiaryAddresses.push(_beneficiary);

    emit BeneficiaryAdded(_beneficiary, _index);
  }

  /**
   * @dev Function for admin to add more chunks for a specific beneficiary.
  */
  function addChunks(address _beneficiary, Chunk[] memory _chunks) public onlyAdmin {
    require(beneficiaries[_beneficiary].index > 0, "TokenVesting: Beneficiary not existed");

    Chunk[] storage _beneficiaryChunks = beneficiaries[_beneficiary].chunks;

    for (uint256 _i = 0; _i < _chunks.length; _i++) {
        _beneficiaryChunks.push(_chunks[_i]);
    }
  }

  /**
   * @dev Remove beneficiary, only used in rare cases that need some modifications.
  */
  function removeBeneficiary(address _beneficiary) external onlyAdmin {
    require(beneficiaries[_beneficiary].index > 0, "TokenVesting: Beneficiary not existed");

    uint32 _currentIndex = beneficiaries[_beneficiary].index;
    uint256 _lastIndex = beneficiaryAddresses.length.sub(1);

    // Replace by last item in array
    beneficiaryAddresses[_currentIndex] = beneficiaryAddresses[_lastIndex];
    beneficiaries[beneficiaryAddresses[_currentIndex]].index = _currentIndex;

    beneficiaryAddresses.pop();
    delete beneficiaries[_beneficiary];

    emit BeneficiaryRemoved(_beneficiary);
  }

  /**
   * @dev Remove beneficiary, only used in rare cases that need some modifications.
  */
  function getBeneficiaryList() external view returns (address[] memory) {
    return beneficiaryAddresses;
  }

  /**
   * @dev Query chunks of a beneficiary.
  */
  function beneficiaryChunks(address _beneficiary) external view returns (Chunk[] memory _chunks) {
    _chunks = beneficiaries[_beneficiary].chunks;
  }

  /**
   * @dev Query total allocated token, both unlocked and locked.
  */
  function totalAllocatedAmount(address _beneficiary) external view returns (uint256 _amount) {
    Chunk[] storage _chunks = beneficiaries[_beneficiary].chunks;

    for (uint256 _i = 0; _i < _chunks.length; _i++) {
      Chunk storage _chunk = _chunks[_i];
      _amount = _amount.add(_chunk.amountPerUnlock.mul(uint256(1).add(_chunk.followUps)));
    }
  }

  /**
   * @dev Query unlocked amount at the current time.
   * @return total unlocked amount so far and claimable amount.
  */
  function unlockedAmount(address _beneficiary) public view returns (uint256 _totalUnlocked, uint256 _claimable) {
    _totalUnlocked = unlockedAt(_beneficiary, block.timestamp);

    uint256 _claimedAmount = beneficiaries[_beneficiary].claimedAmount;
    _claimable = _totalUnlocked.sub(_claimedAmount);
  }

  /**
   * @dev Query unlocked amount at the specific timestamp.
   * @return total unlocked amount at that timestamp
  */
  function unlockedAt(address _beneficiary, uint256 _timestamp) public view returns (uint256 _totalUnlocked) {
    Chunk[] storage _chunks = beneficiaries[_beneficiary].chunks;

    for (uint256 _i = 0; _i < _chunks.length; _i++) {
      Chunk storage _chunk = _chunks[_i];

      if (startTime.add(_chunk.effectiveDate) <= _timestamp) {
        // Calculate how many follow-ups have occured
        uint256 followUps = 0;

        if (_chunk.followUpDuration > 0) {
          followUps = _timestamp.sub(startTime.add(_chunks[_i].effectiveDate)).div(_chunk.followUpDuration);
        }

        if (followUps > _chunk.followUps) {
          followUps = _chunk.followUps;
        }

        // There are (followUps + 1) unlocks have happened
        _totalUnlocked = _totalUnlocked.add(_chunk.amountPerUnlock.mul(followUps.add(1)));
      }
    }
  }

  /**
   * @dev Allows beneficiary to claim claimable tokens, can be called by anyone.
  */
  function claimToken(address _beneficiary) external {
    (uint256 _totalUnlocked, uint256 _claimable) = unlockedAmount(_beneficiary);
    require(_claimable > 0, "TokenVesting: No claimable amount");

    beneficiaries[_beneficiary].claimedAmount = _totalUnlocked;
    token.transfer(_beneficiary, _claimable);

    emit TokenClaimed(_beneficiary, _claimable);
  }
}
function unlockedAt(address _beneficiary, uint256 _timestamp) public view returns (uint256 _totalUnlocked) {
    Chunk[] storage _chunks = beneficiaries[_beneficiary].chunks;

    for (uint256 _i = 0; _i < _chunks.length; _i++) {
      Chunk storage _chunk = _chunks[_i];

      if (startTime.add(_chunk.effectiveDate) <= _timestamp) {
        // Calculate how many follow-ups have occured
        uint256 followUps = 0;

        if (_chunk.followUpDuration > 0) {
          followUps = _timestamp.sub(startTime.add(_chunks[_i].effectiveDate)).div(_chunk.followUpDuration);
        }

        if (followUps > _chunk.followUps) {
          followUps = _chunk.followUps;
        }

        // There are (followUps + 1) unlocks have happened
        _totalUnlocked = _totalUnlocked.add(_chunk.amountPerUnlock.mul(followUps.add(1)));
      }
    }
  }
/**
 * @dev Query unlocked amount at the specific timestamp.
 * @return total unlocked amount at that timestamp
 */