package main

import (
	"bufio"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// guidedMaxSize is the same default build uses: files larger than this are
// not text worth indexing, so they are left out of the picture.
const guidedMaxSize = 5 * 1024 * 1024

// 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 indexdesk writes is the index that build saves. The guided
// session does not write it, and does not ask the reader to choose a place to
// put it: it walks the folder, works out the same facets build would record,
// prints them, and stops. Nothing is saved and nothing is changed.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  indexdesk")
	fmt.Println("  See the shape of a folder full of documents.")
	fmt.Println()
	fmt.Println("  It looks through a folder and everything under it, and counts what")
	fmt.Println("  it finds: how many files of each kind, how big they are, and how")
	fmt.Println("  long since each was last touched.")
	fmt.Println()
	fmt.Println("  It only reads. Nothing is moved, changed or deleted, and no index")
	fmt.Println("  file is saved.")
	fmt.Println()

	suggested := suggestedFolder()
	for {
		fmt.Println("  Which folder shall I look through?")
		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 through. 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("  Looking. On a large folder this can take a minute.")
		fmt.Println()
		surveyFolder(answer)
		break
	}

	fmt.Println()
	fmt.Println("  The command-line version saves this as an index you can then search")
	fmt.Println("  by word, narrowing the results by any of the groupings above.")
	fmt.Println("  indexdesk --help")
	pause(in)
}

// surveyFolder walks the tree the way build walks it, skipping exactly what
// build skips, and prints the facet counts build would have recorded. The
// difference is that nothing is written: no term index is built and no file is
// created.
func surveyFolder(dir string) {
	root, err := filepath.Abs(dir)
	if err != nil {
		root = dir
	}

	var docs []doc
	var skippedCount int
	var indexedBytes int64

	walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			skippedCount++
			return nil
		}
		if d.IsDir() {
			if path != root && d.Name() == ".git" {
				return filepath.SkipDir
			}
			return nil
		}
		if !d.Type().IsRegular() {
			skippedCount++
			return nil
		}
		st, err := d.Info()
		if err != nil {
			skippedCount++
			return nil
		}
		rel, err := filepath.Rel(root, path)
		if err != nil {
			rel = path
		}
		rel = filepath.ToSlash(rel)
		ext := strings.ToLower(filepath.Ext(path))

		if st.Size() > guidedMaxSize || binaryExts[ext] {
			skippedCount++
			return nil
		}
		f, err := os.Open(path)
		if err != nil {
			skippedCount++
			return nil
		}
		head := make([]byte, sniffBytes)
		n, _ := io.ReadFull(f, head)
		f.Close()
		if looksBinary(head[:n]) {
			skippedCount++
			return nil
		}

		docs = append(docs, doc{
			Path:  rel,
			Ext:   ext,
			Size:  st.Size(),
			MTime: st.ModTime().Unix(),
		})
		indexedBytes += st.Size()
		return nil
	})
	if walkErr != nil {
		fmt.Printf("  I could not read all of %s.\n", dir)
		fmt.Println("  It may hold something Windows will not let this program open.")
		return
	}

	if len(docs) == 0 {
		fmt.Printf("  I found no readable text files in %s.\n", dir)
		fmt.Println("  indexdesk skips pictures, videos, archives, PDFs and other")
		fmt.Println("  non-text files, so a folder of those looks empty to it.")
		if skippedCount > 0 {
			fmt.Printf("  (%d file(s) were skipped for that reason.)\n", skippedCount)
		}
		return
	}

	fmt.Printf("Folder:  %s\n", root)
	fmt.Printf("Found:   %d text file(s), %s in total\n", len(docs), humanBytes(indexedBytes))
	if skippedCount > 0 {
		fmt.Printf("Skipped: %d file(s) that are not text, or are over %s\n", skippedCount, humanBytes(guidedMaxSize))
	}
	fmt.Println()

	printFacetSet(computeFacets(docs, time.Now(), true))
}

// 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 ""
	}
	for _, name := range []string{"Documents", "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()
}
