contract PrzToken is ERC20Detailed, ERC20Mintable, ERC20Destroyable, ERC20Pausable, Ownable {

    // Stores the address of the entry credit contract
    address private _entryCreditContract;

    // Stores the address of contract with burned tokens (basis for BME minting)
    address private _balanceSheetContract;

    // Stores the amount of addresses to mint for
    uint256 private _bmeClaimBatchSize;
    uint256 private _bmeMintBatchSize;

    // Stores phase state (default value for bool is false,
    // https://solidity.readthedocs.io/en/v0.5.3/control-structures.html#default-value)
    // Contract will be initialized in "initPhase", i.e. not in bmePhase
    bool private _isInBmePhase;

    modifier whenNotInBME() {
        require(!_isInBmePhase, "Function may no longer be called once BME starts");
        _;
    }

    modifier whenInBME() {
        require(_isInBmePhase, "Function may only be called once BME starts");
        _;
    }

    event EntryCreditContractChanged(
        address indexed previousEntryCreditContract,
        address indexed newEntryCreditContract
    );

    event BalanceSheetContractChanged(
        address indexed previousBalanceSheetContract,
        address indexed newBalanceSheetContract
    );

    event BmeMintBatchSizeChanged(
        uint256 indexed previousSize,
        uint256 indexed newSize
    );

    event BmeClaimBatchSizeChanged(
        uint256 indexed previousSize,
        uint256 indexed newSize
    );

    event PhaseChangedToBME(address account);


    /**
     * @dev Constructor that initializes the PRZToken contract.
     */
    constructor (string memory name, string memory symbol, uint8 decimals)
        ERC20Detailed(name, symbol, decimals)
        ERC20Mintable()
        ERC20Destroyable()
        ERC20Pausable()
        Ownable()
        public
    {
        _isInBmePhase = false;
        pause();
        setEntryCreditContract(address(0));
        setBalanceSheetContract(address(0));
        setBmeMintBatchSize(200);
        setBmeClaimBatchSize(200);
    }

    // Returns _entryCreditContract
    function entryCreditContract() public view returns (address) {
        return _entryCreditContract;
    }

    // Set _entryCreditContract
    function setEntryCreditContract(address contractAddress) public onlyOwner {
        emit EntryCreditContractChanged(_entryCreditContract, contractAddress);
        _entryCreditContract = contractAddress;
    }

    // Returns _balanceSheetContract
    function balanceSheetContract() public view returns (address) {
        return _balanceSheetContract;
    }

    // Set _balanceSheetContract
    function setBalanceSheetContract(address contractAddress) public onlyOwner {
        emit BalanceSheetContractChanged(_balanceSheetContract, contractAddress);
        _balanceSheetContract = contractAddress;
    }

    // Returns _bmeMintBatchSize
    function bmeMintBatchSize() public view returns (uint256) {
        return _bmeMintBatchSize;
    }

    // Set _bmeMintBatchSize
    function setBmeMintBatchSize(uint256 batchSize) public onlyMinter {
        emit BmeMintBatchSizeChanged(_bmeMintBatchSize, batchSize);
        _bmeMintBatchSize = batchSize;
    }

    // Returns _bmeClaimBatchSize
    function bmeClaimBatchSize() public view returns (uint256) {
        return _bmeClaimBatchSize;
    }

    // Set _bmeClaimBatchSize
    function setBmeClaimBatchSize(uint256 batchSize) public onlyMinter {
        emit BmeClaimBatchSizeChanged(_bmeClaimBatchSize, batchSize);
        _bmeClaimBatchSize = batchSize;
    }

    // Overwrites ERC20._transfer.
    // If to = _entryCreditContract, sends tokens to the credit contract according to the
    // exchange rate in credit contract, destroys tokens locally
    function _transfer(address from, address to, uint256 value) internal {

        if (to == _entryCreditContract) {

            _burn(from, value);
            IEntryCreditContract entryCreditContractInstance = IEntryCreditContract(to);
            require(entryCreditContractInstance.mint(from, value), "Failed to mint entry credits");

            IBalanceSheetContract balanceSheetContractInstance = IBalanceSheetContract(_balanceSheetContract);
            require(balanceSheetContractInstance.setPeerzTokenSupply(totalSupply()), "Failed to update token supply");

        } else {

            super._transfer(from, to, value);
        }
    }

    // Call ERC20._burn(from, value)
    function destroy(address from, uint256 value)
        public whenPaused whenNotInBME
        returns (bool)
    {
        return super.destroy(from, value);
    }

    // Run destroy for all entries
    function batchDestroy(address[] calldata from, uint256[] calldata values)
        external onlyDestroyer whenPaused whenNotInBME
        returns (bool)
    {
        uint fromLength = from.length;

        require(fromLength == values.length, "Input arrays must have the same length");

        for (uint256 i = 0; i < fromLength; i++) {
            _burn(from[i], values[i]);
        }

        return true;
    }

    // Call ERC20._mint(to, value)
    function mint(address to, uint256 value)
        public whenPaused whenNotInBME
        returns (bool)
    {
        return super.mint(to, value);
    }

    // Run mint for all entries
    function batchMint(address[] calldata to, uint256[] calldata values)
        external onlyMinter whenPaused whenNotInBME
        returns (bool)
    {
        _batchMint(to, values);

        return true;
    }

    // Uses the balance sheet in _balanceSheetContract as a basis for
    // batchMint call for _bmeMintBatchSize addresses
    function bmeMint()
        public onlyMinter whenInBME whenNotPaused
    {
        IBalanceSheetContract balanceSheetContractInstance = IBalanceSheetContract(_balanceSheetContract);
        (address[] memory receivers, uint256[] memory amounts) = balanceSheetContractInstance.popMintingInformation(_bmeMintBatchSize);

        _batchMint(receivers, amounts);

        require(balanceSheetContractInstance.setPeerzTokenSupply(totalSupply()), "Failed to update token supply");
    }

    // Uses the balance sheet in _balanceSheetContract to create
    // tokens for all addresses in for, limits to _bmeMintBatchSize, emit Transfer
    function _claimFor(address[] memory claimers)
        private
    {
        IBalanceSheetContract balanceSheetContractInstance = IBalanceSheetContract(_balanceSheetContract);
        uint256[] memory amounts = balanceSheetContractInstance.popClaimingInformation(claimers);

        _batchMint(claimers, amounts);

        require(balanceSheetContractInstance.setPeerzTokenSupply(totalSupply()), "Failed to update token supply");
    }

    function _batchMint(address[] memory to, uint256[] memory values)
        private
    {

        // length should not be computed at each iteration
        uint toLength = to.length;

        require(toLength == values.length, "Input arrays must have the same length");

        for (uint256 i = 0; i < toLength; i++) {
            _mint(to[i], values[i]);
        }
    }

    // Calls _claimFor with for = msg.sender
    function claim()
        public whenInBME whenNotPaused
    {
        address[] memory claimers = new address[](1);
        claimers[0] = msg.sender;
        _claimFor(claimers);
    }

    // Calls _claimFor with for as provided
    function claimFor(address[] calldata claimers)
        external whenInBME whenNotPaused
    {
        require(claimers.length <= _bmeClaimBatchSize, "Input array must be shorter than bme claim batch size.");
        _claimFor(claimers);
    }

    // Change possible when in initPhase
    function changePhaseToBME()
        public onlyOwner whenNotPaused whenNotInBME
    {
        _isInBmePhase = true;
        emit PhaseChangedToBME(msg.sender);
    }
}
function mint(address to, uint256 value)
    public whenPaused whenNotInBME
    returns (bool)
{
    return super.mint(to, value);
}

// Call ERC20._mint(to, value)