package main

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

// guidedListLimit caps the file listing the guided session prints. A full
// distribution image holds tens of thousands of entries, and a console window
// that scrolls for a minute has hidden the volume details the reader came for.
const guidedListLimit = 25

// 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.
//
// ISOPilot can write one thing: a file pulled out of an image by the extract
// command. The guided session never extracts, so it writes nothing at all — it
// opens the image read-only, describes it, and lists what is inside.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  ISOPilot")
	fmt.Println("  Look inside a disc image without burning or mounting it.")
	fmt.Println()
	fmt.Println("  Give it a .iso file and it reads the disc's own label — what the")
	fmt.Println("  volume is called, who made it, when, how big it should be — and")
	fmt.Println("  then lists the files stored inside it.")
	fmt.Println()
	fmt.Println("  The image is opened read-only. Nothing inside it is changed and")
	fmt.Println("  nothing is written to your disk.")
	fmt.Println()

	suggested := suggestedImage()
	for {
		fmt.Println("  Which disc image shall I open?")
		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 .iso file to open. 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 the .iso 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 common near-miss, so look inside it
			// rather than simply refusing.
			if found := firstImageIn(answer); found != "" {
				fmt.Println()
				fmt.Printf("  That is a folder. I found %s inside it, so I will use that.\n", filepath.Base(found))
				answer = found
				break
			}
			fmt.Println()
			fmt.Printf("  %q is a folder and I cannot see a .iso file in it.\n", answer)
			fmt.Println("  Give me the .iso file itself.")
			fmt.Println()
			continue
		case !info.Mode().IsRegular():
			fmt.Println()
			fmt.Printf("  %q is not an ordinary file. Give me a .iso file.\n", answer)
			fmt.Println()
			continue
		}

		fmt.Println()
		describeImage(answer)
		break
	}

	fmt.Println()
	fmt.Println("  Names on a plain ISO 9660 disc are short and upper-case, so a file")
	fmt.Println("  authored as readme-first.txt may appear as /README_F.TXT.")
	fmt.Println()
	fmt.Println("  The command-line version can pull a single file back out of the")
	fmt.Println("  image, and print all of this as JSON: isopilot help")
	pause(in)
}

// describeImage runs the read-only half of ISOPilot over one image: the same
// info command, then a capped version of the same listing. Neither opens the
// image for writing and neither creates a file.
func describeImage(path string) {
	if err := cmdInfo([]string{path}); err != nil {
		fmt.Println()
		fmt.Printf("  I could not read %s as a disc image.\n", filepath.Base(path))
		fmt.Println("  ISOPilot reads plain ISO 9660 discs. An image that is UDF-only —")
		fmt.Println("  most video DVDs and Blu-rays are — has no ISO 9660 structure to")
		fmt.Println("  read, and a half-finished download has nothing to read yet.")
		return
	}

	im, err := openImage(path)
	if err != nil {
		return
	}
	defer im.Close()

	entries, err := im.entries()
	if err != nil {
		fmt.Println()
		fmt.Println("  The disc label read correctly, but the list of files inside it is")
		fmt.Println("  damaged, so I stopped there rather than print nonsense.")
		return
	}

	var files, dirs int
	var bytes int64
	for _, e := range entries {
		if e.IsDir {
			dirs++
			continue
		}
		files++
		bytes += e.Size
	}

	fmt.Println()
	fmt.Printf("  Inside: %d file(s) and %d folder(s), %s of file data.\n", files, dirs, humanBytes(bytes))
	fmt.Println()

	shown := entries
	sort.Slice(shown, func(i, j int) bool { return shown[i].Path < shown[j].Path })
	if len(shown) > guidedListLimit {
		shown = shown[:guidedListLimit]
	}
	for _, e := range shown {
		kind := "file"
		size := humanBytes(e.Size)
		if e.IsDir {
			kind = "dir "
			size = ""
		}
		fmt.Printf("  %s  %10s  %s\n", kind, size, displayPath(e.Path))
	}
	if len(entries) > guidedListLimit {
		fmt.Printf("  ... and %d more. Run \"isopilot list\" to see all of them.\n", len(entries)-guidedListLimit)
	}
}

// suggestedImage offers a disc image the reader is likely to have, so the
// prompt can be answered with one keypress. It returns "" when there is
// nothing to suggest, which the prompt copes with.
func suggestedImage() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	candidates := []string{
		filepath.Join(home, "Downloads"),
		filepath.Join(home, "Desktop"),
		home,
	}
	for _, dir := range candidates {
		if found := firstImageIn(dir); found != "" {
			return found
		}
	}
	return ""
}

// firstImageIn returns the first .iso file directly inside dir, or "".
func firstImageIn(dir string) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	var names []string
	for _, e := range entries {
		if e.IsDir() || !strings.EqualFold(filepath.Ext(e.Name()), ".iso") {
			continue
		}
		names = append(names, e.Name())
	}
	if len(names) == 0 {
		return ""
	}
	sort.Strings(names)
	candidate := filepath.Join(dir, names[0])
	if info, err := os.Stat(candidate); err != nil || !info.Mode().IsRegular() {
		return ""
	}
	return candidate
}

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