# Pizza Clicker - the game code
#
# How it works:
#   1. Click the pizza to earn money.
#   2. Buy kitchen tools (more money per click) and helpers (money every second).
#   3. Catch the golden pizza when it shows up for a big bonus!

import asyncio
import json
import random
import time

from pyscript import document, fetch, window, when

import effects
from upgrades import TOOLS, HELPERS


# ---------- Settings ----------

PRICE_GROWTH = 1.15          # each time you buy something, it costs 15% more
MAX_CLICKS_PER_SECOND = 16   # faster than this is not a human
PUNISH_SECONDS = 10          # how long the pizza is locked if we catch an auto clicker
SAVE_KEY = "pizza-clicker-save"


# ---------- Game state (everything we need to remember) ----------

money = 0
total_earned = 0
owned = {}  # how many of each upgrade you have, like {"rolling_pin": 3}


# ---------- Math helpers ----------

def how_many(upgrade):
    return owned.get(upgrade["icon"], 0)


def price(upgrade):
    return int(upgrade["cost"] * PRICE_GROWTH ** how_many(upgrade))


def money_per_click():
    return 1 + sum(tool["per_click"] * how_many(tool) for tool in TOOLS)


def money_per_second():
    return sum(helper["per_second"] * how_many(helper) for helper in HELPERS)


def earn(amount):
    global money, total_earned
    money += amount
    total_earned += amount


def nice_number(number):
    """Turns 1234567 into "1.23 million" so big numbers are easy to read."""
    big_words = [
        (1e18, "quintillion"),
        (1e15, "quadrillion"),
        (1e12, "trillion"),
        (1e9, "billion"),
        (1e6, "million"),
    ]
    for size, word in big_words:
        if number >= size:
            return f"{number / size:.2f} {word}"
    return f"{int(number):,}"


# ---------- Anti auto clicker ----------
#
# We catch auto clickers in 3 ways:
#   1. Fake clicks made by code are not "trusted" by the browser.
#   2. Nobody can click more than 16 times in one second.
#   3. Robots click with the exact same gap every time. Humans are a bit messy.

click_times = []   # when the last 20 clicks happened
locked_until = 0   # the pizza can't be clicked until this time


def is_human_click(event):
    global locked_until

    if not event.isTrusted:
        return False

    now = time.time()
    if now < locked_until:
        return False

    click_times.append(now)
    del click_times[:-20]  # only keep the last 20 clicks

    clicks_last_second = [t for t in click_times if now - t < 1]
    too_fast = len(clicks_last_second) > MAX_CLICKS_PER_SECOND

    gaps = [later - earlier for earlier, later in zip(click_times, click_times[1:])]
    too_perfect = len(gaps) == 19 and max(gaps) - min(gaps) < 0.015

    if too_fast or too_perfect:
        locked_until = now + PUNISH_SECONDS
        click_times.clear()
        return False

    return True


# ---------- Clicking the pizza ----------

@when("click", "#pizza")
def click_pizza(event):
    if not is_human_click(event):
        return
    amount = money_per_click()
    earn(amount)
    effects.popup(f"+${nice_number(amount)}", event.clientX, event.clientY)
    effects.crumbs(event.clientX, event.clientY)
    effects.pop(document.querySelector("#money"))


# ---------- The shop ----------

def build_shop():
    """Makes one card for every upgrade. This runs once when the game starts."""
    for tool in TOOLS:
        add_card("#tools", tool, f"+{nice_number(tool['per_click'])} per click")
    for helper in HELPERS:
        add_card("#helpers", helper, f"+{nice_number(helper['per_second'])} per second")


def add_card(list_id, upgrade, description):
    card = document.createElement("button")
    card.className = "card"
    card.id = upgrade["icon"]
    card.innerHTML = f"""
        <img src="images/icons/{upgrade['icon']}.png" alt="">
        <span class="info">
            <b>{upgrade['name']}</b>
            <small>{description}</small>
            <span class="price"></span>
        </span>
        <span class="count"></span>
    """
    document.querySelector(list_id).append(card)


@when("click", "#shop")
def click_shop(event):
    card = event.target.closest(".card")
    if card:
        buy(card.id)


def is_unlocked(upgrade):
    """Upgrades stay secret until you've earned half their price. It keeps the shop tidy."""
    return upgrade["cost"] <= 50 or total_earned >= upgrade["cost"] / 2 or how_many(upgrade) > 0


def buy(icon):
    global money
    upgrade = next(u for u in TOOLS + HELPERS if u["icon"] == icon)
    cost = price(upgrade)
    if money < cost:
        return
    money -= cost
    owned[icon] = how_many(upgrade) + 1
    effects.pop(document.getElementById(icon))
    save_game()


# ---------- Golden pizza ----------

golden_shows_at = time.time() + random.randint(30, 90)
golden_hides_at = 0


def update_golden_pizza():
    global golden_shows_at, golden_hides_at
    golden = document.querySelector("#golden-pizza")
    now = time.time()

    if golden.hidden and now > golden_shows_at:
        golden.style.left = f"{random.randint(5, 80)}vw"
        golden.style.top = f"{random.randint(10, 75)}vh"
        golden.hidden = False
        golden_hides_at = now + 8  # you have 8 seconds to catch it

    if not golden.hidden and now > golden_hides_at:
        hide_golden_pizza()


def hide_golden_pizza():
    global golden_shows_at
    document.querySelector("#golden-pizza").hidden = True
    golden_shows_at = time.time() + random.randint(60, 120)


@when("click", "#golden-pizza")
def click_golden_pizza(event):
    if not event.isTrusted:
        return
    bonus = 100 + money_per_second() * 60 + money_per_click() * 30
    earn(bonus)
    effects.popup(f"GOLDEN PIZZA! +${nice_number(bonus)}", event.clientX, event.clientY)
    effects.crumbs(event.clientX, event.clientY)
    hide_golden_pizza()


# ---------- Drawing the screen ----------

def update_screen():
    document.querySelector("#money").innerText = f"${nice_number(money)}"
    document.querySelector("#per-click").innerText = f"${nice_number(money_per_click())} per click"
    document.querySelector("#per-second").innerText = f"${nice_number(money_per_second())} per second"

    locked = time.time() < locked_until
    document.querySelector("#warning").hidden = not locked

    for upgrade in TOOLS + HELPERS:
        card = document.getElementById(upgrade["icon"])
        cost = price(upgrade)
        card.hidden = not is_unlocked(upgrade)
        if card.hidden:
            continue  # no need to update cards you can't see
        card.disabled = money < cost
        card.querySelector(".price").innerText = f"${nice_number(cost)}"
        card.querySelector(".count").innerText = how_many(upgrade) or ""


# ---------- Little animations ----------

def play_animations():
    # More money per second = more pizzas falling from the sky
    if money_per_second() > 0:
        digits = len(str(int(money_per_second())))  # 5 -> 1 digit, 5000 -> 4 digits
        if random.random() < digits / 50:
            effects.pizza_rain()

    # Every few seconds, one of your helpers runs (or drives) across the floor
    movers = effects.RUNNERS + effects.DRIVERS
    my_movers = [helper["icon"] for helper in HELPERS if how_many(helper) > 0 and helper["icon"] in movers]
    if my_movers and random.random() < 0.03:
        effects.move_across(random.choice(my_movers))


# ---------- Saving and loading ----------
#
# Your game is always saved in this browser (localStorage).
# If you're logged in, it's ALSO saved to your account on the server,
# so you can keep playing on another computer or phone.

username = None  # your account name, or None if you're not logged in


def save_data():
    return {"money": money, "total_earned": total_earned, "owned": owned}


def use_save(data):
    global money, total_earned, owned
    money = data["money"]
    total_earned = data["total_earned"]
    owned = data["owned"]


def save_game():
    window.localStorage.setItem(SAVE_KEY, json.dumps(save_data()))


def load_game():
    saved = window.localStorage.getItem(SAVE_KEY)
    if saved:
        use_save(json.loads(saved))


async def load_online_save():
    """Asks the server who you are. If you have an online save, use it."""
    global username
    try:
        data = await fetch("/api/save").json()
    except Exception:
        return  # no server (or no internet), so just play offline

    username = data["username"]
    if data["save"]:
        use_save(data["save"])
    show_account()


async def upload_save():
    if not username:
        return
    try:
        await fetch(
            "/api/save",
            method="POST",
            headers={"Content-Type": "application/json"},
            body=json.dumps(save_data()),
        )
    except Exception:
        pass  # no internet right now. We'll try again in 5 seconds.


def show_account():
    account = document.querySelector("#account")
    if username:
        account.innerHTML = f"👤 {username} · <a href='/logout'>Log out</a>"
    else:
        account.innerHTML = "<a href='/login'>Log in to save your progress</a>"


@when("click", "#reset")
def reset_game(event):
    global money, total_earned, owned
    if window.confirm("Start over? You will lose everything!"):
        money = 0
        total_earned = 0
        owned = {}
        save_game()


# ---------- Game loop (runs about 10 times every second) ----------

async def game_loop():
    last_time = time.time()
    last_save = time.time()
    while True:
        # Browsers slow down tabs you aren't looking at, so we check the
        # real clock to see how much time passed, and pay you for all of it.
        now = time.time()
        earn(money_per_second() * (now - last_time))
        last_time = now

        update_golden_pizza()
        update_screen()
        play_animations()

        if now - last_save > 5:  # save every 5 seconds
            save_game()
            await upload_save()
            last_save = now

        await asyncio.sleep(0.1)


# ---------- Start the game! ----------

async def start_game():
    load_game()
    await load_online_save()
    build_shop()
    document.querySelector("#loading").remove()
    await game_loop()


asyncio.ensure_future(start_game())
