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.
//
// Guided mode runs the scan, which writes nothing whatsoever. Carving the
// files back out is a command-line job, and this file offers no way to ask
// for it.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  CardRecovery")
	fmt.Println("  Finds the photos still sitting on a memory card whose files have")
	fmt.Println("  vanished.")
	fmt.Println()
	fmt.Println("  Give it a copy of the card — an image file made with dd, Win32 Disk")
	fmt.Println("  Imager or similar — and it reads the raw bytes looking for the")
	fmt.Println("  beginning of every photo and document, then follows each one to")
	fmt.Println("  its end. It does not need the card's file list; that is the point.")
	fmt.Println()
	fmt.Println("  This only looks and reports. Nothing is written or recovered here,")
	fmt.Println("  and the image file itself is opened read-only.")
	fmt.Println()

	suggested := suggestedImage()
	for {
		fmt.Println("  Which card image shall I scan?")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		} else {
			fmt.Println("  Tip: you can drag the image file, or the folder holding it, from")
			fmt.Println("  Explorer onto this window to paste its location.")
		}
		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 card image to scan. Try again, or close this window.")
			fmt.Println()
			fmt.Println("  If you have not made one yet: copy the whole card to a file")
			fmt.Println("  first, and work on that copy. Never carve from the only copy")
			fmt.Println("  you have.")
			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 the image file, or the folder holding it, from")
			fmt.Println("  Explorer onto this window to paste its location, then press Enter.")
			fmt.Println()
			continue
		case info.IsDir():
			// A dragged folder is a fair guess at where the image lives.
			found := imageInDir(answer)
			if found == "" {
				fmt.Println()
				fmt.Printf("  %q is a folder, and I found no card image inside it.\n", answer)
				fmt.Println("  Drag the image file itself onto this window instead.")
				fmt.Println()
				continue
			}
			fmt.Println()
			fmt.Printf("  That is a folder. Using the image inside it: %s\n", found)
			answer = found
		}

		fmt.Println()
		fmt.Println("  Scanning. A whole card takes a few minutes — it reads every byte.")
		fmt.Println()
		guidedScan(answer)
		break
	}

	fmt.Println()
	fmt.Println("  Done, and nothing was written. To carve those files back out into a")
	fmt.Println("  folder of your choosing, use the command-line version:")
	fmt.Println("  cardrecovery --help")
	pause(in)
}

// guidedScan runs the scan command, which writes nothing and opens the image
// read-only. run returns its exit code rather than calling os.Exit, so an
// unreadable image leaves the window standing.
func guidedScan(path string) {
	if code := run([]string{"scan", path}); code != 0 {
		fmt.Println()
		fmt.Println("  I could not scan that — see the message above. A card image is a")
		fmt.Println("  byte-for-byte copy of the whole card, not a folder of files")
		fmt.Println("  copied off it.")
	}
}

// cardImageExts are the extensions raw card images are usually saved with.
// (imageExts, without the prefix, already means "a picture" in this program.)
var cardImageExts = []string{".img", ".dd", ".raw", ".bin", ".image"}

// imageInDir returns the largest card image directly inside dir, or "".
// Largest, because a raw card copy is bigger than anything else likely to be
// sitting beside it under the same name.
func imageInDir(dir string) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	type candidate struct {
		path string
		size int64
	}
	var found []candidate
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		ext := strings.ToLower(filepath.Ext(e.Name()))
		match := false
		for _, want := range cardImageExts {
			if ext == want {
				match = true
				break
			}
		}
		if !match {
			continue
		}
		info, err := e.Info()
		if err != nil {
			continue
		}
		found = append(found, candidate{filepath.Join(dir, e.Name()), info.Size()})
	}
	if len(found) == 0 {
		return ""
	}
	sort.Slice(found, func(i, j int) bool {
		if found[i].size != found[j].size {
			return found[i].size > found[j].size
		}
		return found[i].path < found[j].path
	})
	return found[0].path
}

// suggestedImage offers a card image that is certain to exist, so the reader
// can get going by pressing one key. It returns "" when there is nothing
// nearby, and the prompt asks for a path instead.
func suggestedImage() string {
	var dirs []string
	if wd, err := os.Getwd(); err == nil {
		dirs = append(dirs, wd)
	}
	// The folder the program was double-clicked in is very often the folder
	// the card image was just written into.
	if exe, err := os.Executable(); err == nil {
		dirs = append(dirs, filepath.Dir(exe))
	}
	if home, err := os.UserHomeDir(); err == nil {
		dirs = append(dirs, home,
			filepath.Join(home, "Desktop"),
			filepath.Join(home, "Documents"),
			filepath.Join(home, "Downloads"))
	}
	for _, dir := range dirs {
		if found := imageInDir(dir); found != "" {
			return found
		}
	}
	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()
}
