package lua

import (
	"bytes"
	"fmt"
	"io"

	"github.com/anchore/syft/syft/internal/parsing"
)

type rockspec struct {
	value []rockspecNode
}

type rockspecNode struct {
	key   string
	value interface{}
}

func (r rockspecNode) Slice() []rockspecNode {
	out, ok := r.value.([]rockspecNode)
	if ok {
		return out
	}
	return nil
}

func (r rockspecNode) String() string {
	out, ok := r.value.(string)
	if ok {
		return out
	}
	return ""
}

var noReturn = rockspec{
	value: nil,
}

// parseRockspec basic parser for rockspec
func parseRockspecData(reader io.Reader) (rockspec, error) {
	data, err := io.ReadAll(reader)
	if err != nil {
		return noReturn, err
	}

	i := 0
	locals := make(map[string]string)
	blocks, err := parseRockspecBlock(data, &i, locals)

	if err != nil {
		return noReturn, err
	}

	return rockspec{
		value: blocks,
	}, nil
}

func parseRockspecBlock(data []byte, i *int, locals map[string]string) ([]rockspecNode, error) {
	var out []rockspecNode
	var iterator func(data []byte, i *int, locals map[string]string) (*rockspecNode, error)

	parsing.SkipWhitespace(data, i)

	if *i >= len(data) && len(out) > 0 {
		return nil, fmt.Errorf("unexpected end of block at %d", *i)
	}

	c := data[*i]

	// Block starting with a comment
	if c == '-' {
		parseComment(data, i)
		parsing.SkipWhitespace(data, i)
		c = data[*i]
	}

	switch {
	case c == '"' || c == '\'':
		iterator = parseRockspecListItem
	case isLiteral(c):
		iterator = parseRockspecNode
	default:
		return nil, fmt.Errorf("unexpected character: %s", string(c))
	}

	for *i < len(data) {
		item, err := iterator(data, i, locals)
		if err != nil {
			return nil, fmt.Errorf("%w\n%s", err, parsing.PrintError(data, *i))
		}

		parsing.SkipWhitespace(data, i)

		if (item.key == "," || item.key == "-") && item.value == nil {
			continue
		}

		if item.key == "}" && item.value == nil {
			break
		}

		out = append(out, *item)
	}

	return out, nil
}

//nolint:funlen, gocognit
func parseRockspecNode(data []byte, i *int, locals map[string]string) (*rockspecNode, error) {
	parsing.SkipWhitespace(data, i)

	if *i >= len(data) {
		return nil, fmt.Errorf("unexpected end of node at %d", *i)
	}

	c := data[*i]

	if c == ',' || c == ';' || c == '}' {
		*i++
		return &rockspecNode{
			key: string(c),
		}, nil
	}

	if c == '-' {
		offset := *i + 1
		if offset >= len(data) {
			return nil, fmt.Errorf("unexpected character: %s", string(c))
		}
		c2 := data[offset]

		if c2 != '-' {
			return nil, fmt.Errorf("unexpected character: %s", string(c2))
		}

		parseComment(data, i)
		return &rockspecNode{
			key: string(c),
		}, nil
	}

	if !isLiteral(c) {
		return nil, fmt.Errorf("invalid literal character: %s", string(c))
	}

	key, err := parseRockspecLiteral(data, i, locals)
	if err != nil {
		return nil, err
	}

	parsing.SkipWhitespace(data, i)

	if *i >= len(data) {
		return nil, fmt.Errorf("unexpected end of node at %d", *i)
	}

	if key == "local" {
		err := parseLocal(data, i, locals)
		if err != nil {
			return nil, err
		}
		return &rockspecNode{
			key: ",",
		}, nil
	}

	c = data[*i]
	if c != '=' {
		return nil, fmt.Errorf("unexpected character: %s", string(c))
	}

	*i++
	parsing.SkipWhitespace(data, i)

	if *i >= len(data) {
		return nil, fmt.Errorf("unexpected end of node at %d", *i)
	}

	if key == "build" {
		skipBuildNode(data, i)

		return &rockspecNode{
			key: ",",
		}, nil
	}

	c = data[*i]

	switch c {
	case '{':
		offset := *i + 1
		parsing.SkipWhitespace(data, &offset)
		if offset >= len(data) {
			return nil, fmt.Errorf("unterminated block at %d", *i)
		}
		c2 := data[offset]

		// Add support for empty lists
		if c == '{' && c2 == '}' {
			*i = offset + 1
			return &rockspecNode{}, nil
		}

		*i = offset
		parsing.SkipWhitespace(data, i)

		obj, err := parseRockspecBlock(data, i, locals)

		if err != nil {
			return nil, err
		}
		value := obj

		return &rockspecNode{
			key, value,
		}, nil
	case '(':
		skipExpression(data, i)
		return &rockspecNode{
			key: ",",
		}, nil
	case '[':
		offset := *i + 1
		if offset >= len(data) {
			return nil, fmt.Errorf("unterminated block at %d", *i)
		}
		c2 := data[offset]

		if c2 != '[' {
			return nil, fmt.Errorf("unexpected character: %s", string(c))
		}

		*i++

		str, err := parseRockspecString(data, i, locals)

		if err != nil {
			return nil, err
		}
		value := str.String()

		c = data[*i]

		if c != ']' {
			return nil, fmt.Errorf("unexpected character: %s", string(c))
		}

		*i++

		return &rockspecNode{
			key, value,
		}, nil
	}

	value, err := parseRockspecValue(data, i, locals, "")

	if err != nil {
		return nil, err
	}

	return &rockspecNode{
		key, value,
	}, nil
}

func parseRockspecListItem(data []byte, i *int, locals map[string]string) (*rockspecNode, error) {
	parsing.SkipWhitespace(data, i)

	if *i >= len(data) {
		return nil, fmt.Errorf("unexpected end of block at %d", *i)
	}

	c := data[*i]
	if c == ',' || c == ';' || c == '}' {
		*i++
		return &rockspecNode{
			key: string(c),
		}, nil
	}

	if c == '-' {
		offset := *i + 1
		if offset >= len(data) {
			return nil, fmt.Errorf("unexpected character: %s", string(c))
		}
		c2 := data[offset]

		if c2 != '-' {
			return nil, fmt.Errorf("unexpected character: %s", string(c2))
		}

		parseComment(data, i)
		return &rockspecNode{
			key: string(c),
		}, nil
	}

	str, err := parseRockspecString(data, i, locals)
	if err != nil {
		return nil, err
	}
	return str, nil
}

func parseRockspecValue(data []byte, i *int, locals map[string]string, initialValue string) (string, error) {
	c := data[*i]

	var value string

	switch c {
	case '"', '\'':
		str, err := parseRockspecString(data, i, locals)

		if err != nil {
			return "", err
		}
		value = str.value.(string)
	default:
		local, err := parseRockspecLiteral(data, i, locals)

		if err != nil {
			return "", err
		}

		l, ok := locals[local]

		if !ok {
			return "", fmt.Errorf("unknown local: %s", local)
		}

		value = l
	}

	value = fmt.Sprintf("%s%s", initialValue, value)

	skipWhitespaceNoNewLine(data, i)

	if len(data) > *i+2 {
		if data[*i] == '.' && data[*i+1] == '.' {
			*i += 2

			skipWhitespaceNoNewLine(data, i)

			if *i >= len(data) {
				return "", fmt.Errorf("unexpected end of expression at %d", *i)
			}

			v, err := parseRockspecValue(data, i, locals, value)

			if err != nil {
				return "", err
			}

			value = v
		}
	}

	return value, nil
}

func parseRockspecLiteral(data []byte, i *int, locals map[string]string) (string, error) {
	var buf bytes.Buffer
out:
	for *i < len(data) {
		c := data[*i]
		switch {
		case c == '[':
			*i++
			nested, err := parseRockspecString(data, i, locals)
			if err != nil {
				return "", err
			}
			c = data[*i]
			if c != ']' {
				return "", fmt.Errorf("unterminated literal at %d", *i)
			}
			buf.WriteString(fmt.Sprintf("[\"%s\"]", nested.String()))
		case isLiteral(c):
			buf.WriteByte(c)
		default:
			break out
		}
		*i++
	}
	return buf.String(), nil
}

func parseRockspecString(data []byte, i *int, _ map[string]string) (*rockspecNode, error) {
	delim := data[*i]
	var endDelim byte
	switch delim {
	case '"', '\'':
		endDelim = delim
	case '[':
		endDelim = ']'
	}

	*i++
	var buf bytes.Buffer
	for *i < len(data) {
		c := data[*i]
		if c == endDelim {
			*i++
			str := rockspecNode{value: buf.String()}
			return &str, nil
		}
		buf.WriteByte(c)
		*i++
	}
	return nil, fmt.Errorf("unterminated string at %d", *i)
}

func parseComment(data []byte, i *int) {
	for *i < len(data) {
		c := data[*i]

		*i++

		// Rest of a line is a comment. Deals with CR, LF and CR/LF
		if c == '\n' {
			break
		} else if c == '\r' && data[*i] == '\n' {
			*i++
			break
		}
	}
}

//nolint:funlen
func parseLocal(data []byte, i *int, locals map[string]string) error {
	keys := []string{}
	values := []string{}

keys:
	for {
		parsing.SkipWhitespace(data, i)

		key, err := parseRockspecLiteral(data, i, locals)
		if err != nil {
			return err
		}

		if key == "function" {
			err := skipFunction(data, i)
			if err != nil {
				return err
			}
			return nil
		}

		keys = append(keys, key)

		parsing.SkipWhitespace(data, i)

		c := data[*i]

		switch c {
		case ',':
			*i++
			continue
		case '=':
			*i++
			break keys
		default:
			return fmt.Errorf("unexpected character: %s", string(c))
		}
	}

values:
	for {
		skipWhitespaceNoNewLine(data, i)

		c := data[*i]

		switch c {
		case '"', '\'':
			value, err := parseRockspecString(data, i, locals)

			if err != nil {
				return err
			}
			values = append(values, value.value.(string))
		default:
			ref, err := parseRockspecLiteral(data, i, locals)
			if err != nil {
				return err
			}

			// Skip if it's an expression
			skipWhitespaceNoNewLine(data, i)
			c := data[*i]

			var value string

			if c != '\n' && c != '\r' {
				skipExpression(data, i)
				value = ""
			} else {
				value = locals[ref]
			}

			values = append(values, value)
		}

		skipWhitespaceNoNewLine(data, i)

		c = data[*i]

		switch c {
		case ',':
			*i++
			continue
		case '\n', '\r':
			parsing.SkipWhitespace(data, i)
			break values
		}
	}

	if len(keys) != len(values) {
		return fmt.Errorf("expected %d values got %d", len(keys), len(values))
	}

	for i := 0; i < len(keys); i++ {
		locals[keys[i]] = values[i]
	}

	return nil
}

func skipBuildNode(data []byte, i *int) {
	bracesCount := 0

	for *i < len(data) {
		c := data[*i]

		switch c {
		case '{':
			bracesCount++
		case '}':
			bracesCount--
		}

		if bracesCount == 0 {
			return
		}

		*i++
	}
}

func skipFunction(data []byte, i *int) error {
	blocks := 1

	for *i < len(data)-5 {
		if parsing.IsWhitespace(data[*i]) {
			switch {
			case string(data[*i+1:*i+3]) == "if" && parsing.IsWhitespace(data[*i+3]):
				blocks++
				*i += 3
			case string(data[*i+1:*i+4]) == "end" && parsing.IsWhitespace(data[*i+4]):
				blocks--
				*i += 4

				if blocks == 0 {
					return nil
				}
			default:
				*i++
			}
		} else {
			*i++
		}
	}

	return fmt.Errorf("unterminated function at %d", *i)
}

func skipExpression(data []byte, i *int) {
	parseComment(data, i)
}

func skipWhitespaceNoNewLine(data []byte, i *int) {
	for *i < len(data) && (data[*i] == ' ' || data[*i] == '\t') {
		*i++
	}
}

func isLiteral(c byte) bool {
	if c == '[' || c == ']' {
		return true
	}
	if c == '.' {
		return false
	}
	return parsing.IsLiteral(c)
}
