// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package analysis

// This file computes the channel "peers" relation over all pairs of
// channel operations in the program.  The peers are displayed in the
// lower pane when a channel operation (make, <-, close) is clicked.

// TODO(adonovan): handle calls to reflect.{Select,Recv,Send,Close} too,
// then enable reflection in PTA.

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

	"golang.org/x/tools/go/pointer"
	"golang.org/x/tools/go/ssa"
)

func (a *analysis) doChannelPeers(ptsets map[ssa.Value]pointer.Pointer) {
	addSendRecv := func(j *commJSON, op chanOp) {
		j.Ops = append(j.Ops, commOpJSON{
			Op: anchorJSON{
				Text: op.mode,
				Href: a.posURL(op.pos, op.len),
			},
			Fn: prettyFunc(nil, op.fn),
		})
	}

	// Build an undirected bipartite multigraph (binary relation)
	// of MakeChan ops and send/recv/close ops.
	//
	// TODO(adonovan): opt: use channel element types to partition
	// the O(n^2) problem into subproblems.
	aliasedOps := make(map[*ssa.MakeChan][]chanOp)
	opToMakes := make(map[chanOp][]*ssa.MakeChan)
	for _, op := range a.ops {
		// Combine the PT sets from all contexts.
		var makes []*ssa.MakeChan // aliased ops
		ptr, ok := ptsets[op.ch]
		if !ok {
			continue // e.g. channel op in dead code
		}
		for _, label := range ptr.PointsTo().Labels() {
			makechan, ok := label.Value().(*ssa.MakeChan)
			if !ok {
				continue // skip intrinsically-created channels for now
			}
			if makechan.Pos() == token.NoPos {
				continue // not possible?
			}
			makes = append(makes, makechan)
			aliasedOps[makechan] = append(aliasedOps[makechan], op)
		}
		opToMakes[op] = makes
	}

	// Now that complete relation is built, build links for ops.
	for _, op := range a.ops {
		v := commJSON{
			Ops: []commOpJSON{}, // (JS wants non-nil)
		}
		ops := make(map[chanOp]bool)
		for _, makechan := range opToMakes[op] {
			v.Ops = append(v.Ops, commOpJSON{
				Op: anchorJSON{
					Text: "made",
					Href: a.posURL(makechan.Pos()-token.Pos(len("make")),
						len("make")),
				},
				Fn: makechan.Parent().RelString(op.fn.Package().Pkg),
			})
			for _, op := range aliasedOps[makechan] {
				ops[op] = true
			}
		}
		for op := range ops {
			addSendRecv(&v, op)
		}

		// Add links for each aliased op.
		fi, offset := a.fileAndOffset(op.pos)
		fi.addLink(aLink{
			start:   offset,
			end:     offset + op.len,
			title:   "show channel ops",
			onclick: fmt.Sprintf("onClickComm(%d)", fi.addData(v)),
		})
	}
	// Add links for makechan ops themselves.
	for makechan, ops := range aliasedOps {
		v := commJSON{
			Ops: []commOpJSON{}, // (JS wants non-nil)
		}
		for _, op := range ops {
			addSendRecv(&v, op)
		}

		fi, offset := a.fileAndOffset(makechan.Pos())
		fi.addLink(aLink{
			start:   offset - len("make"),
			end:     offset,
			title:   "show channel ops",
			onclick: fmt.Sprintf("onClickComm(%d)", fi.addData(v)),
		})
	}
}

// -- utilities --------------------------------------------------------

// chanOp abstracts an ssa.Send, ssa.Unop(ARROW), close(), or a SelectState.
// Derived from cmd/guru/peers.go.
type chanOp struct {
	ch   ssa.Value
	mode string // sent|received|closed
	pos  token.Pos
	len  int
	fn   *ssa.Function
}

// chanOps returns a slice of all the channel operations in the instruction.
// Derived from cmd/guru/peers.go.
func chanOps(instr ssa.Instruction) []chanOp {
	fn := instr.Parent()
	var ops []chanOp
	switch instr := instr.(type) {
	case *ssa.UnOp:
		if instr.Op == token.ARROW {
			// TODO(adonovan): don't assume <-ch; could be 'range ch'.
			ops = append(ops, chanOp{instr.X, "received", instr.Pos(), len("<-"), fn})
		}
	case *ssa.Send:
		ops = append(ops, chanOp{instr.Chan, "sent", instr.Pos(), len("<-"), fn})
	case *ssa.Select:
		for _, st := range instr.States {
			mode := "received"
			if st.Dir == types.SendOnly {
				mode = "sent"
			}
			ops = append(ops, chanOp{st.Chan, mode, st.Pos, len("<-"), fn})
		}
	case ssa.CallInstruction:
		call := instr.Common()
		if blt, ok := call.Value.(*ssa.Builtin); ok && blt.Name() == "close" {
			pos := instr.Common().Pos()
			ops = append(ops, chanOp{call.Args[0], "closed", pos - token.Pos(len("close")), len("close("), fn})
		}
	}
	return ops
}
