package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

// The guided session runs a soak with a fixed shape and a hard ceiling on how
// long it can last. A person who double-clicked the program has no way to stop
// it other than closing the window, so it has to end by itself and it has to
// end soon: the reader picks a length, that length is clamped into this range,
// and then the run finishes on its own. There is no repeat and no loop.
const (
	guidedDefaultMinutes = 1.0
	guidedMinMinutes     = 0.25
	guidedMaxMinutes     = 5.0
	guidedInterval       = "5s"
)

// runGuided is what happens when somebody double-clicks the program instead of
// typing its name at a prompt.
//
// Without this, Explorer opens a console, main() finds no arguments, prints
// the usage text to stderr and exits — and Windows destroys the window in the
// same instant. From the other side of the screen that is indistinguishable
// from a crash. So when we know we were double-clicked, we ask the one thing
// the program needs and stay on screen until the reader is done.
//
// SensorDeck touches nothing on disk: it keeps the processor busy and times
// itself. The guided run deliberately saves no file, so there is nothing to
// overwrite and nothing to clean up afterwards.
//
// This path is entered ONLY when there are no arguments and both ends of the
// program are a real console. Any scripted or piped use takes exactly the same
// code path it always did.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  SensorDeck")
	fmt.Println("  Find out whether this machine holds its speed when it is working hard.")
	fmt.Println()
	fmt.Println("  SensorDeck gives the processor the same piece of work over and over")
	fmt.Println("  with no rest in between, and times each one. A machine that is cooling")
	fmt.Println("  itself properly takes the same time all the way through. A machine that")
	fmt.Println("  is not gets slower as it goes, and you can watch that happen.")
	fmt.Println()
	fmt.Println("  It reads no temperatures and no fan speeds — nothing here can see")
	fmt.Println("  those. It measures the effect instead. Close what you can before you")
	fmt.Println("  start, because anything else busy on this machine muddies the result.")
	fmt.Println()

	minutes, ok := askMinutes(in)
	if !ok {
		return
	}

	fmt.Println()
	fmt.Println("  Working the processor hard now. The machine will feel busy and the fans")
	fmt.Println("  may spin up; that is the test. It stops on its own when the time is up.")
	fmt.Println()
	cmdSoak([]string{
		"--minutes", strconv.FormatFloat(minutes, 'g', -1, 64),
		"--interval", guidedInterval,
	})

	fmt.Println()
	fmt.Println("  Nothing was saved and nothing on this machine was changed.")
	fmt.Println("  A slowdown here is a hint, not a diagnosis — a background task or a")
	fmt.Println("  power-saving setting produces the same shape as poor cooling. Run it")
	fmt.Println("  twice before you believe it.")
	fmt.Println()
	fmt.Println("  From a command prompt SensorDeck can run for longer, keep each run, and")
	fmt.Println("  compare two of them to answer \"did that change actually help\":")
	fmt.Println("  sensordeck --help")
	pause(in)
}

// askMinutes asks how long to keep the machine busy, and keeps asking until it
// gets a number it can use.
func askMinutes(in *bufio.Scanner) (float64, bool) {
	for {
		fmt.Println("  How many minutes shall I keep it busy?")
		fmt.Printf("  Anything from %s to %s. Longer is more revealing: heat takes time to\n",
			trimNumber(guidedMinMinutes), trimNumber(guidedMaxMinutes))
		fmt.Println("  build up, so a short run can miss a machine that would slow down.")
		fmt.Printf("  (press Enter for %s)\n", trimNumber(guidedDefaultMinutes))
		fmt.Print("  > ")

		if !in.Scan() {
			// stdin closed on us; there is nothing sensible left to ask.
			return 0, false
		}
		answer := strings.TrimSpace(in.Text())
		if answer == "" {
			return guidedDefaultMinutes, true
		}

		minutes, err := parseMinutes(answer)
		if err != "" {
			fmt.Println()
			fmt.Println("  " + err)
			fmt.Println()
			continue
		}
		return minutes, true
	}
}

// parseMinutes turns what the reader typed into a run length, or returns a
// sentence explaining in plain words why it cannot. It returns a message
// rather than an error because the reason is going straight onto the screen of
// somebody who did not ask for a Go error string.
func parseMinutes(answer string) (float64, string) {
	answer = strings.TrimSuffix(strings.TrimSpace(strings.ToLower(answer)), "minutes")
	answer = strings.TrimSuffix(strings.TrimSpace(answer), "minute")
	answer = strings.TrimSuffix(strings.TrimSpace(answer), "mins")
	answer = strings.TrimSuffix(strings.TrimSpace(answer), "min")
	answer = strings.TrimSpace(strings.TrimSuffix(answer, "m"))

	minutes, err := strconv.ParseFloat(answer, 64)
	if err != nil || minutes != minutes {
		return 0, "I need a number of minutes, like 2. Try again, or close this window."
	}
	if minutes < guidedMinMinutes {
		return 0, fmt.Sprintf("That is too short to measure anything. %s minutes is the least I can learn from.",
			trimNumber(guidedMinMinutes))
	}
	if minutes > guidedMaxMinutes {
		return 0, fmt.Sprintf("In here I stop at %s minutes, so the window cannot sit busy for hours. "+
			"For a longer soak, run SensorDeck from a command prompt.", trimNumber(guidedMaxMinutes))
	}
	return minutes, ""
}

// trimNumber prints a run length the way a person writes it: 1, not 1.000000.
func trimNumber(v float64) string {
	return strconv.FormatFloat(v, 'g', -1, 64)
}

// pause keeps the console window open. Explorer closes it the moment the
// process exits, so without this the reader never sees the output.
func pause(in *bufio.Scanner) {
	fmt.Println()
	fmt.Print("  Press Enter to close this window. ")
	in.Scan()
}
