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 guided session runs "check" and only "check". "merge" writes a new deck
// file and "export" writes a document; neither is something to do on somebody's
// behalf because they double-clicked an icon. Check reads the binding sets,
// reports every conflict it finds, and writes nothing at all.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  WindowDeck")
	fmt.Println("  Finds the keyboard shortcuts your team is fighting over.")
	fmt.Println()
	fmt.Println("  Give it everyone's shortcut files and it tells you which key")
	fmt.Println("  combinations two people have claimed for different things, which")
	fmt.Println("  ones are spelled differently but mean the same keys, and which ones")
	fmt.Println("  Windows or macOS has already taken for itself.")
	fmt.Println()
	fmt.Println("  It only reads those files. Nothing is merged, written or changed.")
	fmt.Println()

	suggested := suggestedSetFolder()
	for {
		fmt.Println("  Where are the shortcut files?")
		fmt.Println("  (the folder holding everyone's .json files, or one file on its own)")
		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 somewhere to look. Try again, or close this window.")
			fmt.Println()
			continue
		}

		info, err := os.Stat(answer)
		if 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
		}

		var sets []string
		if info.IsDir() {
			sets = bindingSetsIn(answer)
			if len(sets) == 0 {
				fmt.Println()
				fmt.Printf("  I looked in %s and found no shortcut files.\n", answer)
				fmt.Println("  A shortcut file is a .json file with a \"bindings\" list in it.")
				fmt.Println()
				continue
			}
		} else {
			if _, _, err := loadSetFile(answer, false); err != nil {
				fmt.Println()
				fmt.Printf("  %q is not a shortcut file I can read.\n", filepath.Base(answer))
				fmt.Printf("  (%v)\n", err)
				fmt.Println()
				continue
			}
			sets = []string{answer}
		}

		// loadInputs is the same reader "check" uses, and it is where sets that
		// clash with each other are refused. Doing it here means the reader
		// gets a sentence and another go, rather than the process exiting and
		// the window disappearing with it.
		if _, err := loadInputs("", sets); err != nil {
			fmt.Println()
			fmt.Println("  I could not use those files together:")
			fmt.Printf("  %v\n", err)
			fmt.Println()
			continue
		}

		fmt.Println()
		fmt.Printf("  Checking %s:\n", plural(len(sets), "shortcut file"))
		for _, s := range sets {
			fmt.Printf("    %s\n", filepath.Base(s))
		}
		fmt.Println()

		argv := make([]string, 0, len(sets)*2)
		for _, s := range sets {
			argv = append(argv, "--set", s)
		}
		runCheck(argv)
		break
	}

	fmt.Println()
	fmt.Println("  Nothing was changed — this was a check, and the files are as they")
	fmt.Println("  were. The command-line version is the one that merges everyone's")
	fmt.Println("  shortcuts into a single agreed deck: windowdeck --help")
	pause(in)
}

// plural writes a count the way a person would, so one file does not report
// "1 shortcut files".
func plural(n int, noun string) string {
	if n == 1 {
		return "1 " + noun
	}
	return fmt.Sprintf("%d %ss", n, noun)
}

// suggestedSetFolder offers a folder that really does hold shortcut files, so
// the common case is one keypress. The folder the program was launched from
// comes first: somebody who put windowdeck next to their team's files and
// double-clicked it means that folder.
func suggestedSetFolder() string {
	var dirs []string
	if wd, err := os.Getwd(); err == nil {
		dirs = append(dirs, wd)
	}
	if home, err := os.UserHomeDir(); err == nil {
		dirs = append(dirs, filepath.Join(home, "Desktop"),
			filepath.Join(home, "Documents"),
			filepath.Join(home, "Downloads"),
			home)
	}
	for _, dir := range dirs {
		if len(bindingSetsIn(dir)) > 0 {
			return dir
		}
	}
	return ""
}

// bindingSetsIn returns every file directly inside dir that really is a binding
// set, in a stable order. Files are confirmed by loading them, so a folder full
// of unrelated JSON does not produce a default that immediately fails. Only the
// top level is searched: a guided default should be instant, not a disk crawl.
func bindingSetsIn(dir string) []string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil
	}
	var names []string
	for _, e := range entries {
		if e.IsDir() || !strings.EqualFold(filepath.Ext(e.Name()), ".json") {
			continue
		}
		if info, err := e.Info(); err != nil || info.Size() == 0 || info.Size() > 4<<20 {
			continue // not worth parsing a huge file to guess a default
		}
		names = append(names, e.Name())
	}
	sort.Strings(names)

	var sets []string
	for _, name := range names {
		path := filepath.Join(dir, name)
		if _, _, err := loadSetFile(path, false); err == nil {
			sets = append(sets, path)
		}
	}
	return sets
}

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