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

package tsrpc

import (
	"fmt"
	"go/ast"
	"go/doc"
	"go/parser"
	"go/token"
	"log"
	"os"
	"path/filepath"
	"strings"
)

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
	SourceInfo string
}

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

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 (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 (i *TSInfo) getConst(p string, c *doc.Value, src []TSSourceFile) {
	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 AT: %s", x.Value, be.Op.String(), be.Y, getSourceInfo(int(x.ValuePos), src)))
					}
					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,
				SourceInfo: "",
			}
			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
			}
		}

	}
}

func (i *TSInfo) getType(p string, t *doc.Type, src []TSSourceFile) {
	var isType = t.Decl.Tok == token.TYPE && t.Doc == ""
	var isTypescript = strings.HasPrefix(t.Doc, "Typescript:")
	command := ""
	param := ""
	if isTypescript {
		//fmt.Println(t.Doc)
		command = strings.TrimPrefix(t.Doc, "Typescript:")
		command = strings.TrimSpace(command)
		command = strings.Trim(command, "\n")

		if strings.Contains(command, "=") {
			a := strings.Split(command, "=")
			if len(a) == 2 {
				param = a[1]
			}
			command = strings.Trim(a[0], " ")
		}
	}
	if isType {
		//fmt.Println(t.Doc)
		command = "interface"
	}
	for _, spec := range t.Decl.Specs {
		if len(t.Consts) > 0 {
			i.getConst(p, t.Consts[0], src)
			continue
		}
		switch spec.(type) {
		case *ast.TypeSpec:
			typeSpec := spec.(*ast.TypeSpec)
			switch typeSpec.Type.(type) {
			case *ast.StructType:
				if (isTypescript || isType) && command != "interface" {
					exitOnError(fmt.Errorf("mismatch delaration for interface %s AT: %s", t.Doc, getSourceInfo(int(typeSpec.Name.NamePos), src)))
				}
				v := TSStruct{
					Name:       typeSpec.Name.Name,
					Typescript: isTypescript,
					Fields:     []TSSField{},
					SourceInfo: getSourceInfo(int(typeSpec.Name.NamePos), src),
				}
				v.getStruct(typeSpec, src)
				i.Packages[p].structs[typeSpec.Name.Name] = v
			default:
				if isTypescript && command != "type" {
					exitOnError(fmt.Errorf("mismatch delaration for type %s AT: %s", t.Doc, getSourceInfo(int(typeSpec.Name.NamePos), src)))
				}
				tsInfo := getFieldTsInfo(typeSpec.Type)
				if command == "type" && param != "" {
					tsInfo = param
				}
				t := TSType{
					Name:       typeSpec.Name.Name,
					Typescript: isTypescript,
					Type:       getFieldInfo(typeSpec.Type),
					TsType:     tsInfo,
					dependOn:   toBeImported(typeSpec.Type),
					SourceInfo: getSourceInfo(int(typeSpec.Name.NamePos), src),
				}
				//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) Populate(path string) {
	i.Packages = make(map[string]TSInfoPakage)
	err := filepath.Walk(path,
		func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if info.IsDir() {
				fset := token.NewFileSet()
				packages, err := parser.ParseDir(fset, path, nil, parser.ParseComments)

				if err != nil {
					exitOnError(err)
				}

				for pkg, f := range packages {
					if _, ok := i.Packages[pkg]; !ok {
						i.Packages[pkg] = TSInfoPakage{structs: make(map[string]TSStruct), types: make(map[string]TSType), enums: make(map[string]TSEnum), consts: make(map[string]TSConst), decs: make(map[string]TSDec), endpoints: make(map[string]TSEndpoint)}
					}
					if pkg == "typescript" || pkg == "tsrpc" {
						continue
					}

					var src = []TSSourceFile{}
					for n := range f.Files {

						dat, err := os.ReadFile(n)
						if err == nil {
							lines := []TSSourceLine{}
							pos := 0
							line := 1
							for p, k := range dat {
								if string(k) == "\n" {
									l := TSSourceLine{
										Pos:    pos,
										End:    p,
										Line:   line,
										Source: string(dat[pos:p]),
									}
									lines = append(lines, l)
									pos = p + 1
									line++
									if strings.Contains(l.Source, "// Typescript:") {

										if strings.Contains(l.Source, "TStype=") {
											p := strings.Index(l.Source, "TStype=")
											s := strings.Trim(l.Source[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,
													SourceInfo: getSourceInfo(int(l.Pos), src),
												}
												i.Packages[pkg].types[strings.Trim(a[0], " ")] = t
											}

										}
										if strings.Contains(l.Source, "TSDeclaration=") {
											p := strings.Index(l.Source, "TSDeclaration=")
											s := strings.Trim(l.Source[p+len("TSDeclaration="):], " ")
											a := strings.Split(s, "=")
											if len(a) == 2 {
												t := TSDec{
													Name:       strings.Trim(a[0], " "),
													Value:      strings.Trim(a[1], " "),
													SourceInfo: getSourceInfo(int(l.Pos), src),
												}
												i.Packages[pkg].decs[strings.Trim(a[0], " ")] = t
											}
										}

										if strings.Contains(l.Source, "TSEndpoint= ") {
											e := ParseEndpoint(l.Source, n, l.Line)
											if _, ok := i.Packages[pkg].endpoints[e.Name]; ok {
												exitOnError(fmt.Errorf("enpoint name %s allready in use: %s", e.Name, l.Source))
											}
											pkg_info := i.Packages[pkg]
											pkg_info.endpoints[e.Name] = e
											pkg_info.imports = e.Imports
											pkg_info.isUsed = true
											i.Packages[pkg] = pkg_info
										}

									}

								}
							}
							s := TSSourceFile{
								Name:   n,
								Source: string(dat),
								Len:    len(dat),
								Lines:  lines,
							}
							src = append(src, s)
						}
					}

					p := doc.New(f, "./", 0)

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

					for _, c := range p.Consts {
						i.getConst(pkg, c, src)
					}
				}
			}
			return nil
		})
	if err != nil {
		log.Fatal(err)
	}
}

func (ts *TSInfo) findAvailableStruct(n string) (TSStruct, bool) {
	a := strings.Split(n, ".")
	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 {
				return ts.Packages[a[0]].structs[a[1]], true
			}
		}
	}
	if len(a) == 1 {
		n = strings.TrimPrefix(n, "[]")
		n = strings.TrimPrefix(n, "*")
		for p := range ts.Packages {
			if _, ok := ts.Packages[p].structs[n]; ok {
				return ts.Packages[p].structs[n], true
			}
		}
	}
	if n == "" || IsNativeType(n) {
		return TSStruct{}, true
	}

	return TSStruct{}, false
}

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

		}
	}
}
