package editorconfig

import (
	"errors"
	"fmt"
	"strconv"
	"strings"

	"golang.org/x/mod/semver"
	"gopkg.in/ini.v1"
)

// Definition represents a definition inside the .editorconfig file.
// E.g. a section of the file.
// The definition is composed of the selector ("*", "*.go", "*.{js.css}", etc),
// plus the properties of the selected files.
type Definition struct {
	Selector string `ini:"-" json:"-"`

	Charset                string            `ini:"charset"      json:"charset,omitempty"`
	IndentStyle            string            `ini:"indent_style" json:"indent_style,omitempty"`
	IndentSize             string            `ini:"indent_size"  json:"indent_size,omitempty"`
	TabWidth               int               `ini:"-"            json:"-"`
	EndOfLine              string            `ini:"end_of_line"  json:"end_of_line,omitempty"`
	TrimTrailingWhitespace *bool             `ini:"-"            json:"-"`
	InsertFinalNewline     *bool             `ini:"-"            json:"-"`
	Raw                    map[string]string `ini:"-"            json:"-"`
	version                string
}

// NewDefinition builds a definition from a given config.
func NewDefinition(config Config) (*Definition, error) {
	return config.Load(config.Path)
}

// normalize fixes some values to their lowercase value.
func (d *Definition) normalize() error {
	var result error

	d.Charset = strings.ToLower(d.Charset)
	d.EndOfLine = strings.ToLower(d.Raw["end_of_line"])
	d.IndentStyle = strings.ToLower(d.Raw["indent_style"])

	trimTrailingWhitespace, ok := d.Raw["trim_trailing_whitespace"]
	if ok && trimTrailingWhitespace != UnsetValue {
		trim, err := strconv.ParseBool(trimTrailingWhitespace)
		if err != nil {
			result = errors.Join(
				result,
				fmt.Errorf("trim_trailing_whitespace=%s is not an acceptable value. %w", trimTrailingWhitespace, err),
			)
		} else {
			d.TrimTrailingWhitespace = &trim
		}
	}

	insertFinalNewline, ok := d.Raw["insert_final_newline"]
	if ok && insertFinalNewline != UnsetValue {
		insert, err := strconv.ParseBool(insertFinalNewline)
		if err != nil {
			result = errors.Join(
				result,
				fmt.Errorf("insert_final_newline=%s is not an acceptable value. %w", insertFinalNewline, err),
			)
		} else {
			d.InsertFinalNewline = &insert
		}
	}

	// tab_width from Raw
	tabWidth, ok := d.Raw["tab_width"]
	if ok && tabWidth != UnsetValue {
		num, err := strconv.Atoi(tabWidth)
		if err != nil {
			result = errors.Join(result, fmt.Errorf("tab_width=%s is not an acceptable value. %w", tabWidth, err))
		} else {
			d.TabWidth = num
		}
	}

	// tab_width defaults to indent_size:
	// https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#tab_width
	num, err := strconv.Atoi(d.IndentSize)
	if err == nil && d.TabWidth <= 0 {
		d.TabWidth = num
	}

	return result
}

// merge the parent definition into the child definition.
func (d *Definition) merge(md *Definition) {
	if len(d.Charset) == 0 {
		d.Charset = md.Charset
	}

	if len(d.IndentStyle) == 0 {
		d.IndentStyle = md.IndentStyle
	}

	if len(d.IndentSize) == 0 {
		d.IndentSize = md.IndentSize
	}

	if d.TabWidth <= 0 {
		d.TabWidth = md.TabWidth
	}

	if len(d.EndOfLine) == 0 {
		d.EndOfLine = md.EndOfLine
	}

	if trimTrailingWhitespace, ok := d.Raw["trim_trailing_whitespace"]; !ok || trimTrailingWhitespace != UnsetValue {
		if d.TrimTrailingWhitespace == nil {
			d.TrimTrailingWhitespace = md.TrimTrailingWhitespace
		}
	}

	if insertFinalNewline, ok := d.Raw["insert_final_newline"]; !ok || insertFinalNewline != UnsetValue {
		if d.InsertFinalNewline == nil {
			d.InsertFinalNewline = md.InsertFinalNewline
		}
	}

	for k, v := range md.Raw {
		if _, ok := d.Raw[k]; !ok {
			d.Raw[k] = v
		}
	}
}

// InsertToIniFile writes the definition into a ini file.
func (d *Definition) InsertToIniFile(iniFile *ini.File) { //nolint:funlen,gocognit,cyclop
	iniSec := iniFile.Section(d.Selector)

	for k, v := range d.Raw {
		switch k {
		case "insert_final_newline":
			if d.InsertFinalNewline != nil {
				v = strconv.FormatBool(*d.InsertFinalNewline)
			} else {
				insertFinalNewline, ok := d.Raw["insert_final_newline"]
				if !ok {
					break
				}

				v = strings.ToLower(insertFinalNewline)
			}
		case "trim_trailing_whitespace":
			if d.TrimTrailingWhitespace != nil {
				v = strconv.FormatBool(*d.TrimTrailingWhitespace)
			} else {
				trimTrailingWhitespace, ok := d.Raw["trim_trailing_whitespace"]
				if !ok {
					break
				}

				v = strings.ToLower(trimTrailingWhitespace)
			}
		case "charset":
			v = d.Charset
		case "end_of_line":
			v = d.EndOfLine
		case "indent_style":
			v = d.IndentStyle
		case "tab_width":
			tabWidth, ok := d.Raw["tab_width"]
			if ok && tabWidth == UnsetValue {
				v = tabWidth
			} else {
				v = strconv.Itoa(d.TabWidth)
			}
		case "indent_size":
			v = d.IndentSize
		}

		iniSec.NewKey(k, v) //nolint:errcheck
	}

	if _, ok := d.Raw["indent_size"]; !ok {
		tabWidth, ok := d.Raw["tab_width"]

		switch {
		case ok && tabWidth == UnsetValue:
			// do nothing
		case d.TabWidth > 0:
			iniSec.NewKey("indent_size", strconv.Itoa(d.TabWidth)) //nolint:errcheck
		case d.IndentStyle == IndentStyleTab && (d.version == "" || semver.Compare(d.version, "v0.9.0") >= 0):
			iniSec.NewKey("indent_size", IndentStyleTab) //nolint:errcheck
		}
	}

	if _, ok := d.Raw["tab_width"]; !ok {
		if d.IndentSize == UnsetValue {
			iniSec.NewKey("tab_width", d.IndentSize) //nolint:errcheck
		} else {
			_, err := strconv.Atoi(d.IndentSize)
			if err == nil {
				iniSec.NewKey("tab_width", d.Raw["indent_size"]) //nolint:errcheck
			}
		}
	}
}
