package main

import (
	"errors"
	"fmt"
	"os"
	"regexp"
	"strings"
	"unicode"

	parser "github.com/yoheimuta/go-protoparser/v4/parser"
)

type Part int

const (
	Params Part = iota + 1
	Body
	Data
	Res
)

type ApiPart struct {
	// api name
	apiName string
	// api part
	part Part
	// completed pb type pkg.message.message2.<...>.message/enum
	pbType string
}

type Segment struct {
	pbPkg string
	// completed pb type pkg.message.message2.<...>.message/enum
	pbType    string
	pbMessage *parser.Message
	pbEnum    *parser.Enum
	// is compiled
	fragment string
}

type Compiler struct {
	// key for api name
	apis *map[string]map[Part]*ApiPart
	// key for pbType
	segments *map[string]*Segment
}

type CompileOut struct {
	filename string
	content  []rune
}

var newLine = "\n"
var twospace = "  "

var Part2Str = map[Part]string{
	Params: "Params",
	Body:   "Body",
	Data:   "Data",
	Res:    "Res",
}

func (c *Compiler) compile() ([]*CompileOut, error) {
	var res []*CompileOut

	for apiName, api := range *c.apis {
		filename := apiName + ".ts"
		fragments := []string{
			"// Generated by p2ts. DON'T EDIT IT MANUALLY.",
			"// Any feedback about p2ts, contact: shenjing.zhao@pingcap.com.",
			"",
			"/* eslint-disable */",
			"",
		}
		hasExport := make(map[string]bool)
		for _, apiPart := range api {
			frag, err := c.compilePart(apiPart, &hasExport)
			if err != nil {
				return nil, err
			}
			if frag != "" {
				fragments = append(fragments, frag)
			}
		}
		content := []rune(strings.Join(fragments, newLine))
		res = append(res, &CompileOut{
			filename: filename,
			content:  content,
		})
	}

	return res, nil
}

func (c *Compiler) compilePart(ap *ApiPart, hasExport *map[string]bool) (string, error) {
	var lines []string
	exportName := ap.apiName + Part2Str[ap.part]
	seg := (*(c.segments))[ap.pbType]

	if seg.pbMessage != nil {
		clines, err := c.compileMsg(seg, hasExport, exportName)
		if err != nil {
			return "", err
		}
		lines = clines
	}

	return strings.Join(lines, newLine), nil
}

func (c *Compiler) outputComment(lines []string, comts []*parser.Comment, indent int) []string {
	baseIndent := strings.Repeat(twospace, indent)
	var onlyOptionalComtMatchRegex = regexp.MustCompile(`^(//|/\*)\s+\-\s*$`)
	for _, cmt := range comts {
		if onlyOptionalComtMatchRegex.FindString(cmt.Raw) != "" {
			continue
		}
		lines = append(lines, baseIndent+cmt.Raw)
	}
	return lines
}

func (c *Compiler) compileEnum(seg *Segment, hasExport *map[string]bool, exportName string) ([]string, error) {
	var lines []string

	if seg.pbEnum != nil {
		// if export already, do noting
		if (*hasExport)[exportName] {
			return lines, nil
		}
		// if compiled before, resue
		if seg.fragment != "" {
			lines = append(lines, seg.fragment)
		} else {
			var fgmt []string

			fgmt = c.outputComment(fgmt, seg.pbEnum.Comments, 0)
			fgmt = append(fgmt, "export enum "+exportName+" {")

			contentLines, err := c.compileEnumFields(seg, hasExport)
			if err != nil {
				return lines, nil
			}
			fgmt = append(fgmt, strings.Join(contentLines, newLine))
			fgmt = append(fgmt, "}\n")

			fgmtStr := strings.Join(fgmt, newLine)
			// into seg fragment to reuse
			seg.fragment = fgmtStr
			// append into lines
			lines = append(lines, fgmtStr)
		}
	}

	return lines, nil
}

func (c *Compiler) compileMsg(seg *Segment, hasExport *map[string]bool, exportName string) ([]string, error) {
	var lines []string

	if seg.pbMessage != nil {
		// if export already, do noting
		if (*hasExport)[exportName] {
			return lines, nil
		}
		if seg.fragment != "" {
			lines = append(lines, seg.fragment)
		} else {
			var fgmt []string
			fgmt = c.outputComment(fgmt, seg.pbMessage.Comments, 0)
			fgmt = append(fgmt, "export interface "+exportName+" {")

			contentLines, nestedContent, err := c.compileMsgFields(seg, hasExport)
			if err != nil {
				return lines, nil
			}
			fgmt = append(fgmt, strings.Join(contentLines, newLine))
			fgmt = append(fgmt, "}\n")

			if len(nestedContent) > 0 {
				nestedStr := strings.Join(nestedContent, newLine)
				fgmt = append(fgmt, nestedStr)
			}

			fgmtStr := strings.Join(fgmt, newLine)
			// into seg fragment to reuse
			seg.fragment = fgmtStr
			// append into lines
			lines = append(lines, fgmtStr)
		}
	}

	(*hasExport)[exportName] = true

	return lines, nil
}

func (c *Compiler) compileEnumFields(seg *Segment, hasExport *map[string]bool) ([]string, error) {
	var content []string

	pbEnum := seg.pbEnum

	for _, eb := range pbEnum.EnumBody {
		switch v := eb.(type) {
		case *parser.EnumField:
			var fieldLines []string
			fieldLines = c.outputComment(fieldLines, v.Comments, 1)
			field := twospace + v.Ident + " = " + v.Number + ","
			fieldLines = append(fieldLines, field)
			content = append(content, strings.Join(fieldLines, newLine))
		}
	}

	return content, nil
}

func (c *Compiler) compileMsgFields(seg *Segment, hasExport *map[string]bool) ([]string, []string, error) {
	var content []string
	var nestedContent []string

	pbMsg := seg.pbMessage

	for _, mb := range pbMsg.MessageBody {
		switch v := mb.(type) {
		case *parser.Field:
			var fieldLines []string
			fieldLines = c.outputComment(fieldLines, v.Comments, 1)
			field, nested, err := c.compileField(v, seg, hasExport)
			if err != nil {
				return content, nestedContent, err
			}
			fieldLines = append(fieldLines, field)
			nestedContent = append(nestedContent, nested...)
			content = append(content, strings.Join(fieldLines, newLine))
		case *parser.MapField:
			var fieldLines []string
			fieldLines = c.outputComment(fieldLines, v.Comments, 1)
			field, nested, err := c.compileMapField(v, seg, hasExport)
			if err != nil {
				return content, nestedContent, err
			}
			fieldLines = append(fieldLines, field)
			nestedContent = append(nestedContent, nested...)
			content = append(content, strings.Join(fieldLines, newLine))
		}
	}

	return content, nestedContent, nil
}

func (c *Compiler) compileMapField(f *parser.MapField, seg *Segment, hasExport *map[string]bool) (string, []string, error) {
	if f.MapName == "data" && isHoisting(f.Comments) {
		hoistType := seg.pbMessage.MessageName + "Data"
		field := twospace + f.MapName + ": " + hoistType
		isOptional := isOptional(f.Comments)
		if isOptional {
			field = twospace + f.MapName + "?: " + hoistType
		}
		fieldType := "any"
		var nestedC []string
		switch f.Type {
		case "string", "bytes":
			fieldType = "string"
		case "double", "float", "int32", "int64", "uint32", "uint64", "sint32", "sint64", "fixed32", "fixed64", "sfixed32", "sfixed64":
			fieldType = "number"
		case "bool":
			fieldType = "boolean"
		case "google.protobuf.Any":
			fieldType = "any"
		default:
			fieldTypeName, nestedContent, err := c.parseType(f.Type, seg, hasExport)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Unknown Type: %s, use 'any' as fallback. Err: %v\n", f.Type, err)
			} else {
				fieldType = fieldTypeName
				nestedC = nestedContent
			}
		}
		nested := []string{
			"export interface " + hoistType + "{",
			twospace + "[prop: string]: " + fieldType,
			"}\n",
		}
		nested = append(nested, nestedC...)
		return field, nested, nil
	} else {
		// not api res data
		var nested []string
		fieldType := "any"
		isOptional := isOptional(f.Comments)
		field := twospace + f.MapName + ": { " + "[prop: string]: "
		if isOptional {
			field = twospace + f.MapName + "?: { " + "[prop: string]: "
		}
		switch f.Type {
		case "string", "bytes":
			fieldType = "string"
		case "double", "float", "int32", "int64", "uint32", "uint64", "sint32", "sint64", "fixed32", "fixed64", "sfixed32", "sfixed64":
			fieldType = "number"
		case "bool":
			fieldType = "boolean"
		case "google.protobuf.Any":
			fieldType = "any"
		default:
			fieldTypeName, nestedContent, err := c.parseType(f.Type, seg, hasExport)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Unknown Type: %s, use 'any' as fallback. Err: %v\n", f.Type, err)
			} else {
				fieldType = fieldTypeName
				nested = nestedContent
			}
		}

		field = field + fieldType + " }"

		return field, nested, nil
	}
}

func (c *Compiler) compileField(f *parser.Field, seg *Segment, hasExport *map[string]bool) (string, []string, error) {
	var nested []string
	fieldType := "any"
	isOptional := isOptional(f.Comments)
	field := twospace + f.FieldName + ": "
	if isOptional {
		field = twospace + f.FieldName + "?: "
	}
	isArray := f.IsRepeated
	switch f.Type {
	case "string", "bytes":
		fieldType = "string"
	case "double", "float", "int32", "int64", "uint32", "uint64", "sint32", "sint64", "fixed32", "fixed64", "sfixed32", "sfixed64":
		fieldType = "number"
	case "bool":
		fieldType = "boolean"
	case "google.protobuf.Any":
		fieldType = "any"
	default:
		fieldTypeName, nestedContent, err := c.parseType(f.Type, seg, hasExport)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Unknown Type: %s, use 'any' as fallback. Err: %v\n", f.Type, err)
		} else {
			fieldType = fieldTypeName
			nested = nestedContent
		}
	}

	field = field + fieldType
	if isArray {
		field = field + "[]"
	}

	return field, nested, nil
}

func (c *Compiler) parseType(t string, seg *Segment, hasExport *map[string]bool) (string, []string, error) {
	pbType := t
	var completeTypeMatchRegex = regexp.MustCompile(`^([a-z_]+[\w_]*)\.(.+)$`)
	var dotMatchRegex = regexp.MustCompile(`\.`)
	matches := completeTypeMatchRegex.FindStringSubmatch(pbType)
	var nestSeg *Segment
	var tName string
	var nested []string
	var err error
	if len(matches) > 2 {
		nestSg, ok := (*(c.segments))[pbType]
		if !ok {
			return "", nil, errors.New("type does not exist")
		}
		nestSeg = nestSg
		nestedPkg := matches[1]
		tName = dotMatchRegex.ReplaceAllString(matches[2], "")
		if nestedPkg != seg.pbPkg {
			tName = Ucfirst(nestedPkg) + dotMatchRegex.ReplaceAllString(matches[2], "")
		}
	} else {
		pbType = seg.pbPkg + "." + t
		nestSg, ok := (*(c.segments))[pbType]
		if !ok {
			pbType = seg.pbType + "." + t
			nestSg, ok := (*(c.segments))[pbType]
			if !ok {
				return "", nil, errors.New("type does not exist")
			}
			nestSeg = nestSg
		} else {
			nestSeg = nestSg
		}
		nestedPkg := nestSeg.pbPkg
		tName = dotMatchRegex.ReplaceAllString(t, "")
		if nestedPkg != seg.pbPkg {
			tName = Ucfirst(nestedPkg) + dotMatchRegex.ReplaceAllString(matches[2], "")
		}
	}

	if nestSeg.pbMessage != nil {
		nested, err = c.compileMsg(nestSeg, hasExport, tName)
	}
	if nestSeg.pbEnum != nil {
		nested, err = c.compileEnum(nestSeg, hasExport, tName)
	}
	if err != nil {
		return "", nil, err
	}
	return tName, nested, nil
}

func isOptional(comts []*parser.Comment) bool {
	if len(comts) > 0 {
		var optionalComtMatchRegex = regexp.MustCompile(`^(//|/\*)\s+\-`)
		lastComt := comts[len(comts)-1]

		findOptional := optionalComtMatchRegex.FindString(lastComt.Raw)
		if findOptional != "" {
			return true
		}
	}

	return false
}

func isHoisting(comts []*parser.Comment) bool {
	if len(comts) > 0 {
		var hComtMatchRegex = regexp.MustCompile(`^(//|/\*)\s+\-*\^`)
		lastComt := comts[len(comts)-1]

		findOptional := hComtMatchRegex.FindString(lastComt.Raw)
		if findOptional != "" {
			return true
		}
	}

	return false
}

func Ucfirst(str string) string {
	for i, v := range str {
		return string(unicode.ToUpper(v)) + str[i+1:]
	}
	return ""
}
