package callgraph

import (
	"encoding/json"
	"fmt"
	"math"
	"sort"
	"strconv"
	"strings"
	"time"

	"code.justin.tv/release/trace/analysis/paths"
	"code.justin.tv/release/trace/pbmsg"
)

func NewCallGraph() CallNode {
	return newNode([]uint32{})
}

// node represents one call-and-response interaction between a client
// and server. The server might have to go do additional calls to
// respond to the client; if so, those calls are Children of the node.
// node fulfills the CallNode interface.
type node struct {
	data *call

	children map[uint32]*node
	events   []*pbmsg.Event
	path     []uint32
}

func newNode(path []uint32) *node {
	return &node{
		data:     &call{},
		children: make(map[uint32]*node),
		path:     path,
	}
}

// Children returns the children of this node in Path order
func (n *node) Children() []Node {
	// Iterate over children in path order
	// sort the subpaths
	childPaths := make([]int, len(n.children))
	i := 0
	for c, _ := range n.children {
		childPaths[i] = int(c)
		i += 1
	}
	sort.Ints(childPaths)

	children := make([]Node, len(n.children))
	for i, p := range childPaths {
		children[i] = n.children[uint32(p)]
	}
	return children
}

func (n *node) Label(dim StringDimension) string {
	switch dim {
	case Client:
		return n.client()
	case ClientHost:
		return n.data.client.host
	case Server:
		return n.server()
	case ServerHost:
		return n.data.server.host
	case Signature:
		return signature(n)
	case RPCName:
		return n.rpcName()
	default:
		return ""
	}
}

func (n *node) client() string {
	switch {
	case n.data.client.svc != "":
		return n.data.client.svc
	case n.data.client.host != "":
		return n.data.client.host
	default:
		return "unknown"
	}
}

func (n *node) server() string {
	if n.data.server.svc != "" {
		return n.data.server.svc
	}

	if n.data.server.host != "" {
		return n.data.server.host
	}

	if n.data.rpc != nil {
		switch n.data.rpc.Proto() {
		case Memcached:
			// comes out as 'memcached:cache-hit', 'memcache:cache-set', etc
			return fmt.Sprintf("memcached:%s", n.data.rpc.Name())
		}
	}

	if n.data.client.peer != "" {
		return n.data.client.peer
	}

	return "unknown"
}

func (n *node) rpcName() string {
	if n.data.rpc != nil {
		return n.data.rpc.Name()
	}
	return UnknownRPC
}

func (n *node) signature() string {
	from := trimRepo(n.Label(Client))
	to := trimRepo(n.Label(Server))

	if name := n.Label(RPCName); name != UnknownRPC {
		to = to + ":" + name
	}

	children := n.Children()
	deps := ""
	if len(children) > 0 {
		depServers := make([]string, 0)
		depSet := make(map[string]struct{})
		var server string
		for _, child := range children {
			server = child.Label(Server)
			if _, ok := depSet[server]; ok {
				depServers = append(depServers, server)
				depSet[server] = struct{}{}
			}
		}
		sort.Strings(depServers)
		deps = fmt.Sprintf(" [%s]", strings.Join(depServers, ", "))
	}

	sig := fmt.Sprintf("%s->%s%s", from, to, deps)

	return sig
}

// returns a log10 bucket range that val falls into, eg "10-99" or
// "100-999". Behavior is special below 10 to highlight counts of
// exactly 1: If val == 1, this returns just "1", and if 1 < val < 10,
// this returns "2-9"
func log10Bucket(val int) string {
	if val == 1 {
		return "1"
	}

	log := math.Log10(float64(val))
	var lower, upper int
	if math.Mod(log, 1) == 0 {
		lower = int(math.Pow(10, log))
		upper = int(math.Pow(10, log+1)) - 1
	} else {
		lower = int(math.Pow(10, math.Floor(log)))
		upper = int(math.Pow(10, math.Ceil(log))) - 1
	}
	if val < 10 {
		lower += 1
	}
	return fmt.Sprintf("%d-%d", lower, upper)
}

func (n *node) IntValue(dim Int64Dimension) int64 {
	switch dim {
	case ClientPid:
		return int64(n.data.client.pid)
	case ClientStart:
		return timeToInt64(n.data.client.start)
	case ClientEnd:
		return timeToInt64(n.data.client.end)
	case ClientNanos:
		return int64(n.data.client.nanos)
	case ServerPid:
		return int64(n.data.server.pid)
	case ServerStart:
		return timeToInt64(n.data.server.start)
	case ServerEnd:
		return timeToInt64(n.data.server.end)
	case ServerNanos:
		return int64(n.data.server.nanos)
	default:
		return 0
	}
}

func timeToInt64(t *time.Time) int64 {
	if t != nil {
		return t.UnixNano()
	}
	return 0
}

// Add adds an event to the call graph. The event's path should place
// it either in this node, or in one of its descendants. If the
// event's path places it elsewhere, this returns an error.
func (n *node) Add(ev *pbmsg.Event) {
	p := ev.GetPath()
	// If the event belongs on this node, add it
	if paths.PathMatches(p, n.path) {
		n.data.update(ev)
		n.events = append(n.events, ev)
		return
	}
	// Else, it should belong on a child of this node
	if !paths.IsChildPath(p, n.path) {
		// Have to check this, or else we're exposed to infinite
		// recursion
		panic(fmt.Sprintf("%v is not a child of %v", p, n.path))
	}
	// get next component of events path, add new node, try adding
	// event there
	nextPath := p[:len(n.path)+1]
	nextPathID := nextPath[len(nextPath)-1]
	child, ok := n.children[nextPathID]
	if !ok {
		child = newNode(nextPath)
		n.children[nextPathID] = child
	}
	child.Add(ev)
}

func (n *node) MarshalJSON() ([]byte, error) {
	// JSON marshaller only wants maps with string keys
	children := make(map[string]*node)
	for i, x := range n.children {
		children[strconv.Itoa(int(i))] = x
	}

	type data struct {
		Client      string
		ClientStart int64
		ClientEnd   int64
		ClientNanos int64
		ClientHost  string
		ClientPid   int64

		Server      string
		ServerStart int64
		ServerEnd   int64
		ServerNanos int64
		ServerHost  string
		ServerPid   int64

		Signature string
		Path      string

		RPC RPCAnnotation `json:",omitempty"`

		Children map[string]*node
	}

	d := data{
		Client:      n.Label(Client),
		ClientHost:  n.Label(ClientHost),
		ClientPid:   n.IntValue(ClientPid),
		ClientStart: n.IntValue(ClientStart),
		ClientEnd:   n.IntValue(ClientEnd),
		ClientNanos: n.IntValue(ClientNanos),

		Server:      n.Label(Server),
		ServerHost:  n.Label(ServerHost),
		ServerPid:   n.IntValue(ServerPid),
		ServerStart: n.IntValue(ServerStart),
		ServerEnd:   n.IntValue(ServerEnd),
		ServerNanos: n.IntValue(ServerNanos),

		Signature: n.Label(Signature),
		Path:      paths.PathToString(n.path),

		RPC: n.data.rpc,

		Children: children,
	}

	return json.Marshal(d)
}
