package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

// 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.
//
// SectorPilot cannot write to the thing it scans — it opens it for reading and
// only ever reads — so the guided session runs the real surface scan. It does
// not save the scan to a file, because that is the one thing here that would
// put something new on disk; saving stays a choice made at the command line.
//
// 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("  SectorPilot")
	fmt.Println("  Find the part of a disk image that is struggling to be read.")
	fmt.Println()
	fmt.Println("  SectorPilot reads a file from beginning to end in small pieces and")
	fmt.Println("  times every single read. Most come back at the same speed. The ones")
	fmt.Println("  that take far longer, and the ones that fail outright, are the places")
	fmt.Println("  worth worrying about — and it tells you exactly where they are.")
	fmt.Println()
	fmt.Println("  It only reads. It cannot change, repair or erase what it looks at.")
	fmt.Println()

	path, ok := askImage(in, suggestedImage())
	if !ok {
		return
	}

	fmt.Println()
	fmt.Println("  Reading. A large image takes as long as it takes to read the whole")
	fmt.Println("  thing once, so this can run for a while. Leave the machine quiet while")
	fmt.Println("  it works, or the timings will pick up whatever else is using the disk.")
	fmt.Println()
	run([]string{"scan", path, "--top", "5"})

	fmt.Println()
	fmt.Println("  Nothing was written and nothing was saved.")
	fmt.Println("  One slow reading is not a verdict. Run it again on a quiet machine")
	fmt.Println("  before you conclude anything: a block that is slow twice, in the same")
	fmt.Println("  place, is the one that matters. From a command prompt SectorPilot can")
	fmt.Println("  keep a scan and compare it with a later one — sectorpilot help")
	pause(in)
}

// askImage asks for the file to scan and keeps asking until it gets one
// SectorPilot can actually open and read.
func askImage(in *bufio.Scanner, suggested string) (string, bool) {
	for {
		fmt.Println("  Which file shall I read?")
		fmt.Println("  A disk image is the usual answer, but any file will do. You can drag")
		fmt.Println("  it, or the folder holding it, from Explorer onto this window to")
		fmt.Println("  paste the location.")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		}
		fmt.Print("  > ")

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

		info, err := os.Stat(answer)
		switch {
		case err != nil:
			fmt.Println()
			fmt.Printf("  I cannot find %q.\n", answer)
			fmt.Println("  Check the location and try again, or close this window.")
			fmt.Println()
			continue
		case info.IsDir():
			// A dragged folder is a near miss worth rescuing rather than
			// scolding: look inside it for the image they meant.
			if found := firstImageIn(answer); found != "" {
				fmt.Println()
				fmt.Printf("  That is a folder, so I will read the image inside it:\n  %s\n", found)
				return found, true
			}
			fmt.Println()
			fmt.Printf("  %q is a folder and I found no disk image in it. Give me the\n", answer)
			fmt.Println("  file itself.")
			fmt.Println()
			continue
		case !info.Mode().IsRegular():
			fmt.Println()
			fmt.Printf("  %q is not an ordinary file.\n", answer)
			fmt.Println("  SectorPilot reads images and files, not drives themselves. Make an")
			fmt.Println("  image of the drive first, then point me at that.")
			fmt.Println()
			continue
		case info.Size() == 0:
			fmt.Println()
			fmt.Printf("  %q is empty, so there is no surface to read.\n", answer)
			fmt.Println()
			continue
		}
		return answer, true
	}
}

// suggestedImage offers a disk image the reader is likely to have on hand, so
// the common case is one keypress. It returns "" when there is nothing to
// offer, and the prompt simply asks for a location.
func suggestedImage() string {
	var dirs []string
	if wd, err := os.Getwd(); err == nil {
		dirs = append(dirs, wd)
	}
	if home, err := os.UserHomeDir(); err == nil {
		dirs = append(dirs, filepath.Join(home, "Downloads"),
			filepath.Join(home, "Documents"), home)
	}
	for _, dir := range dirs {
		if found := firstImageIn(dir); found != "" {
			return found
		}
	}
	return ""
}

// firstImageIn returns the first disk image in dir, or "" if there is none.
// Only names that plainly announce themselves as images are considered: this
// is a suggestion the reader accepts with one keypress, so guessing at some
// unrelated file would be worse than offering nothing.
func firstImageIn(dir string) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	exts := map[string]bool{
		".img": true, ".iso": true, ".dd": true, ".raw": true,
		".vhd": true, ".vhdx": true, ".vmdk": true, ".dmg": true,
	}
	var names []string
	for _, e := range entries {
		if e.IsDir() || !exts[strings.ToLower(filepath.Ext(e.Name()))] {
			continue
		}
		info, err := e.Info()
		if err != nil || !info.Mode().IsRegular() || info.Size() == 0 {
			continue
		}
		names = append(names, e.Name())
	}
	sort.Strings(names)
	if len(names) == 0 {
		return ""
	}
	return filepath.Join(dir, names[0])
}

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