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 what 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", which reads the files and prints what it
// finds. "render" writes an SVG file somewhere, and nobody who double-clicked
// an icon has said where; so the guided session does not offer it.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  WorkspaceForge")
	fmt.Println("  Checks a window layout before you rely on it.")
	fmt.Println()
	fmt.Println("  A layout file says how your screen should be divided up and which")
	fmt.Println("  program belongs in which part. This works out whether the plan holds")
	fmt.Println("  together: regions that overlap or leave gaps, panes squeezed below")
	fmt.Println("  their minimum size, rules that can never match, and programs with")
	fmt.Println("  nowhere to go.")
	fmt.Println()
	fmt.Println("  It reads your files and writes nothing. No window on this machine is")
	fmt.Println("  moved or resized — WorkspaceForge never touches live windows at all.")
	fmt.Println()

	suggested := suggestedLayout()
	var layout string
	for {
		fmt.Println("  Which layout file shall I check?")
		fmt.Println("  (a .json file describing how the screen is divided)")
		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 layout file to check. 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 file — or the folder holding it — from")
			fmt.Println("  Explorer onto this window to paste its location, then press")
			fmt.Println("  Enter.")
			fmt.Println()
			continue
		case info.IsDir():
			// A dragged-in folder is a common answer; look inside it rather
			// than sending the reader away to find the file themselves.
			found := layoutIn(answer)
			if found == "" {
				fmt.Println()
				fmt.Printf("  %q is a folder, and I cannot see a layout file in it.\n", answer)
				fmt.Println("  Give me the .json file itself.")
				fmt.Println()
				continue
			}
			fmt.Printf("\n  Using the layout I found in that folder: %s\n", found)
			answer = found
		}

		// LoadLayout is the same reader "check" uses, and it is where a file
		// that is not a layout gets turned away — with a sentence instead of a
		// Go error, and without the process exiting on it.
		if _, err := LoadLayout(answer); err != nil {
			fmt.Println()
			fmt.Printf("  %q is not a layout file I can read.\n", filepath.Base(answer))
			fmt.Printf("  (%v)\n", err)
			fmt.Println()
			continue
		}

		layout = answer
		break
	}

	topology := askTopology(in, filepath.Dir(layout))

	fmt.Println()
	argv := []string{layout}
	if topology != "" {
		fmt.Printf("  Checking %s against %s.\n",
			filepath.Base(layout), filepath.Base(topology))
		argv = append(argv, "--topology", topology)
	} else {
		fmt.Printf("  Checking %s on its own: structure and rules, but not the\n",
			filepath.Base(layout))
		fmt.Println("  pixel geometry, which needs a screen description to check against.")
	}
	fmt.Println()
	runCheck(argv)

	fmt.Println()
	fmt.Println("  Nothing was changed — your files are exactly as they were.")
	fmt.Println("  There is a command-line version too, which can solve the exact")
	fmt.Println("  rectangles, draw the layout as a picture, and re-fit it onto a")
	fmt.Println("  different set of screens: workspaceforge --help")
	pause(in)
}

// askTopology offers the optional second half of a check. Geometry can only be
// checked against a described set of screens, but a layout is worth checking
// without one, so this question always has an answer the reader can give by
// pressing Enter.
func askTopology(in *bufio.Scanner, near string) string {
	suggested := topologyIn(near)
	for {
		fmt.Println()
		fmt.Println("  Which screen description shall I check it against?")
		fmt.Println("  (a .json file listing your monitors and their sizes)")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s, or type: skip)\n", suggested)
		} else {
			fmt.Println("  (press Enter to skip this — I will check everything except the")
			fmt.Println("  pixel geometry)")
		}
		fmt.Print("  > ")

		if !in.Scan() {
			return ""
		}
		answer := strings.TrimSpace(in.Text())
		answer = strings.Trim(answer, `"`)
		if answer == "" {
			return suggested
		}
		if strings.EqualFold(answer, "skip") || strings.EqualFold(answer, "none") {
			return ""
		}

		if info, err := os.Stat(answer); err == nil && info.IsDir() {
			found := topologyIn(answer)
			if found == "" {
				fmt.Println()
				fmt.Printf("  %q is a folder, and I cannot see a screen description in it.\n", answer)
				continue
			}
			answer = found
		}
		if _, err := LoadTopology(answer); err != nil {
			fmt.Println()
			fmt.Printf("  %q is not a screen description I can read.\n", filepath.Base(answer))
			fmt.Printf("  (%v)\n", err)
			fmt.Println("  Type skip to carry on without one.")
			continue
		}
		return answer
	}
}

// suggestedLayout offers a layout the reader plausibly has in mind, so the
// common case is one keypress. The folder the program was launched from comes
// first: somebody who put workspaceforge next to their layout files and
// double-clicked it means that folder.
func suggestedLayout() string {
	for _, dir := range guidedSearchDirs() {
		if found := layoutIn(dir); found != "" {
			return found
		}
	}
	return ""
}

func guidedSearchDirs() []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)
	}
	return dirs
}

// layoutIn returns the first .json file directly inside dir that really loads
// as a layout, or "" if there is none.
func layoutIn(dir string) string {
	for _, path := range candidateJSON(dir) {
		if _, err := LoadLayout(path); err == nil {
			return path
		}
	}
	return ""
}

// topologyIn returns the first .json file directly inside dir that really loads
// as a topology and is not itself a layout, or "" if there is none.
func topologyIn(dir string) string {
	for _, path := range candidateJSON(dir) {
		if _, err := LoadTopology(path); err != nil {
			continue
		}
		if _, err := LoadLayout(path); err == nil {
			continue // it is a layout; do not offer it as the screen description
		}
		return path
	}
	return ""
}

// candidateJSON lists the .json files directly inside dir, smallest concerns
// first: only the top level, in a stable order, skipping anything too big to be
// worth parsing to guess a default. A guided default should be instant, not a
// disk crawl.
func candidateJSON(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
		}
		names = append(names, e.Name())
	}
	sort.Strings(names)
	paths := make([]string, 0, len(names))
	for _, n := range names {
		paths = append(paths, filepath.Join(dir, n))
	}
	return paths
}

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