package ini

import (
	"fmt"
	"strings"

	"github.com/wiggin77/merror"
)

// LF is linefeed
const LF byte = 0x0A

// CR is carriage return
const CR byte = 0x0D

// getSections parses an INI formatted string, or string containing just name/value pairs,
// returns map of `Section`'s.
//
// Any name/value pairs appearing before a section name are added to the section named
// with an empty string ("").  Also true for Linux-style config files where all props
// are outside a named section.
//
// Any errors encountered are aggregated and returned, along with the partially parsed
// sections.
func getSections(str string) (map[string]*Section, error) {
	merr := merror.New()
	mapSections := make(map[string]*Section)
	lines := buildLineArray(str)
	section := newSection("")

	for _, line := range lines {
		name, ok := parseSection(line)
		if ok {
			// A section name encountered. Stop processing the current one.
			// Don't add the current section to the map if the section name is blank
			// and the prop map is empty.
			nameCurr := section.GetName()
			if nameCurr != "" || section.hasKeys() {
				mapSections[nameCurr] = section
			}
			// Start processing a new section.
			section = newSection(name)
		} else {
			// Parse the property and add to the current section, or ignore if comment.
			if k, v, comment, err := parseProp(line); !comment && err == nil {
				section.setProp(k, v)
			} else if err != nil {
				merr.Append(err) // aggregate errors
			}
		}

	}
	// If the current section is not empty, add it.
	if section.hasKeys() {
		mapSections[section.GetName()] = section
	}
	return mapSections, merr.ErrorOrNil()
}

// buildLineArray parses the given string buffer and creates a list of strings,
// one for each line in the string buffer.
//
// A line is considered to be terminated by any one of a line feed ('\n'),
// a carriage return ('\r'), or a carriage return followed immediately by a
// linefeed.
//
// Lines prefixed with ';' or '#' are considered comments and skipped.
func buildLineArray(str string) []string {
	arr := make([]string, 0, 10)
	str = str + "\n"

	iLen := len(str)
	iPos, iBegin := 0, 0
	var ch byte

	for iPos < iLen {
		ch = str[iPos]
		if ch == LF || ch == CR {
			sub := str[iBegin:iPos]
			sub = strings.TrimSpace(sub)
			if sub != "" && !strings.HasPrefix(sub, ";") && !strings.HasPrefix(sub, "#") {
				arr = append(arr, sub)
			}
			iPos++
			if ch == CR && iPos < iLen && str[iPos] == LF {
				iPos++
			}
			iBegin = iPos
		} else {
			iPos++
		}
	}
	return arr
}

// parseSection parses the specified string for a section name enclosed in square brackets.
// Returns the section name found, or `ok=false` if `str` is not a section header.
func parseSection(str string) (name string, ok bool) {
	str = strings.TrimSpace(str)
	if !strings.HasPrefix(str, "[") {
		return "", false
	}
	iCloser := strings.Index(str, "]")
	if iCloser == -1 {
		return "", false
	}
	return strings.TrimSpace(str[1:iCloser]), true
}

// parseProp parses the specified string and extracts a key/value pair.
//
// If the string is a comment (prefixed with ';' or '#') then `comment=true`
// and key will be empty.
func parseProp(str string) (key string, val string, comment bool, err error) {
	iLen := len(str)
	iEqPos := strings.Index(str, "=")
	if iEqPos == -1 {
		return "", "", false, fmt.Errorf("not a key/value pair:'%s'", str)
	}

	key = str[0:iEqPos]
	key = strings.TrimSpace(key)
	if iEqPos+1 < iLen {
		val = str[iEqPos+1:]
		val = strings.TrimSpace(val)
	}

	// Check that the key has at least 1 char.
	if key == "" {
		return "", "", false, fmt.Errorf("key is empty for '%s'", str)
	}

	// Check if this line is a comment that just happens
	// to have an equals sign in it. Not an error, but not a
	// useable line either.
	if strings.HasPrefix(key, ";") || strings.HasPrefix(key, "#") {
		key = ""
		val = ""
		comment = true
	}
	return key, val, comment, err
}