// 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 profiling

import (
	"fmt"
	"regexp"

	"github.com/apache/skywalking-rover/pkg/logger"

	"github.com/ianlancetaylor/demangle"
)

type ModuleType int8

var (
	KernelProcSymbolFilePath = "kallsyms" // after host.GetHostProcInHost, should be "/proc/kallsyms"

	log = logger.GetLogger("tools", "profiling")
)

const (
	ModuleTypeExec ModuleType = iota
	ModuleTypeSo
	ModuleTypePerfMap
	ModuleTypeVDSO
	ModuleTypeUnknown
)

// Info of profiling process
type Info struct {
	Modules           []*Module
	cacheAddrToSymbol map[uint64]string
}

type Module struct {
	Ranges           []*ModuleRange
	Name             string
	Path             string
	Type             ModuleType
	SoOffset, SoAddr uint64
	Symbols          []*Symbol
}

type ModuleRange struct {
	StartAddr, EndAddr, FileOffset uint64
}

// Symbol of executable file
type Symbol struct {
	Name     string
	Location uint64
	Size     uint64
}

type StatFinder interface {
	// IsSupport to stat the executable file for profiling
	IsSupport(filePath string) bool
	// AnalyzeSymbols in the file
	AnalyzeSymbols(filePath string) ([]*Symbol, error)
	// ToModule to init a new module
	ToModule(pid int32, modName, modPath string, moduleRange []*ModuleRange) (*Module, error)
}

func NewInfo(modules map[string]*Module) *Info {
	ls := make([]*Module, 0)
	for _, m := range modules {
		ls = append(ls, m)
	}
	return &Info{Modules: ls, cacheAddrToSymbol: make(map[uint64]string)}
}

// FindSymbols from address list, if could not found symbol name then append default symbol to array
func (i *Info) FindSymbols(addresses []uint64, defaultSymbol string) []string {
	if len(addresses) == 0 {
		return nil
	}
	result := make([]string, 0)
	for _, addr := range addresses {
		if addr <= 0 {
			continue
		}
		s := i.FindSymbolName(addr)
		if s == "" {
			s = defaultSymbol
		}
		result = append(result, s)
	}
	return result
}

// FindSymbolName by address
func (i *Info) FindSymbolName(address uint64) string {
	if d := i.cacheAddrToSymbol[address]; d != "" {
		return d
	}
	log.Debugf("ready to find the symbol from address: %d", address)
	foundModule := false
	for _, mod := range i.Modules {
		offset, c := mod.contains(address)
		if !c {
			continue
		}
		foundModule = true

		if sym := mod.findAddr(offset); sym != nil {
			name := processSymbolName(sym.Name)
			i.cacheAddrToSymbol[address] = name
			return name
		}
	}
	if !foundModule {
		log.Debugf("could not found any module to handle address: %d", address)
	}
	return ""
}

func (i *Info) FindSymbolAddress(name string) uint64 {
	for _, m := range i.Modules {
		for _, sym := range m.Symbols {
			if sym.Name == name {
				return sym.Location
			}
		}
	}
	return 0
}

func (i *Info) FindSymbolByRegex(rep string) (string, error) {
	vals, err := i.FindMultipleSymbolByRegex(rep)
	if err != nil {
		return "", err
	}
	return vals[0], nil
}

func (i *Info) FindMultipleSymbolByRegex(rep string) ([]string, error) {
	compile, err := regexp.Compile(rep)
	if err != nil {
		return nil, err
	}
	result := make([]string, 0)
	for _, m := range i.Modules {
		for _, sym := range m.Symbols {
			if compile.MatchString(sym.Name) {
				result = append(result, sym.Name)
			}
		}
	}

	if len(result) == 0 {
		return nil, fmt.Errorf("cannot found any matches symbol: %s", rep)
	}
	return result, nil
}

func (m *Module) contains(addr uint64) (uint64, bool) {
	for _, r := range m.Ranges {
		if addr >= r.StartAddr && addr < r.EndAddr {
			log.Debugf("found module %s could hanlde address: %d", m.Name, addr)
			if m.Type == ModuleTypeSo || m.Type == ModuleTypeVDSO {
				offset := addr - r.StartAddr + r.FileOffset
				offset += m.SoAddr - m.SoOffset
				log.Debugf("update address %d to offset %d", addr, offset)
				return offset, true
			}
			return addr, true
		}
	}
	return 0, false
}

func (m *Module) findAddr(offset uint64) *Symbol {
	start := 0
	end := len(m.Symbols) - 1
	for start < end {
		mid := start + (end-start)/2
		result := int64(offset) - int64(m.Symbols[mid].Location)

		if result < 0 {
			end = mid
		} else if result > 0 {
			start = mid + 1
		} else {
			return m.Symbols[mid]
		}
	}

	if start >= 1 && m.Symbols[start-1].Location < offset && offset < m.Symbols[start].Location {
		return m.Symbols[start-1]
	}
	log.Debugf("could not found the address: %d in module %s", offset, m.Name)

	return nil
}

func processSymbolName(name string) string {
	if name == "" {
		return ""
	}
	// fix process demangle symbol name, such as c++ language symbol
	skip := 0
	if name[0] == '.' || name[0] == '$' {
		skip++
	}
	return demangle.Filter(name[skip:])
}
