// Licensed to 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. Apache Software Foundation (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 plugins

import (
	"fmt"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/apache/skywalking-go/tools/go-agent/instrument/consts"

	"github.com/apache/skywalking-go/plugins/core/instrument"
	"github.com/apache/skywalking-go/tools/go-agent/instrument/agentcore"
	"github.com/apache/skywalking-go/tools/go-agent/instrument/plugins/rewrite"
	"github.com/apache/skywalking-go/tools/go-agent/tools"

	"github.com/dave/dst"

	"golang.org/x/text/cases"
	"golang.org/x/text/language"
)

var methodEnhanceAdapterFiles = make(map[string]bool)
var methodImportAgentCorePackages = []string{
	"log", "tracing", "operator", "metrics",
}

type MethodEnhance struct {
	funcDecl    *dst.FuncDecl
	path        string
	packageName string
	fullPackage string

	InstrumentName           string
	InterceptorDefineName    string
	InterceptorGeneratedName string
	InterceptorVarName       string

	Parameters []*tools.PackagedParameterInfo
	Recvs      []*tools.PackagedParameterInfo
	Results    []*tools.PackagedParameterInfo

	FuncID              string
	AdapterPreFuncName  string
	AdapterPostFuncName string

	replacementKey   string
	replacementValue string

	importAnalyzer *tools.ImportAnalyzer
}

func NewMethodEnhance(inst instrument.Instrument, matcher *instrument.Point, f *dst.FuncDecl, path string,
	importAnalyzer *tools.ImportAnalyzer) *MethodEnhance {
	fullPackage := filepath.Join(inst.BasePackage(), matcher.PackagePath)
	if runtime.GOOS == consts.WindowsGOOS {
		fullPackage = strings.ReplaceAll(fullPackage, `\`, `/`)
	}
	pkgName := filepath.Base(fullPackage)
	if matcher.PackageName != "" {
		pkgName = matcher.PackageName
	}
	enhance := &MethodEnhance{
		funcDecl:              f,
		path:                  path,
		fullPackage:           fullPackage,
		packageName:           pkgName,
		InstrumentName:        inst.Name(),
		InterceptorDefineName: matcher.Interceptor,
		Parameters:            tools.EnhanceParameterNamesWithPackagePrefix(pkgName, f.Type.Params, tools.FieldListTypeParam),
		Results:               tools.EnhanceParameterNamesWithPackagePrefix(pkgName, f.Type.Results, tools.FieldListTypeResult),
	}
	if f.Recv != nil {
		enhance.Recvs = tools.EnhanceParameterNamesWithPackagePrefix(pkgName, f.Recv, tools.FieldListTypeRecv)
	}

	importAnalyzer.AnalyzeNeedsImports(path, f.Type.Params)
	importAnalyzer.AnalyzeNeedsImports(path, f.Type.Results)
	enhance.importAnalyzer = importAnalyzer

	enhance.FuncID = tools.BuildFuncIdentity(fullPackage, f)
	enhance.AdapterPreFuncName = fmt.Sprintf("%s%s", rewrite.GenerateMethodPrefix, enhance.FuncID)
	enhance.AdapterPostFuncName = fmt.Sprintf("%s%s_ret", rewrite.GenerateMethodPrefix, enhance.FuncID)

	// the interceptor name needs to add the function id ensure there no conflict in the framework package
	titleCase := cases.Title(language.English)
	packageTitle := filepath.Base(titleCase.String(filepath.Join(inst.BasePackage(), pkgName)))
	if runtime.GOOS == consts.WindowsGOOS {
		packageTitle = strings.ReplaceAll(packageTitle, `\`, `/`)
	}
	enhance.InterceptorGeneratedName = fmt.Sprintf("%s%s%s", rewrite.TypePrefix, packageTitle, enhance.InterceptorDefineName)
	enhance.InterceptorVarName = fmt.Sprintf("%sinterceptor_%s", rewrite.GenerateVarPrefix, enhance.FuncID)
	return enhance
}

func (m *MethodEnhance) PackageName() string {
	return m.packageName
}

func (m *MethodEnhance) BuildForInvoker() {
	insertsTmpl, err := templatesFS.ReadFile("templates/method_inserts.tmpl")
	if err != nil {
		panic(fmt.Errorf("reading method inserts: %w", err))
	}
	result := tools.ExecuteTemplate(string(insertsTmpl), m)
	m.replacementKey = fmt.Sprintf("//goagent:enhance_%s\n", m.FuncID)
	m.replacementValue = result

	m.funcDecl.Body.Decs.Lbrace.Prepend("\n", m.replacementKey)
}

func (m *MethodEnhance) BuildImports(decl *dst.GenDecl) {
	if !methodEnhanceAdapterFiles[filepath.Dir(m.path)] {
		for _, n := range methodImportAgentCorePackages {
			m.appendImport(decl, "", fmt.Sprintf("%s/%s", agentcore.EnhanceFromBasePackage, n))
		}
		m.appendImport(decl, m.packageName, m.fullPackage)
		methodEnhanceAdapterFiles[filepath.Dir(m.path)] = true
	}

	m.importAnalyzer.AppendUsedImports(decl)
}

func (m *MethodEnhance) appendImport(decl *dst.GenDecl, name, path string) {
	imp := &dst.ImportSpec{
		Path: &dst.BasicLit{
			Value: fmt.Sprintf("%q", path),
		},
	}
	if name != "" {
		imp.Name = dst.NewIdent(name)
	}
	decl.Specs = append(decl.Specs, imp)
}

func (m *MethodEnhance) BuildForDelegator() []dst.Decl {
	result := make([]dst.Decl, 0)

	result = append(result, tools.GoStringToDecls(fmt.Sprintf(`var %s = &%s{}`, m.InterceptorVarName, m.InterceptorGeneratedName))...)
	preFunc := &dst.FuncDecl{
		Name: &dst.Ident{Name: m.AdapterPreFuncName},
		Type: &dst.FuncType{
			Params:  &dst.FieldList{},
			Results: &dst.FieldList{},
		},
	}
	for i, recv := range m.Recvs {
		preFunc.Type.Params.List = append(preFunc.Type.Params.List, &dst.Field{
			Names: []*dst.Ident{dst.NewIdent(fmt.Sprintf("recv_%d", i))},
			Type:  &dst.StarExpr{X: recv.PackagedType()},
		})
	}
	for i, parameter := range m.Parameters {
		preFunc.Type.Params.List = append(preFunc.Type.Params.List, &dst.Field{
			Names: []*dst.Ident{dst.NewIdent(fmt.Sprintf("param_%d", i))},
			Type:  &dst.StarExpr{X: m.changeTypeIfNeeds(parameter.PackagedType())},
		})
	}
	for i, result := range m.Results {
		preFunc.Type.Results.List = append(preFunc.Type.Results.List, &dst.Field{
			Names: []*dst.Ident{dst.NewIdent(fmt.Sprintf("ret_%d", i))},
			Type:  result.PackagedType(),
		})
	}
	preFunc.Type.Results.List = append(preFunc.Type.Results.List, &dst.Field{
		Names: []*dst.Ident{dst.NewIdent("invocation")},
		Type:  &dst.StarExpr{X: &dst.SelectorExpr{X: dst.NewIdent("operator"), Sel: dst.NewIdent("realInvocation")}},
	}, &dst.Field{
		Names: []*dst.Ident{dst.NewIdent("skip")},
		Type:  dst.NewIdent("bool"),
	})

	beforeFile, err := templatesFS.ReadFile("templates/method_intercept_before.tmpl")
	if err != nil {
		panic(fmt.Errorf("reading method before intercept template failure: %w", err))
	}
	preFunc.Body = &dst.BlockStmt{
		List: tools.GoStringToStats(tools.ExecuteTemplate(string(beforeFile), m)),
	}
	result = append(result, preFunc)

	postFunc := &dst.FuncDecl{
		Name: &dst.Ident{Name: m.AdapterPostFuncName},
		Type: &dst.FuncType{
			Params:  &dst.FieldList{},
			Results: &dst.FieldList{},
		},
	}
	postFunc.Type.Params.List = append(postFunc.Type.Params.List, &dst.Field{
		Names: []*dst.Ident{dst.NewIdent("invocation")},
		Type:  &dst.StarExpr{X: &dst.SelectorExpr{X: dst.NewIdent("operator"), Sel: dst.NewIdent("realInvocation")}},
	})
	for inx, f := range m.Results {
		postFunc.Type.Params.List = append(postFunc.Type.Params.List, &dst.Field{
			Names: []*dst.Ident{dst.NewIdent(fmt.Sprintf("ret_%d", inx))},
			Type:  &dst.StarExpr{X: f.PackagedType()},
		})
	}
	afterFile, err := templatesFS.ReadFile("templates/method_intercept_after.tmpl")
	if err != nil {
		panic(fmt.Errorf("reading method after intercept template failure: %w", err))
	}
	postFunc.Body = &dst.BlockStmt{
		List: tools.GoStringToStats(tools.ExecuteTemplate(string(afterFile), m)),
	}
	result = append(result, postFunc)
	return result
}

func (m *MethodEnhance) changeTypeIfNeeds(tp dst.Expr) dst.Expr {
	// change "...XXX" to "[]XXX" for reference type
	if el, ok := tp.(*dst.Ellipsis); ok {
		return &dst.ArrayType{Elt: el.Elt}
	}
	return tp
}

func (m *MethodEnhance) ReplaceFileContent(path, content string) string {
	if m.path == path {
		return strings.Replace(content, m.replacementKey, m.replacementValue, 1)
	}
	return content
}

func (m *MethodEnhance) InitFunctions() []*EnhanceInitFunction {
	return nil
}
