package main

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

// imageExtensions are the file names guided mode recognises as a disk image
// when it goes looking for something to offer the reader.
var imageExtensions = []string{".img", ".iso", ".vhd", ".vhdx", ".raw", ".dd", ".bin"}

// 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.
//
// PartitionGuard has no command that writes anything, so guided mode simply
// runs the checks: the image is opened read-only, exactly as it always is.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  PartitionGuard")
	fmt.Println("  Read the map at the front of a disk image and say whether it still")
	fmt.Println("  makes sense.")
	fmt.Println()
	fmt.Println("  A disk keeps a table describing where each of its parts begins and")
	fmt.Println("  ends, together with checksums over that table. PartitionGuard reads")
	fmt.Println("  the table, recomputes the checksums, compares the main copy against")
	fmt.Println("  the spare at the far end, and reports each check as passed or failed.")
	fmt.Println()
	fmt.Println("  It opens the image read-only. It cannot alter, resize or repair")
	fmt.Println("  anything, by design.")
	fmt.Println()

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

	fmt.Println()
	fmt.Println("  Checking.")
	fmt.Println()
	run([]string{"verify", path})

	fmt.Println()
	fmt.Println("  Done. The image was only read.")
	fmt.Println("  The command-line version also lists each partition and prints the")
	fmt.Println("  table's fields in full: partitionguard --help")
	pause(in)
}

// askImage asks which image file to look at and keeps asking until the answer
// is a file that really exists. It reports false only when stdin closes, at
// which point there is nothing sensible left to ask.
func askImage(in *bufio.Scanner) (string, bool) {
	suggested := suggestedImage()
	for {
		fmt.Println("  Which disk image shall I check?")
		fmt.Println("  That is a single file, usually ending in .img or .iso.")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		}
		fmt.Println("  Tip: you can drag the file, or the folder holding it, from")
		fmt.Println("  Explorer onto this window.")
		fmt.Print("  > ")

		if !in.Scan() {
			return "", false
		}
		answer := strings.TrimSpace(in.Text())
		answer = strings.Trim(answer, `"`)
		if answer == "" {
			answer = suggested
		}
		if answer == "" {
			fmt.Println()
			fmt.Println("  I need a file to look at. 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 spelling, or drag the file onto this window and")
			fmt.Println("  press Enter.")
			fmt.Println()
			continue
		case info.IsDir():
			// A dragged folder is a near miss, so look inside it rather than
			// making the reader hunt for the file themselves.
			if found := imageIn(answer); found != "" {
				fmt.Println()
				fmt.Printf("  That is a folder. I found %s inside it and will use that.\n", filepath.Base(found))
				return found, true
			}
			fmt.Println()
			fmt.Printf("  %q is a folder, and I cannot see a disk image inside it.\n", answer)
			fmt.Println("  Give me the image file itself.")
			fmt.Println()
			continue
		case info.Size() == 0:
			fmt.Println()
			fmt.Printf("  %q is empty — there is no disk in there to read.\n", answer)
			fmt.Println()
			continue
		}
		return answer, true
	}
}

// suggestedImage offers a disk image the reader is likely to have, so the
// question can be answered with one keypress. It looks only in the handful of
// folders a download or an export lands in, and never walks a whole disk.
func suggestedImage() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	for _, name := range []string{"Downloads", "Documents", "Desktop", "."} {
		if found := imageIn(filepath.Join(home, name)); found != "" {
			return found
		}
	}
	return ""
}

// imageIn returns the first file in dir that looks like a disk image, or an
// empty string. It reads one directory and does not descend.
func imageIn(dir string) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		ext := strings.ToLower(filepath.Ext(e.Name()))
		for _, want := range imageExtensions {
			if ext != want {
				continue
			}
			info, err := e.Info()
			if err != nil || info.Size() == 0 {
				continue
			}
			return filepath.Join(dir, e.Name())
		}
	}
	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()
}
