package analyzer

import (
	"fmt"
	"go/ast"
	"go/token"
	"go/types"

	"github.com/gogo/protobuf/proto"
	logpattern "github.com/pingcap-inc/alioth/proto"
	logpattern_go_proto "github.com/pingcap-inc/alioth/proto"
)

// Analyzer used to analyze GO ast
type Aanalyzer interface {
	Run(*ast.File, *AstHelper)
	SetupOutput() <-chan proto.Message
	MarkDone()
}

// logAnalyzer used to find the log of interest
type logAnalyzer struct {
	// todo: add lock
	logChan chan proto.Message

	fn func(logPkg, logFn, object string) (string, bool)
}

func NewAstAnalyzer(fn func(logPkg, logFn, object string) (string, bool)) *logAnalyzer {
	return &logAnalyzer{fn: fn}
}

func (ai *logAnalyzer) SetupOutput() <-chan proto.Message {
	ai.MarkDone()

	ai.logChan = make(chan proto.Message, 256)
	return ai.logChan
}

func (ai *logAnalyzer) MarkDone() {
	if ai.logChan != nil {
		close(ai.logChan)
	}
}

func (ai *logAnalyzer) Run(file *ast.File, helper *AstHelper) {
	ai.fromLogPrint(file, helper)
}

// This algorithm starts by identifying the log printing function call expression,
// and then finding the corresponding log printing function, and log message as the signature
func (ai *logAnalyzer) fromLogPrint(file *ast.File, helper *AstHelper) {
	ast.Walk(newASTVisitor(func(node ast.Node, stack stackFunc) bool {
		switch n := node.(type) {
		case *ast.CallExpr:
			ai.findLog(n, stack, helper)
		}
		return true
	}), file)
}

func (ai *logAnalyzer) findLog(fn *ast.CallExpr, stack stackFunc, helper *AstHelper) {
	fnPkg, fnObject, fnName := ai.resolveFnDecl(fn, helper)

	// must use third log package
	if fnName != "" && fnPkg != "" {
		if level, ok := ai.fn(fnPkg, fnName, fnObject); ok {
			signature := ai.findFunFirstBLArg(fn)
			callFnName, rawCallFnPos, rawCallFnEndPos := ai.callContext(stack, helper)
			callFnPos := helper.GetPos(rawCallFnPos)
			callFnEndPos := helper.GetPos(rawCallFnEndPos)
			callFnProtoPos := &logpattern.Position{
				FilePath: callFnPos.Filename,
				Pos: &logpattern_go_proto.CodePosition{
					Line:   int32(callFnPos.Line),
					Column: int32(callFnPos.Column),
				},
				EndPos: &logpattern_go_proto.CodePosition{
					Line:   int32(callFnEndPos.Line),
					Column: int32(callFnEndPos.Column),
				},
			}

			callFn := &logpattern.FuncInfo{
				Pos:  callFnProtoPos,
				Name: callFnName,
			}

			idDeferLog := ai.checkIsDeferLog(stack)

			logPos := helper.GetPos(fn.Pos())
			endLogPos := helper.GetPos(fn.End())
			logProtoPos := &logpattern.Position{
				PackageInfo: &logpattern.Package{
					Repo: helper.GetRepo(),
				},
				FilePath: logPos.Filename,
				Pos: &logpattern_go_proto.CodePosition{
					Line:   int32(logPos.Line),
					Column: int32(logPos.Column),
				},
				EndPos: &logpattern_go_proto.CodePosition{
					Line:   int32(endLogPos.Line),
					Column: int32(endLogPos.Column),
				},
			}

			var logSignature []string
			if signature != nil {
				logSignature = []string{signature.value}
			}

			ai.logChan <- &logpattern.LogPattern{
				Pos:        logProtoPos,
				Func:       callFn,
				Level:      level,
				Signature:  logSignature,
				IsDeferLog: idDeferLog,
			}
		}
	}
}

// checkIsDeferLog rerurns wether the log like defer log.Info("test", ...)
// defer log position would be the return statement position
// we use function range to match the log for defer log
func (ai *logAnalyzer) checkIsDeferLog(stack stackFunc) bool {
	inFuncLit := false
	inDefer := false

	for i := 1; ; i++ {
		switch stack(i).(type) {
		case *ast.FuncDecl:
			return !inFuncLit && inDefer
		case *ast.FuncLit:
			inFuncLit = true
		case *ast.DeferStmt:
			inDefer = true
		case *ast.File:
			return false
		}
	}
}

type firstBLArg struct {
	value string
}

// findFunFirstBLArg returns the call expr first basicLit argument as log signature
func (ai *logAnalyzer) findFunFirstBLArg(fn *ast.CallExpr) *firstBLArg {
	if len(fn.Args) == 0 {
		return nil
	}

	// get first argument as signature
	arg := fn.Args[0]
	switch val := arg.(type) {
	case *ast.Ident:
		// signature := "test"
		// log.Info(signature, ...)
		// firstBLArg is "test"
		var expr *ast.BasicLit
		decl := ai.findDeclNode(val)
		ok := false
		if expr, ok = decl.(*ast.BasicLit); !ok {
			return nil
		}
		return &firstBLArg{
			value: expr.Value,
		}
	case *ast.CallExpr:
		// log.Info(fmt.Sprintf("test"), ...)
		// firstBLArg is "test"
		return ai.findFunFirstBLArg(val)
	case *ast.BasicLit:
		// log.Info("test", ...)
		// firstBLArg is "test"
		return &firstBLArg{
			value: val.Value,
		}
	}

	return nil
}

// resolveFnDecl find the log print function and resolve its declaration
// contains =  function package, function object, unction name
// if function doesn't belong to any object , function object will be empty
func (ai *logAnalyzer) resolveFnDecl(fn *ast.CallExpr, helper *AstHelper) (string, string, string) {
	switch p := fn.Fun.(type) {
	// figure out  logPrint := log.Error
	case *ast.Ident:
		decl := ai.findDeclNode(p)
		if expr, ok := decl.(*ast.SelectorExpr); ok {
			// return nil  means Defining identifiers are handled by their parent nodes.
			return ai.resolveFunName(helper.GetTypeUsed(expr.Sel))
		}
	case *ast.SelectorExpr:
		// return nil  means Defining identifiers are handled by their parent nodes.
		return ai.resolveFunName(helper.GetTypeUsed(p.Sel))
	}

	return "", "", ""
}

// resolveFunName retuens the log print function declaration
// contains =  function package, function object, unction name
// if function doesn't belong to any object , function object will be empty
func (ai *logAnalyzer) resolveFunName(obj types.Object) (string, string, string) {
	if obj == nil {
		return "", "", ""
	}

	if obj.Pkg() == nil {
		return "", "", obj.Name()
	}

	fnName := obj.Name()
	fnPkg := obj.Pkg().Name()

	signature, ok := obj.Type().(*types.Signature)
	if !ok || signature.Recv() == nil || signature.Recv().Type() == nil {
		return fnPkg, "", fnName
	}

	return fnPkg, signature.Recv().Type().String(), fnName
}

// findDeclNode returns funtion declaration node
// for example:
//    logSeq := log.Debug
//    logSeq.Info(...)
// logSeq declaration = log.Debug
func (ai *logAnalyzer) findDeclNode(fn *ast.Ident) ast.Node {
	if fn.Obj == nil {
		return nil
	}

	obj := fn.Obj
	if obj.Decl == nil {
		return nil
	}

	switch decl := obj.Decl.(type) {
	case *ast.AssignStmt:
		// assign statement: fn := pkg.Func
		if len(decl.Rhs) == 0 {
			return nil
		}

		switch fnDecl := decl.Rhs[0].(type) {
		case *ast.SelectorExpr:
			return fnDecl
		case *ast.BasicLit:
			return fnDecl
		}
	}

	return nil
}

func (ai *logAnalyzer) fromLogSignature(file *ast.File, helper *AstHelper) {
	ast.Walk(newASTVisitor(func(node ast.Node, stack stackFunc) bool {
		switch n := node.(type) {
		case *ast.BasicLit:
			// try to filter log pattern
			ai.filterLog(n, stack, helper)
		}
		return true
	}), file)
}

func (ai *logAnalyzer) filterLog(id *ast.BasicLit, stack stackFunc, helper *AstHelper) {
	switch p := stack(1).(type) {
	case *ast.Ident, *ast.SelectorExpr:
	case *ast.CallExpr:
		if pp, ok := p.Fun.(*ast.SelectorExpr); ok {
			ai.matchLog(id, pp.Sel, stack, helper)
		}
	}
}

func (ai *logAnalyzer) matchLog(l *ast.BasicLit, fn *ast.Ident, stack stackFunc, helper *AstHelper) {
	// get the log print
	obj := helper.GetTypeUsed(fn)
	if obj == nil {
		// Defining identifiers are handled by their parent nodes.
		return
	}

	if _, ok := isCall(fn, obj, stack); ok {
		callFnName, rawCallFnPos, rawCallFnEndPos := ai.callContext(stack, helper)

		fnName := obj.Name()
		fnPkg := obj.Pkg().Name()

		/*if strings.Contains(fnName, "Error") {
			log.Info("fnPkg %s %s %+v", zap.String("package name", fnPkg), zap.String("function name", fnName), zap.String("object", obj.Type().(*types.Signature).Recv().Type()))
		}*/

		if level, ok := ai.fn(fnPkg, fnName, obj.String()); ok {
			fnPos := helper.GetPos(rawCallFnPos)
			fnEndPos := helper.GetPos(rawCallFnEndPos)
			fnProtoPos := &logpattern.Position{
				FilePath: fnPos.Filename,
				Pos: &logpattern_go_proto.CodePosition{
					Line:   int32(fnPos.Line),
					Column: int32(fnPos.Column),
				},
				EndPos: &logpattern_go_proto.CodePosition{
					Line:   int32(fnEndPos.Line),
					Column: int32(fnEndPos.Column),
				},
			}
			logPos := helper.GetPos(l.Pos())
			logProtoPos := &logpattern.Position{
				PackageInfo: &logpattern.Package{
					Repo: helper.GetRepo(),
				},
				FilePath: logPos.Filename,
				Pos: &logpattern_go_proto.CodePosition{
					Line:   int32(logPos.Line),
					Column: int32(logPos.Column),
				},
			}

			fn := &logpattern.FuncInfo{
				Pos:  fnProtoPos,
				Name: callFnName,
			}

			ai.logChan <- &logpattern.LogPattern{
				Pos:       logProtoPos,
				Func:      fn,
				Level:     level,
				Signature: []string{l.Value},
			}
		}
	}
}

// callContext returns: funcInfo for the nearest enclosing parent function,
// not including the node itself,
// or the enclosing package initializer if the node
func (ai *logAnalyzer) callContext(stack stackFunc, helper *AstHelper) (string, token.Pos, token.Pos) {
	for i := 1; ; i++ {
		switch p := stack(i).(type) {
		case *ast.FuncDecl:
			return p.Name.Name, p.Pos(), p.End()
		case *ast.File:
			// Lazily emit a virtual node to represent the static
			// initializer for top-level expressions in this file of the
			// package.  We only do this if there are expressions that need
			// to be initialized.
			return fmt.Sprintf("<init>@%d", p.Package), p.Pos(), p.End()
		}
	}
}

func isCall(id *ast.Ident, obj types.Object, stack stackFunc) (*ast.CallExpr, bool) {
	if _, ok := obj.(*types.Func); ok {
		if cal, ok := stack(1).(*ast.CallExpr); ok {
			if sel, ok := cal.Fun.(*ast.SelectorExpr); ok && sel.Sel == id {
				return cal, true // x.id(...)
			}
		}
	}
	return nil, false
}
