package router

import (
	"fmt"
	"strings"

	"github.com/kami/maven/internal/lexicon"
)

// Half-past and quarter-to, rewritten to digits before either date parser sees
// the sentence.
//
// Russian says a half hour as the ordinal of the hour being ENTERED, in the
// genitive: "половина восьмого" is 07:30, not 08:30. Neither python dateparser
// nor the stub reads that shape, so "напомни в половине восьмого" parsed to
// nothing (V-538). Off by one in the wrong direction is a missed dose, so the
// rewrite is the only place the arithmetic lives, and it is minus one every
// time.
//
// "без четверти восемь" counts the other way — it is 07:45, the hour NOT yet
// reached, spoken as a cardinal. Same minus one, different minutes.
//
// Both shapes come out as "в H:MM", with the preposition kept when the sentence
// had one and added when it did not, because the stub scans for "в" before a
// clock and the contracted "полвосьмого" carries no preposition at all.

// The forms of "половина" a spoken time uses are a closed class and live in the
// lexicon as half_hour, because the ordinal-selection turn has to decline the
// same ordinal this file reads (V-522). Ask lexicon.IsHalfHour.

// minutesTo — the words that name the minutes in a "без X" hour. "четверти" is
// the only one that is not a number; the rest are cardinals and are read as
// cardinals.
var minutesTo = map[string]int{"четверти": 15, "quarter": 15}

// rewriteHalfPast turns a spoken half hour or quarter-to hour into digits.
// Returns text unchanged when it holds neither shape.
func rewriteHalfPast(text string) string {
	toks := strings.Fields(text)
	if len(toks) < 2 {
		return text
	}
	out := make([]string, 0, len(toks))
	for i := 0; i < len(toks); i++ {
		if n, width, ok := halfPastAt(toks, i); ok {
			out = appendClock(out, n, 30)
			i += width - 1
			continue
		}
		if h, m, width, ok := quarterToAt(toks, i); ok {
			out = appendClock(out, h, m)
			i += width - 1
			continue
		}
		out = append(out, toks[i])
	}
	return strings.Join(out, " ")
}

// halfPastAt reads a half hour starting at i, returning the hour it names and
// how many tokens it spans.
func halfPastAt(toks []string, i int) (hour, width int, ok bool) {
	head := cleanWord(toks[i])
	// The contracted one word: "полвосьмого", "пол-восьмого".
	for _, prefix := range []string{"пол-", "пол"} {
		if !strings.HasPrefix(head, prefix) {
			continue
		}
		if h, ok := enteredHour(strings.TrimPrefix(head, prefix)); ok {
			return h, 1, true
		}
	}
	if !lexicon.IsHalfHour(head) || i+1 >= len(toks) {
		return 0, 0, false
	}
	h, ok := enteredHour(cleanWord(toks[i+1]))
	if !ok {
		return 0, 0, false
	}
	return h, 2, true
}

// quarterToAt reads a "без четверти восемь" / "без двадцати восемь" hour
// starting at i.
func quarterToAt(toks []string, i int) (hour, minute, width int, ok bool) {
	if cleanWord(toks[i]) != "без" || i+2 >= len(toks) {
		return 0, 0, 0, false
	}
	to, ok := minutesBefore(cleanWord(toks[i+1]))
	if !ok {
		return 0, 0, 0, false
	}
	h, ok := comingHour(cleanWord(toks[i+2]))
	if !ok {
		return 0, 0, 0, false
	}
	return h, 60 - to, 3, true
}

// minutesBefore reads the minutes still to run in a "без X" hour. A cardinal is
// only minutes when it divides the hour the way a spoken clock does, so "без
// семи восемь" is not a time anyone says and is left alone.
func minutesBefore(word string) (int, bool) {
	if m, ok := minutesTo[word]; ok {
		return m, true
	}
	n, ok := lexicon.Cardinal(word)
	if !ok {
		return 0, false
	}
	switch n {
	case 5, 10, 15, 20, 25:
		return n, true
	}
	return 0, false
}

// enteredHour maps the ordinal of the hour being entered to the hour on the
// clock: "восьмого" is the eighth hour, which is 07:xx. "первого" is 12:xx,
// because the hour before one is twelve and not zero.
func enteredHour(word string) (int, bool) {
	n, ok := lexicon.Ordinal(word)
	if !ok || n < 1 || n > 12 {
		return 0, false
	}
	return clockHourBefore(n), true
}

// comingHour maps the cardinal of the hour not yet reached, which counts the
// same way: "без четверти восемь" is 07:45.
func comingHour(word string) (int, bool) {
	d, ok := numeralDigit(word)
	if !ok {
		return 0, false
	}
	var n int
	if _, err := fmt.Sscanf(d, "%d", &n); err != nil || n < 1 || n > 12 {
		return 0, false
	}
	return clockHourBefore(n), true
}

// clockHourBefore is the whole of the arithmetic: the hour before n, with
// twelve rather than zero before one.
func clockHourBefore(n int) int {
	if n == 1 {
		return 12
	}
	return n - 1
}

// appendClock writes the clock, keeping a preposition the sentence already had
// and adding "в" when it had none. The stub parser scans for "в" before a
// clock, and the contracted form carries no preposition.
func appendClock(out []string, hour, minute int) []string {
	if n := len(out); n == 0 || !clockPrepositions[cleanWord(out[n-1])] {
		out = append(out, "в")
	}
	return append(out, fmt.Sprintf("%d:%02d", hour, minute))
}

// clockPrepositions — the prepositions that already introduce a time, so the
// rewrite does not put a second one in front of them.
var clockPrepositions = map[string]bool{
	"в": true, "во": true, "к": true, "около": true, "на": true, "at": true, "by": true,
}

// cleanWord — one token, lowercased and stripped of the punctuation a spoken
// sentence carries, the same way SpellOutDigits reads its tokens.
func cleanWord(tok string) string {
	return strings.ToLower(strings.Trim(tok, ".,!?;:«»\"'"))
}
