package sherpadoc

import (
	"fmt"
)

type genError struct{ error }

func parseError(path string, format string, args ...interface{}) {
	msg := fmt.Sprintf(format, args...)
	err := fmt.Errorf("invalid sherpadoc at %s: %s", path, msg)
	panic(genError{err})
}

func makePath(path string, field string, index int, name string) string {
	return fmt.Sprintf("%s.%s[%d (%q)]", path, field, index, name)
}

// NOTE: sherpaweb/ts/parse.ts and sherpadoc/check.go contain the same checking.
// The code is very similar. Best keep it in sync and modify the implementations in tandem.
type checker struct {
	types     map[string]struct{}
	functions map[string]struct{}
}

func (c checker) markIdent(path, ident string) {
	if _, ok := c.types[ident]; ok {
		parseError(path, "duplicate type %q", ident)
	}
	c.types[ident] = struct{}{}
}

func (c checker) walkTypeNames(path string, sec *Section) {
	for i, t := range sec.Structs {
		c.markIdent(makePath(path, "Structs", i, t.Name), t.Name)
	}
	for i, t := range sec.Ints {
		npath := makePath(path, "Ints", i, t.Name)
		c.markIdent(npath, t.Name)
		for j, v := range t.Values {
			c.markIdent(makePath(npath, "Values", j, v.Name), v.Name)
		}
	}
	for i, t := range sec.Strings {
		npath := makePath(path, "Strings", i, t.Name)
		c.markIdent(npath, t.Name)
		for j, v := range t.Values {
			c.markIdent(makePath(npath, "Values", j, v.Name), v.Name)
		}
	}
	for i, subsec := range sec.Sections {
		c.walkTypeNames(makePath(path, "Sections", i, subsec.Name), subsec)
	}
}

func (c checker) walkFunctionNames(path string, sec *Section) {
	for i, fn := range sec.Functions {
		npath := makePath(path, "Functions", i, fn.Name)
		if _, ok := c.functions[fn.Name]; ok {
			parseError(npath, "duplicate function %q", fn.Name)
		}
		c.functions[fn.Name] = struct{}{}

		paramNames := map[string]struct{}{}
		for i, arg := range fn.Params {
			if _, ok := paramNames[arg.Name]; ok {
				parseError(makePath(npath, "Params", i, arg.Name), "duplicate parameter name")
			}
			paramNames[arg.Name] = struct{}{}
		}

		returnNames := map[string]struct{}{}
		for i, arg := range fn.Returns {
			if _, ok := returnNames[arg.Name]; ok {
				parseError(makePath(npath, "Returns", i, arg.Name), "duplicate return name")
			}
			returnNames[arg.Name] = struct{}{}
		}
	}
	for i, subsec := range sec.Sections {
		c.walkFunctionNames(makePath(path, "Sections", i, subsec.Name), subsec)
	}
}

func (c checker) checkTypewords(path string, tokens []string, okNullable bool) {
	if len(tokens) == 0 {
		parseError(path, "unexpected end of typewords")
	}
	t := tokens[0]
	tokens = tokens[1:]
	switch t {
	case "nullable":
		if !okNullable {
			parseError(path, "repeated nullable in typewords")
		}
		if len(tokens) == 0 {
			parseError(path, "missing typeword after %#v", t)
		}
		c.checkTypewords(path, tokens, false)
	case "any", "bool", "int8", "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64", "int64s", "uint64s", "float32", "float64", "string", "timestamp":
		if len(tokens) != 0 {
			parseError(path, "leftover typewords %v", tokens)
		}
	case "[]", "{}":
		if len(tokens) == 0 {
			parseError(path, "missing typeword after %#v", t)
		}
		c.checkTypewords(path, tokens, true)
	default:
		_, ok := c.types[t]
		if !ok {
			parseError(path, "referenced type %q does not exist", t)
		}
		if len(tokens) != 0 {
			parseError(path, "leftover typewords %v", tokens)
		}
	}
}

func (c checker) walkTypewords(path string, sec *Section) {
	for i, t := range sec.Structs {
		npath := makePath(path, "Structs", i, t.Name)
		for j, f := range t.Fields {
			c.checkTypewords(makePath(npath, "Fields", j, f.Name), f.Typewords, true)
		}
	}
	for i, fn := range sec.Functions {
		npath := makePath(path, "Functions", i, fn.Name)
		for j, arg := range fn.Params {
			c.checkTypewords(makePath(npath, "Params", j, arg.Name), arg.Typewords, true)
		}
		for j, arg := range fn.Returns {
			c.checkTypewords(makePath(npath, "Returns", j, arg.Name), arg.Typewords, true)
		}
	}
	for i, subsec := range sec.Sections {
		c.walkTypewords(makePath(path, "Sections", i, subsec.Name), subsec)
	}
}

// Check walks the sherpa section and checks it for correctness. It checks for:
//
// - Duplicate type names.
// - Duplicate parameter or return names.
// - References to types that are not defined.
// - Validity of typewords.
func Check(doc *Section) (retErr error) {
	defer func() {
		e := recover()
		if e != nil {
			g, ok := e.(genError)
			if !ok {
				panic(e)
			}
			retErr = error(g)
		}
	}()

	c := checker{map[string]struct{}{}, map[string]struct{}{}}

	c.walkTypeNames("", doc)
	c.walkFunctionNames("", doc)
	c.walkTypewords("", doc)

	return nil
}
