contract BlxToken is
    ERC20Detailed,
    ERC20Capped,
    ERC20Pausable,
    Ownable,
    WhitelistedRole,
    Closable,
    DestroyerRole,
    DocumentStorerRole,
    FrozenRole,
    LimiterRole,
    TokenReassignerRole
{
    struct Document {
        uint256 timestamp;
        string hash;
    }

    // ----- Contract variables

    uint256 public transferFloor;

    uint256 public transferCap;

    bool public whitelistIsActive;

    mapping(address => uint256) private _frozenAmount;

    Document[] private _documents;

    // ----- Contract events

    event BatchMint(address[] account, uint256[] amounts);
    event BatchDestroy(address[] account, uint256[] amounts);

    event WhitelistActivated(address account);
    event WhitelistDeactivated(address account);

    event TransferFloorChanged(
        uint256 indexed previousValue,
        uint256 indexed newValue
    );

    event TransferCapChanged(
        uint256 indexed previousValue,
        uint256 indexed newValue
    );

    event TokensReassigned(
        address indexed from,
        address indexed to,
        uint256 amount
    );

    event DocumentAdded(string hash, uint256 indexed index);

    // ----- Contract initialization

    constructor(
        string memory name,
        string memory symbol,
        uint8 decimals,
        uint256 cap
    )
        public
        ERC20Detailed(name, symbol, decimals)
        ERC20Capped(cap)
        ERC20Pausable()
        Ownable()
        WhitelistedRole()
        Closable()
        DestroyerRole()
        DocumentStorerRole()
        FrozenRole()
        LimiterRole()
        TokenReassignerRole()
    {
        transferFloor = 100 * 1e18;
        transferCap = 100e3 * 1e18;
        whitelistIsActive = false;
    }

    // ----- Contract functionality

    // --- Whitelisting

    function isWhitelisted(address account) public view returns (bool) {
        return !whitelistIsActive || super.isWhitelisted(account);
    }

    function activateWhitelist() external onlyWhitelistAdmin returns (bool) {
        require(!whitelistIsActive, "Whitelist is already active");

        whitelistIsActive = true;

        emit WhitelistActivated(msg.sender);

        return true;
    }

    function deactivateWhitelist() external onlyWhitelistAdmin returns (bool) {
        require(whitelistIsActive, "Whitelist is not active");

        whitelistIsActive = false;

        emit WhitelistDeactivated(msg.sender);

        return true;
    }

    function batchAddWhitelisted(address[] calldata account)
        external
        onlyWhitelistAdmin
        returns (bool)
    {
        uint256 toLength = account.length;

        for (uint256 i = 0; i < toLength; i++) {
            super.addWhitelisted(account[i]);
        }

        return true;
    }

    // --- Minting

    // Intercepts token mints
    function _mint(address account, uint256 amount) internal whenNotClosed {
        super._mint(account, amount);

        // An address can be minted for multiple times
        if (!super.isWhitelisted(account)) {
            addWhitelisted(account);
        }
    }

    // Mints tokens for a list of addresses
    function batchMint(address[] calldata accounts, uint256[] calldata amounts)
        external
        onlyMinter
        returns (bool)
    {
        uint256 accountCount = accounts.length;

        require(
            accountCount == amounts.length,
            "Input arrays must have the same length"
        );

        for (uint256 i = 0; i < accountCount; i++) {
            _mint(accounts[i], amounts[i]);
        }

        emit BatchMint(accounts, amounts);

        return true;
    }

    // --- Destroying

    function destroy(address account, uint256 amount)
        external
        onlyDestroyer
        returns (bool)
    {
        _burn(account, amount);

        return true;
    }

    // Destroys tokens for a list of addresses
    function batchDestroy(
        address[] calldata accounts,
        uint256[] calldata amounts
    ) external onlyDestroyer returns (bool) {
        uint256 accountCount = accounts.length;

        require(
            accountCount == amounts.length,
            "Input arrays must have the same length"
        );

        for (uint256 i = 0; i < accountCount; i++) {
            _burn(accounts[i], amounts[i]);
        }

        emit BatchDestroy(accounts, amounts);

        return true;
    }

    // --- Transfer cap and floor

    // Sets the minimum transfer amount
    function setTransferFloor(uint256 floor)
        external
        onlyLimiter
        returns (bool)
    {
        require(
            floor < transferCap,
            "Transfer floor must be less than transfer cap"
        );

        emit TransferFloorChanged(transferFloor, floor);

        transferFloor = floor;

        return true;
    }

    // Sets the maximum transfer amount
    function setTransferCap(uint256 cap) external onlyLimiter returns (bool) {
        require(
            transferFloor < cap,
            "Transfer cap must be greater than transfer floor"
        );

        emit TransferCapChanged(transferCap, cap);

        transferCap = cap;

        return true;
    }

    // Intercepts secondary token transfers (mint and destroy excluded)
    function _transfer(
        address sender,
        address recipient,
        uint256 amount
    ) internal whenNotClosed {
        require(
            amount >= transferFloor,
            "Transfer amount under the transfer floor"
        );
        require(amount <= transferCap, "Transfer amount over the transfer cap");

        require(isWhitelisted(recipient), "Recipient is not whitelisted");

        require(!isFrozen(sender), "Sender's account is frozen");

        require(
            _frozenAmount[sender] + amount <= balanceOf(sender),
            "Transfer amount exceeds unfrozen balance"
        );

        super._transfer(sender, recipient, amount);
    }

    // --- Token reassigning

    // Transfers the entire balance from an address to another
    function tokenReassign(address from, address to)
        external
        whenNotClosed
        onlyTokenReassigner
        returns (bool)
    {
        uint256 amount = balanceOf(from);

        super._transfer(from, to, amount);

        emit TokensReassigned(from, to, amount);

        return true;
    }

    // --- Special Token transfer for "Invite a friend" incentives

    function transferIncentiveToken(
        address sender,
        address recipient,
        uint256 amount
    ) external whenNotClosed onlyLimiter returns (bool) {
        require(amount <= transferCap, "Transfer amount over the transfer cap");

        require(isWhitelisted(recipient), "Recipient is not whitelisted");

        require(!isFrozen(sender), "Sender's account is frozen");

        require(
            _frozenAmount[sender] + amount <= balanceOf(sender),
            "Transfer amount exceeds unfrozen balance"
        );

        super._transfer(sender, recipient, amount);

        return true;
    }

    // --- Account freezing

    function batchAddFrozen(address[] calldata accounts) external onlyFreezer {
        uint256 accountCount = accounts.length;

        for (uint256 i = 0; i < accountCount; i++) {
            addFrozen(accounts[i]);
        }
    }

    function batchRemoveFrozen(address[] calldata accounts)
        external
        onlyFreezer
    {
        uint256 accountCount = accounts.length;

        for (uint256 i = 0; i < accountCount; i++) {
            removeFrozen(accounts[i]);
        }
    }

    function setFrozenAmount(address account, uint256 amount)
        external
        onlyFreezer
    {
        _frozenAmount[account] = amount;
    }

    // --- Document registry

    function addDocument(string calldata hash) external onlyDocumentStorer {
        require(bytes(hash).length > 0, "Invalid document hash");

        Document memory document = Document({
            timestamp: block.timestamp,
            hash: hash
        });

        _documents.push(document);

        emit DocumentAdded(hash, _documents.length.sub(1));
    }

    function currentDocument()
        external
        view
        returns (
            uint256 timestamp,
            string memory hash,
            uint256 index
        )
    {
        require(_documents.length > 0, "No documents exist");

        uint256 last = _documents.length.sub(1);

        Document storage document = _documents[last];

        return (document.timestamp, document.hash, last);
    }

    function getDocument(uint256 index)
        external
        view
        returns (
            uint256 timestamp,
            string memory hash,
            uint256 inputIndex
        )
    {
        require(index < _documents.length, "Invalid document index");

        Document storage document = _documents[index];

        return (document.timestamp, document.hash, index);
    }

    function documentCount() external view returns (uint256) {
        return _documents.length;
    }
}
function _transfer(
    address sender,
    address recipient,
    uint256 amount
) internal whenNotClosed {
    require(
        amount >= transferFloor,
        "Transfer amount under the transfer floor"
    );
    require(amount <= transferCap, "Transfer amount over the transfer cap");

    require(isWhitelisted(recipient), "Recipient is not whitelisted");

    require(!isFrozen(sender), "Sender's account is frozen");

    require(
        _frozenAmount[sender] + amount <= balanceOf(sender),
        "Transfer amount exceeds unfrozen balance"
    );

    super._transfer(sender, recipient, amount);
}

// Intercepts secondary token transfers (mint and destroy excluded)