// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>

package diff

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/kovidgoyal/kitty/tools/utils"
	"github.com/kovidgoyal/kitty/tools/utils/images"

	"github.com/alecthomas/chroma/v2"
	"github.com/alecthomas/chroma/v2/lexers"
	"github.com/alecthomas/chroma/v2/styles"
)

var _ = fmt.Print
var _ = os.WriteFile

var ErrNoLexer = errors.New("No lexer available for this format")
var DefaultStyle = sync.OnceValue(func() *chroma.Style {
	// Default style generated by python style.py default pygments.styles.default.DefaultStyle
	// with https://raw.githubusercontent.com/alecthomas/chroma/master/_tools/style.py
	return styles.Register(chroma.MustNewStyle("default", chroma.StyleEntries{
		chroma.TextWhitespace:        "#bbbbbb",
		chroma.Comment:               "italic #3D7B7B",
		chroma.CommentPreproc:        "noitalic #9C6500",
		chroma.Keyword:               "bold #008000",
		chroma.KeywordPseudo:         "nobold",
		chroma.KeywordType:           "nobold #B00040",
		chroma.Operator:              "#666666",
		chroma.OperatorWord:          "bold #AA22FF",
		chroma.NameBuiltin:           "#008000",
		chroma.NameFunction:          "#0000FF",
		chroma.NameClass:             "bold #0000FF",
		chroma.NameNamespace:         "bold #0000FF",
		chroma.NameException:         "bold #CB3F38",
		chroma.NameVariable:          "#19177C",
		chroma.NameConstant:          "#880000",
		chroma.NameLabel:             "#767600",
		chroma.NameEntity:            "bold #717171",
		chroma.NameAttribute:         "#687822",
		chroma.NameTag:               "bold #008000",
		chroma.NameDecorator:         "#AA22FF",
		chroma.LiteralString:         "#BA2121",
		chroma.LiteralStringDoc:      "italic",
		chroma.LiteralStringInterpol: "bold #A45A77",
		chroma.LiteralStringEscape:   "bold #AA5D1F",
		chroma.LiteralStringRegex:    "#A45A77",
		chroma.LiteralStringSymbol:   "#19177C",
		chroma.LiteralStringOther:    "#008000",
		chroma.LiteralNumber:         "#666666",
		chroma.GenericHeading:        "bold #000080",
		chroma.GenericSubheading:     "bold #800080",
		chroma.GenericDeleted:        "#A00000",
		chroma.GenericInserted:       "#008400",
		chroma.GenericError:          "#E40000",
		chroma.GenericEmph:           "italic",
		chroma.GenericStrong:         "bold",
		chroma.GenericPrompt:         "bold #000080",
		chroma.GenericOutput:         "#717171",
		chroma.GenericTraceback:      "#04D",
		chroma.Error:                 "border:#FF0000",
		chroma.Background:            " bg:#f8f8f8",
	}))
})

// Clear the background colour.
func clear_background(style *chroma.Style) *chroma.Style {
	builder := style.Builder()
	bg := builder.Get(chroma.Background)
	bg.Background = 0
	bg.NoInherit = true
	builder.AddEntry(chroma.Background, bg)
	style, _ = builder.Build()
	return style
}

func ansi_formatter(w io.Writer, style *chroma.Style, it chroma.Iterator) (err error) {
	const SGR_PREFIX = "\033["
	const SGR_SUFFIX = "m"
	style = clear_background(style)
	before, after := make([]byte, 0, 64), make([]byte, 0, 64)
	nl := []byte{'\n'}
	write_sgr := func(which []byte) (err error) {
		if len(which) > 1 {
			if _, err = w.Write(utils.UnsafeStringToBytes(SGR_PREFIX)); err != nil {
				return err
			}
			if _, err = w.Write(which[:len(which)-1]); err != nil {
				return err
			}
			if _, err = w.Write(utils.UnsafeStringToBytes(SGR_SUFFIX)); err != nil {
				return err
			}
		}
		return
	}
	write := func(text string) (err error) {
		if err = write_sgr(before); err != nil {
			return err
		}
		if _, err = w.Write(utils.UnsafeStringToBytes(text)); err != nil {
			return err
		}
		if err = write_sgr(after); err != nil {
			return err
		}
		return
	}

	for token := it(); token != chroma.EOF; token = it() {
		entry := style.Get(token.Type)
		before, after = before[:0], after[:0]
		if !entry.IsZero() {
			if entry.Bold == chroma.Yes {
				before = append(before, '1', ';')
				after = append(after, '2', '2', '1', ';')
			}
			if entry.Underline == chroma.Yes {
				before = append(before, '4', ';')
				after = append(after, '2', '4', ';')
			}
			if entry.Italic == chroma.Yes {
				before = append(before, '3', ';')
				after = append(after, '2', '3', ';')
			}
			if entry.Colour.IsSet() {
				before = append(before, fmt.Sprintf("38:2:%d:%d:%d;", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())...)
				after = append(after, '3', '9', ';')
			}
		}
		// independently format each line in a multiline token, needed for the diff kitten highlighting to work, also
		// pagers like less reset SGR formatting at line boundaries
		text := sanitize(token.Value)
		for text != "" {
			idx := strings.IndexByte(text, '\n')
			if idx < 0 {
				if err = write(text); err != nil {
					return err
				}
				break
			}
			if err = write(text[:idx]); err != nil {
				return err
			}
			if _, err = w.Write(nl); err != nil {
				return err
			}
			text = text[idx+1:]
		}
	}
	return nil
}

func resolved_chroma_style(use_light_colors bool) *chroma.Style {
	name := utils.IfElse(use_light_colors, conf.Pygments_style, conf.Dark_pygments_style)
	var style *chroma.Style
	if name == "default" {
		style = DefaultStyle()
	} else {
		style = styles.Get(name)
	}
	if style == nil {
		if resolved_colors.Background.IsDark() && !resolved_colors.Foreground.IsDark() {
			style = styles.Get("monokai")
			if style == nil {
				style = styles.Get("github-dark")
			}
		} else {
			style = DefaultStyle()
		}
		if style == nil {
			style = styles.Fallback
		}
	}
	return style
}

var tokens_map map[string][]chroma.Token
var mu sync.Mutex

func highlight_file(path string, use_light_colors bool) (highlighted string, err error) {
	defer func() {
		if r := recover(); r != nil {
			e, ok := r.(error)
			if !ok {
				e = fmt.Errorf("%v", r)
			}
			err = e
		}
	}()
	filename_for_detection := filepath.Base(path)
	ext := filepath.Ext(filename_for_detection)
	if ext != "" {
		ext = strings.ToLower(ext[1:])
		r := conf.Syntax_aliases[ext]
		if r != "" {
			filename_for_detection = "file." + r
		}
	}
	text, err := data_for_path(path)
	if err != nil {
		return "", err
	}
	mu.Lock()
	if tokens_map == nil {
		tokens_map = make(map[string][]chroma.Token)
	}
	tokens := tokens_map[path]
	mu.Unlock()
	if tokens == nil {
		lexer := lexers.Match(filename_for_detection)
		if lexer == nil {
			lexer = lexers.Analyse(text)
		}
		if lexer == nil {
			return "", fmt.Errorf("Cannot highlight %#v: %w", path, ErrNoLexer)
		}
		lexer = chroma.Coalesce(lexer)
		iterator, err := lexer.Tokenise(nil, text)
		if err != nil {
			return "", err
		}
		tokens = iterator.Tokens()
		mu.Lock()
		tokens_map[path] = tokens
		mu.Unlock()
	}
	formatter := chroma.FormatterFunc(ansi_formatter)
	w := strings.Builder{}
	w.Grow(len(text) * 2)
	err = formatter.Format(&w, resolved_chroma_style(use_light_colors), chroma.Literator(tokens...))
	// os.WriteFile(filepath.Base(path+".highlighted"), []byte(w.String()), 0o600)
	return w.String(), err
}

func highlight_all(paths []string, light bool) {
	ctx := images.Context{}
	ctx.Parallel(0, len(paths), func(nums <-chan int) {
		for i := range nums {
			path := paths[i]
			raw, err := highlight_file(path, light)
			if err != nil {
				continue
			}
			if light {
				light_highlighted_lines_cache.Set(path, text_to_lines(raw))
			} else {
				dark_highlighted_lines_cache.Set(path, text_to_lines(raw))
			}
		}
	})
}
