// exportable typescript generated from golang
// Copyright (C) 2022  Fabio Prada

package tsrpc

import (
	"bufio"
	"fmt"
	"go/ast"
	"go/doc"
	"io"
	"log"
	"os"
	"slices"
	"strings"

	"golang.org/x/tools/go/packages"
)

type TSInfoPakage struct {
	structs   map[string]TSStruct
	types     map[string]TSType
	enums     map[string]TSEnum
	consts    map[string]TSConst
	decs      map[string]TSDec
	endpoints map[string]TSEndpoint
	imports   map[string][]string
	isUsed    bool
}

type TSDec struct {
	Name  string
	Value string
}

type TSType struct {
	Name       string
	Type       string
	TsType     string
	Typescript bool
	dependOn   bool
}

type TSInfo struct {
	Packages map[string]TSInfoPakage
}

type TSConst struct {
	Name  string
	Value string
}

type TSEnum struct {
	Name string
	Info []TSEnumInfo
}

type TSEnumInfo struct {
	Key   string
	Value string
}

// / exporter
type TSModuleInfo struct {
	structs map[string]string
	types   map[string]string
}

type TSSourceLine struct {
	Pos    int
	End    int
	Line   int
	Source string
}

type TSSourceFile struct {
	Source string
	Name   string
	Lines  []TSSourceLine
	Len    int
}

func (i *TSInfo) Populate(path string, excludedPackages ExcudedPackages) {
	i.Packages = make(map[string]TSInfoPakage)
	cfg := &packages.Config{
		Mode: packages.NeedName |
			packages.NeedFiles |
			packages.NeedCompiledGoFiles |
			packages.NeedSyntax,
		Dir: path,
	}

	pkgs, err := packages.Load(cfg, "./...")
	if err != nil {
		log.Fatal(err)
	}
	if packages.PrintErrors(pkgs) > 0 {
		log.Fatal("package loading failed")
	}

	//to get line info for endpoints and typescript declarations
	for _, loadedPkg := range pkgs {
		pkg := loadedPkg.Name
		// skip packages
		skip := false
		for _, excluded := range excludedPackages {
			if pkg == excluded {
				skip = true
				break
			}
		}
		if skip {
			continue
		}
		// initialize package info if not exists
		if _, ok := i.Packages[pkg]; !ok {
			ip := TSInfoPakage{}
			ip.MakeMaps()
			i.Packages[pkg] = ip
		}

		// start parsing files
		for _, n := range loadedPkg.CompiledGoFiles {
			// search for declarative // Typescript: declarations and endpoints
			i.parseTypescriptDeclarations(pkg, n)
		}
		// parse doc to get all types and consts
		docPkg, err := doc.NewFromFiles(loadedPkg.Fset, loadedPkg.Syntax, loadedPkg.PkgPath)
		if err != nil {
			exitOnError(err)
		}

		for _, t := range docPkg.Types {
			i.getType(pkg, t)
		}

		for _, c := range docPkg.Consts {
			i.getConst(pkg, c)
		}
	}
}

// estrae le definizioni typescript // Typescript: ...
func (ts *TSInfo) parseTypescriptDeclarations(p string, n string) {
	pkg := ts.Packages[p]
	f, err := os.OpenFile(n, os.O_RDONLY, os.ModePerm)
	if err != nil {
		log.Fatalf("open file error: %v", err)
		return
	}
	defer f.Close()
	l := 1
	dat := ""
	lines := []TSSourceLine{}
	rd := bufio.NewReader(f)
	// scansione del file per trovare le dichiarazioni typescript
	for {
		line, err := rd.ReadString('\n')
		if err != nil {
			if err == io.EOF {
				break
			}
			log.Fatalf("read file line error: %v", err)
			return
		}
		lines = append(lines, TSSourceLine{Pos: l, End: l + len(line), Line: l, Source: line})
		l++
		dat += line
		// è una definizione typescript?
		if strings.Contains(line, "// Typescript:") {
			// typescript type, ad esempio: // Typescript: TStype=MyType=string | number
			if strings.Contains(line, "TStype=") {
				p := strings.Index(line, "TStype=")
				s := strings.Trim(line[p+len("TStype="):], " ")
				a := strings.Split(s, "=")
				if len(a) == 2 {
					t := TSType{
						Name:       strings.Trim(a[0], " "),
						Typescript: true,
						Type:       "UserDefined",
						TsType:     strings.Trim(a[1], " "),
						dependOn:   false,
					}
					pkg.types[strings.Trim(a[0], " ")] = t
				}
			}
			// dicchiarazione typescript, ad esempio: // Typescript: TSDeclaration=MyType=string | number
			if strings.Contains(line, "TSDeclaration=") {
				p := strings.Index(line, "TSDeclaration=")
				s := strings.Trim(line[p+len("TSDeclaration="):], " ")
				a := strings.Split(s, "=")
				if len(a) == 2 {
					t := TSDec{
						Name:  strings.Trim(a[0], " "),
						Value: strings.Trim(a[1], " "),
					}
					pkg.decs[strings.Trim(a[0], " ")] = t
				}
			}
			// dichiarazione endpoint, ad esempio: // Typescript: TSEndpoint= path=/api/auth/login; name=login; method=POST; request=users.LoginRequest; response=tokens.TokenPair
			if strings.Contains(line, "TSEndpoint= ") {
				e := ParseEndpoint(line, n, l)
				if _, ok := pkg.endpoints[e.Name]; ok {
					exitOnError(fmt.Errorf("enpoint name %s allready in use: %s", e.Name, line))
				}
				pkg_info := pkg
				pkg_info.endpoints[e.Name] = e
				pkg_info.imports = e.Imports
				pkg_info.isUsed = true
				pkg = pkg_info
			}
		}
	}
}

func (i *TSInfo) getType(p string, t *doc.Type) {
	var isTypescript = strings.HasPrefix(t.Doc, "Typescript:")
	if isTypescript {
		command := strings.TrimPrefix(t.Doc, "Typescript:")
		command = strings.TrimSpace(command)
		command = strings.Trim(command, "\n")
	}
	for _, spec := range t.Decl.Specs {
		if len(t.Consts) > 0 {
			i.getConst(p, t.Consts[0])
		}
		switch spec.(type) {
		case *ast.TypeSpec:
			typeSpec := spec.(*ast.TypeSpec)
			switch typeSpec.Type.(type) {
			case *ast.StructType:
				v := TSStruct{
					Name:       typeSpec.Name.Name,
					Typescript: false,
					Fields:     []TSSField{},
				}
				v.getStruct(typeSpec)
				if len(v.Fields) == 0 {
					continue
				}
				i.Packages[p].structs[typeSpec.Name.Name] = v
				for _, imp := range v.Imports {
					a := strings.Split(imp, ".")
					if len(a) == 2 {
						if _, ok := i.Packages[p].imports[a[0]]; !ok {
							i.Packages[p].imports[a[0]] = []string{}
						}
						if !slices.Contains(i.Packages[p].imports[a[0]], a[1]) {
							i.Packages[p].imports[a[0]] = append(i.Packages[p].imports[a[0]], a[1])
						}
					}
				}
			case *ast.Ident:
				tsInfo := getFieldTsInfo(typeSpec.Type)
				t := TSType{
					Name:       typeSpec.Name.Name,
					Typescript: true,
					Type:       getFieldInfo(typeSpec.Type),
					TsType:     tsInfo,
					dependOn:   toBeImported(typeSpec.Type),
				}
				//fmt.Printf("Type found: %s Type: %s TsType: %s AT: %s\n", t.Name, t.Type, t.TsType, t.SourceInfo)
				i.Packages[p].types[typeSpec.Name.Name] = t
			default:
				// if isType && command != "interface" {
				// 	exitOnError(fmt.Errorf("mismatch delaration for interface %s AT: %s", t.Doc, getSourceInfo(int(typeSpec.Name.NamePos), src)))
				// }
				tsInfo := getFieldTsInfo(typeSpec.Type)
				t := TSType{
					Name:       typeSpec.Name.Name,
					Typescript: true,
					Type:       getFieldInfo(typeSpec.Type),
					TsType:     tsInfo,
					dependOn:   toBeImported(typeSpec.Type),
				}
				//fmt.Printf("Type found: %s Type: %s TsType: %s AT: %s\n", t.Name, t.Type, t.TsType, t.SourceInfo)
				i.Packages[p].types[typeSpec.Name.Name] = t
			}
		}
	}
}

func (i *TSInfo) getConst(p string, c *doc.Value) {
	var isTypescript = strings.HasPrefix(c.Doc, "Typescript:")
	if isTypescript {
		command := strings.TrimPrefix(c.Doc, "Typescript:")
		command = strings.TrimSpace(command)
		command = strings.Trim(command, "\n")

		if strings.Contains(command, "enum=") {
			enumName := strings.TrimPrefix(command, "enum=")
			enum := TSEnum{
				Name: enumName,
				Info: []TSEnumInfo{},
			}
			d := c.Decl
			iota := false
			iotaValue := 0
			for _, s := range d.Specs {
				v := s.(*ast.ValueSpec) // safe because decl.Tok == token.CONST
				if len(v.Values) > 0 {
					be, ok := v.Values[0].(*ast.BinaryExpr)
					if ok {
						x := be.X.(*ast.BasicLit)
						exitOnError(fmt.Errorf("enum binary expression not implemented %s %s %s", x.Value, be.Op.String(), be.Y))
					}
					ident, ok := v.Values[0].(*ast.Ident)
					if ok {
						if ident.Name == "iota" {
							iota = true
							iotaValue = v.Names[0].Obj.Data.(int)
							enum.Info = append(enum.Info, TSEnumInfo{Key: v.Names[0].Name, Value: fmt.Sprintf("%d", iotaValue)})
						}
					}
					list, ok := v.Values[0].(*ast.BasicLit)
					if ok {
						enum.Info = append(enum.Info, TSEnumInfo{Key: v.Names[0].Name, Value: list.Value})
					}
				} else {
					for _, name := range v.Names {
						if iota {
							iotaValue++
							enum.Info = append(enum.Info, TSEnumInfo{Key: name.Name, Value: fmt.Sprintf("%d", iotaValue)})
						}

					}
				}
			}
			i.Packages[p].enums[enumName] = enum
			t1 := TSType{
				Name:       enumName,
				Typescript: true,
				Type:       "",
				TsType:     fmt.Sprintf("typeof Enum%s[keyof typeof Enum%s] ", enumName, enumName), //getFieldTsInfo(expr.Type),
				dependOn:   false,
			}
			i.Packages[p].types[enumName] = t1
		}

		if strings.Contains(command, "const") {
			d := c.Decl
			for _, s := range d.Specs {
				v := s.(*ast.ValueSpec)
				if len(v.Names) == 0 || len(v.Values) == 0 {
					continue
				}
				c := TSConst{
					Name:  v.Names[0].Name,
					Value: v.Values[0].(*ast.BasicLit).Value,
				}
				i.Packages[p].consts[c.Name] = c
			}
		}

		if strings.Contains(command, "type") {
			fmt.Printf("TypeScript type declaration found in const s, but type parsing is not implemented yet\n")
		}

	}
}

func (ts *TSInfo) findStruct(p string, n string) bool {

	if _, ok := ts.Packages[p]; ok {
		if n == "" || IsNativeType(n) {
			return true
		}
		n = strings.TrimPrefix(n, "[]")
		n = strings.TrimPrefix(n, "*")
		if _, ok := ts.Packages[p].structs[n]; ok {
			return true
		}
	}
	return false
}

func (ts *TSInfo) findType(p string, n string) bool {
	if _, ok := ts.Packages[p]; ok {
		if n == "" || IsNativeType(n) {
			return true
		}
		n = strings.TrimPrefix(n, "[]")
		n = strings.TrimPrefix(n, "*")
		if _, ok := ts.Packages[p].types[n]; ok {
			return true
		}
	}
	return false
}

func (ts TSInfo) setTypescript(p string, n string, v bool) bool {
	if !ts.findStruct(p, n) {
		return false
	}
	if s, ok := ts.Packages[p].structs[n]; ok {
		s.Typescript = v
		ts.Packages[p].structs[n] = s // write back
		return true
	}

	return false
}

func (ts TSInfo) find(p string, n string) bool {
	return ts.findType(p, n) || ts.findStruct(p, n)
}

// popola TsInfo con tutte le definizioni dei tipi

func (ts *TSInfo) findAvailableStruct(pkg string, n string, deep int) (int, bool) {
	a := strings.Split(n, ".")
	if n == "" || IsNativeType(n) {
		return deep, true
	}

	deep++
	if deep > 10 {
		exitOnError(fmt.Errorf("too much deep for struct %s", n))
	}

	if len(a) == 1 {
		n = strings.TrimPrefix(n, "[]")
		n = strings.TrimPrefix(n, "*")
		if _, ok := ts.Packages[pkg].structs[n]; ok {
			s := ts.Packages[pkg].structs[n]
			s.Typescript = true
			ts.Packages[pkg].structs[n] = s // write back
			for _, v := range s.Fields {
				deep, _ = ts.findAvailableStruct(pkg, v.Type, deep)
			}
			deep--
			return deep, true
		}

	}

	if len(a) == 2 {
		a[1] = strings.TrimPrefix(a[1], "[]")
		a[1] = strings.TrimPrefix(a[1], "*")
		if _, ok := ts.Packages[a[0]]; ok {
			if _, ok := ts.Packages[a[0]].structs[a[1]]; ok {
				s := ts.Packages[a[0]].structs[a[1]]
				if s.Typescript {
					deep--
					return deep, true
				}
				s.Typescript = true
				ts.Packages[a[0]].structs[a[1]] = s // write back
				for _, v := range s.Fields {
					ts.findAvailableStruct(pkg, v.Type, deep)
				}
				deep--
				return deep, true
			}
		}
	}

	return deep, false
}

func (i *TSInfo) TestEndpoints() {
	for p := range i.Packages {
		for _, v1 := range i.Packages[p].endpoints {
			_, f := i.findAvailableStruct(p, v1.Request, 0)
			if !f {
				fmt.Printf("\n??Endpoint: request %s not found\n", v1.Request)
			}
			_, f = i.findAvailableStruct(p, v1.Response, 0)
			if !f {
				fmt.Printf("\n??Endpoint: response %s not found\n", v1.Response)
			}

		}
	}
}
