package main

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"
)

// Settings for the guided run. A double-clicked session has somebody watching
// the window, so it takes fewer samples than the command-line default of 30 —
// enough for an honest distribution, quick enough that nobody wonders whether
// it has frozen — and it refuses to sit on a command that never returns.
const (
	guidedRuns    = 10
	guidedWarmup  = 2
	guidedTimeout = 30 * time.Second
)

// 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
// question the program actually needs and stay on screen until the reader is
// done.
//
// 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("  SystemPulse")
	fmt.Println("  Find out how long something really takes on this machine.")
	fmt.Println()
	fmt.Println("  Tell me a command to time and I will run it several times over, then")
	fmt.Println("  report the fastest, the slowest, the typical time, and how much the")
	fmt.Println("  times varied — which is the number that tells you whether a machine")
	fmt.Println("  is merely slow or actually struggling.")
	fmt.Println()
	fmt.Printf("  Nothing is installed or saved. The command runs %d times, exactly as\n", guidedRuns+guidedWarmup)
	fmt.Println("  you would run it yourself.")
	fmt.Println()

	suggested := suggestedCommand()
	var argv []string
	for {
		fmt.Println("  Which command shall I time?")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		}
		fmt.Println("  A program that needs a file or folder is fine — you can drag one")
		fmt.Println("  from Explorer onto this window to paste its location.")
		fmt.Print("  > ")

		if !in.Scan() {
			// stdin closed on us; there is nothing sensible left to ask.
			return
		}
		answer := strings.TrimSpace(in.Text())
		if answer == "" {
			answer = suggested
		}
		if answer == "" {
			fmt.Println()
			fmt.Println("  I need a command to time. Try again, or close this window.")
			fmt.Println()
			continue
		}

		words := splitCommand(answer)
		if len(words) == 0 {
			fmt.Println()
			fmt.Println("  I could not make a command out of that. Try again.")
			fmt.Println()
			continue
		}
		if _, err := exec.LookPath(words[0]); err != nil {
			fmt.Println()
			fmt.Printf("  I cannot find a program called %q on this machine.\n", words[0])
			fmt.Println("  Check the spelling, or give me its full location — you can drag")
			fmt.Println("  the program from Explorer onto this window to paste it.")
			fmt.Println()
			continue
		}
		argv = words
		break
	}

	fmt.Println()
	fmt.Printf("  Working. Running it %d times to warm up, then timing %d runs.\n", guidedWarmup, guidedRuns)
	fmt.Println("  Anything the command prints is captured, not shown.")
	fmt.Println()

	res, err := profile(argv, guidedRuns, guidedWarmup, guidedTimeout, "guided run")
	if err != nil {
		fmt.Printf("  I could not run that: %v\n", err)
		fmt.Println()
		fmt.Println("  That usually means the program needs arguments it did not get, or")
		fmt.Println("  is not allowed to run from here.")
	} else {
		printResult(os.Stdout, res)
	}

	fmt.Println()
	fmt.Println("  Done. Nothing was installed, changed or saved.")
	fmt.Println("  The command-line version can save this as a baseline and tell you")
	fmt.Println("  later whether the machine has got slower: systempulse --help")
	pause(in)
}

// splitCommand breaks a typed command line into a program and its arguments,
// keeping anything inside double quotes together. Windows paths are full of
// spaces and Explorer quotes them when you drag them onto a console window, so
// splitting on whitespace alone would break the commonest case there is.
func splitCommand(line string) []string {
	var words []string
	var cur strings.Builder
	inQuotes, started := false, false

	flush := func() {
		if started {
			words = append(words, cur.String())
			cur.Reset()
			started = false
		}
	}
	for _, r := range line {
		switch {
		case r == '"':
			inQuotes = !inQuotes
			started = true
		case (r == ' ' || r == '\t') && !inQuotes:
			flush()
		default:
			cur.WriteRune(r)
			started = true
		}
	}
	flush()
	return words
}

// suggestedCommand offers something harmless and instant that is certain to be
// installed, so the reader can see a real answer by pressing one key. It
// returns "" rather than offering a command this machine does not have.
func suggestedCommand() string {
	var candidates []string
	if runtime.GOOS == "windows" {
		candidates = []string{"cmd /c ver"}
	} else {
		candidates = []string{"uname -a", "/bin/echo hello"}
	}
	for _, c := range candidates {
		words := splitCommand(c)
		if len(words) == 0 {
			continue
		}
		if _, err := exec.LookPath(words[0]); err == nil {
			return c
		}
	}
	return ""
}

// 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()
}
