package flags

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"reflect"
	"strings"
)

type iniValue struct {
	Name  string
	Value string
}

type iniSection []iniValue
type ini map[string]iniSection

func readFullLine(reader *bufio.Reader) (string, error) {
	var line []byte

	for {
		l, more, err := reader.ReadLine()

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

		if line == nil && !more {
			return string(l), nil
		}

		line = append(line, l...)

		if !more {
			break
		}
	}

	return string(line), nil
}

func optionIniName(option *Option) string {
	name := option.tag.Get("_read-ini-name")

	if len(name) != 0 {
		return name
	}

	name = option.tag.Get("ini-name")

	if len(name) != 0 {
		return name
	}

	return option.field.Name
}

func writeGroupIni(group *Group, namespace string, writer io.Writer, options IniOptions) {
	var sname string

	if len(namespace) != 0 {
		sname = namespace + "." + group.ShortDescription
	} else {
		sname = group.ShortDescription
	}

	sectionwritten := false
	comments := (options & IniIncludeComments) != IniNone

	for _, option := range group.options {
		if option.isFunc() {
			continue
		}

		if len(option.tag.Get("no-ini")) != 0 {
			continue
		}

		val := option.value

		if (options&IniIncludeDefaults) == IniNone && reflect.DeepEqual(val.Interface(), option.defaultValue.Interface()) {
			continue
		}

		if !sectionwritten {
			fmt.Fprintf(writer, "[%s]\n", sname)
			sectionwritten = true
		}

		if comments && len(option.Description) != 0 {
			fmt.Fprintf(writer, "; %s\n", option.Description)
		}

		oname := optionIniName(option)

		commentOption := ""
		if (options&(IniIncludeDefaults|IniCommentDefaults)) == IniIncludeDefaults|IniCommentDefaults && reflect.DeepEqual(val.Interface(), option.defaultValue.Interface()) {
			commentOption = "; "
		}

		switch val.Type().Kind() {
		case reflect.Slice:
			for idx := 0; idx < val.Len(); idx++ {
				v, _ := convertToString(val.Index(idx), option.tag)
				fmt.Fprintf(writer, "%s%s = %s\n", commentOption, oname, v)
			}

			if val.Len() == 0 {
				fmt.Fprintf(writer, "; %s =\n", oname)
			}
		case reflect.Map:
			for _, key := range val.MapKeys() {
				k, _ := convertToString(key, option.tag)
				v, _ := convertToString(val.MapIndex(key), option.tag)

				fmt.Fprintf(writer, "%s%s = %s:%s\n", commentOption, oname, k, v)
			}

			if val.Len() == 0 {
				fmt.Fprintf(writer, "; %s =\n", oname)
			}
		default:
			v, _ := convertToString(val, option.tag)

			if len(v) != 0 {
				fmt.Fprintf(writer, "%s%s = %s\n", commentOption, oname, v)
			} else {
				fmt.Fprintf(writer, "%s%s =\n", commentOption, oname)
			}
		}

		if comments {
			fmt.Fprintln(writer)
		}
	}

	if sectionwritten && !comments {
		fmt.Fprintln(writer)
	}
}

func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) {
	command.eachGroup(func(group *Group) {
		writeGroupIni(group, namespace, writer, options)
	})

	for _, c := range command.commands {
		var nns string

		if len(namespace) != 0 {
			nns = c.Name + "." + nns
		} else {
			nns = c.Name
		}

		writeCommandIni(c, nns, writer, options)
	}
}

func writeIni(parser *IniParser, writer io.Writer, options IniOptions) {
	writeCommandIni(parser.parser.Command, "", writer, options)
}

func readIniFromFile(filename string) (ini, error) {
	file, err := os.Open(filename)

	if err != nil {
		return nil, err
	}

	defer file.Close()

	return readIni(file, filename)
}

func readIni(contents io.Reader, filename string) (ini, error) {
	ret := make(ini)

	reader := bufio.NewReader(contents)

	section := make(iniSection, 0, 10)
	sectionname := ""

	ret[sectionname] = section

	var lineno uint

	for {
		line, err := readFullLine(reader)

		if err == io.EOF {
			break
		}

		if err != nil {
			return nil, err
		}

		lineno++
		line = strings.TrimSpace(line)

		if len(line) == 0 || line[0] == ';' || line[0] == '#' {
			continue
		}

		if line[0] == '[' {
			if line[0] != '[' || line[len(line)-1] != ']' {
				return nil, &IniError{
					Message:    "malformed section header",
					File:       filename,
					LineNumber: lineno,
				}
			}

			name := strings.TrimSpace(line[1 : len(line)-1])

			if len(name) == 0 {
				return nil, &IniError{
					Message:    "empty section name",
					File:       filename,
					LineNumber: lineno,
				}
			}

			sectionname = name
			section = ret[name]

			if section == nil {
				section = make(iniSection, 0, 10)
				ret[name] = section
			}

			continue
		}

		keyval := strings.SplitN(line, "=", 2)

		if len(keyval) != 2 {
			return nil, &IniError{
				Message:    fmt.Sprintf("malformed key=value (%s)", line),
				File:       filename,
				LineNumber: lineno,
			}
		}

		name := strings.TrimSpace(keyval[0])
		value := strings.TrimSpace(keyval[1])

		section = append(section, iniValue{
			Name:  name,
			Value: value,
		})

		ret[sectionname] = section
	}

	return ret, nil
}

func (i *IniParser) matchingGroups(name string) []*Group {
	if len(name) == 0 {
		var ret []*Group

		i.parser.eachGroup(func(g *Group) {
			ret = append(ret, g)
		})

		return ret
	}

	g := i.parser.groupByName(name)

	if g != nil {
		return []*Group{g}
	}

	return nil
}

func (i *IniParser) parse(ini ini) error {
	p := i.parser

	for name, section := range ini {
		groups := i.matchingGroups(name)

		if len(groups) == 0 {
			return newError(ErrUnknownGroup,
				fmt.Sprintf("could not find option group `%s'", name))
		}

		for _, inival := range section {
			var opt *Option

			for _, group := range groups {
				opt = group.optionByName(inival.Name, func(o *Option, n string) bool {
					return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n)
				})

				if opt != nil && len(opt.tag.Get("no-ini")) != 0 {
					opt = nil
				}

				if opt != nil {
					break
				}
			}

			if opt == nil {
				if (p.Options & IgnoreUnknown) == None {
					return newError(ErrUnknownFlag,
						fmt.Sprintf("unknown option: %s", inival.Name))
				}

				continue
			}

			pval := &inival.Value

			if !opt.canArgument() && len(inival.Value) == 0 {
				pval = nil
			}

			if err := opt.set(pval); err != nil {
				return wrapError(err)
			}

			opt.tag.Set("_read-ini-name", inival.Name)
		}
	}

	return nil
}
package flags

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"reflect"
	"strings"
)

type iniValue struct {
	Name  string
	Value string
}

type iniSection []iniValue
type ini map[string]iniSection

func readFullLine(reader *bufio.Reader) (string, error) {
	var line []byte

	for {
		l, more, err := reader.ReadLine()

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

		if line == nil && !more {
			return string(l), nil
		}

		line = append(line, l...)

		if !more {
			break
		}
	}

	return string(line), nil
}

func optionIniName(option *Option) string {
	name := option.tag.Get("_read-ini-name")

	if len(name) != 0 {
		return name
	}

	name = option.tag.Get("ini-name")

	if len(name) != 0 {
		return name
	}

	return option.field.Name
}

func writeGroupIni(group *Group, namespace string, writer io.Writer, options IniOptions) {
	var sname string

	if len(namespace) != 0 {
		sname = namespace + "." + group.ShortDescription
	} else {
		sname = group.ShortDescription
	}

	sectionwritten := false
	comments := (options & IniIncludeComments) != IniNone

	for _, option := range group.options {
		if option.isFunc() {
			continue
		}

		if len(option.tag.Get("no-ini")) != 0 {
			continue
		}

		val := option.value

		if (options&IniIncludeDefaults) == IniNone && reflect.DeepEqual(val.Interface(), option.defaultValue.Interface()) {
			continue
		}

		if !sectionwritten {
			fmt.Fprintf(writer, "[%s]\n", sname)
			sectionwritten = true
		}

		if comments && len(option.Description) != 0 {
			fmt.Fprintf(writer, "; %s\n", option.Description)
		}

		oname := optionIniName(option)

		commentOption := ""
		if (options&(IniIncludeDefaults|IniCommentDefaults)) == IniIncludeDefaults|IniCommentDefaults && reflect.DeepEqual(val.Interface(), option.defaultValue.Interface()) {
			commentOption = "; "
		}

		switch val.Type().Kind() {
		case reflect.Slice:
			for idx := 0; idx < val.Len(); idx++ {
				v, _ := convertToString(val.Index(idx), option.tag)
				fmt.Fprintf(writer, "%s%s = %s\n", commentOption, oname, v)
			}

			if val.Len() == 0 {
				fmt.Fprintf(writer, "; %s =\n", oname)
			}
		case reflect.Map:
			for _, key := range val.MapKeys() {
				k, _ := convertToString(key, option.tag)
				v, _ := convertToString(val.MapIndex(key), option.tag)

				fmt.Fprintf(writer, "%s%s = %s:%s\n", commentOption, oname, k, v)
			}

			if val.Len() == 0 {
				fmt.Fprintf(writer, "; %s =\n", oname)
			}
		default:
			v, _ := convertToString(val, option.tag)

			if len(v) != 0 {
				fmt.Fprintf(writer, "%s%s = %s\n", commentOption, oname, v)
			} else {
				fmt.Fprintf(writer, "%s%s =\n", commentOption, oname)
			}
		}

		if comments {
			fmt.Fprintln(writer)
		}
	}

	if sectionwritten && !comments {
		fmt.Fprintln(writer)
	}
}

func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) {
	command.eachGroup(func(group *Group) {
		writeGroupIni(group, namespace, writer, options)
	})

	for _, c := range command.commands {
		var nns string

		if len(namespace) != 0 {
			nns = c.Name + "." + nns
		} else {
			nns = c.Name
		}

		writeCommandIni(c, nns, writer, options)
	}
}

func writeIni(parser *IniParser, writer io.Writer, options IniOptions) {
	writeCommandIni(parser.parser.Command, "", writer, options)
}

func readIniFromFile(filename string) (ini, error) {
	file, err := os.Open(filename)

	if err != nil {
		return nil, err
	}

	defer file.Close()

	return readIni(file, filename)
}

func readIni(contents io.Reader, filename string) (ini, error) {
	ret := make(ini)

	reader := bufio.NewReader(contents)

	// Empty global section
	section := make(iniSection, 0, 10)
	sectionname := ""

	ret[sectionname] = section

	var lineno uint

	for {
		line, err := readFullLine(reader)

		if err == io.EOF {
			break
		}

		if err != nil {
			return nil, err
		}

		lineno++
		line = strings.TrimSpace(line)

		// Skip empty lines and lines starting with ; (comments)
		if len(line) == 0 || line[0] == ';' || line[0] == '#' {
			continue
		}

		if line[0] == '[' {
			if line[0] != '[' || line[len(line)-1] != ']' {
				return nil, &IniError{
					Message:    "malformed section header",
					File:       filename,
					LineNumber: lineno,
				}
			}

			name := strings.TrimSpace(line[1 : len(line)-1])

			if len(name) == 0 {
				return nil, &IniError{
					Message:    "empty section name",
					File:       filename,
					LineNumber: lineno,
				}
			}

			sectionname = name
			section = ret[name]

			if section == nil {
				section = make(iniSection, 0, 10)
				ret[name] = section
			}

			continue
		}

		// Parse option here
		keyval := strings.SplitN(line, "=", 2)

		if len(keyval) != 2 {
			return nil, &IniError{
				Message:    fmt.Sprintf("malformed key=value (%s)", line),
				File:       filename,
				LineNumber: lineno,
			}
		}

		name := strings.TrimSpace(keyval[0])
		value := strings.TrimSpace(keyval[1])

		section = append(section, iniValue{
			Name:  name,
			Value: value,
		})

		ret[sectionname] = section
	}

	return ret, nil
}

func (i *IniParser) matchingGroups(name string) []*Group {
	if len(name) == 0 {
		var ret []*Group

		i.parser.eachGroup(func(g *Group) {
			ret = append(ret, g)
		})

		return ret
	}

	g := i.parser.groupByName(name)

	if g != nil {
		return []*Group{g}
	}

	return nil
}

func (i *IniParser) parse(ini ini) error {
	p := i.parser

	for name, section := range ini {
		groups := i.matchingGroups(name)

		if len(groups) == 0 {
			return newError(ErrUnknownGroup,
				fmt.Sprintf("could not find option group `%s'", name))
		}

		for _, inival := range section {
			var opt *Option

			for _, group := range groups {
				opt = group.optionByName(inival.Name, func(o *Option, n string) bool {
					return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n)
				})

				if opt != nil && len(opt.tag.Get("no-ini")) != 0 {
					opt = nil
				}

				if opt != nil {
					break
				}
			}

			if opt == nil {
				if (p.Options & IgnoreUnknown) == None {
					return newError(ErrUnknownFlag,
						fmt.Sprintf("unknown option: %s", inival.Name))
				}

				continue
			}

			pval := &inival.Value

			if !opt.canArgument() && len(inival.Value) == 0 {
				pval = nil
			}

			if err := opt.set(pval); err != nil {
				return wrapError(err)
			}

			opt.tag.Set("_read-ini-name", inival.Name)
		}
	}

	return nil
}
