package ace

import (
	"bytes"
	"fmt"
	"io"
	"strings"
)

// Tag names
const (
	tagNameDiv = "div"
)

// Attribute names
const (
	attributeNameID = "id"
)

var inlineElements = map[string]bool{
	"b":        true,
	"big":      true,
	"i":        true,
	"small":    true,
	"tt":       true,
	"abbr":     true,
	"acronym":  true,
	"cite":     true,
	"code":     true,
	"dfn":      true,
	"em":       true,
	"kbd":      true,
	"strong":   true,
	"samp":     true,
	"time":     true,
	"var":      true,
	"a":        true,
	"bdo":      true,
	"br":       true,
	"img":      true,
	"map":      true,
	"object":   true,
	"q":        true,
	"script":   true,
	"span":     true,
	"sub":      true,
	"sup":      true,
	"button":   true,
	"input":    true,
	"label":    true,
	"select":   true,
	"textarea": true,
}

// htmlAttribute represents an HTML attribute.
type htmlAttribute struct {
	key   string
	value string
}

// htmlTag represents an HTML tag.
type htmlTag struct {
	elementBase
	tagName          string
	id               string
	classes          []string
	containPlainText bool
	insertBr         bool
	attributes       []htmlAttribute
	textValue        string
}

// WriteTo writes data to w.
func (e *htmlTag) WriteTo(w io.Writer) (int64, error) {
	var bf bytes.Buffer

	// Write an open tag.
	bf.WriteString(lt)
	bf.WriteString(e.tagName)
	// Write an id.
	if e.id != "" {
		bf.WriteString(space)
		bf.WriteString(attributeNameID)
		bf.WriteString(equal)
		bf.WriteString(doubleQuote)
		bf.WriteString(e.id)
		bf.WriteString(doubleQuote)
	}
	// Write classes.
	if len(e.classes) > 0 {
		bf.WriteString(space)
		bf.WriteString(e.opts.AttributeNameClass)
		bf.WriteString(equal)
		bf.WriteString(doubleQuote)
		for i, class := range e.classes {
			if i > 0 {
				bf.WriteString(space)
			}
			bf.WriteString(class)
		}
		bf.WriteString(doubleQuote)
	}
	// Write attributes.
	if len(e.attributes) > 0 {

		for _, a := range e.attributes {
			bf.WriteString(space)
			bf.WriteString(a.key)
			if a.value != "" {
				bf.WriteString(equal)
				bf.WriteString(doubleQuote)
				bf.WriteString(a.value)
				bf.WriteString(doubleQuote)
			}
		}
	}
	bf.WriteString(gt)

	// Write a text value
	if e.textValue != "" {
		if e.opts.formatter != nil {
			e.opts.formatter.WritingTextValue(&bf, e)
		}
		bf.WriteString(e.textValue)
	}

	if e.containPlainText {
		bf.WriteString(lf)
	}

	// Write children's HTML.
	if i, err := e.writeChildren(&bf); err != nil {
		return i, err
	}

	// Write a close tag.
	if !e.noCloseTag() {
		if e.opts.formatter != nil {
			e.opts.formatter.ClosingElement(&bf, e)
		}
		bf.WriteString(lt)
		bf.WriteString(slash)
		bf.WriteString(e.tagName)
		bf.WriteString(gt)
	}

	// Write the buffer.
	i, err := w.Write(bf.Bytes())

	return int64(i), err
}

// ContainPlainText returns the HTML tag's containPlainText field.
func (e *htmlTag) ContainPlainText() bool {
	return e.containPlainText
}

// InsertBr returns true if the br tag is inserted to the line.
func (e *htmlTag) InsertBr() bool {
	return e.insertBr
}

// setAttributes parses the tokens and set attributes to the element.
func (e *htmlTag) setAttributes() error {
	parsedTokens := e.parseTokens()

	var i int
	var token string
	var setTextValue bool

	// Set attributes to the element.
	for i, token = range parsedTokens {
		kv := strings.Split(token, equal)

		if len(kv) < 2 {
			setTextValue = true
			break
		}

		k := kv[0]
		v := strings.Join(kv[1:], equal)

		// Remove the prefix and suffix of the double quotes.
		if len(v) > 1 && strings.HasPrefix(v, doubleQuote) && strings.HasSuffix(v, doubleQuote) {
			v = v[1 : len(v)-1]
		}

		switch k {
		case attributeNameID:
			if e.id != "" {
				return fmt.Errorf("multiple IDs are specified [file: %s][line: %d]", e.ln.fileName(), e.ln.no)
			}
			e.id = v
		case e.opts.AttributeNameClass:
			e.classes = append(e.classes, strings.Split(v, space)...)
		default:
			e.attributes = append(e.attributes, htmlAttribute{k, v})
		}
	}

	// Set a text value to the element.
	if setTextValue {
		e.textValue = strings.Join(parsedTokens[i:], space)
	}

	return nil
}

// noCloseTag returns true is the HTML tag has no close tag.
func (e *htmlTag) noCloseTag() bool {
	for _, name := range e.opts.NoCloseTagNames {
		if e.tagName == name {
			return true
		}
	}

	return false
}

// true if the element is inline one
func (e *htmlTag) IsBlockElement() bool {
	if inline, found := inlineElements[e.tagName]; found {
		return !inline
	} else {
		return true
	}
}

// newHTMLTag creates and returns an HTML tag.
func newHTMLTag(ln *line, rslt *result, src *source, parent element, opts *Options) (*htmlTag, error) {
	if len(ln.tokens) < 1 {
		return nil, fmt.Errorf("an HTML tag is not specified [file: %s][line: %d]", ln.fileName(), ln.no)
	}

	s := ln.tokens[0]

	tagName := extractTagName(s)

	id, err := extractID(s, ln)
	if err != nil {
		return nil, err
	}

	classes := extractClasses(s)

	e := &htmlTag{
		elementBase:      newElementBase(ln, rslt, src, parent, opts),
		tagName:          tagName,
		id:               id,
		classes:          classes,
		containPlainText: strings.HasSuffix(s, dot),
		insertBr:         strings.HasSuffix(s, doubleDot),
		attributes:       make([]htmlAttribute, 0, 2),
	}

	if err := e.setAttributes(); err != nil {
		return nil, err
	}

	return e, nil
}

// extractTag extracts and returns a tag.
func extractTagName(s string) string {
	tagName := strings.Split(strings.Split(s, sharp)[0], dot)[0]

	if tagName == "" {
		tagName = tagNameDiv
	}

	return tagName
}

// extractID extracts and returns an ID.
func extractID(s string, ln *line) (string, error) {
	tokens := strings.Split(s, sharp)

	l := len(tokens)

	if l < 2 {
		return "", nil
	}

	if l > 2 {
		return "", fmt.Errorf("multiple IDs are specified [file: %s][line: %d]", ln.fileName(), ln.no)
	}

	return strings.Split(tokens[1], dot)[0], nil
}

// extractClasses extracts and returns classes.
func extractClasses(s string) []string {
	var classes []string

	for i, token := range strings.Split(s, dot) {
		if i == 0 {
			continue
		}

		class := strings.Split(token, sharp)[0]

		if class == "" {
			continue
		}

		classes = append(classes, class)
	}

	return classes
}

// parseTokens parses the tokens and return them
func (e *htmlTag) parseTokens() []string {
	var inQuote bool
	var inDelim bool
	var tokens []string
	var token string

	str := strings.Join(e.ln.tokens[1:], space)
	for _, chr := range str {
		switch c := string(chr); c {
		case space:
			if inQuote || inDelim {
				token += c
			} else {
				tokens = append(tokens, token)
				token = ""
			}
		case doubleQuote:
			if !inDelim {
				if inQuote {
					inQuote = false
				} else {
					inQuote = true
				}
			}
			token += c
		default:
			token += c
			if inDelim {
				if strings.HasSuffix(token, e.opts.DelimRight) {
					inDelim = false
				}
			} else {
				if strings.HasSuffix(token, e.opts.DelimLeft) {
					inDelim = true
				}
			}
		}
	}
	if len(token) > 0 {
		tokens = append(tokens, token)
	}
	return tokens
}
