package main

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

// defaultGuidedSize is the stick size the guided prompt offers first: 8 GB is
// the smallest USB stick anyone still buys and the commonest one in a drawer.
const defaultGuidedSize = "8GB"

// 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 questions
// 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.
//
// BootBuilder's build command writes an image file. Guided mode runs the plan
// instead: it measures the folder, works out the exact FAT32 layout that would
// be produced, and says whether it fits. It writes nothing at all, and this
// file offers no way to ask it to.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  BootBuilder")
	fmt.Println("  Works out whether your installer files will fit on a USB stick, and")
	fmt.Println("  exactly how they would be laid out on it.")
	fmt.Println()
	fmt.Println("  Tell it which folder holds the files and how big the stick is, and it")
	fmt.Println("  measures the tree, chooses the cluster size the way Windows does, and")
	fmt.Println("  reports how much room would be left over — or how many bytes short")
	fmt.Println("  you are.")
	fmt.Println()
	fmt.Println("  Nothing is written. No image file, no stick, no disk.")
	fmt.Println()

	src := askForSource(in)
	if src == "" {
		return
	}

	fmt.Println()
	size := askForSize(in)
	if size == 0 {
		return
	}

	fmt.Println()
	fmt.Println("  Measuring. A big installer tree takes a moment.")
	fmt.Println()
	guidedPlan(src, size)

	fmt.Println()
	fmt.Println("  Done, and nothing was written. When you are ready to make the image")
	fmt.Println("  file itself, that is a command-line job: bootbuilder help")
	pause(in)
}

// askForSource gets a real folder out of the reader. It returns "" only when
// stdin has gone away.
func askForSource(in *bufio.Scanner) string {
	suggested := suggestedFolder()
	for {
		fmt.Println("  Which folder holds the files that should go on the stick?")
		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 ""
		}
		answer := strings.TrimSpace(in.Text())
		answer = strings.Trim(answer, `"`)
		if answer == "" {
			answer = suggested
		}
		if answer == "" {
			fmt.Println()
			fmt.Println("  I need a folder to measure. 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("  Tip: you can drag a folder from Explorer onto this window to")
			fmt.Println("  paste its location, then press Enter.")
			fmt.Println()
			continue
		case !info.IsDir():
			fmt.Println()
			fmt.Printf("  %q is a file, not a folder. Give me the folder it sits in.\n", answer)
			fmt.Println()
			continue
		}
		return answer
	}
}

// askForSize gets a stick size in bytes out of the reader, using the same
// parser the command line uses so the answers mean the same thing in both
// places. It returns 0 only when stdin has gone away.
func askForSize(in *bufio.Scanner) int64 {
	for {
		fmt.Println("  How big is the stick?")
		fmt.Printf("  (press Enter for %s — or type 16GB, 32GB, 4096MB, and so on)\n", defaultGuidedSize)
		fmt.Print("  > ")

		if !in.Scan() {
			return 0
		}
		answer := strings.TrimSpace(in.Text())
		if answer == "" {
			answer = defaultGuidedSize
		}
		size, err := parseSize(answer)
		if err != nil {
			fmt.Println()
			fmt.Printf("  I did not understand %q as a size.\n", answer)
			fmt.Println("  Write it like 8GB, 16GB, 512MB or a plain number of bytes.")
			fmt.Println()
			continue
		}
		return size
	}
}

// guidedPlan does exactly what the plan command does — measure the tree, work
// out the FAT32 geometry, and compare the two — but reports trouble as a
// sentence instead of calling os.Exit, which on a tree that does not fit would
// slam the window shut on the very answer the reader came for.
func guidedPlan(src string, size int64) {
	g, err := PlanGeometry(size, "mbr", defaultLabel, 0)
	if err != nil {
		fmt.Printf("  That size will not make a FAT32 volume: %v\n", err)
		return
	}
	abs, err := filepath.Abs(src)
	if err != nil {
		abs = src
	}
	root, scan, err := ScanTree(abs)
	if err != nil {
		fmt.Printf("  I could not read through %s: %v\n", abs, err)
		return
	}
	req := ComputeLayout(root, g.ClusterBytes, g.Label != "")

	fmt.Printf("  Folder    : %s\n", abs)
	fmt.Printf("  Contents  : %d files in %d folders, %s of data\n", scan.Files, scan.Dirs, humanBytes(scan.DataBytes))
	if len(scan.Skipped) > 0 {
		fmt.Printf("  Skipped   : %d entries that are not ordinary files\n", len(scan.Skipped))
		for _, s := range scan.Skipped {
			fmt.Printf("                %s\n", s)
		}
	}
	fmt.Println()
	fmt.Printf("  Stick     : %s, laid out as %s with a %s cluster\n",
		g.ImageHuman, strings.ToUpper(g.Scheme), humanBytes(g.ClusterBytes))
	fmt.Printf("  Room for  : %d clusters\n", g.ClusterCount)
	fmt.Printf("  Needs     : %d clusters (%s once rounded up to whole clusters)\n",
		req.TotalCluster, humanBytes(req.BytesOnDisk))
	fmt.Println()

	if req.TotalCluster <= g.ClusterCount {
		free := g.ClusterCount - req.TotalCluster
		fmt.Printf("  IT FITS — %s would still be free.\n", humanBytes(free*g.ClusterBytes))
		return
	}
	short := (req.TotalCluster - g.ClusterCount) * g.ClusterBytes
	fmt.Printf("  IT DOES NOT FIT — short by %s (%d bytes).\n", humanBytes(short), short)
	fmt.Println()
	fmt.Println("  Either use a bigger stick, or take something out of the folder.")
	fmt.Println("  Remember FAT32 cannot hold a single file larger than 4 GB, however")
	fmt.Println("  big the stick is.")
}

// suggestedFolder offers somewhere worth measuring that is certain to exist,
// so the reader can get going by pressing one key.
func suggestedFolder() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	// An installer tree has almost always just been downloaded or extracted.
	for _, name := range []string{"Downloads", "Documents"} {
		candidate := filepath.Join(home, name)
		if info, err := os.Stat(candidate); err == nil && info.IsDir() {
			return candidate
		}
	}
	return home
}

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