// 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 attributes

import (
	"fmt"
	"strconv"
	"strings"
	"sync"

	"github.com/gohugoio/hugo/common/hugio"
	"github.com/spf13/cast"
	"github.com/yuin/goldmark/ast"
	"github.com/yuin/goldmark/util"
)

// Markdown attributes used as options by the Chroma highlighter.
var chromaHighlightProcessingAttributes = map[string]bool{
	"anchorLineNos":      true,
	"guessSyntax":        true,
	"hl_Lines":           true,
	"hl_inline":          true,
	"lineAnchors":        true,
	"lineNos":            true,
	"lineNoStart":        true,
	"lineNumbersInTable": true,
	"noClasses":          true,
	"style":              true,
	"tabWidth":           true,
}

func init() {
	for k, v := range chromaHighlightProcessingAttributes {
		chromaHighlightProcessingAttributes[strings.ToLower(k)] = v
	}
}

type AttributesOwnerType int

const (
	AttributesOwnerGeneral AttributesOwnerType = iota
	AttributesOwnerCodeBlockChroma
	AttributesOwnerCodeBlockCustom
)

func New(astAttributes []ast.Attribute, ownerType AttributesOwnerType) *AttributesHolder {
	var (
		attrs []Attribute
		opts  []Attribute
	)
	for _, v := range astAttributes {
		nameLower := strings.ToLower(string(v.Name))
		if strings.HasPrefix(string(nameLower), "on") {
			continue
		}
		var vv any
		switch vvv := v.Value.(type) {
		case bool, float64:
			vv = vvv
		case []any:
			// Highlight line number hlRanges.
			var hlRanges [][2]int
			for _, l := range vvv {
				if ln, ok := l.(float64); ok {
					hlRanges = append(hlRanges, [2]int{int(ln) - 1, int(ln) - 1})
				} else if rng, ok := l.([]uint8); ok {
					slices := strings.Split(string([]byte(rng)), "-")
					lhs, err := strconv.Atoi(slices[0])
					if err != nil {
						continue
					}
					rhs := lhs
					if len(slices) > 1 {
						rhs, err = strconv.Atoi(slices[1])
						if err != nil {
							continue
						}
					}
					hlRanges = append(hlRanges, [2]int{lhs - 1, rhs - 1})
				}
			}
			vv = hlRanges
		case []byte:
			// Note that we don't do any HTML escaping here.
			// We used to do that, but that changed in #9558.
			// Now it's up to the templates to decide.
			vv = string(vvv)
		default:
			panic(fmt.Sprintf("not implemented: %T", vvv))
		}

		if ownerType == AttributesOwnerCodeBlockChroma && chromaHighlightProcessingAttributes[nameLower] {
			attr := Attribute{Name: string(v.Name), Value: vv}
			opts = append(opts, attr)
		} else {
			attr := Attribute{Name: nameLower, Value: vv}
			attrs = append(attrs, attr)
		}

	}

	return &AttributesHolder{
		attributes: attrs,
		options:    opts,
	}
}

type Attribute struct {
	Name  string
	Value any
}

func (a Attribute) ValueString() string {
	return cast.ToString(a.Value)
}

// Empty holds no attributes.
var Empty = &AttributesHolder{}

type AttributesHolder struct {
	// What we get from Goldmark.
	attributes []Attribute

	// Attributes considered to be an option (code blocks)
	options []Attribute

	// What we send to the render hooks.
	attributesMapInit sync.Once
	attributesMap     map[string]any
	optionsMapInit    sync.Once
	optionsMap        map[string]any
}

type Attributes map[string]any

func (a *AttributesHolder) Attributes() map[string]any {
	a.attributesMapInit.Do(func() {
		a.attributesMap = make(map[string]any)
		for _, v := range a.attributes {
			a.attributesMap[v.Name] = v.Value
		}
	})
	return a.attributesMap
}

func (a *AttributesHolder) Options() map[string]any {
	a.optionsMapInit.Do(func() {
		a.optionsMap = make(map[string]any)
		for _, v := range a.options {
			a.optionsMap[v.Name] = v.Value
		}
	})
	return a.optionsMap
}

func (a *AttributesHolder) AttributesSlice() []Attribute {
	return a.attributes
}

func (a *AttributesHolder) OptionsSlice() []Attribute {
	return a.options
}

// RenderASTAttributes writes the AST attributes to the given as attributes to an HTML element.
// This is used by the default HTML renderers, e.g. for headings etc. where no hook template could be found.
// This performs HTML escaping of string attributes.
func RenderASTAttributes(w hugio.FlexiWriter, attributes ...ast.Attribute) {
	for _, attr := range attributes {

		a := strings.ToLower(string(attr.Name))
		if strings.HasPrefix(a, "on") {
			continue
		}

		_, _ = w.WriteString(" ")
		_, _ = w.Write(attr.Name)
		_, _ = w.WriteString(`="`)

		switch v := attr.Value.(type) {
		case []byte:
			_, _ = w.Write(util.EscapeHTML(v))
		default:
			w.WriteString(cast.ToString(v))
		}

		_ = w.WriteByte('"')
	}
}

// RenderAttributes Render writes the attributes to the given as attributes to an HTML element.
// This is used for the default codeblock rendering.
// This performs HTML escaping of string attributes.
func RenderAttributes(w hugio.FlexiWriter, skipClass bool, attributes ...Attribute) {
	for _, attr := range attributes {
		a := strings.ToLower(string(attr.Name))
		if skipClass && a == "class" {
			continue
		}
		_, _ = w.WriteString(" ")
		_, _ = w.WriteString(attr.Name)
		_, _ = w.WriteString(`="`)

		switch v := attr.Value.(type) {
		case []byte:
			_, _ = w.Write(util.EscapeHTML(v))
		default:
			w.WriteString(cast.ToString(v))
		}

		_ = w.WriteByte('"')
	}
}
