# hlst.py ham license study tool 9/2025 by akbiocca w6akb with chatgpt

# run with batch file run_hlst.bat; set path to python; example follows
#
# @echo off
# REM Auto-generated batch file to run Streamlit app
# cd /d %~dp0
# "C:\Users\<user>\AppData\Local\Programs\Thonny\python.exe" -m streamlit run "hlst.py"
# pause

import re, random, secrets, os
import streamlit as st          # requires streamlit library
from pathlib import Path

LOG_FILE = "hlstLog.txt"

# ---------- Utilities ----------
def load_logfile(filename):
    "load the logfile into last result and miss count dicts"
    result, resm = {}, {}  # last result, result miss count
    try:
        with open(filename, "r", encoding="utf-8") as f:
            for line in f:
                parts = line.strip().split()
                if len(parts) != 2:
                    continue
                try:
                    n = int(parts[0]); qid = parts[1]
                    if n == 1 and result.get(qid,2) == 3:  # prevent correct over removed
                        result[qid] = 3
                    else:
                        result[qid] = n                    # most recent answer
                    if n == 0:
                        resm[qid] = resm.get(qid, 0) + 1   # count misses
                except ValueError:
                    continue
    except FileNotFoundError:
        pass
    return result, resm

def append_line(filename, line):
    " logfile append entry"
    with open(filename, "a", encoding="utf-8") as f:
        f.write(line.rstrip("\n") + "\n")

def load_questions(txt_file, log_data, log_data_m, mode, section):
    "load question pool"
    text = Path(txt_file).read_text(encoding="utf-8")
    pattern = re.compile(
        r"(?P<ID>[EGT]\d+[A-Z]\d+)\s+\((?P<correct>[A-D])\)\s+(?P<question>.*?)\n"
        r"A\.\s*(?P<A>.*?)\n"
        r"B\.\s*(?P<B>.*?)\n"
        r"C\.\s*(?P<C>.*?)\n"
        r"D\.\s*(?P<D>.*?)\n",
        re.DOTALL
    )

    questions = []
    for m in pattern.finditer(text):
        qid = m.group("ID").strip()               # question id
        ra = log_data.get(qid, 2)                 # most recent answer
                                                  # 0 incorrect, 1 correct, 2 new, 3 reject
        mc = log_data_m.get(qid, 0)               # incorrect count

        if section != "*" and qid[1] != section:  # section filter
            continue
        if mode == "N" and (ra in (0, 1, 3)):     # new filter
            continue
        if mode == "R" and (ra in (1, 2, 3)):     # recently missed filter
            continue
        if mode == "O" and (ra in (1, 3)):        # new or recently missed filter
            continue
        if mode == "E" and (mc == 0):             # ever missed filter
            continue
        if mode == "T" and (mc == 0 or ra == 3):  # trouble filter (not accurate)
            continue

        questions.append({                        # add to question list
            "ID": qid,
            "Question": re.sub(r"\s+", " ", m.group("question")).strip(),
            "A": m.group("A").strip(),
            "B": m.group("B").strip(),
            "C": m.group("C").strip(),
            "D": m.group("D").strip(),
            "Correct": m.group("correct").strip(),
        })

    rng = random.Random(secrets.randbits(64))     # shuffle questions
    rng.shuffle(questions)
    return questions

# ---------- App ----------
st.set_page_config(page_title="Ham License Study Tool", layout="wide")
st.title("📡 Ham License Study Tool")
st.caption("HLST v1r0 9/2025 by Alan Biocca W6AKB with help from ChatGPT")

log_data, log_data_m = load_logfile(LOG_FILE) 

# Sidebar filters
license = st.sidebar.selectbox(
    "License Class",
    options=["T","G","E"],
    format_func=lambda x: {"T":"Technician","G":"General","E":"Extra"}[x]
)
mode = st.sidebar.selectbox(
    "Question Set",
    options=["A","N","R","O","E","T"],
    format_func=lambda x: {"A":"All Questions","N":"New","R":"Recently Missed",
                           "O":"New or Recently Missed","E":"Ever Missed",
                           "T":"Troublesome"}[x]
)
section = st.sidebar.selectbox(
    "Section Focus",
    options=["*"] + [str(i) for i in range(10)],
    format_func=lambda x: {'*':"All Sections",'0':"0-Safety",'1':"1-Rules",
                           '2':"2-Procedures",'3':"3-Propagation",'4':"4-Practices",
                           '5':"5-Electrical",'6':"6-Components",'7':"7-Circuits",
                           '8':"8-Signals",'9':"9-Antennas"}[x]
)

# --- session-state defaults (place after mode/section are read) ---
defaults = {
    "last_mode": mode,
    "last_section": section,
    "questions": None,
    "index": 0,
    "score": 0,
    "answered": 0,
    "answer_submitted": False,
    "selected_answer": None,
    "show_explanation": False,
    "remove": False,
    "last_license":None
}

for k, v in defaults.items():  # install defaults as needed
    if k not in st.session_state:
        st.session_state[k] = v

# links to figures required for some questions
if license == 'T':
    pool_file = "technician/questions.txt"
    explanations = "technician/explanations"
    figs = "https://ncvec.org/downloads/2022-2026%20Tech%20Graphics.pdf"
if license == 'G':
    pool_file = "general/questions.txt"
    explanations = "general/explanations"
    figs = "https://www.ncvec.org/downloads/G7-1.pdf"
if license == 'E':
    pool_file = "extra/questions.txt"
    explanations = "extra/explanations"
    figs = "https://www.ncvec.org/downloads/Extra_Figures_2024-2028-1.pdf"

ops = []
for d in ("V4m","V4o","V5"):
    if os.path.isdir(f"{explanations}/{d}"): ops.append(d)

version = st.sidebar.selectbox(
    "Explanation Level",
    options=ops,
    format_func=lambda x: {"V5":"V5","V4o":"V4","V4m":"V4-mini"}[x]
)

# If questions haven't been loaded yet, load them now
if st.session_state["questions"] is None:
    st.session_state.questions = load_questions(pool_file,log_data,log_data_m,mode,section)

# Session state
if "last_mode" not in st.session_state: st.session_state.last_mode = mode
if "last_section" not in st.session_state: st.session_state.last_section = section
if "questions" not in st.session_state:
    st.session_state.questions = load_questions(pool_file,log_data,log_data_m,mode,section)
    st.session_state.index = 0
    st.session_state.score = 0
    st.session_state.answered = 0   # number of submitted answers this session
    st.session_state.answer_submitted = False
    st.session_state.selected_answer = None
    st.session_state.show_explanation = False
    st.session_state.remove = False

# Reload questions if filters changed
if mode != st.session_state.last_mode or section != st.session_state.last_section or \
  license != st.session_state.last_license:
    st.session_state.questions = load_questions(pool_file,log_data,log_data_m,mode,section)
    st.session_state.index = 0
    st.session_state.score = 0
    st.session_state.answered = 0
    st.session_state.answer_submitted = False
    st.session_state.selected_answer = None
    st.session_state.show_explanation = False
    st.session_state.last_mode = mode
    st.session_state.last_section = section
    st.session_state.last_license = license

qs = st.session_state.questions

# Check filter results
if not qs:
    st.warning("No questions matched your filter.")
    st.stop()

# --- Four buttons in a row, must be before question---
c1, c2, c3, c4 = st.columns([1,1,1,1])
with c1:
    if mode == "T":
        remove_clicked = st.button("Remove Q")  # remove from troubled
    else:
        remove_clicked = False
with c2:
    submit_clicked = st.button("Submit Answer")
with c3:
    next_clicked = st.button("Next Question")      # advances; Skip if not submitted
with c4:
    explain_clicked = st.button("Explain Answers")    
    
# --- Next (always advances; Skip when not pre-submitted) ---
if next_clicked:
    # Reset per-question state and move forward
    st.session_state.index += 1
    st.session_state.answer_submitted = False
    st.session_state.selected_answer = None
    st.session_state.show_explanation = False
    st.session_state.remove = False

# Detect End
if st.session_state.index >= len(qs):
    st.success(f"🏁 Quiz Completed! Final Score: {st.session_state.score}/{st.session_state.answered} "
               f"({(100*st.session_state.score/max(1,st.session_state.answered)):.1f}%)")
    st.stop()
    
# Show Current question after buttons
q = qs[st.session_state.index]
st.subheader(f"{q['ID']}: {q['Question']}")

# Show Answers Radio
st.session_state.selected_answer = st.radio(
    "Choose your answer:",
    ["A","B","C","D"],
    index=None,
    format_func=lambda x: f"{x}. {q[x]}",
    key=f"q_{st.session_state.index}"
)

# --- Remove from Trouble List ---
if remove_clicked:
    st.session_state.remove = True
    
# --- Submit (does NOT advance) ---
if submit_clicked:
    if st.session_state.answer_submitted:
        # already submitted; quietly ignore duplicate submits
        st.warning("No Submitting after Explaining")
        pass
    elif st.session_state.selected_answer is None:
        st.warning("Please select an answer before submitting.")
    else:
        st.session_state.answer_submitted = True
        st.session_state.answered += 1
        if st.session_state.selected_answer == q["Correct"]:
            st.success(f"✅ Correct! {q[q['Correct']]}")
            st.session_state.score += 1
            append_line(LOG_FILE, f"1 {q['ID']}")     # record correct answer
            if st.session_state.remove:
                st.info("Question Removed from Trouble List")
                append_line(LOG_FILE, f"3 {q['ID']}") # record reject from trouble list
        else:
            st.error(f"❌ Incorrect. Correct answer: {q[q['Correct']]}")
            append_line(LOG_FILE, f"0 {q['ID']}")     # record incorrect answer
            st.session_state.answer_submitted = True
            st.session_state.show_explanation = True  # show explanation on incorrect

# --- Explain (show explanation, does NOT advance) ---
if explain_clicked:
    st.session_state.show_explanation = True

# show progress
st.info(
    f"**Progress:** Question {st.session_state.index+1} of {len(qs)}  |  "
    f"**Score:** {st.session_state.score}/{st.session_state.answered} "
    f"({(100*st.session_state.score/max(1,st.session_state.answered)):.1f}%)  |  "
    f"  [**(Open Figures Page)**]({figs})"
)

# show explanation last
if st.session_state.show_explanation:
    exp_file = Path(f"{explanations}/{version}/{q['ID']}.txt")
    if exp_file.exists():
        st.markdown(exp_file.read_text(encoding="utf-8"))
        st.session_state.answer_submitted = True      # lockout peek cheating
    else:
        st.info(f"Explanation not found ({exp_file})")
        #st.session_state.answer_submitted = False
        #st.session_state.show_explanation = False
