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.
//
// TransferForge's one command COPIES FILES, so the guided session deliberately
// does not run it. Somebody who double-clicked an unfamiliar icon has not
// chosen a destination and has not agreed to write anything, and a copy that
// starts on its own is the last thing they want. What this session does
// instead is the read-only half: walk the folder, classify it exactly the way
// a real transfer would, and show what a transfer would contain. It never
// writes a file, never creates a destination, and never offers to.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  TransferForge")
	fmt.Println("  Copies a camera card or shoot folder somewhere safe, checks every")
	fmt.Println("  file arrived byte for byte, and writes a receipt you can hand to a")
	fmt.Println("  client.")
	fmt.Println()
	fmt.Println("  This window does the looking, not the copying. Point it at a folder")
	fmt.Println("  and it tells you what a transfer would contain: how many photos,")
	fmt.Println("  videos and sound files, and how much space they take. Nothing is")
	fmt.Println("  copied, moved or changed by anything you do here.")
	fmt.Println()

	suggested := suggestedFolder()
	for {
		fmt.Println("  Which folder shall I look at?")
		fmt.Println("  (your camera card, or the folder you copied it into)")
		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 the folder. On a full card this can take a minute.")
		fmt.Println()
		previewTransfer(answer)
		break
	}

	fmt.Println()
	fmt.Println("  Nothing was copied — this was a look, not a transfer.")
	fmt.Println("  The command-line version does the verified copy and writes the")
	fmt.Println("  client receipt: transferforge --help")
	pause(in)
}

// previewTransfer reports what a transfer of this folder would contain. It
// opens nothing and writes nothing: every number here comes from the directory
// listing, using the same classification a real transfer applies.
func previewTransfer(root string) {
	rels, err := listFiles(root)
	if err != nil {
		fmt.Printf("  I could not read all of %q: %v\n", root, err)
		return
	}
	if len(rels) == 0 {
		fmt.Printf("  %s is empty — there is nothing here to transfer.\n", root)
		return
	}

	counts := map[string]int{}
	bytes := map[string]int64{}
	largestOf := map[string]string{}
	largestSize := map[string]int64{}
	var totalFiles int
	var totalBytes int64
	var unreadable int

	for _, rel := range rels {
		info, err := os.Stat(filepath.Join(root, rel))
		if err != nil {
			unreadable++
			continue
		}
		group := classify(rel)
		counts[group]++
		bytes[group] += info.Size()
		totalFiles++
		totalBytes += info.Size()
		if info.Size() >= largestSize[group] {
			largestSize[group] = info.Size()
			largestOf[group] = rel
		}
	}

	fmt.Printf("A transfer of %s would carry:\n\n", root)
	for _, g := range groupOrder {
		if counts[g] == 0 {
			continue
		}
		fmt.Printf("  %-6s %-8s %10s   largest: %s\n",
			g, plural(counts[g], "file"), humanBytes(bytes[g]), largestOf[g])
	}
	fmt.Printf("\n  Total  %-8s %10s\n", plural(totalFiles, "file"), humanBytes(totalBytes))
	if unreadable > 0 {
		fmt.Printf("\n  %s could not be read and would need attention first.\n", plural(unreadable, "file"))
	}

	fmt.Println("\nFolders it would come from:")
	for _, d := range topFolders(rels, 8) {
		fmt.Printf("  %s\n", d)
	}
}

// plural writes a count in words a person would use, so a one-file folder does
// not report "1 files".
func plural(n int, noun string) string {
	if n == 1 {
		return fmt.Sprintf("1 %s", noun)
	}
	return fmt.Sprintf("%d %ss", n, noun)
}

// topFolders lists the sub-folders holding files, so the reader can recognise
// the card they are looking at (DCIM/100CANON and friends).
func topFolders(rels []string, limit int) []string {
	seen := map[string]bool{}
	var dirs []string
	for _, rel := range rels {
		dir := filepath.ToSlash(filepath.Dir(rel))
		if dir == "." {
			dir = "(top level)"
		}
		if !seen[dir] {
			seen[dir] = true
			dirs = append(dirs, dir)
		}
	}
	sort.Strings(dirs)
	if len(dirs) > limit {
		extra := len(dirs) - limit
		dirs = append(dirs[:limit:limit], fmt.Sprintf("... and %d more", extra))
	}
	return dirs
}

// 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 ""
	}
	// A card is usually offloaded into Pictures, and downloaded footage lands
	// in Downloads; both are far more interesting than a bare home directory.
	for _, name := range []string{"Pictures", "Downloads"} {
		candidate := filepath.Join(home, name)
		if info, err := os.Stat(candidate); err == nil && info.IsDir() {
			return candidate
		}
	}
	if info, err := os.Stat(home); err != nil || !info.IsDir() {
		return ""
	}
	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()
}
