import SwiftUI

/// Poker words the player can tap to find out what they mean.
///
/// Two pieces, and one rule they share: a block of writing shows **one** definition at a time,
/// underneath itself. Tapping a second word replaces the card rather than stacking another one up,
/// and tapping the same word again puts it away.
///
/// `GlossaryText` marks up a paragraph — the first appearance of each word only, because a
/// paragraph with every occurrence underlined is a paragraph nobody reads. `TermChip` is a single
/// word the screen places deliberately: a statistic above a number, a seat name under a row.
///
/// Both work the same on a Mac and on a phone. The marked-up words are links carrying a scheme of
/// our own, and the block answers them itself, so a tap never leaves the app; the chips are
/// ordinary buttons. Everything is sized through `Theme`, so both follow the text-size setting, and
/// the only colours used are the palette's own.

/// One definition, opened in place under whatever was tapped.
///
/// The definition is itself marked up. Several of them lean on another entry — "trips" is explained
/// against "a set", "continuation bet" against "the flop" — and a definition that ends in a word the
/// reader has to go and look up somewhere else has sent them back where they started. Tapping a word
/// inside the card swaps the card for that word's, because both are bound to the same one piece of
/// state, so the cards can never stack up.
struct DefinitionCard: View {
    let term: GlossaryTerm
    @Binding var showing: GlossaryTerm?
    var close: (() -> Void)?

    var body: some View {
        HStack(alignment: .top, spacing: 8) {
            VStack(alignment: .leading, spacing: 3) {
                Text(term.term).heading().foregroundStyle(Theme.gold)
                GlossaryText(term.definition, size: TextRole.body.base, colour: Theme.text, showing: $showing)
            }
            Spacer(minLength: 0)
            if let close {
                Button(action: close) {
                    Symbol("xmark", size: Theme.size(10, capped: true))
                        .foregroundStyle(Theme.dim)
                        .padding(4)
                        .contentShape(Rectangle())
                }
                .buttonStyle(.plain)
            }
        }
        .padding(11)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(RoundedRectangle(cornerRadius: 9, style: .continuous).fill(Theme.panelRaised))
        .overlay(RoundedRectangle(cornerRadius: 9, style: .continuous)
            .stroke(Theme.gold.opacity(0.3), lineWidth: 1))
    }
}

/// Where a block puts its definition. Nothing at all until a word is tapped.
struct DefinitionSlot: View {
    @Binding var showing: GlossaryTerm?

    var body: some View {
        if let term = showing {
            DefinitionCard(term: term, showing: $showing) { showing = nil }
                .transition(.opacity)
        }
    }
}

@MainActor
private extension Text {
    /// A term chip's type: on the felt (capped) it is a felt label; anywhere else it is a caption,
    /// however small the caller used to ask for it.
    func chipType(_ size: CGFloat, capped: Bool) -> Text {
        capped ? label(size, capped: true) : caption(weight: .medium)
    }
}

/// A paragraph with its poker words made tappable.
struct GlossaryText: View {
    let text: String
    var size: CGFloat = 12
    var colour: Color = Theme.dim
    @Binding var showing: GlossaryTerm?

    init(_ text: String, size: CGFloat = 12, colour: Color = Theme.dim,
         showing: Binding<GlossaryTerm?>) {
        self.text = text
        self.size = size
        self.colour = colour
        self._showing = showing
    }

    var body: some View {
        prose
            .body(Theme.coachBase(size))
            .foregroundStyle(colour)
            .fixedSize(horizontal: false, vertical: true)
            .tint(Theme.gold)
            .environment(\.openURL, OpenURLAction { url in
                guard let term = Glossary.term(forLink: url) else { return .systemAction }
                withAnimation(.easeOut(duration: 0.14)) { toggle(term) }
                return .handled
            })
    }

    private func toggle(_ term: GlossaryTerm) {
        showing = showing?.id == term.id ? nil : term
    }

    /// The paragraph as a `Text`. Apple's SwiftUI takes attributed text; Skip's Android SwiftUI
    /// cannot yet, but reads Markdown, and a Markdown link is the same tap target.
    private var prose: Text {
        #if os(Android)
        Text(LocalizedStringKey(markedDown))
        #else
        Text(marked)
        #endif
    }

    /// The writing as Markdown: each first mention becomes `[word](link)`, everything else escaped
    /// so that a stray asterisk in the coach's prose cannot turn into emphasis.
    private var markedDown: String {
        func escaped(_ plain: Substring) -> String {
            var out = ""
            for ch in plain {
                if "\\*_[]`<>#".contains(ch) { out.append("\\") }
                out.append(ch)
            }
            return out
        }
        var out = ""
        var cursor = text.startIndex
        for mention in Glossary.mentions(in: text) {
            out += escaped(text[cursor..<mention.range.lowerBound])
            out += "[" + escaped(text[mention.range]) + "](" + mention.term.link.absoluteString + ")"
            cursor = mention.range.upperBound
        }
        out += escaped(text[cursor...])
        return out
    }

    /// The writing, with the first appearance of each word turned into a tap target. Dotted rather
    /// than solid, and in the palette's own gold: visibly offered, not shouted.
    private var marked: AttributedString {
        var out = AttributedString()
        var cursor = text.startIndex
        for mention in Glossary.mentions(in: text) {
            out.append(AttributedString(String(text[cursor..<mention.range.lowerBound])))
            var word = AttributedString(String(text[mention.range]))
            word.link = mention.term.link
            #if !os(Android)
            // SwiftUI's own text attributes. Skip's Android text has no equivalent yet, so there
            // the words are links in the usual link colour rather than dotted gold.
            word.foregroundColor = Theme.gold
            word.underlineStyle = Text.LineStyle(pattern: .dot, color: Theme.gold.opacity(0.75))
            #endif
            out.append(word)
            cursor = mention.range.upperBound
        }
        out.append(AttributedString(String(text[cursor...])))
        return out
    }
}

/// A single word the screen places itself — a statistic, a seat name, a label on a tile.
///
/// A word with no entry behind it draws as ordinary text rather than as a tap that does nothing.
/// That way a renamed entry degrades to plain writing on screen, and the test that walks the
/// glossary is what catches it.
struct TermChip: View {
    let key: String
    var display: String?
    var size: CGFloat = 11
    var colour: Color = Theme.dim
    var capped = false
    @Binding var showing: GlossaryTerm?

    init(_ key: String, showing: Binding<GlossaryTerm?>, display: String? = nil,
         size: CGFloat = 11, colour: Color = Theme.dim, capped: Bool = false) {
        self.key = key
        self._showing = showing
        self.display = display
        self.size = size
        self.colour = colour
        self.capped = capped
    }

    private var term: GlossaryTerm? { Glossary.look(up: key) }
    private var words: String { display ?? term?.term ?? key }

    var body: some View {
        if let term {
            Button {
                withAnimation(.easeOut(duration: 0.14)) {
                    showing = showing?.id == term.id ? nil : term
                }
            } label: {
                Text(words)
                    .chipType(size, capped: capped)
                    .underline(true, pattern: .dot, color: Theme.gold.opacity(0.75))
                    .foregroundStyle(showing?.id == term.id ? Theme.gold : colour)
                    .contentShape(Rectangle())
            }
            .buttonStyle(.plain)
            .tooltip(term.definition)
        } else {
            Text(words).chipType(size, capped: capped).foregroundStyle(colour)
        }
    }
}

// MARK: - The whole list

/// Every word in one place, alphabetically, so the vocabulary can be browsed rather than only
/// stumbled on mid-hand. Opened from Settings.
struct GlossaryView: View {
    @Environment(\.metrics) var metrics
    @Environment(\.dismiss) var dismiss
    @State var search = ""

    private var shown: [GlossaryTerm] {
        let query = search.trimmingCharacters(in: .whitespaces).lowercased()
        guard !query.isEmpty else { return Glossary.alphabetical }
        return Glossary.alphabetical.filter {
            $0.term.lowercased().contains(query)
                || $0.definition.lowercased().contains(query)
                || $0.otherNames.contains { $0.lowercased().contains(query) }
        }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            header
            Divider().overlay(Color.white.opacity(0.07))
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 14) {
                    if shown.isEmpty {
                        Text(Copy.noWordMatches)
                            .body().foregroundStyle(Theme.dim)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                    ForEach(shown) { term in
                        GlossaryEntryRow(term: term)
                    }
                }
                .padding(metrics.gutter)
            }
        }
        .frame(minWidth: metrics.compact ? nil : 560, minHeight: metrics.compact ? nil : 520)
        .background(Theme.background)
    }

    /// One entry on the browsable list, with its own definition slot.
    ///
    /// A word inside a definition is tappable here for the same reason it is inside a card: the
    /// definitions lean on each other, and dead-ending somebody in the one place they came to look
    /// a word up is the worst place in the app to do it. Each row holds its own state, so the
    /// answer opens under the entry that was asked rather than somewhere else on a long list.
    struct GlossaryEntryRow: View {
        let term: GlossaryTerm
        @State var showing: GlossaryTerm?

        var body: some View {
            VStack(alignment: .leading, spacing: 3) {
                HStack(alignment: .firstTextBaseline, spacing: 8) {
                    Text(term.term).heading().foregroundStyle(Theme.text)
                    if let other = term.otherNames.first {
                        Text("also called \(other)").caption().foregroundStyle(Theme.dim.opacity(0.8))
                    }
                }
                GlossaryText(term.definition, size: TextRole.body.base, colour: Theme.dim, showing: $showing)
                DefinitionSlot(showing: $showing)
            }
        }
    }

    private var header: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack(alignment: .firstTextBaseline) {
                VStack(alignment: .leading, spacing: 3) {
                    Text(Copy.pokerWordsTitle)
                        .font(.system(size: metrics.titleSize, weight: .bold, design: .rounded))
                        .foregroundStyle(Theme.text)
                    Text(Copy.pokerWordsIntro(Glossary.terms.count))
                        .body().foregroundStyle(Theme.dim)
                        .fixedSize(horizontal: false, vertical: true)
                }
                Spacer(minLength: 12)
                GhostButton(title: "Done") { dismiss() }
            }
            HStack(spacing: 7) {
                Symbol("magnifyingglass", size: Theme.size(11))
                    .foregroundStyle(Theme.dim)
                TextField("Find a word", text: $search)
                    .textFieldStyle(.plain)
                    .font(.system(size: Theme.size(13), weight: .regular, design: .rounded))
                    .foregroundStyle(Theme.text)
                    .autocorrectionDisabled()
                if !search.isEmpty {
                    Button { search = "" } label: {
                        Symbol("xmark.circle.fill", size: Theme.size(11))
                            .foregroundStyle(Theme.dim)
                    }
                    .buttonStyle(.plain)
                }
            }
            .padding(.horizontal, 12)
            .frame(minHeight: metrics.compact ? 40 : 32)
            .background(RoundedRectangle(cornerRadius: 9, style: .continuous).fill(Theme.panelRaised))
        }
        .padding(metrics.gutter)
    }
}
