package main

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

// runGuided is what happens when somebody double-clicks this program instead
// of typing its name at a prompt.
//
// Every other Techlosoft program treats the guided flow as a rescue for a
// vanishing console window. This one is different: the Suite is the program
// most likely to be double-clicked, because it is the first thing a customer
// opens after paying, so the guided flow IS the interface and the command
// line is the thing underneath it.
//
// Three rules run through all of it:
//
//   - Nothing is written until an explicit yes, and the yes is preceded by a
//     sentence saying exactly what will be written and where.
//   - Every prompt accepts an empty answer, and every prompt says what an
//     empty answer will do. Pressing Enter all the way through has to be a
//     sensible run, not a crash.
//   - It never dead-ends. Every path leads back to the menu, and the menu
//     always leads to "Press Enter to close this window."
func runGuided() {
	in := bufio.NewScanner(os.Stdin)
	in.Buffer(make([]byte, 0, 64*1024), 1<<20)

	fmt.Println()
	fmt.Println("  ================================================================")
	fmt.Println("   Techlosoft Suite")
	fmt.Println("   The one program that manages all the others")
	fmt.Println("  ================================================================")
	fmt.Println()
	fmt.Printf("  You have paid for %d programs, in %d groups. This wizard puts the\n", len(catalog), len(groups()))
	fmt.Println("  ones you want on this machine, takes off the ones you do not, and")
	fmt.Println("  tells you when there is a newer build of something you already have.")
	fmt.Println()
	fmt.Println("  It does not use the internet. It copies from a folder you already")
	fmt.Println("  have. It writes to one folder and nowhere else, and it never")
	fmt.Println("  deletes anything -- removing a program moves it to a trash folder")
	fmt.Println("  you can look inside.")
	fmt.Println()

	root, ok := askRoot(in)
	if !ok {
		closing(in)
		return
	}

	src := askSource(in, root)

	// The running total lives out here so it survives a trip back to the menu:
	// pick three from one group, four from another, and the total is seven.
	basket := map[string]bool{}

	for {
		choice, ok := mainMenu(in, root, src, basket)
		if !ok {
			break
		}
		switch choice.kind {
		case menuQuit:
			closing(in)
			return
		case menuPick:
			if src == nil {
				fmt.Println()
				fmt.Println("  I do not have a folder of program files to copy from yet, so")
				fmt.Println("  there is nothing to install. Pick 'F' to point me at one.")
				fmt.Println()
				continue
			}
			if !pickAndInstall(in, root, src, basket, choice.programs, choice.label) {
				closing(in)
				return
			}
		case menuInstalled:
			showInstalled(root)
		case menuRemove:
			if !removeFlow(in, root) {
				closing(in)
				return
			}
		case menuUpdate:
			updateFlow(in, root, src)
		case menuSource:
			if s := askSourceOnce(in); s != nil {
				src = s
			}
		}
	}
	closing(in)
}

// ---------------------------------------------------------- the prompts ---

// ask puts one question and returns the trimmed answer. The second return is
// false only when stdin has closed for good, which is the one case where
// there is nothing sensible left to do.
func ask(in *bufio.Scanner, prompt string) (string, bool) {
	fmt.Print(prompt)
	if !in.Scan() {
		fmt.Println()
		return "", false
	}
	return strings.Trim(strings.TrimSpace(in.Text()), `"`), true
}

// yes reads a yes/no answer. def is what Enter means, and the caller has
// already printed a sentence saying so.
func yes(in *bufio.Scanner, prompt string, def bool) (bool, bool) {
	for {
		a, ok := ask(in, prompt)
		if !ok {
			return false, false
		}
		switch strings.ToLower(a) {
		case "":
			return def, true
		case "y", "yes", "ok", "go", "go ahead":
			return true, true
		case "n", "no", "stop", "cancel":
			return false, true
		default:
			fmt.Println("  Please answer y or n, or press Enter.")
		}
	}
}

// askRoot settles the one thing that must be settled before anything is
// written: which folder this program owns.
func askRoot(in *bufio.Scanner) (*Root, bool) {
	def := defaultRoot()
	for {
		fmt.Println("  WHERE SHALL THE PROGRAMS GO?")
		fmt.Println()
		fmt.Println("  Everything I install lives in one folder. I write there and")
		fmt.Println("  nowhere else on your machine.")
		fmt.Println()
		fmt.Printf("  Press Enter to use %s\n", def)
		fmt.Println("  or type a different folder.")
		answer, ok := ask(in, "  > ")
		if !ok {
			return nil, false
		}
		if answer == "" {
			answer = def
		}

		abs, err := normalizeRoot(answer)
		if err != nil {
			fmt.Printf("\n  I cannot make sense of that: %v\n\n", err)
			continue
		}
		if err := checkRootSafety(abs); err != nil {
			fmt.Println()
			fmt.Printf("  %v\n", err)
			fmt.Println("  Pick a folder of your own -- somewhere inside your home folder is")
			fmt.Println("  the usual answer.")
			fmt.Println()
			continue
		}

		if r, err := openRoot(abs); err == nil {
			db, _ := r.loadInstalled()
			fmt.Println()
			fmt.Printf("  Using %s (already set up, %d program(s) in it).\n", r.Path, len(db.Programs))
			fmt.Println()
			return r, true
		}

		fmt.Println()
		fmt.Printf("  I will use this folder:  %s\n", abs)
		fmt.Println()
		fmt.Println("  If it does not exist I will create it, together with:")
		fmt.Printf("     %-14s the programs themselves\n", binDir+string(filepath.Separator))
		fmt.Printf("     %-14s removed programs, kept, never deleted\n", trashDir+string(filepath.Separator))
		fmt.Printf("     %-14s what is installed, at what version\n", stateDir+string(filepath.Separator))
		fmt.Printf("     %-14s a record of everything I do\n", ledgerName)
		fmt.Println()
		fmt.Println("  Nothing outside this folder is written, now or later.")
		fmt.Println()
		fmt.Println("  Press Enter to accept it, or n to type a different folder.")
		okYes, ok := yes(in, "  Use this folder? [Y/n] ", true)
		if !ok {
			return nil, false
		}
		if !okYes {
			fmt.Println()
			continue
		}
		r, err := confirmRoot(abs)
		if err != nil {
			fmt.Printf("\n  I could not set that folder up: %v\n\n", err)
			continue
		}
		fmt.Println()
		fmt.Printf("  Ready: %s\n", r.Path)
		fmt.Println()
		return r, true
	}
}

// guessSource looks in the obvious places for the folder of downloaded
// program files, so most people can press Enter here too.
func guessSource() string {
	var cands []string
	if v := strings.TrimSpace(os.Getenv("TECHLOSOFT_SOURCE")); v != "" {
		cands = append(cands, v)
	}
	if exe, err := os.Executable(); err == nil {
		dir := filepath.Dir(exe)
		cands = append(cands, filepath.Join(dir, "programs"), filepath.Join(dir, "techlosoft"), dir)
	}
	if cwd, err := os.Getwd(); err == nil {
		cands = append(cands, filepath.Join(cwd, "programs"), cwd)
	}
	for _, c := range cands {
		s, err := openSource(c)
		if err != nil {
			continue
		}
		if len(s.available()) > 0 {
			return s.Dir
		}
	}
	return ""
}

func askSource(in *bufio.Scanner, root *Root) *Source {
	fmt.Println("  WHERE ARE THE PROGRAM FILES?")
	fmt.Println()
	fmt.Println("  I do not download anything. I copy from the folder you already")
	fmt.Println("  have -- the one holding the files you downloaded, or a USB stick,")
	fmt.Println("  or a .tsb bundle somebody made for you.")
	fmt.Println()
	return askSourceOnce(in)
}

func askSourceOnce(in *bufio.Scanner) *Source {
	guess := guessSource()
	for {
		if guess != "" {
			fmt.Printf("  Press Enter to use %s\n", guess)
			fmt.Println("  or type a different folder.")
		} else {
			fmt.Println("  I could not find one automatically. Type the folder holding the")
			fmt.Println("  program files, or press Enter to carry on without one -- you can")
			fmt.Println("  still browse the catalogue and I will ask again when you install.")
		}
		answer, ok := ask(in, "  > ")
		if !ok {
			return nil
		}
		if answer == "" {
			if guess == "" {
				fmt.Println()
				fmt.Println("  Carrying on without a source folder. Nothing can be installed")
				fmt.Println("  until you give me one -- pick 'F' in the menu when you have it.")
				fmt.Println()
				return nil
			}
			answer = guess
		}
		s, err := openSource(answer)
		if err != nil {
			fmt.Println()
			fmt.Printf("  %v\n", err)
			fmt.Println("  Tip: you can drag the folder from Explorer onto this window to")
			fmt.Println("  paste its location, then press Enter.")
			fmt.Println()
			continue
		}
		have := s.available()
		if len(have) == 0 {
			fmt.Println()
			fmt.Printf("  %s exists, but I cannot see any Techlosoft program files in it.\n", s.Dir)
			fmt.Println("  I look for files named like  drivepulse-linux-amd64  or")
			fmt.Println("  drivepulse/drivepulse-linux-amd64  for this machine.")
			fmt.Println()
			fmt.Println("  Press Enter to carry on without one, or type another folder.")
			again, ok2 := ask(in, "  > ")
			if !ok2 {
				return nil
			}
			if again == "" {
				fmt.Println()
				return nil
			}
			answer = again
			guess = again
			continue
		}
		fmt.Println()
		fmt.Printf("  Found %d of the %d programs in %s\n", len(have), len(catalog), s.Dir)
		fmt.Println()
		return s
	}
}

// ------------------------------------------------------------- the menu ---

type menuKind int

const (
	menuPick menuKind = iota
	menuInstalled
	menuRemove
	menuUpdate
	menuSource
	menuQuit
)

type menuChoice struct {
	kind     menuKind
	programs []Program
	label    string
}

func mainMenu(in *bufio.Scanner, root *Root, src *Source, basket map[string]bool) (menuChoice, bool) {
	gs := groups()
	counts := map[string]int{}
	for _, p := range catalog {
		counts[p.Group]++
	}
	db, _ := root.loadInstalled()
	installedIn := map[string]int{}
	for _, e := range db.Programs {
		installedIn[e.Group]++
	}

	fmt.Println("  ----------------------------------------------------------------")
	fmt.Println("  WHAT WOULD YOU LIKE?")
	fmt.Println()
	fmt.Println("  The groups, and how many programs each one has:")
	fmt.Println()
	for i, g := range gs {
		have := ""
		if n := installedIn[g]; n > 0 {
			have = fmt.Sprintf("  (%d installed)", n)
		}
		fmt.Printf("   %2d  %-32s %2d%s\n", i+1, g, counts[g], have)
	}
	fmt.Println()
	fmt.Printf("    A  show me everything            %3d programs\n", len(catalog))
	fmt.Printf("    P  just the popular ones         %3d programs\n", popularCount())
	fmt.Printf("    I  what I already have           %3d installed\n", len(db.Programs))
	fmt.Println("    R  remove something")
	fmt.Println("    U  check for updates")
	fmt.Println("    F  change the folder I copy from")
	fmt.Println("    Q  finished, close this")
	fmt.Println()
	if len(basket) > 0 {
		fmt.Printf("  (you have picked %d program(s) so far this session)\n", len(basket))
	}
	fmt.Println("  Type a number or a letter. Press Enter for P, the popular ones.")

	for {
		answer, ok := ask(in, "  > ")
		if !ok {
			return menuChoice{}, false
		}
		if answer == "" {
			answer = "p"
		}
		switch strings.ToLower(answer) {
		case "a", "all", "everything", "show me everything":
			return menuChoice{kind: menuPick, programs: catalog, label: "the whole catalogue"}, true
		case "p", "popular":
			var pop []Program
			for _, p := range catalog {
				if p.Popular {
					pop = append(pop, p)
				}
			}
			return menuChoice{kind: menuPick, programs: pop, label: "the popular ones"}, true
		case "i", "installed":
			return menuChoice{kind: menuInstalled}, true
		case "r", "remove":
			return menuChoice{kind: menuRemove}, true
		case "u", "update", "updates":
			return menuChoice{kind: menuUpdate}, true
		case "f", "folder", "from":
			return menuChoice{kind: menuSource}, true
		case "q", "quit", "exit", "done", "finished":
			return menuChoice{kind: menuQuit}, true
		}
		// A group number, or a group name typed out.
		if n := atoiSafe(answer); n >= 1 && n <= len(gs) {
			ps, _ := programsInGroup(gs[n-1])
			return menuChoice{kind: menuPick, programs: ps, label: gs[n-1]}, true
		}
		if ps, err := programsInGroup(answer); err == nil {
			return menuChoice{kind: menuPick, programs: ps, label: answer}, true
		}
		fmt.Printf("  I did not understand %q. Type a number from 1 to %d, or one of\n", answer, len(gs))
		fmt.Println("  the letters above, or press Enter for the popular ones.")
	}
}

func popularCount() int {
	n := 0
	for _, p := range catalog {
		if p.Popular {
			n++
		}
	}
	return n
}

func atoiSafe(s string) int {
	n := 0
	if s == "" {
		return -1
	}
	for _, r := range s {
		if r < '0' || r > '9' {
			return -1
		}
		n = n*10 + int(r-'0')
	}
	return n
}

// ------------------------------------------------------ pick and install ---

// pickAndInstall shows a numbered list, takes a selection, keeps a running
// total, confirms, and installs. Returns false only if stdin closed.
func pickAndInstall(in *bufio.Scanner, root *Root, src *Source, basket map[string]bool, options []Program, label string) bool {
	db, err := root.loadInstalled()
	if err != nil {
		fmt.Printf("\n  I could not read what is already installed: %v\n\n", err)
		return true
	}
	have := src.available()

	fmt.Println()
	fmt.Printf("  %s -- %d programs\n", strings.ToUpper(label), len(options))
	fmt.Println()
	for i, p := range options {
		state := "        "
		if _, ok := db.Programs[p.Slug]; ok {
			state = "have it "
		} else if !have[p.Slug] {
			state = "no file "
		}
		size := ""
		if have[p.Slug] {
			size = humanBytes(src.sizeOf(p.Slug))
		}
		fmt.Printf("   %3d  %s %-18s %-9s %s\n", i+1, state, p.Slug, size, p.Plain)
	}
	fmt.Println()
	fmt.Println("  'have it' means it is already on this machine. 'no file' means")
	fmt.Println("  that program is not in the folder I am copying from.")
	fmt.Println()
	fmt.Println("  Pick by number: 3   or a range: 1-5   or several: 2,4,9")
	fmt.Println("  or type all, or type a name like drivepulse.")
	fmt.Println("  Press Enter on its own to go back to the menu without picking.")

	// The picking loop: each answer adds to the running total, and the total
	// is shown after every answer, so nobody has to hold it in their head.
	chosen := map[string]bool{}
	for {
		answer, ok := ask(in, "  > ")
		if !ok {
			return false
		}
		if answer == "" {
			if len(chosen) == 0 {
				fmt.Println()
				fmt.Println("  Nothing picked. Back to the menu.")
				fmt.Println()
				return true
			}
			break
		}
		slugs, err := ParseSelection(answer, options)
		if err != nil {
			fmt.Println()
			fmt.Printf("  %v\n", err)
			fmt.Printf("  Numbers run from 1 to %d. Try again, or press Enter to go back.\n", len(options))
			fmt.Println()
			continue
		}
		for _, s := range slugs {
			chosen[s] = true
		}
		if len(slugs) == 0 {
			chosen = map[string]bool{}
		}

		var total int64
		var installable, already, nofile int
		for s := range chosen {
			if _, ok := db.Programs[s]; ok {
				already++
			}
			if have[s] {
				total += src.sizeOf(s)
				installable++
			} else {
				nofile++
			}
		}
		fmt.Println()
		fmt.Printf("  Picked so far: %d program(s), %s of disk\n", len(chosen), humanBytes(total))
		if already > 0 {
			fmt.Printf("                 %d of them you already have (I will leave those alone\n", already)
			fmt.Println("                 unless the file has changed)")
		}
		if nofile > 0 {
			fmt.Printf("                 %d of them are not in the source folder and will be\n", nofile)
			fmt.Println("                 skipped")
		}
		_ = installable
		fmt.Println()
		fmt.Println("  Add more the same way, or press Enter to go ahead with these.")
	}

	// Reduce to what can actually be installed.
	var todo []string
	var total int64
	for _, p := range options {
		if chosen[p.Slug] && have[p.Slug] {
			todo = append(todo, p.Slug)
			total += src.sizeOf(p.Slug)
		}
	}
	if len(todo) == 0 {
		fmt.Println()
		fmt.Println("  None of what you picked is in the source folder, so there is")
		fmt.Println("  nothing I can copy. Back to the menu.")
		fmt.Println()
		return true
	}

	fmt.Println()
	fmt.Println("  ----------------------------------------------------------------")
	fmt.Println("  HERE IS EXACTLY WHAT WILL HAPPEN")
	fmt.Println()
	fmt.Printf("  %d file(s), %s in total, will be copied\n", len(todo), humanBytes(total))
	fmt.Printf("    from  %s\n", src.Dir)
	fmt.Printf("    into  %s\n", root.bin())
	fmt.Println()
	for _, s := range todo {
		p, _ := lookupProgram(s)
		fmt.Printf("    %-18s %-24s %s\n", s, p.Name, humanBytes(src.sizeOf(s)))
	}
	fmt.Println()
	fmt.Println("  Each file is checked against its SHA-256 after it is written, and")
	fmt.Println("  only then put in place. If a check fails that program is not")
	fmt.Println("  installed and nothing is left behind.")
	fmt.Println()
	fmt.Printf("  Nothing outside %s is written.\n", root.Path)
	fmt.Println("  Nothing is deleted.")
	fmt.Println()
	fmt.Println("  Press Enter to go ahead, or n to change your mind.")
	go1, ok := yes(in, "  Go ahead? [Y/n] ", true)
	if !ok {
		return false
	}
	if !go1 {
		fmt.Println()
		fmt.Println("  Nothing written. Back to the menu.")
		fmt.Println()
		return true
	}

	fmt.Println()
	fmt.Println("  Working.")
	fmt.Println()
	var okCount, failCount, sameCount int
	for i, slug := range todo {
		sf, err := src.find(slug)
		if err != nil {
			fmt.Printf("   [%d/%d] %-18s could not read it: %v\n", i+1, len(todo), slug, err)
			failCount++
			continue
		}
		res := root.installOne(db, payloadFromSource(sf), false)
		switch res.Action {
		case "installed":
			fmt.Printf("   [%d/%d] %-18s installed  %s\n", i+1, len(todo), slug, humanBytes(res.Size))
			okCount++
			basket[slug] = true
		case "updated":
			fmt.Printf("   [%d/%d] %-18s updated    %s -> %s\n", i+1, len(todo), slug, res.From, res.Version)
			okCount++
			basket[slug] = true
		case "unchanged":
			fmt.Printf("   [%d/%d] %-18s already exactly this, left alone\n", i+1, len(todo), slug)
			sameCount++
		default:
			fmt.Printf("   [%d/%d] %-18s FAILED: %s\n", i+1, len(todo), slug, res.Error)
			failCount++
		}
	}
	if err := root.saveInstalled(db); err != nil {
		fmt.Printf("\n  Warning: I could not save the record of that: %v\n", err)
	}

	fmt.Println()
	fmt.Println("  ----------------------------------------------------------------")
	fmt.Printf("  DONE. %d installed, %d already there, %d failed.\n", okCount, sameCount, failCount)
	fmt.Println()
	fmt.Printf("  You now have %d Techlosoft programs on this machine:\n", len(db.Programs))
	fmt.Println()
	showInstalledInto(db, root)
	fmt.Println()
	fmt.Printf("  They are in %s\n", root.bin())
	fmt.Println("  Run one by name from a command prompt, or add that folder to your")
	fmt.Println("  PATH so you can run them from anywhere.")
	fmt.Println()
	fmt.Printf("  Everything I just did is written down in %s\n", root.ledgerPath())
	fmt.Println()

	// Offer the update check, as promised.
	fmt.Println("  Shall I check whether anything you have is out of date?")
	fmt.Println("  Press Enter for yes.")
	doUpd, ok := yes(in, "  Check for updates? [Y/n] ", true)
	if !ok {
		return false
	}
	if doUpd {
		updateFlow(in, root, src)
	}
	fmt.Println()
	return true
}

// --------------------------------------------------------- the other bits ---

func showInstalled(root *Root) {
	db, err := root.loadInstalled()
	if err != nil {
		fmt.Printf("\n  I could not read that: %v\n\n", err)
		return
	}
	fmt.Println()
	if len(db.Programs) == 0 {
		fmt.Println("  Nothing installed yet. Pick a group from the menu and I will")
		fmt.Println("  show you what is in it.")
		fmt.Println()
		return
	}
	fmt.Printf("  YOU HAVE %d PROGRAM(S)\n", len(db.Programs))
	fmt.Println()
	showInstalledInto(db, root)
	used, _ := root.diskUsed()
	trash, _ := root.listTrash()
	fmt.Println()
	fmt.Printf("  in %s, using %s\n", root.bin(), humanBytes(used))
	if len(trash) > 0 {
		fmt.Printf("  plus %d item(s) sitting in the trash at %s\n", len(trash), root.trash())
	}
	fmt.Println()
}

func showInstalledInto(db *InstalledDB, root *Root) {
	byGroup := map[string][]InstalledEntry{}
	for _, e := range db.Programs {
		byGroup[e.Group] = append(byGroup[e.Group], e)
	}
	var gs []string
	for g := range byGroup {
		gs = append(gs, g)
	}
	sort.Strings(gs)
	for _, g := range gs {
		es := byGroup[g]
		sort.Slice(es, func(i, j int) bool { return catalogRank(es[i].Slug) < catalogRank(es[j].Slug) })
		fmt.Printf("    %s\n", g)
		for _, e := range es {
			fmt.Printf("      %-18s %-8s %s\n", e.Slug, e.Version, humanBytes(e.Size))
		}
	}
}

func removeFlow(in *bufio.Scanner, root *Root) bool {
	db, err := root.loadInstalled()
	if err != nil {
		fmt.Printf("\n  I could not read that: %v\n\n", err)
		return true
	}
	slugs := db.installedSlugs()
	if len(slugs) == 0 {
		fmt.Println()
		fmt.Println("  Nothing is installed, so there is nothing to remove.")
		fmt.Println()
		return true
	}
	var options []Program
	for _, s := range slugs {
		if p, ok := lookupProgram(s); ok {
			options = append(options, p)
		}
	}

	fmt.Println()
	fmt.Println("  WHAT SHALL I TAKE OFF?")
	fmt.Println()
	for i, p := range options {
		e := db.Programs[p.Slug]
		fmt.Printf("   %3d  %-18s %-8s %-9s %s\n", i+1, p.Slug, e.Version, humanBytes(e.Size), p.Name)
	}
	fmt.Println()
	fmt.Println("  Removing does NOT delete the file. It moves it to")
	fmt.Printf("    %s\n", root.trash())
	fmt.Println("  where you can look at it, copy it back, or delete it yourself.")
	fmt.Println()
	fmt.Println("  Pick by number, range or name. Press Enter to change nothing.")

	answer, ok := ask(in, "  > ")
	if !ok {
		return false
	}
	if answer == "" {
		fmt.Println()
		fmt.Println("  Nothing removed. Back to the menu.")
		fmt.Println()
		return true
	}
	picked, err := ParseSelection(answer, options)
	if err != nil {
		fmt.Println()
		fmt.Printf("  %v\n", err)
		fmt.Println("  Nothing removed. Back to the menu.")
		fmt.Println()
		return true
	}
	if len(picked) == 0 {
		fmt.Println()
		fmt.Println("  Nothing removed. Back to the menu.")
		fmt.Println()
		return true
	}

	fmt.Println()
	fmt.Printf("  I will move %d program(s) into %s:\n", len(picked), root.trash())
	for _, s := range picked {
		fmt.Printf("    %s\n", s)
	}
	fmt.Println()
	fmt.Println("  Nothing is deleted. Press Enter to go ahead, or n to stop.")
	go1, ok := yes(in, "  Go ahead? [Y/n] ", true)
	if !ok {
		return false
	}
	if !go1 {
		fmt.Println()
		fmt.Println("  Nothing removed.")
		fmt.Println()
		return true
	}
	fmt.Println()
	for _, s := range picked {
		res := root.removeOne(db, s)
		switch res.Action {
		case "moved-to-trash":
			fmt.Printf("   %-18s moved to %s\n", s, res.Trash)
		case "record-only":
			fmt.Printf("   %-18s the file had already gone; record cleared\n", s)
		case "not-installed":
			fmt.Printf("   %-18s was not installed\n", s)
		default:
			fmt.Printf("   %-18s FAILED: %s\n", s, res.Error)
		}
	}
	if err := root.saveInstalled(db); err != nil {
		fmt.Printf("\n  Warning: could not save the record: %v\n", err)
	}
	fmt.Println()
	fmt.Printf("  %d program(s) left installed. The removed files are still in\n", len(db.Programs))
	fmt.Printf("  %s if you want them back.\n", root.trash())
	fmt.Println()
	return true
}

func updateFlow(in *bufio.Scanner, root *Root, src *Source) {
	db, err := root.loadInstalled()
	if err != nil {
		fmt.Printf("\n  I could not read that: %v\n\n", err)
		return
	}
	fmt.Println()
	if len(db.Programs) == 0 {
		fmt.Println("  Nothing is installed, so there is nothing to update.")
		fmt.Println()
		return
	}
	if src == nil {
		fmt.Println("  I need a folder of program files to compare against, and I do")
		fmt.Println("  not have one. Pick 'F' in the menu to point me at it.")
		fmt.Println()
		return
	}

	fmt.Printf("  Comparing your %d installed program(s) against %s\n", len(db.Programs), src.Dir)
	fmt.Println()
	var updates []UpdateInfo
	var missing int
	for _, slug := range db.installedSlugs() {
		sf, err := src.find(slug)
		if err != nil {
			missing++
			continue
		}
		u := needsUpdate(db.Programs[slug], sf)
		if u.Reason != "up-to-date" {
			updates = append(updates, u)
		}
	}
	if len(updates) == 0 {
		fmt.Println("  Everything you have is up to date.")
		if missing > 0 {
			fmt.Printf("  (%d program(s) are not in that folder, so I could not check them)\n", missing)
		}
		fmt.Println()
		return
	}
	for _, u := range updates {
		switch u.Reason {
		case "newer-version":
			fmt.Printf("    %-18s %s -> %s   (newer version)\n", u.Slug, u.Installed, u.Available)
		case "rebuilt":
			fmt.Printf("    %-18s %s        (same version, different build)\n", u.Slug, u.Installed)
		}
	}
	fmt.Println()
	fmt.Printf("  %d update(s) waiting, %s to copy.\n", len(updates), humanBytes(totalOf(updates)))
	fmt.Println("  Installing an update replaces the file only after the new one has")
	fmt.Println("  been written and checked. If anything goes wrong the version you")
	fmt.Println("  have now stays exactly where it is.")
	fmt.Println()
	fmt.Println("  Press Enter to install them, or n to leave things as they are.")
	go1, ok := yes(in, "  Install the updates? [Y/n] ", true)
	if !ok || !go1 {
		fmt.Println()
		fmt.Println("  Left alone. You can do this any time from the menu.")
		fmt.Println()
		return
	}
	fmt.Println()
	var done, failed int
	for i, u := range updates {
		sf, err := src.find(u.Slug)
		if err != nil {
			fmt.Printf("   [%d/%d] %-18s could not read it: %v\n", i+1, len(updates), u.Slug, err)
			failed++
			continue
		}
		res := root.installOne(db, payloadFromSource(sf), false)
		if res.Action == "failed" {
			fmt.Printf("   [%d/%d] %-18s FAILED: %s\n", i+1, len(updates), u.Slug, res.Error)
			failed++
			continue
		}
		fmt.Printf("   [%d/%d] %-18s now at %s\n", i+1, len(updates), u.Slug, res.Version)
		done++
	}
	if err := root.saveInstalled(db); err != nil {
		fmt.Printf("\n  Warning: could not save the record: %v\n", err)
	}
	fmt.Println()
	fmt.Printf("  %d updated, %d failed.\n", done, failed)
	fmt.Println()
}

func totalOf(us []UpdateInfo) int64 {
	var n int64
	for _, u := range us {
		n += u.Size
	}
	return n
}

// closing is the last thing on screen, always. Explorer destroys the console
// the instant the process exits, so without this nobody reads any of it.
func closing(in *bufio.Scanner) {
	fmt.Println()
	fmt.Println("  ----------------------------------------------------------------")
	fmt.Println("  All done.")
	fmt.Println()
	fmt.Println("  Run this again any time to add programs, take them off, or check")
	fmt.Println("  for updates. There is a command-line version of everything here:")
	fmt.Println("    techlosoft-suite --help")
	fmt.Println()
	fmt.Print("  Press Enter to close this window. ")
	in.Scan()
	fmt.Println()
}
