#!/usr/bin/python
#
# Perform stepwise refinement (aka bisection) to pinpoint the times at
# which the build was broken or fixed.  Also, do additional builds to
# fill in long gaps between existing builds.
#
# Copyright (c) 2009-2021 Andreas Gustafsson.  All rights reserved.
# Please refer to the file COPYRIGHT for detailed copyright information.
#

from __future__ import print_function

import math
import sys
import os
import optparse

from bracket import *
from utils import adjacent_pairs

def refine(test_in_background = False):
    # Determine the oldest timestamp for which we will do
    # new builds.  This is taken from build_from if defined,
    # or otherwise, report_from_year and report_from_month.
    begin_rcsdate = config.get('build_from')
    if not begin_rcsdate:
        begin_rcsdate = '%04d.%02d.01.00.00.00' % \
        (int(config['report_from_year']), int(config['report_from_month']))

    begin_ts = rcs2ts(begin_rcsdate)

    # Fill in any build-less intervals at least this many seconds long
    max_interval_str = config.get('max_interval')
    if max_interval_str:
        max_interval = int(max_interval_str)
    else:
        max_interval = 3600 * 16

    use_current_repository()

    print("get build dates")
    build_dates = [ts for ts in existing_build_dates_at_commits()
        if ts >= begin_ts and \
            get_cached_status_if_any(ts, 'build_status') is not None]

    # Find long intervals with no builds
    print("find long intervals")
    est_interval_builds = 0
    # List of (timestamp pair, reason string) tuples
    need_sample = []

    for tsp in adjacent_pairs(build_dates):
        diff = tsp[1] - tsp[0]
        if diff > max_interval and ts2cno(tsp[1]) - ts2cno(tsp[0]) > 1:
            need_sample.append((tsp, 'long_interval %.0fh' % (diff / 3600)))
            est_interval_builds += diff / max_interval

    # Find intervals where something changed
    print("find changes")
    # Another list of (timestamp pair, reason string) tuples
    need_refinement = []

    for tsp in adjacent_pairs(build_dates):
        reason = any_status_changed(tsp)
        if reason:
            ncommits = ts2cno(tsp[1]) - ts2cno(tsp[0])
            if ncommits > 1:
                need_refinement.append((tsp, reason))

    # Estimate the number of builds needed

    def log2(x):
        return math.log(x) / math.log(2)

    est_change_builds = 0.0
    for tsp, reason in need_refinement:
        est_change_builds += log2(ts2cno(tsp[1]) - ts2cno(tsp[0]))

    print("number of intervals needing sample: %d, needing %.1f builds" % \
        (len(need_sample), est_interval_builds))
    print("number of breaks needing refinement: %d, needing %.1f builds" % \
        (len(need_refinement), est_change_builds))
    print("estimated total number of builds required: %.1f" % \
        (est_interval_builds + est_change_builds))

    need_refinement = need_sample + need_refinement

    # Optimization: don't regenerate reports if no builds are needed
    if len(need_refinement) == 0:
        sys.exit(0)

    # Determine which intervals to refine first.

    def largest_first(ch):
        tsp, reason = ch
        return ts2cno(tsp[1]) - ts2cno(tsp[0])

    def newest_first(ch):
        tsp, reason = ch
        return tsp[1]

    order_str = config.get('refine_order', 'newest_first')
    if order_str == 'largest_first':
        order = largest_first
    elif order_str == 'newest_first':
        order = newest_first
    else:
        raise RuntimeError("unknown refine_order: %s" % order_str)

    need_refinement.sort(key = order)

    #for r in need_refinement:
    #    tsp, reason = r
    #    print([ts2rcs(ts) for ts in tsp], reason)

    def midpoint(a, b):
        return (a + b) // 2

    while len(need_refinement) > 0:
        last = need_refinement.pop()
        tsp, reason = last
        c0 = ts2cno(tsp[0])
        c1 = ts2cno(tsp[1])
        print("testing between", commit2human(c0), \
            "and", commit2human(c1), \
            "(distance %i)" % (c1 - c0), \
            "because", reason)
        c = find_hint_between(c0, c1)
        if c is not None:
            print("taking hint")
        else:
            print("no applicable hint, using midpoint")
            c = midpoint(c0, c1)
        print("testing at", commit2human(c))
        ts = cno2ts(c)
        try:
            build_and_test(ts, test_in_background = test_in_background)
        except PrerequisiteFailed as e:
            print("could not run test %s: %s", (ts2rcs(ts), str(e)))
        break

def refine_main(argv1):
    parser = optparse.OptionParser()
    parser.add_option("--test-in-background", action="store_true")
    add_bracket_options(parser)
    (options, args) = parser.parse_args(argv1)
    refine(options.test_in_background)
