contract Prediction is usingOraclize {

    address owner;
    uint public start_date;
    uint public end_date;
    uint8 public winning_month;
    uint16 public winning_year;
    uint24 public cutoff;
    uint24 public price_target;
    uint public pot_total;

    // ==== flags, constants
    bool public is_btc;
    uint public CRYPTO_USD;
    bool public closed;
    bool public target_hit;
    bool can_fetch_price = true;
    bool check_hourly = false;
    bool public can_refund = false;
    uint8 public constant FEE = 20; // = 1/20 = 5%;
    uint public constant MIN_AMOUNT = 0.01 ether;

    // ==== payout vars
    uint public payout_normalizer;
    mapping (uint16 => uint) public payout_multiples;

    // ==== prediction data
    struct Predictor {
        bool withdrawn;
        uint16[] days_since_start;
        uint[] amounts;
        uint total_amount;
    }
    struct Totals {
        uint amount;
        uint num_predictions;
        mapping(uint16 => uint) amount_on_day;
        uint16[] days_with_predictions;
    }
    // yearmonth => (address => amount)
    mapping (uint24 => mapping (address => Predictor)) public predictions;
    // for faster processing
    mapping (address => Predictor) public user_predictions;
    // yearmonth => totals
    mapping (uint24 => Totals) public ym_totals;
    // store months with predictions
    uint24[] public ym_index;

    event PredictionPlaced();
    event Print(string note, uint val);

    function Prediction(uint24 target, uint24 _cutoff, bool _is_btc) public {
        owner = msg.sender;
        require(target > 0);
        price_target = target;
        cutoff = _cutoff;
        is_btc = _is_btc;
        start_date = now - (now % DAY_IN_SECONDS) - DAY_IN_SECONDS;
    }

    /*
     * predict =====================================================================
     */

    function predict(uint16 year, uint8 month) public payable canPredict {
        uint commission = calculateCommission(msg.value);
        owner.transfer(commission);
        uint after_commission = msg.value - commission;
        uint16 days_since_start = uint16((now - start_date) / DAY_IN_SECONDS);

        // store prediction
        uint24 ym = concatYearMonth(year, month);
        Predictor storage predictor = predictions[ym][msg.sender];
        predictor.amounts.push(after_commission);
        predictor.total_amount += after_commission;
        predictor.days_since_start.push(days_since_start);
        Predictor storage user_prediction = user_predictions[msg.sender];
        user_prediction.amounts.push(after_commission);
        // user_prediction.days_since_start.push(days_since_start);

        // totals
        pot_total += after_commission;
        Totals storage ymt = ym_totals[ym];
        if (ymt.amount == 0) {
            ym_index.push(ym);
        }
        ymt.amount += after_commission;
        ymt.num_predictions++;
        if (ymt.amount_on_day[days_since_start] == 0) {
            ymt.days_with_predictions.push(days_since_start);
        }
        ymt.amount_on_day[days_since_start] += after_commission;

        PredictionPlaced();
    }

    function getWinningPrediction(address predictor) public view returns (uint[], uint16[]) {
        uint24 ym = concatYearMonth(winning_year, winning_month);
        Predictor storage _b = predictions[ym][predictor];
        return (_b.amounts, _b.days_since_start);
    }

    function getPredictions() public view returns (uint24[], uint[] amounts, uint[] user_amounts) {
        amounts = new uint[](ym_index.length);
        user_amounts = new uint[](ym_index.length);
        for (uint24 i = 0; i < ym_index.length; i++) {
            uint24 ym = ym_index[i];
            amounts[i] = ym_totals[ym].amount;
            user_amounts[i] = predictions[ym][msg.sender].total_amount;
        }
        return (ym_index, amounts, user_amounts);
    }

    /*
     * price ===================================================================
     */

    function __callback(bytes32 myid, string result) public {
        require(msg.sender == oraclize_cbAddress());
        CRYPTO_USD = parseInt(result, 0); // discard cents

        if (CRYPTO_USD >= price_target) {
            winning_month = getMonth(now);
            winning_year = getYear(now);

            target_hit = true;
            can_fetch_price = false;
            end_date = now - (now % DAY_IN_SECONDS) + DAY_IN_SECONDS;
        } else {
            _fetch(getNextFetchTime());
        }
        if (CRYPTO_USD >= cutoff) {
            closed = true;
        } else {
            closed = false;
        }
    }

    function _fetch(uint delay) public payable {
        require(msg.sender == owner || msg.sender == oraclize_cbAddress());
        require(can_fetch_price);
        if (is_btc) {
            oraclize_query(delay, "URL", "json(https://api.bitfinex.com/v1/pubticker/btcusd).high");
        } else {
            oraclize_query(delay, "URL", "json(https://api.bitfinex.com/v1/pubticker/ethusd).high");
        }
    }

    /*
     * payout ==================================================================
     */

    function calculatePayoutVariables() public canPayout {
        // idempotent.. but only needs to be run once
        uint contract_duration = (end_date - start_date) / DAY_IN_SECONDS;
        uint24 ym = concatYearMonth(winning_year, winning_month);
        Totals storage ymt = ym_totals[ym];

        // calculate normalizer using only winning predictions
        for (uint16 i = 0; i < ymt.days_with_predictions.length; i++) {
            uint16 day = ymt.days_with_predictions[i];
            uint amount_on_day = ymt.amount_on_day[day];
            uint multiple_for_day = ((contract_duration - day) * amount_on_day);
            payout_multiples[day] = multiple_for_day;
            payout_normalizer += multiple_for_day;
        }
    }

    function calculatePayout(uint amount, uint16 days_since_start) public view returns(uint) {
        uint payout_multiple = payout_multiples[days_since_start];
        uint weight_total = ( payout_multiple * 10**18 ) / payout_normalizer;

        uint24 ym = concatYearMonth(winning_year, winning_month);
        uint winning_pot = ym_totals[ym].amount;
        uint losing_pot = pot_total - winning_pot;

        uint available_pot = ( weight_total * losing_pot ) / 10**18;
        // calculate correct reward percentage of available pot
        uint amount_on_day = ym_totals[ym].amount_on_day[days_since_start];
        uint weight_winning = ( amount * 10**18) / amount_on_day;
        return amount + ( ( weight_winning * available_pot ) / 10**18 );
    }

    function collect() public payable canPayout {
        uint24 ym = concatYearMonth(winning_year, winning_month);
        Predictor storage predictor = predictions[ym][msg.sender];
        require(!predictor.withdrawn);
        uint predictor_payout;

        for(uint8 i = 0; i < predictor.amounts.length; i++) {
            uint amount = predictor.amounts[i];
            if (amount == 0) {
                return;
            }

            uint payout = calculatePayout(amount, predictor.days_since_start[i]);
            if (payout > 0) {
                predictor_payout += payout;
            }
        }
        predictor.withdrawn = true;
        msg.sender.transfer(predictor_payout);
    }

    function refund() public payable {
        require(can_refund);
        uint predictor_refund;
        Predictor storage predictor = user_predictions[msg.sender];
        require(!predictor.withdrawn);
        for (uint16 i = 0; i < predictor.amounts.length; i++){
            predictor_refund += predictor.amounts[i];
        }
        predictor.withdrawn = true;
        msg.sender.transfer(predictor_refund);
    }

    /*
     * admin ===================================================================
     */

    function closeContract() public adminOnly { closed = true; }
    function updateCutoff(uint24 _cutoff) public adminOnly { cutoff = _cutoff; }
    function updateCanFetch(bool can) public adminOnly { can_fetch_price = can; }
    function updateCanRefund(bool _refund) public adminOnly { can_refund = _refund; }
    function updateCheckHourly(bool is_hourly) public adminOnly { check_hourly = is_hourly; }

    /*
     * misc ====================================================================
     */

    modifier canPredict() { require(!closed && msg.value >= MIN_AMOUNT); _; }
    modifier adminOnly() { require(msg.sender == owner); _; }
    modifier canPayout() { require(closed && target_hit); _; }

    function getBalance() public view returns (uint) { return this.balance; }

    function calculateCommission(uint value) public view returns (uint) {
        return (value / FEE);
    }

    function getNextFetchTime() internal view returns(uint next_time) {
        if(check_hourly) {
            next_time = now - (now % HOUR_IN_SECONDS) + HOUR_IN_SECONDS;
        } else {
            next_time = now - (now % DAY_IN_SECONDS) + DAY_IN_SECONDS;
        }
        return next_time;
    }

    function concatYearMonth(uint16 year, uint8 month) internal pure returns (uint24) {
        return ( uint24(year) * 100 ) + month;
    }

    function transferLeftover() public adminOnly canPayout {
        // if there is any balance left over in the contract
        uint days_since_end = (now - end_date) / DAY_IN_SECONDS;
        if(days_since_end > 30) {
            owner.transfer(this.balance);
        }
    }

    /*
     * pipermerriam ============================================================
     */
    /*
     *  Date and Time utilities for ethereum contracts
     *
     */
    
    struct _DateTime {
        uint16 year;
        uint8 month;
        uint8 day;
        uint8 hour;
        uint8 minute;
        uint8 second;
        uint8 weekday;
    }

    uint constant DAY_IN_SECONDS = 86400;
    uint constant YEAR_IN_SECONDS = 31536000;
    uint constant LEAP_YEAR_IN_SECONDS = 31622400;

    uint constant HOUR_IN_SECONDS = 3600;
    uint constant MINUTE_IN_SECONDS = 60;

    uint16 constant ORIGIN_YEAR = 1970;

    function isLeapYear(uint16 year) internal pure returns (bool) {
        if (year % 4 != 0) {
            return false;
        }
        if (year % 100 != 0) {
            return true;
        }
        if (year % 400 != 0) {
            return false;
        }
        return true;
    }

    function leapYearsBefore(uint year) internal pure returns (uint) {
        year -= 1;
        return year / 4 - year / 100 + year / 400;
    }

    function getDaysInMonth(uint8 month, uint16 year) internal pure returns (uint8) {
        if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) {
            return 31;
        }
        else if (month == 4 || month == 6 || month == 9 || month == 11) {
            return 30;
        }
        else if (isLeapYear(year)) {
            return 29;
        }
        else {
            return 28;
        }
    }

    function parseTimestamp(uint timestamp) internal pure returns (_DateTime dt) {
        uint secondsAccountedFor = 0;
        uint buf;
        uint8 i;

        // Year
        dt.year = getYear(timestamp);
        buf = leapYearsBefore(dt.year) - leapYearsBefore(ORIGIN_YEAR);

        secondsAccountedFor += LEAP_YEAR_IN_SECONDS * buf;
        secondsAccountedFor += YEAR_IN_SECONDS * (dt.year - ORIGIN_YEAR - buf);

        // Month
        uint secondsInMonth;
        for (i = 1; i <= 12; i++) {
            secondsInMonth = DAY_IN_SECONDS * getDaysInMonth(i, dt.year);
            if (secondsInMonth + secondsAccountedFor > timestamp) {
                dt.month = i;
                break;
            }
            secondsAccountedFor += secondsInMonth;
        }

        // Day
        for (i = 1; i <= getDaysInMonth(dt.month, dt.year); i++) {
            if (DAY_IN_SECONDS + secondsAccountedFor > timestamp) {
                dt.day = i;
                break;
            }
            secondsAccountedFor += DAY_IN_SECONDS;
        }
    }

    function getYear(uint timestamp) internal pure returns (uint16) {
        uint secondsAccountedFor = 0;
        uint16 year;
        uint numLeapYears;

        // Year
        year = uint16(ORIGIN_YEAR + timestamp / YEAR_IN_SECONDS);
        numLeapYears = leapYearsBefore(year) - leapYearsBefore(ORIGIN_YEAR);

        secondsAccountedFor += LEAP_YEAR_IN_SECONDS * numLeapYears;
        secondsAccountedFor += YEAR_IN_SECONDS * (year - ORIGIN_YEAR - numLeapYears);

        while (secondsAccountedFor > timestamp) {
            if (isLeapYear(uint16(year - 1))) {
                secondsAccountedFor -= LEAP_YEAR_IN_SECONDS;
            }
            else {
                secondsAccountedFor -= YEAR_IN_SECONDS;
            }
            year -= 1;
        }
        return year;
    }

    function getMonth(uint timestamp) internal pure returns (uint8) {
        return parseTimestamp(timestamp).month;
    }
}
// </ORACLIZE_API>
function closeContract() public adminOnly { closed = true; }

/*
 * admin ===================================================================
 */