import random
import json

# ---------- PARAMETERS (change these) ----------
NUM_TRIALS = 10000                      # how many times to repeat the experiment per round-count
ROUND_COUNTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]  # test these numbers of rounds

MIN_ALTERNATIONS = 10                  # min number of times the hands switch sides during one riffle
MAX_ALTERNATIONS = 20                  # max number of times the hands switch sides during one riffle

FIRST_ASIDE_FRACTION = 1 / 3           # fraction pulled aside in round 1 (the initial third)
FRACTION_WOBBLE = 0.03                 # how much that fraction (and later halves) vary randomly
HALF_WOBBLE_CARDS = 2                  # +/- cards of imperfection when halving a pile in later rounds

# deck definitions: name -> (ordered "new deck" list, expected rising sequences if truly random)
DECKS = {
    "Standard": {
        "deck": list(range(52)),               # 52 cards, all unique, ace..king order
        "target_random_avg": 26.5,             # (n+1)/2 for n=52
    },
    "Skibbo": {
        "deck": [v for v in range(1, 13) for _ in range(5)],  # 60 cards, values 1-12, 5 copies each
        "target_random_avg": 30.5,              # (n+1)/2 for n=60
    },
}
# one output json per deck, named "<DeckName>_carousel_shuffle_results.json"
# ------------------------------------------------


def split_pile(pile_size, num_segments):
    """Break pile_size cards into num_segments non-negative chunks (some can be 0),
    using random cut points so any distribution (incl. lumpy or empty) is possible."""
    if num_segments <= 1:
        return [pile_size]
    cuts = sorted(random.uniform(0, pile_size) for _ in range(num_segments - 1))
    cuts = [0] + cuts + [pile_size]
    return [round(cuts[i + 1]) - round(cuts[i]) for i in range(num_segments)]


def extract_middle_fraction(deck, fraction):
    """Pull a middle chunk of roughly `fraction` of the deck out.
    Returns (middle_chunk, outer_chunk) where outer_chunk = top + bottom joined."""
    n = len(deck)
    f = fraction + random.uniform(-FRACTION_WOBBLE, FRACTION_WOBBLE)
    f = max(0.05, min(0.95, f))

    middle_size = round(n * f)
    remaining = n - middle_size
    top_size = remaining // 2 + random.randint(-2, 2)
    top_size = max(0, min(remaining, top_size))

    top = deck[:top_size]
    middle = deck[top_size:top_size + middle_size]
    bottom = deck[top_size + middle_size:]

    return middle, top + bottom


def split_in_half_imperfect(pile):
    """Split a pile into two roughly-equal, imperfect halves."""
    n = len(pile)
    cut = n // 2 + random.randint(-HALF_WOBBLE_CARDS, HALF_WOBBLE_CARDS)
    cut = max(0, min(n, cut))
    return pile[:cut], pile[cut:]


def riffle_two_piles(pile_a, pile_b, min_alt=MIN_ALTERNATIONS, max_alt=MAX_ALTERNATIONS):
    """Alternation-based riffle mixing of two already-formed piles."""
    num_alternations = random.randint(min_alt, max_alt)

    a_segments_count = (num_alternations + 1) // 2
    b_segments_count = num_alternations // 2
    if random.random() < 0.5:
        a_segments_count, b_segments_count = b_segments_count, a_segments_count

    a_sizes = split_pile(len(pile_a), max(a_segments_count, 1))
    b_sizes = split_pile(len(pile_b), max(b_segments_count, 1))

    result = []
    ai = bi = 0
    current = random.randint(0, 1)

    while ai < len(a_sizes) or bi < len(b_sizes):
        if current == 0 and ai < len(a_sizes):
            size = a_sizes[ai]
            result.extend(pile_a[:size])
            pile_a = pile_a[size:]
            ai += 1
        elif current == 1 and bi < len(b_sizes):
            size = b_sizes[bi]
            result.extend(pile_b[:size])
            pile_b = pile_b[size:]
            bi += 1
        current = 1 - current

    result.extend(pile_a)
    result.extend(pile_b)
    return result


def carousel_shuffle(deck, num_rounds):
    """Round 1: pull middle third aside, riffle the remaining two-thirds (split in half).
    Round 2+: split the shuffled pile in half, set one half aside (replacing the old aside),
    and riffle the other half together with the previous aside pile."""
    aside, remaining = extract_middle_fraction(deck, FIRST_ASIDE_FRACTION)
    r1, r2 = split_in_half_imperfect(remaining)
    shuffled = riffle_two_piles(r1, r2)

    for _ in range(num_rounds - 1):
        h1, h2 = split_in_half_imperfect(shuffled)
        if random.random() < 0.5:  # which half gets set aside is not fixed
            h1, h2 = h2, h1
        new_aside = h1
        shuffled = riffle_two_piles(h2, aside)
        aside = new_aside

    return aside + shuffled  # combine reserve pile with the actively shuffled pile


def count_rising_sequences(deck):
    # counts runs of cards still in ascending original order
    count = 1
    for i in range(1, len(deck)):
        if deck[i] < deck[i - 1]:
            count += 1
    return count


def run_experiment_for_deck(deck_name, deck_template, target_random_avg):
    all_results = {}

    for num_rounds in ROUND_COUNTS:
        rising_seq_counts = []
        for _ in range(NUM_TRIALS):
            deck = list(deck_template)
            final_deck = carousel_shuffle(deck, num_rounds)
            rising_seq_counts.append(count_rising_sequences(final_deck))

        avg = sum(rising_seq_counts) / len(rising_seq_counts)
        all_results[str(num_rounds)] = {
            "num_shuffles": num_rounds,
            "avg_rising_sequences": avg,
            "target_random_avg": target_random_avg,
            "raw_counts": rising_seq_counts,
        }

    settings = {
        "METHOD": "Carousel Shuffle",
        "NUM_TRIALS": NUM_TRIALS,
        "MIN_ALTERNATIONS": MIN_ALTERNATIONS,
        "MAX_ALTERNATIONS": MAX_ALTERNATIONS,
        "FIRST_ASIDE_FRACTION": FIRST_ASIDE_FRACTION,
        "FRACTION_WOBBLE": FRACTION_WOBBLE,
        "DECK": deck_name,
        "DECK_SIZE": len(deck_template),
    }

    output_file = f"{deck_name}_carousel_shuffle_results.json"
    with open(output_file, "w") as f:
        json.dump({"settings": settings, "results": all_results}, f, indent=2)

    print(f"Done. Results written to {output_file}\n")
    for k, v in all_results.items():
        print(f"{v['num_shuffles']} rounds -> avg rising sequences: {v['avg_rising_sequences']:.2f} "
              f"(target: {target_random_avg})")
    print()


def run_experiment():
    for deck_name, deck_info in DECKS.items():
        run_experiment_for_deck(deck_name, deck_info["deck"], deck_info["target_random_avg"])


if __name__ == "__main__":
    run_experiment()