import random
import json

# ---------- PARAMETERS (change these) ----------
NUM_TRIALS = 10000                      # how many times to repeat the experiment per shuffle-count
SHUFFLE_COUNTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]  # test these riffle-shuffle counts
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

UNDERCUT_FRACTION = 1 / 3              # roughly what fraction is cut from the bottom to the top
FRACTION_WOBBLE = 0.03                 # how much that fraction varies randomly each shuffle

# 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>_undercut_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 undercut(deck):
    """Take roughly the bottom third of the deck and place it on top."""
    n = len(deck)
    f = UNDERCUT_FRACTION + random.uniform(-FRACTION_WOBBLE, FRACTION_WOBBLE)
    f = max(0.05, min(0.95, f))

    bottom_size = round(n * f)
    bottom_size = max(0, min(n, bottom_size))

    top_part = deck[:n - bottom_size]
    bottom_part = deck[n - bottom_size:]
    return bottom_part + top_part  # bottom third moved to the top


def clump_riffle(deck, min_alt=MIN_ALTERNATIONS, max_alt=MAX_ALTERNATIONS):
    """Standard imperfect-split, alternation-based riffle (same mechanic as the base method)."""
    n = len(deck)
    cut = n // 2 + random.randint(-3, 3)   # imperfect split near the middle
    cut = max(0, min(n, cut))
    left, right = deck[:cut], deck[cut:]

    num_alternations = random.randint(min_alt, max_alt)

    left_segments_count = (num_alternations + 1) // 2
    right_segments_count = num_alternations // 2
    if random.random() < 0.5:
        left_segments_count, right_segments_count = right_segments_count, left_segments_count

    left_sizes = split_pile(len(left), max(left_segments_count, 1))
    right_sizes = split_pile(len(right), max(right_segments_count, 1))

    result = []
    li = ri = 0
    current = random.randint(0, 1)

    while li < len(left_sizes) or ri < len(right_sizes):
        if current == 0 and li < len(left_sizes):
            size = left_sizes[li]
            result.extend(left[:size])
            left = left[size:]
            li += 1
        elif current == 1 and ri < len(right_sizes):
            size = right_sizes[ri]
            result.extend(right[:size])
            right = right[size:]
            ri += 1
        current = 1 - current

    result.extend(left)
    result.extend(right)
    return result


def undercut_shuffle(deck):
    cut_deck = undercut(deck)
    return clump_riffle(cut_deck)


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_shuffles in SHUFFLE_COUNTS:
        rising_seq_counts = []
        for _ in range(NUM_TRIALS):
            deck = list(deck_template)
            for _ in range(num_shuffles):
                deck = undercut_shuffle(deck)
            rising_seq_counts.append(count_rising_sequences(deck))

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

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

    output_file = f"{deck_name}_undercut_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']} shuffles -> 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()