package chroma

import (
	"encoding/xml"
	"fmt"
	"io"
	"sort"
	"strings"
)

// Trilean value for StyleEntry value inheritance.
type Trilean uint8

// Trilean states.
const (
	Pass Trilean = iota
	Yes
	No
)

func (t Trilean) String() string {
	switch t {
	case Yes:
		return "Yes"
	case No:
		return "No"
	default:
		return "Pass"
	}
}

// Prefix returns s with "no" as a prefix if Trilean is no.
func (t Trilean) Prefix(s string) string {
	if t == Yes {
		return s
	} else if t == No {
		return "no" + s
	}
	return ""
}

// A StyleEntry in the Style map.
type StyleEntry struct {
	// Hex colours.
	Colour     Colour
	Background Colour
	Border     Colour

	Bold      Trilean
	Italic    Trilean
	Underline Trilean
	NoInherit bool
}

func (s StyleEntry) MarshalText() ([]byte, error) {
	return []byte(s.String()), nil
}

func (s StyleEntry) String() string {
	out := []string{}
	if s.Bold != Pass {
		out = append(out, s.Bold.Prefix("bold"))
	}
	if s.Italic != Pass {
		out = append(out, s.Italic.Prefix("italic"))
	}
	if s.Underline != Pass {
		out = append(out, s.Underline.Prefix("underline"))
	}
	if s.NoInherit {
		out = append(out, "noinherit")
	}
	if s.Colour.IsSet() {
		out = append(out, s.Colour.String())
	}
	if s.Background.IsSet() {
		out = append(out, "bg:"+s.Background.String())
	}
	if s.Border.IsSet() {
		out = append(out, "border:"+s.Border.String())
	}
	return strings.Join(out, " ")
}

// Sub subtracts e from s where elements match.
func (s StyleEntry) Sub(e StyleEntry) StyleEntry {
	out := StyleEntry{}
	if e.Colour != s.Colour {
		out.Colour = s.Colour
	}
	if e.Background != s.Background {
		out.Background = s.Background
	}
	if e.Bold != s.Bold {
		out.Bold = s.Bold
	}
	if e.Italic != s.Italic {
		out.Italic = s.Italic
	}
	if e.Underline != s.Underline {
		out.Underline = s.Underline
	}
	if e.Border != s.Border {
		out.Border = s.Border
	}
	return out
}

// Inherit styles from ancestors.
//
// Ancestors should be provided from oldest to newest.
func (s StyleEntry) Inherit(ancestors ...StyleEntry) StyleEntry {
	out := s
	for i := len(ancestors) - 1; i >= 0; i-- {
		if out.NoInherit {
			return out
		}
		ancestor := ancestors[i]
		if !out.Colour.IsSet() {
			out.Colour = ancestor.Colour
		}
		if !out.Background.IsSet() {
			out.Background = ancestor.Background
		}
		if !out.Border.IsSet() {
			out.Border = ancestor.Border
		}
		if out.Bold == Pass {
			out.Bold = ancestor.Bold
		}
		if out.Italic == Pass {
			out.Italic = ancestor.Italic
		}
		if out.Underline == Pass {
			out.Underline = ancestor.Underline
		}
	}
	return out
}

func (s StyleEntry) IsZero() bool {
	return s.Colour == 0 && s.Background == 0 && s.Border == 0 && s.Bold == Pass && s.Italic == Pass &&
		s.Underline == Pass && !s.NoInherit
}

// A StyleBuilder is a mutable structure for building styles.
//
// Once built, a Style is immutable.
type StyleBuilder struct {
	entries map[TokenType]string
	name    string
	parent  *Style
}

func NewStyleBuilder(name string) *StyleBuilder {
	return &StyleBuilder{name: name, entries: map[TokenType]string{}}
}

func (s *StyleBuilder) AddAll(entries StyleEntries) *StyleBuilder {
	for ttype, entry := range entries {
		s.entries[ttype] = entry
	}
	return s
}

func (s *StyleBuilder) Get(ttype TokenType) StyleEntry {
	// This is less than ideal, but it's the price for not having to check errors on each Add().
	entry, _ := ParseStyleEntry(s.entries[ttype])
	if s.parent != nil {
		entry = entry.Inherit(s.parent.Get(ttype))
	}
	return entry
}

// Add an entry to the Style map.
//
// See http://pygments.org/docs/styles/#style-rules for details.
func (s *StyleBuilder) Add(ttype TokenType, entry string) *StyleBuilder { // nolint: gocyclo
	s.entries[ttype] = entry
	return s
}

func (s *StyleBuilder) AddEntry(ttype TokenType, entry StyleEntry) *StyleBuilder {
	s.entries[ttype] = entry.String()
	return s
}

// Transform passes each style entry currently defined in the builder to the supplied
// function and saves the returned value. This can be used to adjust a style's colours;
// see Colour's ClampBrightness function, for example.
func (s *StyleBuilder) Transform(transform func(StyleEntry) StyleEntry) *StyleBuilder {
	types := make(map[TokenType]struct{})
	for tt := range s.entries {
		types[tt] = struct{}{}
	}
	if s.parent != nil {
		for _, tt := range s.parent.Types() {
			types[tt] = struct{}{}
		}
	}
	for tt := range types {
		s.AddEntry(tt, transform(s.Get(tt)))
	}
	return s
}

func (s *StyleBuilder) Build() (*Style, error) {
	style := &Style{
		Name:    s.name,
		entries: map[TokenType]StyleEntry{},
		parent:  s.parent,
	}
	for ttype, descriptor := range s.entries {
		entry, err := ParseStyleEntry(descriptor)
		if err != nil {
			return nil, fmt.Errorf("invalid entry for %s: %s", ttype, err)
		}
		style.entries[ttype] = entry
	}
	return style, nil
}

// StyleEntries mapping TokenType to colour definition.
type StyleEntries map[TokenType]string

// NewXMLStyle parses an XML style definition.
func NewXMLStyle(r io.Reader) (*Style, error) {
	dec := xml.NewDecoder(r)
	style := &Style{}
	return style, dec.Decode(style)
}

// MustNewXMLStyle is like NewXMLStyle but panics on error.
func MustNewXMLStyle(r io.Reader) *Style {
	style, err := NewXMLStyle(r)
	if err != nil {
		panic(err)
	}
	return style
}

// NewStyle creates a new style definition.
func NewStyle(name string, entries StyleEntries) (*Style, error) {
	return NewStyleBuilder(name).AddAll(entries).Build()
}

// MustNewStyle creates a new style or panics.
func MustNewStyle(name string, entries StyleEntries) *Style {
	style, err := NewStyle(name, entries)
	if err != nil {
		panic(err)
	}
	return style
}

// A Style definition.
//
// See http://pygments.org/docs/styles/ for details. Semantics are intended to be identical.
type Style struct {
	Name    string
	entries map[TokenType]StyleEntry
	parent  *Style
}

func (s *Style) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if s.parent != nil {
		return fmt.Errorf("cannot marshal style with parent")
	}
	start.Name = xml.Name{Local: "style"}
	start.Attr = []xml.Attr{{Name: xml.Name{Local: "name"}, Value: s.Name}}
	if err := e.EncodeToken(start); err != nil {
		return err
	}
	sorted := make([]TokenType, 0, len(s.entries))
	for ttype := range s.entries {
		sorted = append(sorted, ttype)
	}
	sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
	for _, ttype := range sorted {
		entry := s.entries[ttype]
		el := xml.StartElement{Name: xml.Name{Local: "entry"}}
		el.Attr = []xml.Attr{
			{Name: xml.Name{Local: "type"}, Value: ttype.String()},
			{Name: xml.Name{Local: "style"}, Value: entry.String()},
		}
		if err := e.EncodeToken(el); err != nil {
			return err
		}
		if err := e.EncodeToken(xml.EndElement{Name: el.Name}); err != nil {
			return err
		}
	}
	return e.EncodeToken(xml.EndElement{Name: start.Name})
}

func (s *Style) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	for _, attr := range start.Attr {
		if attr.Name.Local == "name" {
			s.Name = attr.Value
		} else {
			return fmt.Errorf("unexpected attribute %s", attr.Name.Local)
		}
	}
	if s.Name == "" {
		return fmt.Errorf("missing style name attribute")
	}
	s.entries = map[TokenType]StyleEntry{}
	for {
		tok, err := d.Token()
		if err != nil {
			return err
		}
		switch el := tok.(type) {
		case xml.StartElement:
			if el.Name.Local != "entry" {
				return fmt.Errorf("unexpected element %s", el.Name.Local)
			}
			var ttype TokenType
			var entry StyleEntry
			for _, attr := range el.Attr {
				switch attr.Name.Local {
				case "type":
					ttype, err = TokenTypeString(attr.Value)
					if err != nil {
						return err
					}

				case "style":
					entry, err = ParseStyleEntry(attr.Value)
					if err != nil {
						return err
					}

				default:
					return fmt.Errorf("unexpected attribute %s", attr.Name.Local)
				}
			}
			s.entries[ttype] = entry

		case xml.EndElement:
			if el.Name.Local == start.Name.Local {
				return nil
			}
		}
	}
}

// Types that are styled.
func (s *Style) Types() []TokenType {
	dedupe := map[TokenType]bool{}
	for tt := range s.entries {
		dedupe[tt] = true
	}
	if s.parent != nil {
		for _, tt := range s.parent.Types() {
			dedupe[tt] = true
		}
	}
	out := make([]TokenType, 0, len(dedupe))
	for tt := range dedupe {
		out = append(out, tt)
	}
	return out
}

// Builder creates a mutable builder from this Style.
//
// The builder can then be safely modified. This is a cheap operation.
func (s *Style) Builder() *StyleBuilder {
	return &StyleBuilder{
		name:    s.Name,
		entries: map[TokenType]string{},
		parent:  s,
	}
}

// Has checks if an exact style entry match exists for a token type.
//
// This is distinct from Get() which will merge parent tokens.
func (s *Style) Has(ttype TokenType) bool {
	return !s.get(ttype).IsZero() || s.synthesisable(ttype)
}

// Get a style entry. Will try sub-category or category if an exact match is not found, and
// finally return the Background.
func (s *Style) Get(ttype TokenType) StyleEntry {
	return s.get(ttype).Inherit(
		s.get(Background),
		s.get(Text),
		s.get(ttype.Category()),
		s.get(ttype.SubCategory()))
}

func (s *Style) get(ttype TokenType) StyleEntry {
	out := s.entries[ttype]
	if out.IsZero() && s.parent != nil {
		return s.parent.get(ttype)
	}
	if out.IsZero() && s.synthesisable(ttype) {
		out = s.synthesise(ttype)
	}
	return out
}

func (s *Style) synthesise(ttype TokenType) StyleEntry {
	bg := s.get(Background)
	text := StyleEntry{Colour: bg.Colour}
	text.Colour = text.Colour.BrightenOrDarken(0.5)

	switch ttype {
	// If we don't have a line highlight colour, make one that is 10% brighter/darker than the background.
	case LineHighlight:
		return StyleEntry{Background: bg.Background.BrightenOrDarken(0.1)}

	// If we don't have line numbers, use the text colour but 20% brighter/darker
	case LineNumbers, LineNumbersTable:
		return text

	default:
		return StyleEntry{}
	}
}

func (s *Style) synthesisable(ttype TokenType) bool {
	return ttype == LineHighlight || ttype == LineNumbers || ttype == LineNumbersTable
}

// MustParseStyleEntry parses a Pygments style entry or panics.
func MustParseStyleEntry(entry string) StyleEntry {
	out, err := ParseStyleEntry(entry)
	if err != nil {
		panic(err)
	}
	return out
}

// ParseStyleEntry parses a Pygments style entry.
func ParseStyleEntry(entry string) (StyleEntry, error) { // nolint: gocyclo
	out := StyleEntry{}
	parts := strings.Fields(entry)
	for _, part := range parts {
		switch {
		case part == "italic":
			out.Italic = Yes
		case part == "noitalic":
			out.Italic = No
		case part == "bold":
			out.Bold = Yes
		case part == "nobold":
			out.Bold = No
		case part == "underline":
			out.Underline = Yes
		case part == "nounderline":
			out.Underline = No
		case part == "inherit":
			out.NoInherit = false
		case part == "noinherit":
			out.NoInherit = true
		case part == "bg:":
			out.Background = 0
		case strings.HasPrefix(part, "bg:#"):
			out.Background = ParseColour(part[3:])
			if !out.Background.IsSet() {
				return StyleEntry{}, fmt.Errorf("invalid background colour %q", part)
			}
		case strings.HasPrefix(part, "border:#"):
			out.Border = ParseColour(part[7:])
			if !out.Border.IsSet() {
				return StyleEntry{}, fmt.Errorf("invalid border colour %q", part)
			}
		case strings.HasPrefix(part, "#"):
			out.Colour = ParseColour(part)
			if !out.Colour.IsSet() {
				return StyleEntry{}, fmt.Errorf("invalid colour %q", part)
			}
		default:
			return StyleEntry{}, fmt.Errorf("unknown style element %q", part)
		}
	}
	return out, nil
}