// File: Commune.sol



pragma solidity ^0.7.0;

import "SafeMath.sol";

import 'IERC20.sol';
import 'ICommune.sol';

contract Commune is ICommune {
    using SafeMath for uint256;

    struct aCommune {
        bool allowsJoining;
        bool allowsRemoving;
        bool allowsOutsideContribution;
        address asset;
        uint256 proratedTotal;
        uint256 memberCount;
        address controller;
        string uri;
    }

    mapping (uint256 => aCommune) public getCommune;

    // maybe we should rather use 
    // mapping (address => EnumerableSet.UintSet) private _holderTokens;
    // like https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L33
    mapping (uint256 => mapping(address => bool)) private _isCommuneMember;
    
    // commune -> address -> balance
    mapping (uint256 => mapping (address => uint256)) private _balanceAtJoin;

    // commune -> address -> balance 
    mapping (uint256 => mapping (address => uint256)) private _spentBalance;

    // in basis points, 0 through 500, i.e. max take 5%
    uint256 private _feeRate = 100;

    // where fee goes to
    address private _treasuryAddress;

    // for creating new commune IDs
    uint256 private _nonce;

    // can update treasury address and fee rate 
    address private _controllerAddress;


    // Getters /// 

    function numberOfCommunes() external override view returns (uint256){
        return _nonce;
    }

    function isCommuneMember(uint256 commune, address account) external communeExists(commune) override view returns (bool){
        return _isCommuneMember[commune][account];
    }

    function feeRate() external override view returns (uint256){
        return _feeRate;
    }

    function treasuryAddress() external override view returns (address){
        return _treasuryAddress;
    }

    function controller() external override view returns (address){
        return _controllerAddress;
    }
    

    // modifiers 
    modifier controllerOnly(){
        require(msg.sender == _controllerAddress, "Commune: only contract controller can do this");
        _;
    }

    modifier communeControllerOnly(uint256 commune){
        require(getCommune[commune].controller == msg.sender, "Commune: only the commune controller can do this");
        _;
    }

    modifier communeExists(uint256 commune){
        require(commune <= _nonce, "Commune: commune does not exists");
        _;
    }

    function contribute(uint256 amount, uint256 commune) external communeExists(commune) override { 
        require(_isCommuneMember[commune][msg.sender] || getCommune[commune].allowsOutsideContribution, "Commune: Must be a member to contribute");
        require(getCommune[commune].memberCount > 0, "Commune: commune has no members, cannot accept contributions");

        address assetAddress = getCommune[commune].asset;
        IERC20 asset = IERC20(assetAddress);

        uint256 fee = amount
            .mul(_feeRate)
            .div(10000);
        
        uint256 amountToCommune = amount
            .sub(fee);

        asset.transferFrom(msg.sender, address(this), amountToCommune);
        asset.transferFrom(msg.sender, _treasuryAddress, fee);

        getCommune[commune].proratedTotal = getCommune[commune].proratedTotal.add(
            amountToCommune.div(
                getCommune[commune].memberCount
                )
            );

        emit Contribute(msg.sender, commune, amount);
    }

    /**
        @dev _uri should link to a JSON file of the form 
        {
            "title": "Commune Title",
            "description": "Commune description"
        }
    */
    function createCommune(string memory _uri, address asset, bool allowJoining, bool allowRemoving, bool allowOutsideContribution) external override returns(uint256 _id) {  
        require((!allowJoining && allowRemoving) || (allowJoining && !allowRemoving) || (!allowJoining && !allowRemoving), "Commune: cannot both allow joining and removing");

        _id = ++_nonce;

        getCommune[_id].controller = msg.sender;
        getCommune[_id].allowsJoining = allowJoining;
        getCommune[_id].allowsRemoving = allowRemoving;
        getCommune[_id].allowsOutsideContribution = allowOutsideContribution;
        getCommune[_id].asset = asset;
        getCommune[_id].uri = _uri;

        emit URI(_uri, _id);
    }

    //Join/Add Functions
    function joinCommune(uint256 commune) external override {
        require(getCommune[commune].allowsJoining, "Commune: commune does not allow joining");
        _addCommuneMember(msg.sender, commune);
    }

    function addCommuneMember(address account, uint256 commune) external communeControllerOnly(commune) override {
        _addCommuneMember(account, commune);
    }

    function _addCommuneMember(address account, uint256 commune) private communeExists(commune) {
        require(!_isCommuneMember[commune][account], "Commune: account is already in commune");

        _isCommuneMember[commune][account] = true;
        ++getCommune[commune].memberCount;
        _balanceAtJoin[commune][account] = getCommune[commune].proratedTotal;

        emit AddCommuneMember(account, commune);
    }


    // Leave/Remove Functions
    function leaveCommune(uint256 commune) external override {
        _removeCommuneMember(msg.sender, commune);
    }

    function removeCommuneMember(address account, uint256 commune) external communeControllerOnly(commune) override {
        require(getCommune[commune].allowsRemoving, "Commune: commune does not allow removing");

        _removeCommuneMember(account, commune);
    }

    function _removeCommuneMember(address account, uint256 commune) private communeExists(commune) {
        require(_isCommuneMember[commune][account], "Commune: account is not in commune");

        _isCommuneMember[commune][account] = false;
        getCommune[commune].memberCount = getCommune[commune].memberCount.sub(1);
        // we reset the spent balance, incase they'e added back later, to prevent a negative number 
         _spentBalance[commune][account] = 0;

        emit RemoveCommuneMember(account, commune);
    }



    constructor(address _controller) {
        _setURI("your-uri-here");

        _controllerAddress = _controller;
        _treasuryAddress = _controller;
    }

    /// controller functions ///

    function updateCommuneController(address account, uint256 commune) external communeControllerOnly(commune) override {
        getCommune[commune].controller = account;

        emit UpdateCommuneController(account, commune);
    }

    function updateCommuneURI(string memory _uri, uint256 commune) external communeControllerOnly(commune) override {
        getCommune[commune].uri = _uri;

        emit URI(_uri, commune);
    }

    function updateController(address account) external controllerOnly override {
        _controllerAddress = account;

        emit UpdateController(account);
    }

    function updateFee(uint256 rate) external controllerOnly override {
        // max fee is 5%
        require(rate <= 500 && rate >= 0, "Commune: fee rate must be between 0 and 500");
        _feeRate = rate;

        emit UpdateFee(rate);
    }

    function setTreasuryAddress(address newTreasury) external controllerOnly override {
        _treasuryAddress = newTreasury;

        emit UpdateTreasuryAddress(newTreasury);
    }

    // boiler, mostly ripped from ERC1155 then modified

    // Mapping from account to operator approvals
    mapping (address => mapping(address => bool)) private _operatorApprovals;

    string private _uri;
    
    function uri(uint256 commune) communeExists(commune) external view  returns (string memory) {

        string memory _communeURI = getCommune[commune].uri;

        // if the commune URI is set, return it. Note, might still need to replace `\{id\}`
        if (bytes(_communeURI).length > 0) {
            return _communeURI;
        } 

        // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI.
        return _uri;
    }

    function _setURI(string memory newuri) internal virtual {
        _uri = newuri;
    }

    function balanceOf(address account, uint256 commune) public view virtual override returns (uint256) {
        require(account != address(0), "Commune: balance query for the zero address");
        if(!_isCommuneMember[commune][account]){
            return 0;
        }
        return getCommune[commune].proratedTotal
            .sub(_balanceAtJoin[commune][account])
            .sub(_spentBalance[commune][account]);
    }

    /**
     *
     * Requirements:
     *
     * - `accounts` and `ids` must have the same length.
     */
    function balanceOfBatch(
        address[] memory accounts,
        uint256[] memory ids
    )
        public
        view
        override
        returns (uint256[] memory)
    {
        require(accounts.length == ids.length, "Commune: accounts and ids length mismatch");

        uint256[] memory batchBalances = new uint256[](accounts.length);

        for (uint256 i = 0; i < accounts.length; ++i) {
            require(accounts[i] != address(0), "Commune: batch balance query for the zero address");
            batchBalances[i] = balanceOf(accounts[i], ids[i]);
        }

        return batchBalances;
    }


    function setApprovalForAll(address operator, bool approved) public virtual {
        require(msg.sender != operator, "Commune: setting approval status for self");

        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }


    function isApprovedForAll(address account, address operator) public view returns (bool) {
        return _operatorApprovals[account][operator];
    }

    function withdraw(address account, address to, uint256 commune, uint256 amount) public override {
        require(to != address(0), "Commune: Cannot withdraw to the zero address");
        require(
            account == msg.sender || isApprovedForAll(account, msg.sender),
            "Commune: Caller is not owner nor approved"
        );

        address operator = msg.sender;

        balanceOf(account, commune).sub(amount, "Commune: withdraw amount exceeds balance");
        _spentBalance[commune][account] = _spentBalance[commune][account].add(amount);

        IERC20(getCommune[commune].asset).transfer(to, amount);

        emit Withdraw(operator, account, to, commune, amount);
    }

    function withdrawBatch(address account, address to, uint256[] memory communes, uint256[] memory amounts) public override {
        require(to != address(0), "Commune: Cannot withdraw to the zero address");
        require(
            account == msg.sender || isApprovedForAll(account, msg.sender),
            "Commune: Caller is not owner nor approved"
        );

        address operator = msg.sender;

        for (uint i = 0; i < communes.length; i++) {
            balanceOf(account, communes[i]).sub(amounts[i], "Commune: withdraw amount exceeds balance");
            _spentBalance[communes[i]][account] = _spentBalance[communes[i]][account].add(amounts[i]);
            IERC20(getCommune[communes[i]].asset).transfer(to, amounts[i]);
        }

        emit WithdrawBatch(operator, account, to, communes, amounts);
    }
    
}
// File: ICommune.sol



pragma solidity ^0.7.0;

interface ICommune {

	function numberOfCommunes() external view returns (uint256);
	function isCommuneMember(uint256 commune, address account) external view returns (bool);
	function feeRate() external view returns (uint256);
	function treasuryAddress() external view returns (address);
	function controller() external view returns (address);

	function createCommune(string memory _uri, address asset, bool allowJoining, bool allowRemoving, bool allowOutsideContribution) external returns(uint256 _id);
	function contribute(uint256 amount, uint256 commune) external;
	function joinCommune(uint256 commune) external;
	function addCommuneMember(address account, uint256 commune) external;
	function leaveCommune(uint256 commune) external;
	function removeCommuneMember(address account, uint256 commune) external;
	function balanceOf(address account, uint256 commune) external view returns (uint256);
	function balanceOfBatch(address[] memory accounts, uint256[] memory ids) external view returns (uint256[] memory);
	function withdraw(address account, address to, uint256 commune, uint256 amount) external;
	function withdrawBatch(address account, address to, uint256[] memory communes, uint256[] memory amounts) external;

	function updateCommuneController(address account, uint256 commune) external;
	function updateCommuneURI(string memory _uri, uint256 commune) external;
	function updateController(address account) external;
	function updateFee(uint256 rate) external;
	function setTreasuryAddress(address newTreasury) external;



	event AddCommuneMember(address indexed account, uint256 indexed commune);
    event RemoveCommuneMember(address indexed account, uint256 indexed commune);
    event Withdraw(address indexed operator, address indexed account, address to, uint256 indexed commune, uint256 amount);
    event WithdrawBatch(address indexed operator, address indexed account, address indexed to, uint256[] communes, uint256[] amounts);
    event URI(string value, uint256 indexed commune);
    event ApprovalForAll(address indexed account, address indexed operator, bool approved);
    event Contribute(address indexed account, uint256 indexed commune, uint256 amount);
    event UpdateCommuneController(address indexed account, uint256 indexed commune);
    event UpdateController(address indexed account);
    event UpdateFee(uint256 basisPoints);
    event UpdateTreasuryAddress(address indexed account);
}
// File: IERC20.sol



pragma solidity >=0.5.0;

interface IERC20 {
    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
    function totalSupply() external view returns (uint);
    function balanceOf(address owner) external view returns (uint);
    function allowance(address owner, address spender) external view returns (uint);

    function approve(address spender, uint value) external returns (bool);
    function transfer(address to, uint value) external returns (bool);
    function transferFrom(address from, address to, uint value) external returns (bool);
}
// File: SafeMath.sol



// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

/**
 * @dev Wrappers over Solidity's arithmetic operations with added overflow
 * checks.
 *
 * Arithmetic operations in Solidity wrap on overflow. This can easily result
 * in bugs, because programmers usually assume that an overflow raises an
 * error, which is the standard behavior in high level programming languages.
 * `SafeMath` restores this intuition by reverting the transaction when an
 * operation overflows.
 *
 * Using this library instead of the unchecked operations eliminates an entire
 * class of bugs, so it's recommended to use it always.
 */
library SafeMath {
    /**
     * @dev Returns the addition of two unsigned integers, with an overflow flag.
     *
     * _Available since v3.4._
     */
    function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        uint256 c = a + b;
        if (c < a) return (false, 0);
        return (true, c);
    }

    /**
     * @dev Returns the substraction of two unsigned integers, with an overflow flag.
     *
     * _Available since v3.4._
     */
    function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        if (b > a) return (false, 0);
        return (true, a - b);
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, with an overflow flag.
     *
     * _Available since v3.4._
     */
    function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
        if (a == 0) return (true, 0);
        uint256 c = a * b;
        if (c / a != b) return (false, 0);
        return (true, c);
    }

    /**
     * @dev Returns the division of two unsigned integers, with a division by zero flag.
     *
     * _Available since v3.4._
     */
    function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        if (b == 0) return (false, 0);
        return (true, a / b);
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag.
     *
     * _Available since v3.4._
     */
    function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        if (b == 0) return (false, 0);
        return (true, a % b);
    }

    /**
     * @dev Returns the addition of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `+` operator.
     *
     * Requirements:
     *
     * - Addition cannot overflow.
     */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");
        return c;
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a, "SafeMath: subtraction overflow");
        return a - b;
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `*` operator.
     *
     * Requirements:
     *
     * - Multiplication cannot overflow.
     */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) return 0;
        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");
        return c;
    }

    /**
     * @dev Returns the integer division of two unsigned integers, reverting on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b > 0, "SafeMath: division by zero");
        return a / b;
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * reverting when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b > 0, "SafeMath: modulo by zero");
        return a % b;
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting with custom message on
     * overflow (when the result is negative).
     *
     * CAUTION: This function is deprecated because it requires allocating memory for the error
     * message unnecessarily. For custom revert reasons use {trySub}.
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        return a - b;
    }

    /**
     * @dev Returns the integer division of two unsigned integers, reverting with custom message on
     * division by zero. The result is rounded towards zero.
     *
     * CAUTION: This function is deprecated because it requires allocating memory for the error
     * message unnecessarily. For custom revert reasons use {tryDiv}.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b > 0, errorMessage);
        return a / b;
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * reverting with custom message when dividing by zero.
     *
     * CAUTION: This function is deprecated because it requires allocating memory for the error
     * message unnecessarily. For custom revert reasons use {tryMod}.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b > 0, errorMessage);
        return a % b;
    }
}