package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"

	generatedlexers "github.com/johnkerl/pgpg/apps/go/generated/pkg/lexers"
	generatedparsers "github.com/johnkerl/pgpg/apps/go/generated/pkg/parsers"
	"github.com/johnkerl/pgpg/go/lib/pkg/asts"
)

func usage() {
	fmt.Fprintf(os.Stderr, "Usage: %s [options] [-e | -l] [file ...]\n", os.Args[0])
	fmt.Fprintf(os.Stderr, "  -e: arguments are expressions to parse (at least one required).\n")
	fmt.Fprintf(os.Stderr, "  -l: read stdin line-by-line, evaluate each line, print result (REPL mode).\n")
	fmt.Fprintf(os.Stderr, "  -mode: int (default), float, mod.\n")
	fmt.Fprintf(os.Stderr, "  -mod: integer modulus for -mode=mod (required).\n")
	fmt.Fprintf(os.Stderr, "  With -l and stdin a TTY, -p sets the prompt (default \"> \"); use -p \"\" to disable.\n")
	fmt.Fprintf(os.Stderr, "  Without -e/-l: zero arguments = read from stdin; one or more = read from those files.\n")
	flag.PrintDefaults()
	os.Exit(1)
}

func main() {
	var verbose bool
	var exprMode bool
	var lineMode bool
	var prompt string
	var mode string
	var modN int
	flag.BoolVar(&verbose, "v", false, "Print AST before evaluation")
	flag.BoolVar(&exprMode, "e", false, "Arguments are expressions to parse (at least one required)")
	flag.BoolVar(&lineMode, "l", false, "Read stdin line-by-line, evaluate each, print result (REPL)")
	flag.StringVar(&prompt, "p", "> ", "In -l mode with TTY stdin, prompt string (default \"> \"; use \"\" to disable)")
	flag.StringVar(&mode, "mode", "int", "Arithmetic mode: int, float, mod")
	flag.IntVar(&modN, "mod", 0, "Integer modulus for -mode=mod")
	flag.Usage = usage
	flag.Parse()

	args := flag.Args()

	validModes := map[string]bool{"int": true, "float": true, "mod": true}
	if !validModes[mode] {
		fmt.Fprintf(os.Stderr, "pemdas-eval: -mode must be int, float, or mod (got %q)\n", mode)
		os.Exit(1)
	}
	if mode == "mod" && modN <= 0 {
		fmt.Fprintln(os.Stderr, "pemdas-eval: -mode=mod requires -mod N with N > 0")
		os.Exit(1)
	}

	if lineMode {
		if exprMode {
			fmt.Fprintln(os.Stderr, "pemdas-eval: -e and -l are mutually exclusive")
			os.Exit(1)
		}
		runREPL(verbose, prompt, mode, modN)
		return
	}

	if exprMode {
		if len(args) == 0 {
			fmt.Fprintln(os.Stderr, "pemdas-eval: -e requires at least one argument")
			os.Exit(1)
		}
		for _, arg := range args {
			if err := runParserOnce(strings.NewReader(arg), verbose, mode, modN); err != nil {
				fmt.Fprintln(os.Stderr, err)
				os.Exit(1)
			}
		}
		return
	}
	if len(args) == 0 {
		if err := runParserOnce(os.Stdin, verbose, mode, modN); err != nil {
			fmt.Fprintln(os.Stderr, err)
			os.Exit(1)
		}
	} else if err := runParserOnFiles(args, verbose, mode, modN); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func stdinIsTTY() bool {
	fi, err := os.Stdin.Stat()
	if err != nil {
		return false
	}
	return (fi.Mode() & os.ModeCharDevice) != 0
}

func runREPL(verbose bool, prompt string, mode string, modN int) {
	usePrompt := stdinIsTTY() && prompt != ""
	scanner := bufio.NewScanner(os.Stdin)
	for {
		if usePrompt {
			fmt.Fprint(os.Stdout, prompt)
			os.Stdout.Sync()
		}
		if !scanner.Scan() {
			break
		}
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		if err := runParserOnce(strings.NewReader(line), verbose, mode, modN); err != nil {
			fmt.Fprintln(os.Stderr, err)
			continue
		}
	}
	if err := scanner.Err(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func runParserOnFiles(filenames []string, verbose bool, mode string, modN int) error {
	for _, filename := range filenames {
		f, err := os.Open(filename)
		if err != nil {
			return err
		}
		if err := runParserOnce(f, verbose, mode, modN); err != nil {
			_ = f.Close()
			return err
		}
		if err := f.Close(); err != nil {
			return err
		}
	}
	return nil
}

func runParserOnce(r io.Reader, verbose bool, mode string, modN int) error {
	ast, err := parseWithMode(r, mode)
	if err != nil {
		return err
	}
	switch mode {
	case "int":
		var b IntNumeric
		result, err := evaluateAST[int, int](ast, b, verbose)
		if err != nil {
			return err
		}
		fmt.Println(b.String(result))
	case "float":
		var b FloatNumeric
		result, err := evaluateAST[float64, float64](ast, b, verbose)
		if err != nil {
			return err
		}
		fmt.Println(b.String(result))
	case "mod":
		numeric, err := NewModNumeric(modN)
		if err != nil {
			return err
		}
		result, err := evaluateAST[ModInt, int](ast, numeric, verbose)
		if err != nil {
			return err
		}
		fmt.Println(numeric.String(result))
	default:
		return fmt.Errorf("unsupported mode %q", mode)
	}
	return nil
}

// parseWithMode returns an AST using the lexer/parser for the given mode.
func parseWithMode(r io.Reader, mode string) (*asts.AST, error) {
	switch mode {
	case "int":
		lexer := generatedlexers.NewPEMDASIntLexer(r)
		return generatedparsers.NewPEMDASIntParser().Parse(lexer, "")
	case "float":
		lexer := generatedlexers.NewPEMDASFloatLexer(r)
		return generatedparsers.NewPEMDASFloatParser().Parse(lexer, "")
	case "mod":
		lexer := generatedlexers.NewPEMDASModLexer(r)
		return generatedparsers.NewPEMDASModParser().Parse(lexer, "")
	default:
		return nil, fmt.Errorf("unsupported mode %q", mode)
	}
}
