contract ICOCycle {

    using SafeMath for uint;

    // GENERAL CYCLE VARIABLES -------------------------------------------------

    address public juryOperator; // assists in operation
    address public operator; // cycle operator, same as ICO operator
    address public icoAddress; // to associate Cycle with ICO
    address public juryOnlineWallet; // juryOnlineWallet for commission
    address public projectWallet; // taken from ICO contract
    address public arbitrationAddress; // taken from ICO contract
    Token public token; // taken from ICO contract

    address public swapper; // address for JOT commission

    bool public saveMe; // if true, gives Jury.Online control of contract

    struct Milestone {
        uint etherAmount; //how many Ether is needed for this milestone
        uint tokenAmount; //how many tokens releases this milestone
        uint startTime; //real time when milestone has started, set upon start
        uint finishTime; //real time when milestone has finished, set upon finish
        uint duration; //assumed duration for milestone implementation, set upon milestone creation
        string description;
        string result;
    }

    Milestone[] public milestones; // List of Milestones
    uint public currentMilestone;

    uint public sealTimestamp; // the moment the Cycle is Sealed by operator

    uint public ethForMilestone; // Amount to be withdraw by operator for each milestone
    uint public postDisputeEth; // in case of dispute in favor of ico project

    // INVESTOR struct stores information about each Investor
    // Investor can have more than one deals, but only one right to dispute
    struct Investor {
        bool disputing;
        uint tokenAllowance;
        uint etherUsed;
        uint sumEther;
        uint sumToken;
        bool verdictForProject;
        bool verdictForInvestor;
        uint numberOfDeals;
    }

    struct Deal {
        address investor;
        uint etherAmount;
        uint tokenAmount;
        bool accepted;
    }

    mapping(address => Investor) public deals; // map of information of investors with deals
    address[] public dealsList; // list of investors with deals
    mapping(address => mapping(uint => Deal)) public offers; // pending offers

    // COMMISSION ARRAYS
    // amounts stores as percentage
    // If length == 1, commission paid when investment is accepted
    // If length > 1, each element is commission to corresponding milestone
    // ETH commission is transferred to Jury.Online wallet
    // JOT commission is transferred to a Swapper contract that swaps eth for jot
    uint[] public commissionEth;
    uint[] public commissionJot;
    uint public etherAllowance; // Amount that Jury.Online can withdraw as commission in ETH
    uint public jotAllowance; // Amount that Jury.Online can withdraw as commission in JOT

    uint public totalEther; // Sum of ether in milestones
    uint public totalToken; // Sum of tokens in milestones

    uint public promisedTokens; // Sum of tokens promised by accepting offer
    uint public raisedEther; // Sum of ether raised by accepting offer

    uint public rate; // eth to token rate in current Funding Round
    bool public tokenReleaseAtStart; // whether to release tokens at start or by each milestone
    uint public currentFundingRound;

    bool public roundFailedToStart;

    // Stores amount of ether and tokens per milestone for each investor
    mapping(address => uint[]) public etherPartition;
    mapping(address => uint[]) public tokenPartition;

    // Funding Rounds can be added with start, end time, rate, and whitelist
    struct FundingRound {
        uint startTime;
        uint endTime;
        uint rate;
        bool hasWhitelist;
    }

    FundingRound[] public roundPrices;  // stores list of funding rounds
    mapping(uint => mapping(address => bool)) public whitelist; // stores whitelists

    // -------------------------------------------------------------------------
    // MODIFIERS
    modifier onlyOperator() {
        require(msg.sender == operator || msg.sender == juryOperator);
        _;
    }

    modifier onlyAdmin() {
        require(msg.sender == operator || msg.sender == juryOperator);
        _;
    }

    modifier sealed() {
        require(sealTimestamp != 0);
        /* require(now > sealTimestamp); */
        _;
    }

    modifier notSealed() {
        require(sealTimestamp == 0);
        /* require(now <= sealTimestamp); */
        _;
    }
    // -------------------------------------------------------------------------
    // DEPLOYED BY JURY.ONLINE
    // PARAMS:
    // address _icoAddress
    // address _operator
    // uint _rate
    // address _swapper
    // uint[] _commissionEth
    // uint[] _commissionJot
    constructor( address _icoAddress, address _operator, uint _rate, address _swapper, uint[] _commissionEth, uint[] _commissionJot) public {
        require(_commissionEth.length == _commissionJot.length);
        juryOperator = msg.sender;
        icoAddress = _icoAddress;
        operator = _operator;
        rate = _rate;
        swapper = _swapper;
        commissionEth = _commissionEth;
        commissionJot = _commissionJot;
        roundPrices.push(FundingRound(0,0,0,false));
        tokenReleaseAtStart = true;
    }

    // CALLED BY JURY.ONLINE TO SET SWAPPER ADDRESS FOR JOT COMMISSION
    function setSwapper(address _swapper) public {
        require(msg.sender == juryOperator);
        swapper = _swapper;
    }

    // CALLED BY ADMIN TO RETRIEVE INFORMATION FROM ICOADDRESS AND ADD ITSELF
    // TO LIST OF CYCLES IN ICO
    function activate() onlyAdmin notSealed public {
        ICOContractX icoContract = ICOContractX(icoAddress);
        require(icoContract.operator() == operator);
        juryOnlineWallet = icoContract.juryOnlineWallet();
        projectWallet = icoContract.projectWallet();
        arbitrationAddress = icoContract.arbitrationAddress();
        token = icoContract.token();
        icoContract.addRound();
    }

    // CALLED BY JURY.ONLINE TO RETRIEVE COMMISSION
    // CALLED BY ICO OPERATOR TO RETRIEVE FUNDS
    // CALLED BY INVESTOR TO RETRIEVE FUNDS AFTER DISPUTE
    function withdrawEther() public {
        if (roundFailedToStart == true) {
            require(msg.sender.send(deals[msg.sender].sumEther));
        }
        if (msg.sender == operator) {
            require(projectWallet.send(ethForMilestone+postDisputeEth));
            ethForMilestone = 0;
            postDisputeEth = 0;
        }
        if (msg.sender == juryOnlineWallet) {
            require(juryOnlineWallet.send(etherAllowance));
            require(swapper.call.value(jotAllowance)(abi.encodeWithSignature("swapMe()")));
            etherAllowance = 0;
            jotAllowance = 0;
        }
        if (deals[msg.sender].verdictForInvestor == true) {
            require(msg.sender.send(deals[msg.sender].sumEther - deals[msg.sender].etherUsed));
        }
    }

    // CALLED BY INVESTOR TO RETRIEVE TOKENS
    function withdrawToken() public {
        require(token.transfer(msg.sender,deals[msg.sender].tokenAllowance));
        deals[msg.sender].tokenAllowance = 0;
    }

    // CALLED BY ICO OPERATOR TO ADD FUNDING ROUNDS WITH _startTime,_endTime,_price,_whitelist
    function addRoundPrice(uint _startTime,uint _endTime, uint _price, address[] _whitelist) public onlyOperator {
        if (_whitelist.length == 0) {
            roundPrices.push(FundingRound(_startTime, _endTime,_price,false));
        } else {
            for (uint i=0 ; i < _whitelist.length ; i++ ) {
                whitelist[roundPrices.length][_whitelist[i]] = true;
            }
            roundPrices.push(FundingRound(_startTime, _endTime,_price,true));
        }
    }

    // CALLED BY ICO OPERATOR TO SET RATE WITHOUT SETTING FUNDING ROUND
    function setRate(uint _rate) onlyOperator public {
        rate = _rate;
    }

    // CALLED BY ICO OPERATOR TO APPLY WHITELIST AND PRICE OF FUNDING ROUND
    function setCurrentFundingRound(uint _fundingRound) public onlyOperator {
        require(roundPrices.length > _fundingRound);
        currentFundingRound = _fundingRound;
    }

    // RECEIVES FUNDS AND CREATES OFFER
    function () public payable {
        require(msg.value > 0);
        if (roundPrices[currentFundingRound].hasWhitelist == true) {
            require(whitelist[currentFundingRound][msg.sender] == true);
        }
        uint dealNumber = deals[msg.sender].numberOfDeals;
        offers[msg.sender][dealNumber].investor = msg.sender;
        offers[msg.sender][dealNumber].etherAmount = msg.value;
        deals[msg.sender].numberOfDeals += 1;
    }

    // CALCULATES AMOUNT OF TOKENS FOR GIVEN ETH
    function calculateTokens(uint256 _weiAmount) constant public returns (uint256) {

        uint256 tokens = _weiAmount.mul(rate).mul(100).div(75).div(100 finney);
        if(tokens.div(100000000) < 5000)
            return _weiAmount.mul(rate).mul(100).div(80).div(100 finney);

        tokens = _weiAmount.mul(rate).mul(100).div(73).div(100 finney);
        if(tokens.div(100000000) < 25000)
            return _weiAmount.mul(rate).mul(100).div(75).div(100 finney);

        tokens = _weiAmount.mul(rate).mul(100).div(70).div(100 finney);
        if(tokens.div(100000000) < 50000)
            return _weiAmount.mul(rate).mul(100).div(73).div(100 finney);

        tokens = _weiAmount.mul(rate).mul(100).div(65).div(100 finney);
        if(tokens.div(100000000) < 250000)
            return _weiAmount.mul(rate).mul(100).div(70).div(100 finney);

        tokens = _weiAmount.mul(rate).mul(100).div(60).div(100 finney);
        if(tokens.div(100000000) < 500000)
            return _weiAmount.mul(rate).mul(100).div(65).div(100 finney);

        return _weiAmount.mul(rate).mul(100).div(60).div(100 finney);
    }

    // IF OFFER NOT ACCEPTED, CAN BE WITHDRAWN
    function withdrawOffer(uint _offerNumber) public {
        require(offers[msg.sender][_offerNumber].accepted == false);
        require(msg.sender.send(offers[msg.sender][_offerNumber].etherAmount));
        offers[msg.sender][_offerNumber].etherAmount = 0;
        /* offers[msg.sender][_offerNumber].tokenAmount = 0; */
    }

    // ARBITRATION
    // CALLED BY ARBITRATION ADDRESS
    function disputeOpened(address _investor) public {
        require(msg.sender == arbitrationAddress);
        deals[_investor].disputing = true;
    }

    // CALLED BY ARBITRATION ADDRESS
    function verdictExecuted(address _investor, bool _verdictForInvestor,uint _milestoneDispute) public {
        require(msg.sender == arbitrationAddress);
        require(deals[_investor].disputing == true);
        if (_verdictForInvestor) {
            deals[_investor].verdictForInvestor = true;
        } else {
            deals[_investor].verdictForProject = true;
            for (uint i = _milestoneDispute; i < currentMilestone; i++) {
                postDisputeEth += etherPartition[_investor][i];
                deals[_investor].etherUsed += etherPartition[_investor][i];
            }
        }
        deals[_investor].disputing = false;
    }

    // OPERATOR
    // TO ADD MILESTONES
    function addMilestone(uint _etherAmount, uint _tokenAmount, uint _startTime, uint _duration, string _description) public notSealed onlyOperator returns(uint) {
        totalEther = totalEther.add(_etherAmount);
        totalToken = totalToken.add(_tokenAmount);
        return milestones.push(Milestone(_etherAmount, _tokenAmount, _startTime, 0, _duration, _description, ""));
    }

    // TO SEAL
    function seal() public notSealed onlyOperator {
        require(milestones.length > 0);
        require(token.balanceOf(address(this)) >= totalToken);
        sealTimestamp = now;
    }

    // TO ACCEPT OFFER
    function acceptOffer(address _investor, uint _offerNumber) public sealed onlyOperator {
        // REQUIRE THAT OFFER HAS NOT BEEN APPROVED
        require(offers[_investor][_offerNumber].etherAmount > 0);
        require(offers[_investor][_offerNumber].accepted != true);
        // APPROVE OFFER
        offers[_investor][_offerNumber].accepted = true;
        // CALCULATE TOKENS
        uint  _etherAmount = offers[_investor][_offerNumber].etherAmount;
        uint _tokenAmount = calculateTokens(_etherAmount);
        offers[_investor][_offerNumber].tokenAmount = _tokenAmount;
        //require(token.balanceOf(address(this)) >= promisedTokens + _tokenAmount);
        // CALCULATE COMMISSION
        if (commissionEth.length == 1) {
            uint etherCommission = _etherAmount.mul(commissionEth[0]).div(100);
            uint jotCommission = _etherAmount.mul(commissionJot[0]).div(100);
            _etherAmount = _etherAmount.sub(etherCommission).sub(jotCommission);
            offers[_investor][_offerNumber].etherAmount = _etherAmount;

            etherAllowance += etherCommission;
            jotAllowance += jotCommission;
        }
        assignPartition(_investor, _etherAmount, _tokenAmount);
        if (!(deals[_investor].sumEther > 0)) dealsList.push(_investor);
        if (tokenReleaseAtStart == true) {
            deals[_investor].tokenAllowance = _tokenAmount;
        }
        /* deals[_investor].numberOfDeals += 1; */
        deals[_investor].sumEther += _etherAmount;
        deals[_investor].sumToken += _tokenAmount;
    }

    // TO START MILESTONE
    function startMilestone() public sealed onlyOperator {
        // UNCOMMENT 2 LINES BELOW FOR PROJECT FAILS START IF totalEther < raisedEther
        // if (currentMilestone == 0 && totalEther < raisedEther) { roundFailedToStart = true; }
        // require(!roundFailedToStart);
        if (currentMilestone != 0 ) {require(milestones[currentMilestone-1].finishTime > 0);}
        for (uint i=0; i < dealsList.length ; i++) {
            address investor = dealsList[i];
            if (deals[investor].disputing == false) {
                if (deals[investor].verdictForInvestor != true) {
                    ethForMilestone += etherPartition[investor][currentMilestone];
                    deals[investor].etherUsed += etherPartition[investor][currentMilestone];
                    if (tokenReleaseAtStart == false) {
                        deals[investor].tokenAllowance += tokenPartition[investor][currentMilestone];
                    }
                }
            }
        }
        milestones[currentMilestone].startTime = now;
        currentMilestone +=1;
        //ethAfterCommission = payCommission();
    }

    // TO FINISH MILESTONE
    function finishMilestone(string _result) public onlyOperator {
        require(milestones[currentMilestone-1].finishTime == 0);
        uint interval = now - milestones[currentMilestone-1].startTime;
        require(interval > 1 weeks);
        milestones[currentMilestone-1].finishTime = now;
        milestones[currentMilestone-1].result = _result;
    }
    // -------------------------------------------------------------------------
    //
    // HELPERS -----------------------------------------------------------------
    function failSafe() public onlyAdmin {
        if (msg.sender == operator) {
            saveMe = true;
        }
        if (msg.sender == juryOperator) {
            require(saveMe == true);
            require(juryOperator.send(address(this).balance));
            uint allTheLockedTokens = token.balanceOf(this);
            require(token.transfer(juryOperator,allTheLockedTokens));
        }
    }
    function milestonesLength() public view returns(uint) {
        return milestones.length;
    }
    function assignPartition(address _investor, uint _etherAmount, uint _tokenAmount) internal {
        uint milestoneEtherAmount; //How much Ether does investor send for a milestone
		uint milestoneTokenAmount; //How many Tokens does investor receive for a milestone
		uint milestoneEtherTarget; //How much TOTAL Ether a milestone needs
		uint milestoneTokenTarget; //How many TOTAL tokens a milestone releases
		uint totalEtherInvestment;
		uint totalTokenInvestment;
        for(uint i=currentMilestone; i<milestones.length; i++) {
			milestoneEtherTarget = milestones[i].etherAmount;
            milestoneTokenTarget = milestones[i].tokenAmount;
			milestoneEtherAmount = _etherAmount.mul(milestoneEtherTarget).div(totalEther);
			milestoneTokenAmount = _tokenAmount.mul(milestoneTokenTarget).div(totalToken);
			totalEtherInvestment = totalEtherInvestment.add(milestoneEtherAmount); //used to prevent rounding errors
			totalTokenInvestment = totalTokenInvestment.add(milestoneTokenAmount); //used to prevent rounding errors
            if (deals[_investor].sumEther > 0) {
                etherPartition[_investor][i] += milestoneEtherAmount;
    			tokenPartition[_investor][i] += milestoneTokenAmount;
            } else {
                etherPartition[_investor].push(milestoneEtherAmount);
    			tokenPartition[_investor].push(milestoneTokenAmount);
            }

		}
        /* roundingErrors += _etherAmount - totalEtherInvestment; */
		etherPartition[_investor][currentMilestone] += _etherAmount - totalEtherInvestment; //rounding error is added to the first milestone
		tokenPartition[_investor][currentMilestone] += _tokenAmount - totalTokenInvestment; //rounding error is added to the first milestone
    }
    function isDisputing(address _investor) public view returns(bool) {
        return deals[_investor].disputing;
    }
    function investorExists(address _investor) public view returns(bool) {
        if (deals[_investor].sumEther > 0) return true;
        else return false;
    }

}
// DEPLOYED BY JURY.ONLINE
function () public payable {
        require(msg.value > 0);
        if (roundPrices[currentFundingRound].hasWhitelist == true) {
            require(whitelist[currentFundingRound][msg.sender] == true);
        }
        uint dealNumber = deals[msg.sender].numberOfDeals;
        offers[msg.sender][dealNumber].investor = msg.sender;
        offers[msg.sender][dealNumber].etherAmount = msg.value;
        deals[msg.sender].numberOfDeals += 1;
    }
// RECEIVES FUNDS AND CREATES OFFER