// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package chartconfig

import (
	_ "embed"
	"fmt"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"unicode"
)

//go:embed config.txt
var chartConfig []byte

func Raw() []byte {
	return chartConfig
}

// Load loads and parses the current chart config.
func Load() ([]ChartConfig, error) {
	return Parse(chartConfig)
}

// Parse parses ChartConfig records from the provided raw data, returning an
// error if the config has invalid syntax. See the package documentation for a
// description of the record syntax.
//
// Even with correct syntax, the resulting chart config may not meet all the
// requirements described in the package doc. Call [Validate] to check whether
// the config data is coherent.
func Parse(data []byte) ([]ChartConfig, error) {
	// Collect field information for the record type.
	var (
		prefixes []string                               // for parse errors
		fields   = make(map[string]reflect.StructField) // key -> struct field
	)
	{
		typ := reflect.TypeOf(ChartConfig{})
		for i := 0; i < typ.NumField(); i++ {
			f := typ.Field(i)
			key := strings.ToLower(f.Name)
			if _, ok := fieldParsers[key]; !ok {
				panic(fmt.Sprintf("no parser for field %q", f.Name))
			}
			prefixes = append(prefixes, "'"+key+":'")
			fields[key] = f
		}
		sort.Strings(prefixes)
	}

	// Read records, separated by '---'
	var (
		records    []ChartConfig
		inProgress = new(ChartConfig)      // record value currently being parsed
		set        = make(map[string]bool) // fields that are set so far; empty records are skipped
	)
	flushRecord := func() {
		if len(set) > 0 { // only flush non-empty records
			records = append(records, *inProgress)
		}
		inProgress = new(ChartConfig)
		set = make(map[string]bool)
	}

	// Within bucket braces in counter fields, newlines are ignored.
	// if we're in the middle of a multiline counter field, accumulatedCounterText
	// contains the joined lines of the field up to the current line. Once
	// a line containing an end brace is reached, line will be set to the
	// joined lines of accumulatedCounterText and processed as a single line.
	var accumulatedCounterText string

	for lineNum, line := range strings.Split(string(data), "\n") {
		if line == "---" {
			if accumulatedCounterText != "" {
				return nil, fmt.Errorf("line %d: reached end of record while processing multiline counter field", lineNum)
			}
			flushRecord()
			continue
		}
		text, _, _ := strings.Cut(line, "#") // trim comments

		// Processing of counter fields which can appear across multiple lines.
		// See comment on accumulatedCounterText.
		if accumulatedCounterText == "" {
			if oi := strings.Index(text, "{"); oi >= 0 {
				if strings.Contains(text[:oi], "}") {
					return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
				}
				if strings.Contains(text[oi+len("{"):], "{") {
					return nil, fmt.Errorf("line %d: invalid line %q: unexpected '{'", lineNum, line)
				}
				if !strings.HasPrefix(text, "counter:") {
					return nil, fmt.Errorf("line %d: invalid line %q: '{' is only allowed to appear within a counter field", lineNum, line)
				}
				accumulatedCounterText = strings.TrimRightFunc(text, unicode.IsSpace)
				// Don't continue here. If the counter field is a single line
				// the check for the close brace below will close the line
				// and process it as text. Set text to "" so when it's appended to
				// accumulatedCounterText we don't add the line twice.
				text = ""
			} else if strings.Contains(text, "}") {
				return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
			}
		}
		if accumulatedCounterText != "" {
			if strings.Contains(text, "{") {
				return nil, fmt.Errorf("line %d: invalid line %q: '{' is only allowed to appear once within a counter field", lineNum, line)
			}
			accumulatedCounterText += strings.TrimSpace(text)
			if ci := strings.Index(accumulatedCounterText, "}"); ci >= 0 {
				if strings.Contains(accumulatedCounterText[ci+len("}"):], "}") {
					return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
				}
				if ci > 0 && strings.HasSuffix(accumulatedCounterText[:ci], ",") {
					return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}' after ','", lineNum, line)
				}
				text = accumulatedCounterText
				accumulatedCounterText = ""
			} else {
				// We're in the middle of a multiline counter field. Continue
				// processing.
				continue
			}
		}

		var key string
		for k := range fields {
			prefix := k + ":"
			if strings.HasPrefix(text, prefix) {
				key = k
				text = text[len(prefix):]
				break
			}
		}

		text = strings.TrimSpace(text)
		if text == "" {
			// Check for empty lines before the field == nil check below.
			// Lines consisting only of whitespace and comments are OK.
			continue
		}
		if key == "" {
			return nil, fmt.Errorf("line %d: invalid line %q: lines must be '---', consist only of whitespace/comments, or start with %s", lineNum, line, strings.Join(prefixes, ", "))
		}
		field := fields[key]
		v := reflect.ValueOf(inProgress).Elem().FieldByName(field.Name)
		if set[key] && field.Type.Kind() != reflect.Slice {
			return nil, fmt.Errorf("line %d: field %s may not be repeated", lineNum, strings.ToLower(field.Name))
		}
		parser := fieldParsers[key]
		if err := parser(v, text); err != nil {
			return nil, fmt.Errorf("line %d: field %q: %v", lineNum, field.Name, err)
		}
		set[key] = true
	}

	if accumulatedCounterText != "" {
		return nil, fmt.Errorf("reached end of file while processing multiline counter field")
	}

	flushRecord()
	return records, nil
}

// A fieldParser parses the provided input and writes to v, which must be
// addressable.
type fieldParser func(v reflect.Value, input string) error

var fieldParsers = map[string]fieldParser{
	"title":       parseString,
	"description": parseString,
	"issue":       parseSlice(parseString),
	"type":        parseString,
	"program":     parseString,
	"module":      parseString,
	"counter":     parseString,
	"depth":       parseInt,
	"error":       parseFloat,
	"version":     parseString,
}

func parseString(v reflect.Value, input string) error {
	v.SetString(input)
	return nil
}

func parseInt(v reflect.Value, input string) error {
	i, err := strconv.ParseInt(input, 10, 64)
	if err != nil {
		return fmt.Errorf("invalid int value %q", input)
	}
	v.SetInt(i)
	return nil
}

func parseFloat(v reflect.Value, input string) error {
	f, err := strconv.ParseFloat(input, 64)
	if err != nil {
		return fmt.Errorf("invalid float value %q", input)
	}
	v.SetFloat(f)
	return nil
}

func parseSlice(elemParser fieldParser) fieldParser {
	return func(v reflect.Value, input string) error {
		elem := reflect.New(v.Type().Elem()).Elem()
		v.Set(reflect.Append(v, elem))
		elem = v.Index(v.Len() - 1)
		if err := elemParser(elem, input); err != nil {
			return err
		}
		return nil
	}
}
