contract DividendsWithETH is IDividendsWithETH, Ownable {
    using SafeMath for uint256;

    enum State {INACTIVE, ACTIVE} // enum value for the states of DividendsWithETH contract.

    struct Dividend {
        uint256 propertyId; // ID of the property for which the dividend is created.
        uint256 totalDividendAmount; // amount of dividend.
        uint256 totalInvestment; // total investment amount of the property.
        uint256 dividendsAmountPaid; // the amount of dividend that are withdrawn by the investors.
        mapping(address => uint256) amountWithdrawnByInvestor;
    }

    mapping(uint256 => Dividend) public dividends; // mapping from dividendId to dividend

    mapping(address => bool) public managers; // mapping of manager address to its status(true or false) of eligibility.

    State stateOfDividendsWithETH; // current state( ACTIVE, INACTIVE ) of the contract
    uint256 dividendIdCount = 0; // total count of the dividens

    IProperties public properties; // properties interface to interact with the properties contract

    /**
    @dev constructor of the  DividendsWithETH contract
    */
    constructor() public {
        // set the current state of the contract as ACTIVE
        stateOfDividendsWithETH = State.ACTIVE;

        emit StateChanged(uint256(stateOfDividendsWithETH));
    }

    /**
     * @dev Throws if called by any account other than Managers.
     */
    modifier onlyManager() {
        require(isOwner() || managers[msg.sender], "Only owner/manager can call this function.");
        _;
    }

    /// @notice Sets status for a manager
    /// @param manager The address of manager for which the status is to be updated
    /// @param managerStatus The status for the manager
    /// @return status of the transaction
    function setManagerStatus(address manager, bool managerStatus)
        external
        onlyOwner
        returns (bool)
    {
        require(
            manager != address(0),
            "Provided mannager address is not valid."
        );
        require(
            managers[manager] != managerStatus,
            "This status of manager is already set."
        );

        managers[manager] = managerStatus;

        emit ManagerStatusUpdated(manager, managerStatus);

        return true;
    }

    /**
    @notice is called by owner to set property address
    @param _properties it is the address of the property
     */
    function setProperty(IProperties _properties) external onlyOwner {
        require(
            address(_properties) != address(0),
            "properties address must be a valid address."
        );
        properties = _properties;

        emit PropertiesSet(address(properties));
    }

    /**
    @notice call is only allowed to pass when exchange is in ACTIVE state
     */
    modifier isStateActive() {
        require(
            stateOfDividendsWithETH == State.ACTIVE,
            "contract state is INACTIVE."
        );
        _;
    }

    /**
    @notice this function sets/changes state of this smart contract and only manager/owner can call it
    @param state it can be either 0 (ACTIVE) or 1 (INACTIVE)
     */
    function setState(uint256 state) external onlyOwner {
        require(state == 0 || state == 1, "Provided state is invalid.");
        require(
            state != uint256(stateOfDividendsWithETH),
            "Provided state is already set."
        );

        stateOfDividendsWithETH = State(state);

        emit StateChanged(uint256(stateOfDividendsWithETH));
    }

    /**
    @notice Used to pay/create dividend for a property
    @param  propertyId The ID of the property for which the dividend is being paid/created
    */
    function payDividend(uint256 propertyId)
        external
        payable
        onlyManager
        isStateActive
    {
        require(propertyId > 0, "propertyId should be greater than zero.");
        require(msg.value > 0, "msg.value should be greater than 0");

        uint256 dividendAmount = msg.value;
        bool exists = false;
        for (uint256 i = 1; i <= dividendIdCount; i++) {
            Dividend memory dividend = dividends[i];
            if (dividend.propertyId == propertyId) {
                // For existing dividends, just update totalDividendAmount
                dividend.totalDividendAmount = dividend.totalDividendAmount.add(dividendAmount);
                dividends[i] = dividend;
                break;
            }
        }

        if (!exists) {
            // get total property investment from the properties contract
            (, , , , , , uint256 totalPropertyInvestment, , , ) = properties
                .getProperty(propertyId);
            Dividend memory newDividend = Dividend(
                propertyId,
                dividendAmount,
                totalPropertyInvestment,
                uint256(0)
            );
            dividendIdCount = dividendIdCount.add(1);

            dividends[dividendIdCount] = newDividend;
        }

        emit DividendPaid(propertyId, dividendIdCount, dividendAmount);

    }

    /**
    @notice Called to withdraw ETH dividend by investor
    @param dividendId The ID of the dividend from where the user wants to withdraw his dividend amount
     */
    function withdrawDividend(uint256 dividendId)
        public
        isStateActive
        returns (bool)
    {
        require(dividendId > 0, "dividendId should be greater than zero");

        Dividend storage dividend = dividends[dividendId];

        require(
            dividend.propertyId != 0,
            "dividend with the given property ID does not exists"
        );

        // Get amount of the shares of the investor in the given property
        (, uint256 investmentByUser) = properties.getTotalAndHolderShares(
            dividend.propertyId,
            msg.sender
        );

        // calculate dividend amount for the given investor
        uint256 userDividendAmount = calculateDividend(
            dividend.totalInvestment,
            dividend.totalDividendAmount,
            investmentByUser
        );

        require(
            dividend.amountWithdrawnByInvestor[msg.sender] < userDividendAmount,
            "No dividend amount available for withdrawal"
        );

        uint256 dividendAmount = userDividendAmount.sub(
            dividend.amountWithdrawnByInvestor[msg.sender]
        );

        require(
            dividendAmount <= address(this).balance,
            "The dividendWithETH contract does not have enough ETH balance to pay dividend."
        );

        dividend.amountWithdrawnByInvestor[msg.sender] = dividend
            .amountWithdrawnByInvestor[msg.sender]
            .add(dividendAmount);
        dividend.dividendsAmountPaid = dividend.dividendsAmountPaid.add(
            dividendAmount
        );

        // transfer dividend to investor
        msg.sender.transfer(dividendAmount);

        emit DividendWithdrawn(
            dividend.propertyId,
            dividendId,
            msg.sender,
            dividendAmount
        );

        return true;
    }

    /**
    @notice Used to withdraw ETH from the contract by the manager
    @param amount The amount of ETH to withdraw | if amount is zero then all ETH balance is withdrawn from the contract
     */
    function withdrawETHByManager(uint256 amount) external onlyManager {
        uint256 contractBalance = address(this).balance;

        require(contractBalance > 0, "Contract has no ETH in it.");

        if (amount == 0) {
            msg.sender.transfer(contractBalance);
            emit ETHWithdrawn(msg.sender, contractBalance);
        } else {
            require(
                amount <= contractBalance,
                "Contract has less balance than the amount specified."
            );
            msg.sender.transfer(amount);
            emit ETHWithdrawn(msg.sender, amount);
        }
    }

    /**
    @notice Returns the complete list of all dividends
    @return The list of IDs of all dividends
    @return The list of totalDividendAmount of all dividends
    @return The list of totalInvestment of all dividends
    @return The list of dividendsAmountPaid of all dividends
     */
    function getAllDividendsList()
        public
        view
        returns (
            uint256[] memory propertyId,
            uint256[] memory totalDividendAmount,
            uint256[] memory totalInvestment,
            uint256[] memory dividendsAmountPaid
        )
    {
        propertyId = new uint256[](dividendIdCount);
        totalDividendAmount = new uint256[](dividendIdCount);
        totalInvestment = new uint256[](dividendIdCount);
        dividendsAmountPaid = new uint256[](dividendIdCount);

        for (uint256 i = 1; i <= dividendIdCount; i++) {
            Dividend memory dividend = dividends[i];

            propertyId[i - 1] = dividend.propertyId;
            totalDividendAmount[i - 1] = dividend.totalDividendAmount;
            totalInvestment[i - 1] = dividend.totalInvestment;
            dividendsAmountPaid[i - 1] = dividend.dividendsAmountPaid;
        }

    }

    function getDividendsByPropertyId(uint256 propertyId)
        public
        view
        returns (
            uint256[] memory dividendId,
            uint256[] memory totalDividendAmount,
            uint256[] memory totalInvestment,
            uint256[] memory dividendsAmountPaid
        )
    {
        require(propertyId > 0, "propertyId should be greater than zero.");

        dividendId = new uint256[](dividendIdCount);
        totalDividendAmount = new uint256[](dividendIdCount);
        totalInvestment = new uint256[](dividendIdCount);
        dividendsAmountPaid = new uint256[](dividendIdCount);

        uint256 counter = 0;

        for (uint256 i = 1; i <= dividendIdCount; i++) {
            Dividend memory dividend = dividends[i];

            if (dividend.propertyId == propertyId) {
                dividendId[counter] = i;
                totalDividendAmount[counter] = dividend.totalDividendAmount;
                totalInvestment[counter] = dividend.totalInvestment;
                dividendsAmountPaid[counter] = dividend.dividendsAmountPaid;
                counter++;
            }
        }

    }

    function getDividendByPropertyId(uint256 propertyId)
        public
        view
        returns (
            uint256 dividendId,
            uint256 totalDividendAmount,
            uint256 totalInvestment,
            uint256 dividendsAmountPaid
        )
    {
        require(propertyId > 0, "propertyId should be greater than zero.");

        dividendId = 0;
        totalDividendAmount = 0;
        totalInvestment = 0;
        dividendsAmountPaid = 0;

        for (uint256 i = 1; i <= dividendIdCount; i++) {
            Dividend memory dividend = dividends[i];

            if (dividend.propertyId == propertyId) {
                dividendId = i;
                totalDividendAmount = dividend.totalDividendAmount;
                totalInvestment = dividend.totalInvestment;
                dividendsAmountPaid = dividend.dividendsAmountPaid;
            }
        }
    }

    function getInvestorBalanceByDividendId(
        uint256 dividendId,
        address investor
    )
        public
        view
        returns (
            uint256 dividendBalanceAvailable,
            uint256 dividendBalanceWithdrawn
        )
    {
        require(dividendId > 0, "dividendId cannot be zero");

        Dividend storage dividend = dividends[dividendId];

        // Get amount of the shares of the investor in the given property
        (, uint256 investmentByUser) = properties.getTotalAndHolderShares(
            dividend.propertyId,
            investor
        );

        // calculate dividend amount for the given investor
        uint256 totalDividendBalance = calculateDividend(
            dividend.totalInvestment,
            dividend.totalDividendAmount,
            investmentByUser
        );

        dividendBalanceAvailable = totalDividendBalance.sub(
            dividend.amountWithdrawnByInvestor[investor]
        );
        dividendBalanceWithdrawn = dividend.amountWithdrawnByInvestor[investor];
    }

    /**
    @dev Fallback function to accept ETH
     */
    function() external payable {
        require(msg.data.length == 0, "Error in calling the function");
    }

    /**
    @notice Function to calculated dividend amount
    @param  totalInvestments The total nvestments in a property
    @param totalDividend The amount of total dividends allocated to a dividend
    @param userInvestment The investment of a user in a property
    @return The amount of dividend withdrawable by user
     */
    function calculateDividend(
        uint256 totalInvestments,
        uint256 totalDividend,
        uint256 userInvestment
    ) private pure returns (uint256) {
        return
            userInvestment
                .mul(1000000000000000000)
                .div(totalInvestments)
                .mul(totalDividend)
                .div(1000000000000000000);
    }

}
function withdrawETHByManager(uint256 amount) external onlyManager {
    uint256 contractBalance = address(this).balance;

    require(contractBalance > 0, "Contract has no ETH in it.");

    if (amount == 0) {
        msg.sender.transfer(contractBalance);
        emit ETHWithdrawn(msg.sender, contractBalance);
    } else {
        require(
            amount <= contractBalance,
            "Contract has less balance than the amount specified."
        );
        msg.sender.transfer(amount);
        emit ETHWithdrawn(msg.sender, amount);
    }
}

/**
 @notice Used to withdraw ETH from the contract by the manager
 @param amount The amount of ETH to withdraw | if amount is zero then all ETH balance is withdrawn from the contract
 */