#!/usr/bin/env python3
"""
hestia_loan.py: Can Hestia pay off the Hestia Knife?

Recording mode plays the scenes from the script one by one.
Sandbox mode lets you try your own numbers.

    python hestia_loan.py                    # recording mode, Enter = next scene
    python hestia_loan.py --auto 4           # auto-advance every 4 seconds
    python hestia_loan.py --fast             # no typing animation
    python hestia_loan.py --wage 5000 --tax 20000 --days-off 2   # sandbox

Canon numbers are marked below. Hestia's wage and the Guild tax are NOT canon,
which is why they are sliders.
"""

import argparse
import os
import sys
import time

# ---------------------------------------------------------------------------
# Canon numbers (sources: see the source table in the script)
# ---------------------------------------------------------------------------
PRINCIPAL = 200_000_000        # Hestia Knife, valis
TERM_YEARS = 35                # item sheet in the light novel
PAYMENTS = 420                 # monthly payments, item sheet
PARTY_OF_FIVE_DAILY = 25_000   # Level 1 party of five, per day
ADVENTURERS_IN_PARTY = 5
BELL_LV2_DAILY = 36_000        # Bell's half of 72,000 (short story "Blue Twilight")


# ---------------------------------------------------------------------------
# Math
# ---------------------------------------------------------------------------
def monthly_payment():
    return PRINCIPAL / PAYMENTS


def daily_needed(days_per_month=30):
    return monthly_payment() / days_per_month


def per_adventurer_daily():
    return PARTY_OF_FIVE_DAILY / ADVENTURERS_IN_PARTY


def work_days_per_year(days_off_per_month):
    return 365 - 12 * days_off_per_month


def payoff_years(daily_wage, monthly_tax=0, days_off_per_month=0):
    """Years until the knife is paid off. None means never."""
    yearly = daily_wage * work_days_per_year(days_off_per_month) - monthly_tax * 12
    if yearly <= 0:
        return None
    return PRINCIPAL / yearly


def days_per_payment(daily_income):
    return monthly_payment() / daily_income


# ---------------------------------------------------------------------------
# Terminal output
# ---------------------------------------------------------------------------
if os.name == "nt":
    os.system("")  # enables ANSI colors in the Windows terminal
try:
    sys.stdout.reconfigure(encoding="utf-8")
except AttributeError:
    pass

RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
GOLD = "\033[38;2;242;193;78m"
BLUE = "\033[38;2;76;123;244m"
RED = "\033[38;2;224;68;58m"
WHITE = "\033[38;2;243;244;248m"

FAST = False


def valis(n):
    return f"{n:,.0f}"


def clear():
    print("\033[2J\033[H", end="", flush=True)


def type_out(text, delay=0.018, end="\n"):
    """Prints text like it is being typed."""
    if FAST:
        print(text, end=end, flush=True)
        return
    i = 0
    while i < len(text):
        # Print ANSI color codes in one go, so they do not get typed out
        if text[i] == "\033":
            j = text.index("m", i) + 1
            print(text[i:j], end="", flush=True)
            i = j
            continue
        print(text[i], end="", flush=True)
        time.sleep(delay)
        i += 1
    print(end=end, flush=True)


def pause(seconds=0.5):
    if not FAST:
        time.sleep(seconds)


def bar(value, maximum, width=30):
    filled = round(width * min(value, maximum) / maximum)
    return BLUE + "█" * filled + DIM + "░" * (width - filled) + RESET


def slider(label, start, end, maximum, suffix=" valis"):
    """Animates a slider from start to end on one line."""
    frames = 1 if FAST else 36
    for f in range(1, frames + 1):
        v = start + (end - start) * f / frames
        line = f"  {label:<24} [{bar(v, maximum)}]  {GOLD}{valis(v)}{suffix}{RESET}"
        print("\r" + line + "   ", end="", flush=True)
        if not FAST:
            time.sleep(0.03)
    print()


def result_years(years):
    if years is None:
        type_out(f"  {RED}{BOLD}Paid off in: never{RESET}  {DIM}(OverflowError: debt){RESET}")
        return
    color = RED if years > TERM_YEARS else GOLD
    type_out(f"  {color}{BOLD}Paid off in: {years:.1f} years{RESET}  {DIM}(contract: {TERM_YEARS} years){RESET}")


def prompt_line(cmd):
    type_out(f"{DIM}${RESET} {WHITE}{cmd}{RESET}", delay=0.04)
    pause(0.4)


# ---------------------------------------------------------------------------
# Scenes (in script order)
# ---------------------------------------------------------------------------
def scene_loan():
    prompt_line("python hestia_loan.py")
    type_out(f"\n  {BOLD}HESTIA KNIFE: LOAN SUMMARY{RESET}\n")
    type_out(f"  Principal          {GOLD}{valis(PRINCIPAL)} valis{RESET}")
    type_out(f"  Term               {TERM_YEARS} years")
    type_out(f"  Payments           {PAYMENTS}")
    type_out(f"  Collateral         {DIM}love (unverified){RESET}\n")
    pause()
    type_out(f"  Monthly payment    {RED}{BOLD}{valis(monthly_payment())} valis{RESET}")
    type_out(f"  Per day            {RED}{BOLD}~{valis(daily_needed())} valis{RESET}")


def scene_benchmark():
    need = daily_needed()
    per_adv = per_adventurer_daily()
    ratio = need / per_adv
    type_out(f"\n  {BOLD}BENCHMARK{RESET}\n")
    type_out(f"  Level 1 party of five, per day   {GOLD}{valis(PARTY_OF_FIVE_DAILY)} valis{RESET}")
    type_out(f"  One adventurer, per day          {GOLD}{valis(per_adv)} valis{RESET}\n")
    pause()

    # One segment of 10 blocks = one adventurer's daily income
    blocks = round(ratio * 10)
    segments = []
    for i in range(0, blocks, 10):
        segments.append("█" * min(10, blocks - i))
    drawn = f"{DIM}|{RESET}".join(BLUE + s + RESET for s in segments)
    type_out(f"  Hestia needs   {drawn}")
    type_out(f"  {DIM}               one segment = one adventurer{RESET}\n")
    type_out(f"  {RED}{BOLD}= {ratio:.1f} adventurers. From behind a shop counter.{RESET}")


def scene_sliders():
    type_out(f"\n  {BOLD}UNKNOWN VALUES{RESET}  {DIM}(not in canon, so they are sliders){RESET}\n")
    type_out(f"  {'Hestia wage per day':<24} [{bar(0, 10_000)}]  {DIM}unknown{RESET}")
    type_out(f"  {'Guild tax per month':<24} [{bar(0, 100_000)}]  {DIM}unknown{RESET}")
    type_out(f"  {'Days off per month':<24} [{bar(0, 8)}]  {DIM}unknown{RESET}")


def scene_generous():
    type_out(f"\n  {BOLD}ABSURDLY GENEROUS MODE{RESET}  {DIM}(paid like an adventurer, no days off, no rent, no food){RESET}\n")
    slider("Hestia wage per day", 0, 5_000, 10_000)
    slider("Guild tax per month", 0, 0, 100_000)
    slider("Days off per month", 0, 0, 8, suffix="")
    pause()
    print()
    result_years(payoff_years(5_000))


def scene_two_jobs():
    type_out(f"\n  {BOLD}TWO JOBS{RESET}\n")
    slider("Hestia wage per day", 5_000, 10_000, 10_000)
    pause()
    print()
    result_years(payoff_years(10_000))


def scene_bell():
    prompt_line("python hestia_loan.py --payer bell --level 2")
    type_out(f"\n  {BOLD}PAYER: BELL CRANEL, LEVEL 2{RESET}\n")
    type_out(f"  Party of three, floor 12, per day   {GOLD}72,000 valis{RESET}")
    type_out(f"  Bell's half                         {GOLD}{valis(BELL_LV2_DAILY)} valis{RESET}\n")
    pause()
    days = days_per_payment(BELL_LV2_DAILY)
    type_out(f"  {BLUE}{BOLD}{days:.1f} days per monthly payment{RESET}")
    pause()
    type_out(f"\n  {DIM}Status: payment refused by debtor. \"The debt is mine.\"{RESET}")


SCENES = [scene_loan, scene_benchmark, scene_sliders, scene_generous, scene_two_jobs, scene_bell]


# ---------------------------------------------------------------------------
# Modes
# ---------------------------------------------------------------------------
def recording_mode(auto):
    for i, scene in enumerate(SCENES):
        clear()
        print()
        scene()
        if i == len(SCENES) - 1:
            break
        if auto is not None:
            time.sleep(auto)
        else:
            input(f"\n{DIM}  [Enter]{RESET}")
    print()


def sandbox_mode(wage, tax, days_off):
    print(f"\n  Monthly payment    {valis(monthly_payment())} valis")
    print(f"  Wage per day       {valis(wage)} valis")
    print(f"  Guild tax/month    {valis(tax)} valis")
    print(f"  Days off/month     {days_off}\n")
    result_years(payoff_years(wage, tax, days_off))
    print()


def main():
    global FAST
    p = argparse.ArgumentParser(description="Can Hestia pay off the Hestia Knife?")
    p.add_argument("--fast", action="store_true", help="no typing animation")
    p.add_argument("--auto", type=float, help="auto-advance scenes every N seconds")
    p.add_argument("--wage", type=float, help="sandbox: Hestia's wage per day")
    p.add_argument("--tax", type=float, default=0, help="sandbox: Guild tax per month")
    p.add_argument("--days-off", type=int, default=0, help="sandbox: days off per month")
    args = p.parse_args()
    FAST = args.fast

    if args.wage is not None:
        sandbox_mode(args.wage, args.tax, args.days_off)
    else:
        recording_mode(args.auto)


if __name__ == "__main__":
    main()
