// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package commands

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"slices"
	"strings"

	"github.com/alecthomas/chroma/v2"
	"github.com/alecthomas/chroma/v2/formatters/html"
	"github.com/alecthomas/chroma/v2/styles"
	"github.com/bep/simplecobra"
	"github.com/goccy/go-yaml"
	"github.com/gohugoio/hugo/common/hugo"
	"github.com/gohugoio/hugo/docshelper"
	"github.com/gohugoio/hugo/helpers"
	"github.com/gohugoio/hugo/hugofs"
	"github.com/gohugoio/hugo/hugolib"
	"github.com/gohugoio/hugo/parser"
	"github.com/spf13/cobra"
	"github.com/spf13/cobra/doc"
)

func newGenCommand() *genCommand {
	var (
		// Flags.
		gendocdir string
		genmandir string

		// Chroma flags.
		style                  string
		highlightStyle         string
		lineNumbersInlineStyle string
		lineNumbersTableStyle  string
		omitEmpty              bool
		omitClassComments      bool
	)

	newChromaStyles := func() simplecobra.Commander {
		return &simpleCommand{
			name:  "chromastyles",
			short: "Generate CSS stylesheet for the Chroma code highlighter",
			long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.

See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview of the available styles.`,

			run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
				style = strings.ToLower(style)
				if !slices.Contains(styles.Names(), style) {
					return fmt.Errorf("invalid style: %s", style)
				}
				builder := styles.Get(style).Builder()
				if highlightStyle != "" {
					builder.Add(chroma.LineHighlight, highlightStyle)
				}
				if lineNumbersInlineStyle != "" {
					builder.Add(chroma.LineNumbers, lineNumbersInlineStyle)
				}
				if lineNumbersTableStyle != "" {
					builder.Add(chroma.LineNumbersTable, lineNumbersTableStyle)
				}
				style, err := builder.Build()
				if err != nil {
					return err
				}

				if omitEmpty {
					// See https://github.com/alecthomas/chroma/commit/5b2a4c5a26c503c79bc86ba3c4ae5b330028bd3d
					hugo.Deprecate("--omitEmpty", "Flag is no longer needed, empty classes are now always omitted.", "v0.149.0")
				}
				options := []html.Option{
					html.WithCSSComments(!omitClassComments),
					html.WithCustomCSS(chromaCSSOverrides(style)),
				}
				formatter := html.New(options...)

				w := os.Stdout
				fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
				formatter.WriteCSS(w, style)
				return nil
			},
			withc: func(cmd *cobra.Command, r *rootCommand) {
				cmd.ValidArgsFunction = cobra.NoFileCompletions
				cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style")
				_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
				cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
				_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
				cmd.PersistentFlags().StringVar(&lineNumbersInlineStyle, "lineNumbersInlineStyle", "", `foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff"`)
				_ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions)
				cmd.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`)
				_ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions)
				cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules (deprecated, no longer needed)`)
				_ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions)
				cmd.PersistentFlags().BoolVar(&omitClassComments, "omitClassComments", false, `omit CSS class comment prefixes in the generated CSS`)
				_ = cmd.RegisterFlagCompletionFunc("omitClassComments", cobra.NoFileCompletions)
			},
		}
	}

	newMan := func() simplecobra.Commander {
		return &simpleCommand{
			name:  "man",
			short: "Generate man pages for the Hugo CLI",
			long: `This command automatically generates up-to-date man pages of Hugo's
	command-line interface.  By default, it creates the man page files
	in the "man" directory under the current directory.`,

			run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
				header := &doc.GenManHeader{
					Section: "1",
					Manual:  "Hugo Manual",
					Source:  fmt.Sprintf("Hugo %s", hugo.CurrentVersion),
				}
				if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) {
					genmandir += helpers.FilePathSeparator
				}
				if found, _ := helpers.Exists(genmandir, hugofs.Os); !found {
					r.Println("Directory", genmandir, "does not exist, creating...")
					if err := hugofs.Os.MkdirAll(genmandir, 0o777); err != nil {
						return err
					}
				}
				cd.CobraCommand.Root().DisableAutoGenTag = true

				r.Println("Generating Hugo man pages in", genmandir, "...")
				doc.GenManTree(cd.CobraCommand.Root(), header, genmandir)

				r.Println("Done.")

				return nil
			},
			withc: func(cmd *cobra.Command, r *rootCommand) {
				cmd.ValidArgsFunction = cobra.NoFileCompletions
				cmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.")
				_ = cmd.MarkFlagDirname("dir")
			},
		}
	}

	newGen := func() simplecobra.Commander {
		const gendocFrontmatterTemplate = `---
title: "%s"
slug: %s
url: %s
---
`

		return &simpleCommand{
			name:  "doc",
			short: "Generate Markdown documentation for the Hugo CLI",
			long: `Generate Markdown documentation for the Hugo CLI.
			This command is, mostly, used to create up-to-date documentation
	of Hugo's command-line interface for https://gohugo.io/.

	It creates one Markdown file per command with front matter suitable
	for rendering in Hugo.`,
			run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
				cd.CobraCommand.VisitParents(func(c *cobra.Command) {
					// Disable the "Auto generated by spf13/cobra on DATE"
					// as it creates a lot of diffs.
					c.DisableAutoGenTag = true
				})
				if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) {
					gendocdir += helpers.FilePathSeparator
				}
				if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found {
					r.Println("Directory", gendocdir, "does not exist, creating...")
					if err := hugofs.Os.MkdirAll(gendocdir, 0o777); err != nil {
						return err
					}
				}
				prepender := func(filename string) string {
					name := filepath.Base(filename)
					base := strings.TrimSuffix(name, path.Ext(name))
					url := "/docs/reference/commands/" + strings.ToLower(base) + "/"
					return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
				}

				linkHandler := func(name string) string {
					base := strings.TrimSuffix(name, path.Ext(name))
					return "/docs/reference/commands/" + strings.ToLower(base) + "/"
				}
				r.Println("Generating Hugo command-line documentation in", gendocdir, "...")
				doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler)
				r.Println("Done.")

				return nil
			},
			withc: func(cmd *cobra.Command, r *rootCommand) {
				cmd.ValidArgsFunction = cobra.NoFileCompletions
				cmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
				_ = cmd.MarkFlagDirname("dir")
			},
		}
	}

	var docsHelperTarget string

	newDocsHelper := func() simplecobra.Commander {
		return &simpleCommand{
			name:  "docshelper",
			short: "Generate some data files for the Hugo docs",

			run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
				r.Println("Generate docs data to", docsHelperTarget)

				var buf bytes.Buffer
				jsonEnc := json.NewEncoder(&buf)

				configProvider := func() docshelper.DocProvider {
					conf := hugolib.DefaultConfig()
					conf.CacheDir = "" // The default value does not make sense in the docs.
					defaultConfig := parser.NullBoolJSONMarshaller{Wrapped: parser.LowerCaseCamelJSONMarshaller{Value: conf}}
					return docshelper.DocProvider{"config": defaultConfig}
				}

				docshelper.AddDocProviderFunc(configProvider)
				if err := jsonEnc.Encode(docshelper.GetDocProvider()); err != nil {
					return err
				}

				// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
				var m map[string]any
				if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
					return err
				}

				targetFile := filepath.Join(docsHelperTarget, "docs.yaml")

				f, err := os.Create(targetFile)
				if err != nil {
					return err
				}
				defer f.Close()
				yamlEnc := yaml.NewEncoder(f, yaml.UseSingleQuote(true), yaml.AutoInt())
				if err := yamlEnc.Encode(m); err != nil {
					return err
				}

				r.Println("Done!")
				return nil
			},
			withc: func(cmd *cobra.Command, r *rootCommand) {
				cmd.Hidden = true
				cmd.ValidArgsFunction = cobra.NoFileCompletions
				cmd.PersistentFlags().StringVarP(&docsHelperTarget, "dir", "", "docs/data", "data dir")
			},
		}
	}

	return &genCommand{
		commands: []simplecobra.Commander{
			newChromaStyles(),
			newGen(),
			newMan(),
			newDocsHelper(),
		},
	}
}

func chromaCSSOverrides(style *chroma.Style) map[chroma.TokenType]string {
	bg := style.Get(chroma.Background)
	m := make(map[chroma.TokenType]string)
	for tt := range chroma.StandardTypes {
		if tt == chroma.Background || !style.Has(tt) || !chromaLeafToken(tt) {
			continue
		}
		entry := style.Get(tt)
		if !entry.Sub(bg).IsZero() || !entry.Colour.IsSet() {
			continue
		}
		if css := html.StyleEntryToCSS(chroma.StyleEntry{Colour: entry.Colour}); css != "" {
			m[tt] = css
		}
	}
	return m
}

func chromaLeafToken(tt chroma.TokenType) bool {
	for other := range chroma.StandardTypes {
		if other != tt && (other.Category() == tt || other.SubCategory() == tt) {
			return false
		}
	}
	return true
}

type genCommand struct {
	rootCmd *rootCommand

	commands []simplecobra.Commander
}

func (c *genCommand) Commands() []simplecobra.Commander {
	return c.commands
}

func (c *genCommand) Name() string {
	return "gen"
}

func (c *genCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
	return nil
}

func (c *genCommand) Init(cd *simplecobra.Commandeer) error {
	cmd := cd.CobraCommand
	cmd.Short = "Generate documentation and syntax highlighting styles"
	cmd.Long = "Generate documentation for your project using Hugo's documentation engine, including syntax highlighting for various programming languages."

	cmd.RunE = nil
	return nil
}

func (c *genCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
	c.rootCmd = cd.Root.Command.(*rootCommand)
	return nil
}
