package main

import (
	"bufio"
	"flag"
	"fmt"
	"go/format"
	"io"
	"os"
	"regexp"
	"strings"
	"text/template"

	"github.com/charmbracelet/glamour"
)

var (
	inputFile  = flag.String("i", "", "input file")
	outputFile = flag.String("o", "", "output file")

	commandsRegexp      = regexp.MustCompile(`^## Commands`)
	commandRegexp       = regexp.MustCompile("^### `(\\S+)`")
	exampleRegexp       = regexp.MustCompile("^#### `\\w+` examples")
	endOfCommandsRegexp = regexp.MustCompile(`^## `)
	trailingSpaceRegexp = regexp.MustCompile(` +\n`)

	funcs = template.FuncMap{
		"printMultiLineString": printMultiLineString,
	}

	outputTemplate = template.Must(template.New("output").Funcs(funcs).Parse(`// Code generated by github.com/twpayne/chezmoi/internal/generate-helps. DO NOT EDIT.

package cmd

type help struct {
	long    string
	example string
}

var helps = map[string]help{
{{- range $command, $help := .Helps }}
	"{{ $command }}": {
		long: {{ printMultiLineString $help.Long "\t\t\t" }},
{{- if $help.Example }}
		example: {{ printMultiLineString $help.Example "\t\t\t" }},
{{- end }}
	},
{{- end }}
}
`))
)

type help struct {
	Long    string
	Example string
}

func printMultiLineString(s, indent string) string {
	if len(s) == 0 {
		return `""`
	}
	b := &strings.Builder{}
	b.WriteString("\"\" +\n" + indent)
	for i, line := range strings.SplitAfter(s, "\n") {
		if line == "" {
			continue
		}
		if i != 0 {
			b.WriteString(" +\n" + indent)
		}
		fmt.Fprintf(b, "%q", line)
	}
	return b.String()
}

func extractHelps(r io.Reader) (map[string]*help, error) {
	longStyleConfig := glamour.ASCIIStyleConfig
	longStyleConfig.H4.Prefix = ""
	longTermRenderer, err := glamour.NewTermRenderer(
		glamour.WithStyles(longStyleConfig),
		glamour.WithWordWrap(80),
	)
	if err != nil {
		return nil, err
	}

	examplesStyleConfig := glamour.ASCIIStyleConfig
	examplesStyleConfig.Document.Indent = nil
	examplesTermRenderer, err := glamour.NewTermRenderer(
		glamour.WithStyles(examplesStyleConfig),
		glamour.WithWordWrap(80),
	)
	if err != nil {
		return nil, err
	}

	var (
		state = "find-commands"
		sb    = &strings.Builder{}
		h     *help
	)

	saveAndReset := func() error {
		var tr *glamour.TermRenderer
		switch state {
		case "in-command":
			tr = longTermRenderer
		case "in-example":
			tr = examplesTermRenderer
		default:
			panic(fmt.Sprintf("%s: invalid state", state))
		}
		s, err := tr.Render(sb.String())
		if err != nil {
			return err
		}
		s = trailingSpaceRegexp.ReplaceAllString(s, "\n")
		s = strings.Trim(s, "\n")
		switch state {
		case "in-command":
			h.Long = "Description:\n" + s
		case "in-example":
			h.Example = s
		default:
			panic(fmt.Sprintf("%s: invalid state", state))
		}
		sb.Reset()
		return nil
	}

	helps := make(map[string]*help)
	s := bufio.NewScanner(r)
FOR:
	for s.Scan() {
		switch state {
		case "find-commands":
			if commandsRegexp.MatchString(s.Text()) {
				state = "find-first-command"
			}
		case "find-first-command":
			if m := commandRegexp.FindStringSubmatch(s.Text()); m != nil {
				h = &help{}
				helps[m[1]] = h
				state = "in-command"
			}
		case "in-command", "in-example":
			m := commandRegexp.FindStringSubmatch(s.Text())
			switch {
			case m != nil:
				if err := saveAndReset(); err != nil {
					return nil, err
				}
				h = &help{}
				helps[m[1]] = h
				state = "in-command"
			case exampleRegexp.MatchString(s.Text()):
				if err := saveAndReset(); err != nil {
					return nil, err
				}
				state = "in-example"
			case endOfCommandsRegexp.MatchString(s.Text()):
				if err := saveAndReset(); err != nil {
					return nil, err
				}
				break FOR
			default:
				if _, err := sb.WriteString(s.Text()); err != nil {
					return nil, err
				}
				if err := sb.WriteByte('\n'); err != nil {
					return nil, err
				}
			}
		}
	}
	if err := s.Err(); err != nil {
		return nil, err
	}
	return helps, nil
}

func run() error {
	flag.Parse()

	var r io.Reader
	if *inputFile == "" {
		r = os.Stdin
	} else {
		fr, err := os.Open(*inputFile)
		if err != nil {
			return err
		}
		defer fr.Close()
		r = fr
	}

	helps, err := extractHelps(r)
	if err != nil {
		return err
	}

	data := struct {
		Helps      map[string]*help
		InputFile  string
		OutputFile string
	}{
		Helps:      helps,
		InputFile:  *inputFile,
		OutputFile: *outputFile,
	}
	sb := &strings.Builder{}
	if err := outputTemplate.ExecuteTemplate(sb, "output", data); err != nil {
		return err
	}

	var w io.Writer
	if *outputFile == "" {
		w = os.Stdout
	} else {
		fw, err := os.Create(*outputFile)
		if err != nil {
			return err
		}
		defer fw.Close()
		w = fw
	}

	output, err := format.Source([]byte(sb.String()))
	if err != nil {
		return err
	}

	_, err = w.Write(output)
	return err
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}
