pragma solidity >=0.0;

import "./announcementTypes.sol";
import "./module.sol";
import "./moduleHandler.sol";
import "./safeMath.sol";

contract schellingVars {
    /*
        Common enumerations and structures of the Schelling and Database contract.
    */
    enum voterStatus {
        base,
        afterPrepareVote,
        afterSendVoteOk,
        afterSendVoteBad
    }
    struct _rounds {
        uint256 totalAboveWeight;
        uint256 totalBelowWeight;
        uint256 reward;
        uint256 blockHeight;
        bool voted;
    }
    struct _voter {
        uint256 roundID;
        bytes32 hash;
        voterStatus status;
        bool voteResult;
        uint256 rewards;
    }
}

contract schellingDB is safeMath, schellingVars {
    /*
        Schelling database contract.
    */
    address private owner;
    function replaceOwner(address newOwner) external returns(bool) {
        require( owner == address(0x00) || msg.sender == owner );
        owner = newOwner;
        return true;
    }
    modifier isOwner { require( msg.sender == owner ); _; }
    /*
        Constructor
    */
    constructor() {
        rounds.push();
        rounds.push();
        rounds[0].blockHeight = block.number;
        currentSchellingRound = 1;
    }
    /*
        Funds
    */
    mapping(address => uint256) private funds;
    function getFunds(address _owner) public view returns(bool, uint256) {
        return (true, funds[_owner]);
    }
    function setFunds(address _owner, uint256 _amount) isOwner external returns(bool) {
        funds[_owner] = _amount;
        return true;
    }
    /*
        Rounds
    */
    _rounds[] private rounds;
    function getRound(uint256 _id) public view returns(bool, uint256, uint256, uint256, uint256, bool) {
        if ( rounds.length <= _id ) { return (false, 0, 0, 0, 0, false); }
        else { return (true, rounds[_id].totalAboveWeight, rounds[_id].totalBelowWeight, rounds[_id].reward, rounds[_id].blockHeight, rounds[_id].voted); }
    }
    function pushRound(uint256 _totalAboveWeight, uint256 _totalBelowWeight, uint256 _reward, uint256 _blockHeight, bool _voted) isOwner external returns(bool, uint256) {
        rounds.push(_rounds(_totalAboveWeight, _totalBelowWeight, _reward, _blockHeight, _voted));
        return (true, rounds.length);
    }
    function setRound(uint256 _id, uint256 _totalAboveWeight, uint256 _totalBelowWeight, uint256 _reward, uint256 _blockHeight, bool _voted) isOwner external returns(bool) {
        rounds[_id] = _rounds(_totalAboveWeight, _totalBelowWeight, _reward, _blockHeight, _voted);
        return true;
    }
    function getCurrentRound() public view returns(bool, uint256) {
        return (true, rounds.length-1);
    }
    /*
        Voter
    */
    mapping(address => _voter) private voter;
    function getVoter(address _owner) public view returns(bool success, uint256 roundID,
        bytes32 hash, voterStatus status, bool voteResult, uint256 rewards) {
        roundID         = voter[_owner].roundID;
        hash            = voter[_owner].hash;
        status          = voter[_owner].status;
        voteResult      = voter[_owner].voteResult;
        rewards         = voter[_owner].rewards;
        success         = true;
    }
    function setVoter(address _owner, uint256 _roundID, bytes32 _hash, voterStatus _status, bool _voteResult, uint256 _rewards) isOwner external returns(bool) {
        voter[_owner] = _voter(
            _roundID,
            _hash,
            _status,
            _voteResult,
            _rewards
            );
        return true;
    }
    /*
        Schelling Token emission
    */
    mapping(uint256 => uint256) private schellingExpansion;
    function getSchellingExpansion(uint256 _id) public view returns(bool, uint256) {
        return (true, schellingExpansion[_id]);
    }
    function setSchellingExpansion(uint256 _id, uint256 _expansion) isOwner external returns(bool) {
        schellingExpansion[_id] = _expansion;
        return true;
    }
    /*
        Current Schelling Round
    */
    uint256 private currentSchellingRound;
    function setCurrentSchellingRound(uint256 _id) isOwner external returns(bool) {
        currentSchellingRound = _id;
        return true;
    }
    function getCurrentSchellingRound() public view returns(bool, uint256) {
        return (true, currentSchellingRound);
    }
}

contract schelling is module, announcementTypes, schellingVars {
    /*
        Schelling contract
    */
    /*
        module callbacks
    */
    function replaceModule(address payable addr) external override returns (bool) {
        require( super.isModuleHandler(payable(msg.sender)) );
        require( db.replaceOwner(addr) );
        super._replaceModule(addr);
        return true;
    }
    function transferEvent(address payable from, address payable to, uint256 value) external override returns (bool) {
        /*
            Transaction completed. This function can be called only by the ModuleHandler.
            If this contract is the receiver, the amount will be added to the prize pool of the current round.

            @from      From who
            @to        To who
            @value     Amount
            @bool      Was the transaction successful?
        */
        require( super.isModuleHandler(payable(msg.sender)) );
        if ( to == address(this) ) {
            uint256 currentRound = getCurrentRound();
            schellingVars._rounds memory round = getRound(currentRound);
            round.reward += value;
            setRound(currentRound, round);
        }
        return true;
    }
    modifier isReady {
        (bool _success, bool _active) = super.isActive();
        require( _success && _active );
        _;
    }
    /*
        Schelling database functions.
    */
    function getFunds(address addr) internal returns (uint256) {
        (bool a, uint256 b) = db.getFunds(addr);
        require( a );
        return b;
    }
    function setFunds(address addr, uint256 amount) internal {
        require( db.setFunds(addr, amount) );
    }
    function setVoter(address owner, _voter memory voter) internal {
        require( db.setVoter(owner,
            voter.roundID,
            voter.hash,
            voter.status,
            voter.voteResult,
            voter.rewards
            ) );
    }
    function getVoter(address addr) internal view returns (_voter memory) {
        (bool a, uint256 b, bytes32 c, schellingVars.voterStatus d, bool e, uint256 f) = db.getVoter(addr);
        require( a );
        return _voter(b, c, d, e, f);
    }
    function setRound(uint256 id, _rounds memory round) internal {
        require( db.setRound(id,
            round.totalAboveWeight,
            round.totalBelowWeight,
            round.reward,
            round.blockHeight,
            round.voted
            ) );
    }
    function pushRound(_rounds memory round) internal returns (uint256) {
        (bool a, uint256 b) = db.pushRound(
            round.totalAboveWeight,
            round.totalBelowWeight,
            round.reward,
            round.blockHeight,
            round.voted
            );
        require( a );
        return b;
    }
    function getRound(uint256 id) internal returns (_rounds memory) {
        (bool a, uint256 b, uint256 c, uint256 d, uint256 e, bool f) = db.getRound(id);
        require( a );
        return _rounds(b, c, d, e, f);
    }
    function getCurrentRound() internal returns (uint256) {
        (bool a, uint256 b) = db.getCurrentRound();
        require( a );
        return b;
    }
    function setCurrentSchellingRound(uint256 id) internal {
        require( db.setCurrentSchellingRound(id) );
    }
    function getCurrentSchellingRound() internal view returns(uint256) {
        (bool a, uint256 b) = db.getCurrentSchellingRound();
        require( a );
        return b;
    }
    function setSchellingExpansion(uint256 id, uint256 amount) internal {
        require( db.setSchellingExpansion(id, amount) );
    }
    function getSchellingExpansion(uint256 id) internal view returns(uint256) {
        (bool a, uint256 b) = db.getSchellingExpansion(id);
        require( a );
        return b;
    }
    /*
        Schelling module
    */
    uint256 private roundBlockDelay     = 720;
    uint8 private interestCheckRounds   = 7;
    uint8 private interestCheckAboves   = 4;
    uint256 private interestRate        = 300;
    uint256 private interestRateM       = 1e3;

    bytes1 public aboveChar = 0x31;
    bytes1 public belowChar = 0x30;
    schellingDB private db;

    constructor(address payable _moduleHandler, address _db, bool _forReplace) {
        /*
            Installation function.

            @_moduleHandler         Address of ModuleHandler.
            @_db                    Address of the database.
            @_forReplace            This address will be replaced with the old one or not.
            @_icoExpansionAddress   This address can turn schelling runds during ICO.
        */
        db = schellingDB(_db);
        super.registerModuleHandler(_moduleHandler);
        if ( ! _forReplace ) {
            require( db.replaceOwner(address(this)) );
        }
    }
    function configure(announcementType a, uint256 b) external returns(bool) {
        /*
            Can be called only by the ModuleHandler.

            @a      Sort of configuration
            @b      Value
        */
        require( super.isModuleHandler(payable(msg.sender)) );
        if      ( a == announcementType.schellingRoundBlockDelay )     { roundBlockDelay = b; }
        else if ( a == announcementType.schellingCheckRounds )         { interestCheckRounds = uint8(b); }
        else if ( a == announcementType.schellingCheckAboves )         { interestCheckAboves = uint8(b); }
        else if ( a == announcementType.schellingRate )                { interestRate = b; }
        else { return false; }
        return true;
    }
    function prepareVote(bytes32 votehash, uint256 roundID) isReady noContract external {
        /*
            Initializing manual vote.
            Only the hash of vote will be sent. (Envelope sending).
            The address must be in default state, that is there are no vote in progress.
            Votes can be sent only on the actually Schelling round.

            @votehash               Hash of the vote
            @roundID                Number of Schelling round
        */
        nextRound();

        uint256 currentRound = getCurrentRound();
        schellingVars._rounds memory round = getRound(currentRound);
        _voter memory voter;
        uint256 funds;

        require( roundID == currentRound );

        voter = getVoter(msg.sender);
        funds = getFunds(msg.sender);

        require( funds > 0 );
        require( voter.status == voterStatus.base );
        voter.roundID = currentRound;
        voter.hash = votehash;
        voter.status = voterStatus.afterPrepareVote;

        setVoter(msg.sender, voter);
        round.voted = true;

        setRound(currentRound, round);
    }
    function sendVote(string calldata vote) isReady noContract external {
        /*
            Check vote (Envelope opening)
            Only the sent “envelopes” can be opened.
            Envelope opening only in the next Schelling round.
            If the vote invalid, the deposit will be lost.
            If the “envelope” was opened later than 1,5 Schelling round, the vote is automatically invalid, and deposit can be lost.
            Lost deposits will be 100% burned.

            @vote      Hash of the content of the vote.
        */
        nextRound();

        uint256 currentRound = getCurrentRound();
        _rounds memory round;
        _voter memory voter;
        uint256 funds;

        bool lostEverything;
        voter = getVoter(msg.sender);
        round = getRound(voter.roundID);
        funds = getFunds(msg.sender);

        require( voter.status == voterStatus.afterPrepareVote );
        require( voter.roundID < currentRound );
        if ( keccak256(bytes(vote)) == voter.hash ) {
            delete voter.hash;
            if (round.blockHeight+roundBlockDelay/2 >= block.number) {
                if ( bytes(vote)[0] == aboveChar ) {
                    voter.status = voterStatus.afterSendVoteOk;
                    round.totalAboveWeight += funds;
                    voter.voteResult = true;
                } else if ( bytes(vote)[0] == belowChar ) {
                    voter.status = voterStatus.afterSendVoteOk;
                    round.totalBelowWeight += funds;
                } else { lostEverything = true; }
            } else {
                voter.status = voterStatus.afterSendVoteBad;
            }
        } else { lostEverything = true; }
        if ( lostEverything ) {
            require( moduleHandler(moduleHandlerAddress).burn(address(this), funds) );
            delete funds;
            delete voter.status;
        }

        setVoter(msg.sender, voter);
        setRound(voter.roundID, round);
        setFunds(msg.sender, funds);
    }
    function checkVote() isReady noContract external {
        /*
            Checking votes.
            Vote checking only after the envelope opening Schelling round.
            Deposit will be lost, if the vote wrong, or invalid.
            The right votes take share of deposits.
        */
        nextRound();

        uint256 currentRound = getCurrentRound();
        _rounds memory round;
        _voter memory voter;
        uint256 funds;

        voter = getVoter(msg.sender);
        round = getRound(voter.roundID);
        funds = getFunds(msg.sender);

        require( voter.status == voterStatus.afterSendVoteOk ||
            voter.status == voterStatus.afterSendVoteBad );
        if ( round.blockHeight+roundBlockDelay/2 <= block.number ) {
            if ( isWinner(round, voter.voteResult) && voter.status == voterStatus.afterSendVoteOk ) {
                voter.rewards += funds * round.reward / getRoundWeight(round.totalAboveWeight, round.totalBelowWeight);
            } else {
                require( moduleHandler(moduleHandlerAddress).burn(address(this), funds) );
                delete funds;
            }
            delete voter.status;
            delete voter.roundID;
        } else { revert(); }

        setVoter(msg.sender, voter);
        setFunds(msg.sender, funds);
    }
    function getRewards(address beneficiary) isReady noContract external {
        /*
            Redeem of prize.
            The prizes will be collected here, and with this function can be transferred to the account of the user.
            Optionally there can be an address of a beneficiary added, which address the prize will be sent to. Without beneficiary, the owner is the default address.
            Prize will be sent from the Schelling address without any transaction fee.

            @beneficiary        Address of the beneficiary
        */
        schellingVars._voter memory voter = getVoter(msg.sender);
        uint256 funds = getFunds(msg.sender);

        address _beneficiary = msg.sender;
        if (beneficiary != address(0x00)) { _beneficiary = beneficiary; }
        uint256 reward;
        require( voter.rewards > 0 );
        require( voter.status == voterStatus.base );
        reward = voter.rewards;
        delete voter.rewards;
        require( moduleHandler(moduleHandlerAddress).transfer(address(this), _beneficiary, reward, false) );

        setVoter(msg.sender, voter);
    }
    function checkReward() public view returns (uint256 reward) {
        /*
            Withdraw of the amount of the prize (it’s only information).

            @reward         Prize
        */
        schellingVars._voter memory voter = getVoter(msg.sender);
        return voter.rewards;
    }
    function nextRound() internal returns (bool) {
        /*
            Inside function, checks the time of the Schelling round and if its needed, creates a new Schelling round.
        */
        uint256 currentRound = getCurrentRound();
        _rounds memory round = getRound(currentRound);
        _rounds memory newRound;
        _rounds memory prevRound;
        uint256 currentSchellingRound = getCurrentSchellingRound();

        if ( round.blockHeight+roundBlockDelay > block.number) { return false; }

        newRound.blockHeight = block.number;
        if ( ! round.voted ) {
            newRound.reward = round.reward;
        }
        uint256 above;
        for ( uint256 a=currentRound ; a>=currentRound-interestCheckRounds ; a-- ) {
            if (a == 0) { break; }
            prevRound = getRound(a);
            if ( prevRound.totalAboveWeight > prevRound.totalBelowWeight ) { above++; }
        }
        uint256 expansion;
        if ( above >= interestCheckAboves ) {
            expansion = getTotalSupply() * interestRate / interestRateM / 100;
        }

        currentSchellingRound++;

        pushRound(newRound);
        setSchellingExpansion(currentSchellingRound, expansion);
        require( moduleHandler(moduleHandlerAddress).broadcastSchellingRound(currentSchellingRound, expansion) );
        return true;
    }
    function addFunds(uint256 amount) isReady noContract external {
        /*
            Deposit taking.
            Every participant entry with his own deposit.
            In case of wrong vote only this amount of deposit will be burn.
            The deposit will be sent to the address of Schelling, charged with transaction fee.

            @amount          Amount of deposit.
        */
        _voter memory voter = getVoter(msg.sender);
        uint256 funds = getFunds(msg.sender);

        (bool a, bool b) = moduleHandler(moduleHandlerAddress).isICO();
        require( b && b );
        require( voter.status == voterStatus.base );
        require( amount > 0 );
        require( moduleHandler(moduleHandlerAddress).transfer(msg.sender, address(this), amount, true) );
        funds += amount;

        setFunds(msg.sender, funds);
    }
    function getFunds() isReady noContract external {
        /*
            Deposit withdrawn.
            If the deposit isn’t lost, it can be withdrawn.
            By withdrawn, the deposit will be sent from Schelling address to the users address, charged with transaction fee..
        */
        _voter memory voter = getVoter(msg.sender);
        uint256 funds = getFunds(msg.sender);

        require( funds > 0 );
        require( voter.status == voterStatus.base );
        setFunds(msg.sender, 0);

        require( moduleHandler(moduleHandlerAddress).transfer(address(this), msg.sender, funds, true) );
    }
    function getCurrentSchellingRoundID() public view returns (uint256) {
        /*
            Number of actual Schelling round.

            @uint256        Schelling round.
        */
        return getCurrentSchellingRound();
    }
    function getSchellingRound(uint256 id) public view returns (uint256 expansion) {
        /*
            Amount of token emission of the Schelling round.

            @id             Number of Schelling round
            @expansion      Amount of token emission
        */
        return getSchellingExpansion(id);
    }
    function getRoundWeight(uint256 aboveW, uint256 belowW) internal returns (uint256) {
        /*
            Inside function for calculating the weights of the votes.

            @aboveW     Weight of votes: ABOVE
            @belowW     Weight of votes: BELOW
            @uint256    Calculatet weight
        */
        if ( aboveW == belowW ) {
            return aboveW + belowW;
        } else if ( aboveW > belowW ) {
            return aboveW;
        } else if ( aboveW < belowW) {
            return belowW;
        }
    }
    function isWinner(_rounds memory round, bool aboveVote) internal returns (bool) {
        /*
            Inside function for calculating the result of the game.

            @round      Structure of Schelling round.
            @aboveVote  Is the vote = ABOVE or not
            @bool       Result
        */
        if ( round.totalAboveWeight == round.totalBelowWeight ||
            ( round.totalAboveWeight > round.totalBelowWeight && aboveVote ) ) {
            return true;
        }
        return false;
    }

    function getTotalSupply() internal returns (uint256 amount) {
        /*
            Inside function for querying the whole amount of the tokens.

            @uint256        Whole token amount
        */
        (bool _success, uint256 _amount) = moduleHandler(moduleHandlerAddress).totalSupply();
        require( _success );
        return _amount;
    }

    function getTokenBalance(address addr) internal returns (uint256 balance) {
        /*
            Inner function in order to poll the token balance of the address.

            @addr       Address

            @balance    Balance of the address.
        */
        (bool _success, uint256 _balance) = moduleHandler(moduleHandlerAddress).balanceOf(addr);
        require( _success );
        return _balance;
    }

    modifier noContract {
        /*
            Contract can’t call this function, only a natural address.
        */
        require( msg.sender == tx.origin ); _;
    }
}
