// Copyright 2020 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package kconfig

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"regexp"
)

// ConfigFile represents a parsed .config file.
// It should not be modified directly, only by means of calling methods.
// The only exception is Config.Value which may be modified directly.
// Note: config names don't include CONFIG_ prefix, here and in other public interfaces,
// users of this package should never mention CONFIG_.
// Use Yes/Mod/No consts to check for/set config to particular values.
type ConfigFile struct {
	Configs  []*Config
	Map      map[string]*Config // duplicates Configs for convenience
	comments []string
}

type Config struct {
	Name     string
	Value    string
	comments []string
}

const (
	Yes    = "y"
	Mod    = "m"
	No     = "---===[[[is not set]]]===---" // to make it more obvious when some code writes it directly
	prefix = "CONFIG_"
)

// Value returns config value, or No if it's not present at all.
func (cf *ConfigFile) Value(name string) string {
	cfg := cf.Map[name]
	if cfg == nil {
		return No
	}
	return cfg.Value
}

// Set changes config value, or adds it if it's not yet present.
func (cf *ConfigFile) Set(name, val string) {
	cfg := cf.Map[name]
	if cfg == nil {
		cfg = &Config{
			Name:  name,
			Value: val,
		}
		cf.Map[name] = cfg
		cf.Configs = append(cf.Configs, cfg)
	}
	cfg.Value = val
	cfg.comments = append(cfg.comments, cf.comments...)
	cf.comments = nil
}

// Unset sets config value to No, if it's present in the config.
func (cf *ConfigFile) Unset(name string) {
	cfg := cf.Map[name]
	if cfg == nil {
		return
	}
	cfg.Value = No
}

func (cf *ConfigFile) ModToYes() {
	for _, cfg := range cf.Configs {
		if cfg.Value == Mod {
			cfg.Value = Yes
		}
	}
}

func (cf *ConfigFile) ModToNo() {
	for _, cfg := range cf.Configs {
		if cfg.Value == Mod {
			cfg.Value = No
		}
	}
}

func (cf *ConfigFile) Serialize() []byte {
	buf := new(bytes.Buffer)
	for _, cfg := range cf.Configs {
		for _, comment := range cfg.comments {
			fmt.Fprintf(buf, "%v\n", comment)
		}
		if cfg.Value == No {
			fmt.Fprintf(buf, "# %v%v is not set\n", prefix, cfg.Name)
		} else {
			fmt.Fprintf(buf, "%v%v=%v\n", prefix, cfg.Name, cfg.Value)
		}
	}
	for _, comment := range cf.comments {
		fmt.Fprintf(buf, "%v\n", comment)
	}
	return buf.Bytes()
}

func ParseConfig(file string) (*ConfigFile, error) {
	data, err := os.ReadFile(file)
	if err != nil {
		return nil, fmt.Errorf("failed to open .config file %v: %v", file, err)
	}
	return ParseConfigData(data, file)
}

func ParseConfigData(data []byte, file string) (*ConfigFile, error) {
	cf := &ConfigFile{
		Map: make(map[string]*Config),
	}
	s := bufio.NewScanner(bytes.NewReader(data))
	for s.Scan() {
		cf.parseLine(s.Text())
	}
	return cf, nil
}

func (cf *ConfigFile) Clone() *ConfigFile {
	cf1 := &ConfigFile{
		Map:      make(map[string]*Config),
		comments: cf.comments,
	}
	for _, cfg := range cf.Configs {
		cfg1 := new(Config)
		*cfg1 = *cfg
		cf1.Configs = append(cf1.Configs, cfg1)
		cf1.Map[cfg1.Name] = cfg1
	}
	return cf1
}

func (cf *ConfigFile) parseLine(text string) {
	if match := reConfigY.FindStringSubmatch(text); match != nil {
		cf.Set(match[1], match[2])
	} else if match := reConfigN.FindStringSubmatch(text); match != nil {
		cf.Set(match[1], No)
	} else {
		cf.comments = append(cf.comments, text)
	}
}

var (
	reConfigY = regexp.MustCompile(`^` + prefix + `([A-Za-z0-9_]+)=(y|m|(?:-?[0-9]+)|(?:0x[0-9a-fA-F]+)|(?:".*?"))$`)
	reConfigN = regexp.MustCompile(`^# ` + prefix + `([A-Za-z0-9_]+) is not set$`)
)
