// Copyright 2021 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package topdown

import (
	"fmt"
	"io"
	"strings"

	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/topdown/builtins"
	"github.com/open-policy-agent/opa/v1/topdown/print"
)

func NewPrintHook(w io.Writer) print.Hook {
	return printHook{w: w}
}

type printHook struct {
	w io.Writer
}

func (h printHook) Print(_ print.Context, msg string) error {
	_, err := fmt.Fprintln(h.w, msg)
	return err
}

func builtinPrint(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
	if bctx.PrintHook == nil {
		return iter(nil)
	}

	arr, err := builtins.ArrayOperand(operands[0].Value, 1)
	if err != nil {
		return err
	}

	buf := make([]string, arr.Len())

	err = builtinPrintCrossProductOperands(bctx.Location, buf, arr, 0, func(buf []string) error {
		pctx := print.Context{
			Context:  bctx.Context,
			Location: bctx.Location,
		}
		return bctx.PrintHook.Print(pctx, strings.Join(buf, " "))
	})
	if err != nil {
		return err
	}

	return iter(nil)
}

func builtinPrintCrossProductOperands(loc *ast.Location, buf []string, operands *ast.Array, i int, f func([]string) error) error {
	if i >= operands.Len() {
		return f(buf)
	}

	operand := operands.Elem(i)

	// We allow primitives ...
	switch x := operand.Value.(type) {
	case ast.String:
		buf[i] = string(x)
		return builtinPrintCrossProductOperands(loc, buf, operands, i+1, f)
	case ast.Number, ast.Boolean, ast.Null:
		buf[i] = x.String()
		return builtinPrintCrossProductOperands(loc, buf, operands, i+1, f)
	}

	// ... but all other operand types must be sets.
	xs, ok := operand.Value.(ast.Set)
	if !ok {
		return Halt{Err: internalErr(loc, "illegal argument type: "+ast.ValueName(operand.Value))}
	}

	if xs.Len() == 0 {
		buf[i] = "<undefined>"
		return builtinPrintCrossProductOperands(loc, buf, operands, i+1, f)
	}

	return xs.Iter(func(x *ast.Term) error {
		switch v := x.Value.(type) {
		case ast.String:
			buf[i] = string(v)
		default:
			buf[i] = v.String()
		}
		return builtinPrintCrossProductOperands(loc, buf, operands, i+1, f)
	})
}

func init() {
	RegisterBuiltinFunc(ast.InternalPrint.Name, builtinPrint)
}
