// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package main

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"

	"gopkg.in/yaml.v3"
)

const PackageName = "ierror"

type Field struct {
	Name string `yaml:"name"`
	Type string `yaml:"type"`
}
type ErrorDef struct {
	Name   string  `yaml:"name"`
	Format string  `yaml:"format"`
	Code   uint32  `yaml:"code"`
	Fields []Field `yaml:"fields"`
}

const OutputFile = "errors_gen.go"

func main() {
	data, err := os.ReadFile("./errors.yaml")
	if err != nil {
		panic(err)
	}
	var errors []ErrorDef
	if err = yaml.Unmarshal(data, &errors); err != nil {
		panic(err)
	}

	f, err := os.Create(OutputFile)
	if err != nil {
		panic(err)
	}
	_, _ = fmt.Fprint(f, `// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

`)
	printGeneratedNotice(f)
	types(f, errors)
	sentinelErrors(f, errors)
	errorCodes(f, errors)
	codeString(f, errors)
	fromCode(f, errors)

	if err = f.Close(); err != nil {
		panic(err)
	}

	// gofmt the generated file
	if err = exec.Command("gofmt", "-w", OutputFile).Run(); err != nil {
		panic("failed to gofmt: " + err.Error())
	}
}

func printGeneratedNotice(f io.Writer) {
	_, err := fmt.Fprintln(f, `// Code generated by generator/main.go; DO NOT EDIT.`)
	if err != nil {
		panic(err)
	}
}

func types(f io.Writer, errors []ErrorDef) {
	_, err := fmt.Fprintf(f, `
package %s

import "fmt"

type IggyError interface {
    error
    Code() Code
}

`, PackageName)
	if err != nil {
		panic(err)
	}

	for _, e := range errors {
		if len(e.Fields) != 0 &&
			(strings.Contains(e.Format, "%v") ||
				strings.Contains(e.Format, "%s") ||
				strings.Contains(e.Format, "%d")) {
			// error with fields and need to use fmt.Sprintf.
			_, _ = fmt.Fprintf(f, "type %s struct {\n", e.Name)
			for _, field := range e.Fields {
				_, _ = fmt.Fprintf(f, "    %s %s\n", field.Name, field.Type)
			}
			_, _ = fmt.Fprintln(f, "}")
			// Error() function
			args := make([]string, len(e.Fields))
			for i, field := range e.Fields {
				args[i] = "e." + field.Name
			}
			_, _ = fmt.Fprintf(f, `func (e %s) Error() string {
    return fmt.Sprintf("%s", %s)
}
`, e.Name, e.Format, strings.Join(args, ", "))
		} else {
			_, err = fmt.Fprintf(f, "type %s struct{}\n", e.Name)
			if err != nil {
				panic(err)
			}
			// Error() function
			_, err = fmt.Fprintf(f, "func (e %s) Error() string { return \"%s\" }\n", e.Name, e.Format)
			if err != nil {
				panic(err)
			}
		}

		// Code() function
		_, _ = fmt.Fprintf(f, "func (e %s) Code() Code { return %d }\n", e.Name, e.Code)

		// Is() function
		_, _ = fmt.Fprintf(f, `func (e %s) Is(target error) bool {
	_, ok := target.(%s)
	return ok
}

`, e.Name, e.Name)
	}
}

func sentinelErrors(f io.Writer, errors []ErrorDef) {
	_, _ = fmt.Fprintln(f, "var (")
	for _, e := range errors {
		_, _ = fmt.Fprintf(f, "    Err%s = %s{}\n", e.Name, e.Name)
	}
	_, _ = fmt.Fprint(f, ")\n\n")
}

func errorCodes(f io.Writer, errors []ErrorDef) {
	// define enum
	_, _ = fmt.Fprint(f, "type Code uint32\n\n")
	_, _ = fmt.Fprintln(f, "const (")
	for _, e := range errors {
		_, _ = fmt.Fprintf(f, "    %sCode Code = %d\n", e.Name, e.Code)
	}
	_, _ = fmt.Fprintln(f, ")")
	_, _ = fmt.Fprintln(f)
}

// codeString generates the String() method for the Code type.
func codeString(f io.Writer, errors []ErrorDef) {
	_, _ = fmt.Fprintln(f, "func (c Code) String() string {")
	_, _ = fmt.Fprintln(f, "    switch c {")
	for _, e := range errors {
		_, _ = fmt.Fprintf(f, "    case %sCode:\n", e.Name)
		_, _ = fmt.Fprintf(f, "        return \"%s\"\n", e.Name)
	}
	_, _ = fmt.Fprintln(f, "    default:")
	_, _ = fmt.Fprintln(f, "        return \"Unknown error code\"")
	_, _ = fmt.Fprintln(f, "    }")
	_, _ = fmt.Fprintln(f, "}")
	_, _ = fmt.Fprintln(f)
}

// fromCode generates the FromCode function that maps a Code to an IggyError.
func fromCode(f io.Writer, errors []ErrorDef) {
	_, _ = fmt.Fprintf(f, "func FromCode(code Code) IggyError {\n")
	_, _ = fmt.Fprintln(f, "    switch code {")
	for _, e := range errors {
		_, _ = fmt.Fprintf(f, "    case %sCode:\n", e.Name)
		_, _ = fmt.Fprintf(f, "        return Err%s\n", e.Name)
	}
	_, _ = fmt.Fprintln(f, "    default:")
	_, _ = fmt.Fprintln(f, "        return ErrError")
	_, _ = fmt.Fprintln(f, "    }")
	_, _ = fmt.Fprintln(f, "}")
	_, _ = fmt.Fprintln(f)
}
