// COPYRIGHT(C) David N. Cohen.
// This file is part of src.d10.dev/command
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package command

import (
	"flag"
	"fmt"
	"log"
	"os"
	"slices"
	"strings"
)

var errs = 0 // running count of errors encountered

func Check(err error) {
	if err != nil {
		errs++
		fmt.Println(err.Error())
	}
}

// CheckFatal exits with error message, if err is not nil.
func CheckFatal(err error) {
	if err != nil {
		errs++
		fmt.Println(err.Error())
		// log.Panic(err) // produce stack trace (verbose)
		os.Exit(1)
	}
}

// CheckUsage writes usage and exits, if err is not nil.
func CheckUsage(err error) {
	if err != nil {
		errs++
		fmt.Println(err.Error())
		flag.Usage()
		os.Exit(2)
	}
}

type Path string

type Route struct {
	description string
	path        Path
	route       map[Path]*Route // sub-commands and sub-sub-commands
	syntax      string

	// a route is either an operation or a (sub) command
	command   Command
	operation Operation
}

type Command func() Operation

type Operation func() error

func New(p Path, syntax string, description string, c Command) *Route {
	return &Route{
		command:     c,
		description: strings.TrimSpace(description),
		path:        p,
		syntax:      strings.TrimSpace(syntax),
	}
}

func (r *Route) New(p Path, syntax string, description string, c Command) *Route {
	sc := New(p, syntax, description, c)
	r.add(sc)
	return sc
}

func (r *Route) Operation(p Path, syntax string, description string, o Operation) {
	op := &Route{
		description: strings.TrimSpace(description),
		operation:   o,
		path:        p,
		syntax:      strings.TrimSpace(syntax),
	}
	r.add(op)
}

func (r *Route) add(op *Route) {
	if r.route == nil {
		r.route = map[Path]*Route{}
	}
	_, ok := r.route[op.path]
	if ok {
		log.Panicf("duplicate route (%q)", op.path)
	}
	r.route[op.path] = op
}

func (r Route) Run() error {
	if r.operation != nil {
		return r.operation()
	}

	// determine the default operation, even if we do not run the
	// default, this gives the command handler a chance to initialize
	// variables and declare flags.
	defaultOp := r.command()

	// flag package masks the special arg "--", so here we remember what comes after it
	separator := slices.Index(os.Args, "--")
	var extra []string // args that follow "--"
	if separator != -1 {
		extra = os.Args[separator:]
		os.Args = os.Args[:separator]
		// see additional manipulation of args near end of this function
	}

	// parse, in order to navigate further down route
	flag.Usage = r.usage
	flag.Parse()

	// Restore extra os.Args, in case defaultOp handler calls
	// flag.Parse(). See also where we descend() below.
	os.Args = append(os.Args, extra...)

	var path Path
	arg := flag.Args()
	if len(arg) > 0 {
		path = Path(arg[0])
	}

	next, ok := r.route[path]
	if !ok && defaultOp != nil {
		return defaultOp()
	}
	if !ok && path != "" {
		err := fmt.Errorf("unknown operation (%q)", path)
		CheckUsage(err)
	}
	if !ok {
		r.usage()
		os.Exit(2)
	}

	os.Args, flag.CommandLine = next.descend()

	// restore extra os.Args, because they were removed when flag.Parse was called
	os.Args = append(os.Args, extra...)

	return next.Run()
}

func (r Route) descend() (arg []string, fs *flag.FlagSet) {
	if flag.NArg() > 0 {
		arg = flag.Args()
	}
	fs = flag.NewFlagSet(string(r.path), flag.ExitOnError)
	fs.Usage = r.usage

	// inject flags already parsed into our flagset
	flag.VisitAll(func(f *flag.Flag) {
		fs.Var(f.Value, f.Name, f.Usage)
	})

	return
}

func (r Route) usage() {
	output := flag.CommandLine.Output()

	if errs == 0 && r.description != "" {
		fmt.Fprintf(output, "\n"+r.description+"\n")
	}
	if r.syntax != "" {
		fmt.Fprintf(output, "\nSyntax:\n")
		fmt.Fprintf(output, "\n  %s", r.syntax)
		fmt.Fprintf(output, "\n")
	}

	if len(r.route) > 0 {
		fmt.Fprintf(output, "\nOperations:\n")
		for p, op := range r.route {
			syntax := op.syntax
			if syntax == "" {
				syntax = string(p)
			}

			fmt.Fprintf(output, "\n  %s", syntax)
			if op.description != "" {
				fmt.Fprintf(output, "\n\t%s", firstLine(op.description))
			}
			fmt.Fprintf(output, "\n")
		}
	}

	// show flags only if some have been declared
	flags := 0
	flag.VisitAll(func(*flag.Flag) {
		flags++
	})
	if flags > 0 {
		fmt.Fprintf(output, "\nFlags:\n\n")
		flag.PrintDefaults()
	}
	fmt.Println("") // blank line
}

func firstLine(txt string) string {
	split := strings.SplitN(txt, "\n", 2)
	return split[0]
}
