package render

import (
	"context"

	opentracing "github.com/opentracing/opentracing-go"
	otlog "github.com/opentracing/opentracing-go/log"

	"github.com/weaveworks/scope/report"
)

// MapFunc is anything which can take an arbitrary Node and
// return another Node.
//
// If the output ID is blank, the node shall be omitted from the rendered topology.
// (we chose not to return an extra bool because it adds clutter)
type MapFunc func(report.Node) report.Node

// Renderer is something that can render a report to a set of Nodes.
type Renderer interface {
	Render(context.Context, report.Report) Nodes
}

// Nodes is the result of Rendering
type Nodes struct {
	report.Nodes
	Filtered int
}

// Merge merges the results of Rendering
func (r Nodes) Merge(o Nodes) Nodes {
	return Nodes{
		Nodes:    r.Nodes.Merge(o.Nodes),
		Filtered: r.Filtered + o.Filtered,
	}
}

// Transformer is something that transforms one set of Nodes to
// another set of Nodes.
type Transformer interface {
	Transform(nodes Nodes) Nodes
}

// Transformers is a composition of Transformers
type Transformers []Transformer

// Transform implements Transformer
func (ts Transformers) Transform(nodes Nodes) Nodes {
	for _, t := range ts {
		nodes = t.Transform(nodes)
	}
	return nodes
}

// Render renders the report and then transforms it
func Render(ctx context.Context, rpt report.Report, renderer Renderer, transformer Transformer) Nodes {
	span, ctx := opentracing.StartSpanFromContext(ctx, "Render:"+typeName(renderer))
	defer span.Finish()
	return transformer.Transform(renderer.Render(ctx, rpt))
}

// Reduce renderer is a Renderer which merges together the output of several
// other renderers.
type Reduce []Renderer

// MakeReduce is the only sane way to produce a Reduce Renderer.
func MakeReduce(renderers ...Renderer) Renderer {
	return Reduce(renderers)
}

// Render produces a set of Nodes given a Report.
func (r Reduce) Render(ctx context.Context, rpt report.Report) Nodes {
	if ctx.Err() != nil {
		return Nodes{}
	}
	span, ctx := opentracing.StartSpanFromContext(ctx, "Reduce.Render")
	defer span.Finish()
	l := len(r)
	switch l {
	case 0:
		return Nodes{}
	}
	c := make(chan Nodes, l)
	for _, renderer := range r {
		renderer := renderer // Pike!!
		go func() {
			span, ctx := opentracing.StartSpanFromContext(ctx, typeName(renderer))
			c <- renderer.Render(ctx, rpt)
			span.Finish()
		}()
	}
	for ; l > 1; l-- {
		left, right := <-c, <-c
		go func() {
			c <- left.Merge(right)
		}()
	}
	return <-c
}

// Map is a Renderer which produces a set of Nodes from the set of
// Nodes produced by another Renderer.
type Map struct {
	MapFunc
	Renderer
}

// MakeMap makes a new Map
func MakeMap(f MapFunc, r Renderer) Renderer {
	return Map{f, r}
}

// Render transforms a set of Nodes produces by another Renderer.
// using a map function
func (m Map) Render(ctx context.Context, rpt report.Report) Nodes {
	span, ctx := opentracing.StartSpanFromContext(ctx, "Map.Render:"+functionName(m.MapFunc))
	defer span.Finish()
	var (
		input  = m.Renderer.Render(ctx, rpt)
		output = newJoinResults(nil)
	)

	// Rewrite all the nodes according to the map function
	for _, inRenderable := range input.Nodes {
		if ctx.Err() != nil { // check if cancelled
			return Nodes{}
		}
		outRenderable := m.MapFunc(inRenderable)
		if outRenderable.ID != "" {
			output.add(inRenderable.ID, outRenderable)
		}
	}
	span.LogFields(otlog.Int("input.nodes", len(input.Nodes)),
		otlog.Int("ouput.nodes", len(output.nodes)))

	return output.result(ctx, input)
}

// Condition is a predecate over the entire report that can evaluate to true or false.
type Condition func(report.Report) bool

type conditionalRenderer struct {
	Condition
	Renderer
}

// ConditionalRenderer renders nothing if the condition is false, otherwise it defers
// to the wrapped Renderer.
func ConditionalRenderer(c Condition, r Renderer) Renderer {
	return conditionalRenderer{c, r}
}

func (cr conditionalRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
	if cr.Condition(rpt) {
		return cr.Renderer.Render(ctx, rpt)
	}
	return Nodes{}
}

// joinResults is used by Renderers that join sets of nodes
type joinResults struct {
	nodes  report.Nodes
	mapped map[string]string   // input node ID -> output node ID - common case
	multi  map[string][]string // input node ID -> output node IDs - exceptional case
}

func newJoinResults(inputNodes report.Nodes) joinResults {
	nodes := make(report.Nodes, len(inputNodes))
	for id, n := range inputNodes {
		n.Adjacency = nil              // result() assumes all nodes start with no adjacencies
		n.Children = n.Children.Copy() // so we can do unsafe adds
		nodes[id] = n
	}
	return joinResults{nodes: nodes, mapped: map[string]string{}, multi: map[string][]string{}}
}

func (ret *joinResults) mapChild(from, to string) {
	if _, ok := ret.mapped[from]; !ok {
		ret.mapped[from] = to
	} else {
		ret.multi[from] = append(ret.multi[from], to)
	}
}

// Add m into the results as a top-level node, mapped from original ID
// Note it is not safe to mix calls to add() with addChild(), addChildAndChildren() or addUnmappedChild()
func (ret *joinResults) add(from string, m report.Node) {
	if existing, ok := ret.nodes[m.ID]; ok {
		m = m.Merge(existing)
	}
	ret.nodes[m.ID] = m
	ret.mapChild(from, m.ID)
}

// Add m as a child of the node at id, creating a new result node in
// the specified topology if not already there.
func (ret *joinResults) addUnmappedChild(m report.Node, id string, topology string) {
	result, exists := ret.nodes[id]
	if !exists {
		result = report.MakeNode(id).WithTopology(topology)
	}
	result.Children.UnsafeAdd(m)
	if m.Topology != report.Endpoint { // optimisation: we never look at endpoint counts
		result = result.AddCounter(m.Topology, 1)
	}
	ret.nodes[id] = result
}

// Add m as a child of the node at id, creating a new result node in
// the specified topology if not already there, and updating the
// mapping from old ID to new ID.
func (ret *joinResults) addChild(m report.Node, id string, topology string) {
	ret.addUnmappedChild(m, id, topology)
	ret.mapChild(m.ID, id)
}

// Like addChild, but also add m's children.
func (ret *joinResults) addChildAndChildren(m report.Node, id string, topology string) {
	ret.addUnmappedChild(m, id, topology)
	result := ret.nodes[id]
	result.Children.UnsafeMerge(m.Children)
	ret.nodes[id] = result
	ret.mapChild(m.ID, id)
}

// Add a copy of n straight into the results
func (ret *joinResults) passThrough(n report.Node) {
	n.Adjacency = nil // result() assumes all nodes start with no adjacencies
	ret.nodes[n.ID] = n
	n.Children = n.Children.Copy() // so we can do unsafe adds
	ret.mapChild(n.ID, n.ID)
}

// Rewrite Adjacency of nodes in ret mapped from original nodes in
// input, and return the result.
func (ret *joinResults) result(ctx context.Context, input Nodes) Nodes {
	for _, n := range input.Nodes {
		if ctx.Err() != nil { // check if cancelled
			return Nodes{}
		}
		outID, ok := ret.mapped[n.ID]
		if !ok {
			continue
		}
		ret.rewriteAdjacency(outID, n.Adjacency)
		for _, outID := range ret.multi[n.ID] {
			ret.rewriteAdjacency(outID, n.Adjacency)
		}
	}
	return Nodes{Nodes: ret.nodes}
}

func (ret *joinResults) rewriteAdjacency(outID string, adjacency report.IDList) {
	out := ret.nodes[outID]
	// for each adjacency in the original node, find out what it maps
	// to (if any), and add that to the new node
	for _, a := range adjacency {
		if mappedDest, found := ret.mapped[a]; found {
			out.Adjacency = out.Adjacency.Add(mappedDest)
			out.Adjacency = out.Adjacency.Add(ret.multi[a]...)
		}
	}
	ret.nodes[outID] = out
}

// ResetCache blows away the rendered node cache, and known service
// cache.
func ResetCache() {
	renderCache.Purge()
	purgeKnownServiceCache()
}
