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
// 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.
//
// The one file imagedeck can write is the catalog. The guided session does not
// write it: it runs the same scan the index command runs, holds the result in
// memory, prints it, and lets it go. Nothing is saved, and nothing in the
// scanned folder is touched.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  imagedeck")
	fmt.Println("  Take stock of a folder full of disc images.")
	fmt.Println()
	fmt.Println("  Point it at a folder of .iso files and it opens each one, reads the")
	fmt.Println("  disc label inside, works out whether it can boot, and tells you")
	fmt.Println("  which of them are identical copies of each other wasting disk space.")
	fmt.Println()
	fmt.Println("  Every image is opened read-only. Nothing is moved, changed or")
	fmt.Println("  deleted, and no catalog file is saved.")
	fmt.Println()

	suggested := suggestedFolder()
	for {
		fmt.Println("  Which folder holds your disc images?")
		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 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("  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
		}

		fmt.Println()
		fmt.Println("  Reading. Every image is hashed from end to end, so a folder of")
		fmt.Println("  large images takes a while.")
		fmt.Println()
		surveyLibrary(answer)
		break
	}

	fmt.Println()
	fmt.Println("  The command-line version saves all of this as a catalog file, so it")
	fmt.Println("  can tell you later what has appeared, vanished or changed since")
	fmt.Println("  today, and whether a new download is one you already have.")
	fmt.Println("  imagedeck --help")
	pause(in)
}

// surveyLibrary runs the same scan the index command runs, including
// sub-folders, and prints the same tables — but keeps the result in memory
// instead of saving a catalog.
func surveyLibrary(dir string) {
	root := absDir(dir)

	entries, failures, err := scanDir(dir, true)
	if err != nil {
		fmt.Printf("  I could not read the files in %s.\n", dir)
		fmt.Println("  It may be a folder Windows will not let this program open.")
		return
	}

	if len(entries) == 0 && len(failures) == 0 {
		fmt.Printf("  There are no .iso files in %s or its sub-folders.\n", dir)
		fmt.Println("  imagedeck only looks at files whose name ends in .iso.")
		return
	}

	if len(entries) > 0 {
		fmt.Printf("Found %s:\n\n", plural(len(entries), "disc image", "disc images"))
		printEntryTable(root, entries)
	}
	printTruncated(root, entries)
	printFailures(root, failures)
	printGuidedDuplicates(root, entries)
}

// printGuidedDuplicates answers the question the tool exists for: which of
// these files are byte-for-byte the same disc under different names. It groups
// by the SHA-256 already computed during the scan, exactly as the duplicates
// command does.
func printGuidedDuplicates(root string, entries []CatalogEntry) {
	if len(entries) < 2 {
		return
	}

	byHash := map[string][]CatalogEntry{}
	for _, e := range entries {
		if e.SHA256 == "" {
			continue
		}
		byHash[e.SHA256] = append(byHash[e.SHA256], e)
	}

	var hashes []string
	for h, group := range byHash {
		if len(group) > 1 {
			hashes = append(hashes, h)
		}
	}
	if len(hashes) == 0 {
		fmt.Println()
		fmt.Println("No duplicates: every image here holds different bytes.")
		return
	}
	sort.Strings(hashes)

	var reclaimable int64
	fmt.Printf("\nDUPLICATES - %s of identical images:\n", plural(len(hashes), "group", "groups"))
	for _, h := range hashes {
		group := byHash[h]
		sort.Slice(group, func(i, j int) bool { return group[i].Path < group[j].Path })
		waste := int64(len(group)-1) * group[0].Size
		reclaimable += waste
		fmt.Printf("  %s  %s x%d  (%s recoverable)\n",
			shortHash(h), humanBytes(group[0].Size), len(group), humanBytes(waste))
		for _, e := range group {
			fmt.Printf("      %s\n", displayPath(root, e.Path))
		}
	}
	fmt.Printf("\n  %s could be recovered by keeping one copy of each.\n", humanBytes(reclaimable))
	fmt.Println("  These are identical byte for byte, not merely the same size.")
}

// suggestedFolder offers somewhere worth looking that is certain to exist, so
// the reader can get a useful answer by pressing one key.
func suggestedFolder() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	// Downloads is where .iso files land before anybody files them away.
	for _, name := range []string{"Downloads", "Desktop"} {
		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()
}
