// This is for Lvalues, i.e. things on the left-hand-side of an assignment
// statement.

package cst

import (
	"errors"
	"fmt"
	"os"

	"github.com/johnkerl/miller/v6/pkg/lib"
	"github.com/johnkerl/miller/v6/pkg/mlrval"
	"github.com/johnkerl/miller/v6/pkg/runtime"
	"github.com/johnkerl/pgpg/go/lib/pkg/asts"
)

func (root *RootNode) BuildAssignableNode(
	astNode *asts.ASTNode,
) (IAssignable, error) {

	switch astNode.Type {

	case asts.NodeType(NodeTypeDirectFieldValue), asts.NodeType(NodeTypeBracedFieldValue):
		return root.BuildDirectFieldValueLvalueNode(astNode)
	case asts.NodeType(NodeTypeIndirectFieldValue):
		return root.BuildIndirectFieldValueLvalueNode(astNode)

	case asts.NodeType(NodeTypeFullSrec):
		return root.BuildFullSrecLvalueNode(astNode)

	case asts.NodeType(NodeTypeDirectOosvarValue), asts.NodeType(NodeTypeBracedOosvarValue):
		return root.BuildDirectOosvarValueLvalueNode(astNode)
	case asts.NodeType(NodeTypeIndirectOosvarValue):
		return root.BuildIndirectOosvarValueLvalueNode(astNode)
	case asts.NodeType(NodeTypeFullOosvar):
		return root.BuildFullOosvarLvalueNode(astNode)
	case asts.NodeType(NodeTypeLocalVariable):
		return root.BuildLocalVariableLvalueNode(astNode)

	case asts.NodeType(NodeTypeArrayOrMapIndexAccess):
		return root.BuildIndexedLvalueNode(astNode)

	case asts.NodeType(NodeTypeDotOperator):
		return root.BuildIndexedLvalueNode(astNode)

	case asts.NodeType(NodeTypeEnvironmentVariable):
		return root.BuildEnvironmentVariableLvalueNode(astNode)

	case asts.NodeType("TypedeclLocalVariable"):
		return root.BuildTypedeclLocalVariableLvalueNode(astNode)
	}

	return nil, errors.New(
		"at CST BuildAssignableNode: unhandled AST node " + string(astNode.Type),
	)
}

type DirectFieldValueLvalueNode struct {
	lhsFieldName *mlrval.Mlrval
}

func (root *RootNode) BuildDirectFieldValueLvalueNode(
	astNode *asts.ASTNode,
) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeDirectFieldValue) &&
		astNode.Type != asts.NodeType(NodeTypeBracedFieldValue))

	var name string
	if astNode.Type == asts.NodeType(NodeTypeBracedFieldValue) {
		name = tokenLitStripBraced(astNode)
	} else {
		name = tokenLitStripDollarOrAt(astNode)
	}
	lhsFieldName := mlrval.FromString(name)
	return NewDirectFieldValueLvalueNode(lhsFieldName), nil
}

func NewDirectFieldValueLvalueNode(lhsFieldName *mlrval.Mlrval) *DirectFieldValueLvalueNode {
	return &DirectFieldValueLvalueNode{
		lhsFieldName: lhsFieldName,
	}
}

func (node *DirectFieldValueLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	return node.AssignIndexed(rvalue, nil, state)
}

func (node *DirectFieldValueLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {

	// For normal DSL use the CST validator will prohibit this from being
	// called in places the current record is undefined (begin and end blocks).
	// However in the REPL people can read past end of stream and still try to
	// print inrec attributes. Also, a UDF/UDS invoked from begin/end could try
	// to access the inrec, and that would get past the validator.
	if state.Inrec == nil {
		return fmt.Errorf("there is no current record to assign to")
	}

	// AssignmentNode checks for absent, so we just assign whatever we get
	lib.InternalCodingErrorIf(rvalue.IsAbsent())
	if indices == nil {
		err := state.Inrec.PutCopyWithMlrvalIndex(node.lhsFieldName, rvalue)
		if err != nil {
			return err
		}
		return nil
	}
	return state.Inrec.PutIndexed(
		append([]*mlrval.Mlrval{node.lhsFieldName}, indices...),
		rvalue,
	)
}

func (node *DirectFieldValueLvalueNode) Unassign(
	state *runtime.State,
) {
	node.UnassignIndexed(nil, state)
}

func (node *DirectFieldValueLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {

	// For normal DSL use the CST validator will prohibit this from being
	// called in places the current record is undefined (begin and end blocks).
	// However in the REPL people can read past end of stream and still try to
	// print inrec attributes. Also, a UDF/UDS invoked from begin/end could try
	// to access the inrec, and that would get past the validator.
	if state.Inrec == nil {
		return
	}

	if indices == nil {
		lib.InternalCodingErrorIf(!node.lhsFieldName.IsString())
		name := node.lhsFieldName.String()
		state.Inrec.Remove(name)
	} else {
		state.Inrec.RemoveIndexed(
			append([]*mlrval.Mlrval{node.lhsFieldName}, indices...),
		)
	}
}

type IndirectFieldValueLvalueNode struct {
	lhsFieldNameExpression IEvaluable
}

func (root *RootNode) BuildIndirectFieldValueLvalueNode(
	astNode *asts.ASTNode,
) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeIndirectFieldValue))
	lib.InternalCodingErrorIf(astNode == nil)
	lib.InternalCodingErrorIf(len(astNode.Children) != 1)

	child := astNode.Children[0]
	if child.Type == asts.NodeType(NodeTypeArrayLiteral) && len(child.Children) == 1 {
		inner := child.Children[0]
		if inner.Type == asts.NodeType(NodeTypeArrayLiteral) && len(inner.Children) == 1 {
			// $[[[n]]] → positional field value lvalue
			indexASTNode := inner.Children[0]
			syntheticAST := asts.NewASTNode(nil, asts.NodeType(NodeTypePositionalFieldValue), []*asts.ASTNode{indexASTNode})
			return root.BuildPositionalFieldValueLvalueNode(syntheticAST)
		}
		// $[[n]] → positional field name lvalue
		indexASTNode := inner
		syntheticAST := asts.NewASTNode(nil, asts.NodeType(NodeTypePositionalFieldName), []*asts.ASTNode{indexASTNode})
		return root.BuildPositionalFieldNameLvalueNode(syntheticAST)
	}

	lhsFieldNameExpression, err := root.BuildEvaluableNode(child)
	if err != nil {
		return nil, err
	}
	return NewIndirectFieldValueLvalueNode(lhsFieldNameExpression), nil
}

func NewIndirectFieldValueLvalueNode(
	lhsFieldNameExpression IEvaluable,
) *IndirectFieldValueLvalueNode {
	return &IndirectFieldValueLvalueNode{
		lhsFieldNameExpression: lhsFieldNameExpression,
	}
}

func (node *IndirectFieldValueLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	return node.AssignIndexed(rvalue, nil, state)
}

func (node *IndirectFieldValueLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {
	// AssignmentNode checks for absentness of the rvalue, so we just assign
	// whatever we get
	lib.InternalCodingErrorIf(rvalue.IsAbsent())

	// For normal DSL use the CST validator will prohibit this from being
	// called in places the current record is undefined (begin and end blocks).
	// However in the REPL people can read past end of stream and still try to
	// print inrec attributes. Also, a UDF/UDS invoked from begin/end could try
	// to access the inrec, and that would get past the validator.
	if state.Inrec == nil {
		return fmt.Errorf("there is no current record to assign to")
	}

	lhsFieldName := node.lhsFieldNameExpression.Evaluate(state)

	if indices == nil {
		err := state.Inrec.PutCopyWithMlrvalIndex(lhsFieldName, rvalue)
		if err != nil {
			return err
		}
		return nil
	}
	return state.Inrec.PutIndexed(
		append([]*mlrval.Mlrval{lhsFieldName.Copy()}, indices...),
		rvalue,
	)
}

func (node *IndirectFieldValueLvalueNode) Unassign(
	state *runtime.State,
) {
	node.UnassignIndexed(nil, state)
}

func (node *IndirectFieldValueLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {
	// For normal DSL use the CST validator will prohibit this from being
	// called in places the current record is undefined (begin and end blocks).
	// However in the REPL people can read past end of stream and still try to
	// print inrec attributes. Also, a UDF/UDS invoked from begin/end could try
	// to access the inrec, and that would get past the validator.
	if state.Inrec == nil {
		return
	}

	lhsFieldName := node.lhsFieldNameExpression.Evaluate(state)
	if indices == nil {
		name := lhsFieldName.String()
		state.Inrec.Remove(name)
	} else {
		state.Inrec.RemoveIndexed(
			append([]*mlrval.Mlrval{lhsFieldName.Copy()}, indices...),
		)
	}
}

// Set the name at 2nd positional index in the current stream record: e.g.
// '$[[2]] = "abc"

type PositionalFieldNameLvalueNode struct {
	lhsFieldIndexExpression IEvaluable
}

func (root *RootNode) BuildPositionalFieldNameLvalueNode(
	astNode *asts.ASTNode,
) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypePositionalFieldName))
	lib.InternalCodingErrorIf(astNode == nil)
	lib.InternalCodingErrorIf(len(astNode.Children) != 1)
	lhsFieldIndexExpression, err := root.BuildEvaluableNode(astNode.Children[0])
	if err != nil {
		return nil, err
	}

	return NewPositionalFieldNameLvalueNode(lhsFieldIndexExpression), nil
}

func NewPositionalFieldNameLvalueNode(
	lhsFieldIndexExpression IEvaluable,
) *PositionalFieldNameLvalueNode {
	return &PositionalFieldNameLvalueNode{
		lhsFieldIndexExpression: lhsFieldIndexExpression,
	}
}

func (node *PositionalFieldNameLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	// AssignmentNode checks for absentness of the rvalue, so we just assign
	// whatever we get
	lib.InternalCodingErrorIf(rvalue.IsAbsent())

	// For normal DSL use the CST validator will prohibit this from being
	// called in places the current record is undefined (begin and end blocks).
	// However in the REPL people can read past end of stream and still try to
	// print inrec attributes. Also, a UDF/UDS invoked from begin/end could try
	// to access the inrec, and that would get past the validator.
	if state.Inrec == nil {
		return fmt.Errorf("there is no current record to assign to")
	}

	lhsFieldIndex := node.lhsFieldIndexExpression.Evaluate(state)

	index, ok := lhsFieldIndex.GetIntValue()
	if ok {
		// TODO: incorporate error-return into this API
		state.Inrec.PutNameWithPositionalIndex(index, rvalue)
		return nil
	}
	return fmt.Errorf(
		"positional index for $[[...]] assignment must be integer; got %s",
		lhsFieldIndex.GetTypeName(),
	)
}

func (node *PositionalFieldNameLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {
	// TODO: reconsider this if /when we decide to allow string-slice
	// assignments.
	return fmt.Errorf(
		"$[[...]] = ... expressions are not indexable",
	)
}

func (node *PositionalFieldNameLvalueNode) Unassign(
	state *runtime.State,
) {
	node.UnassignIndexed(nil, state)
}

func (node *PositionalFieldNameLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {
	lhsFieldIndex := node.lhsFieldIndexExpression.Evaluate(state)

	// For normal DSL use the CST validator will prohibit this from being
	// called in places the current record is undefined (begin and end blocks).
	// However in the REPL people can read past end of stream and still try to
	// print inrec attributes. Also, a UDF/UDS invoked from begin/end could try
	// to access the inrec, and that would get past the validator.
	if state.Inrec == nil {
		return
	}

	if indices == nil {
		index, ok := lhsFieldIndex.GetIntValue()
		if ok {
			state.Inrec.RemoveWithPositionalIndex(index)
		} else {
			// TODO: incorporate error-return into this API
		}
	} else {
		// xxx positional
		state.Inrec.RemoveIndexed(
			append([]*mlrval.Mlrval{lhsFieldIndex}, indices...),
		)
	}
}

// Set the value at 2nd positional index in the current stream record: e.g.
// '$[[[2]]] = "abc"

type PositionalFieldValueLvalueNode struct {
	lhsFieldIndexExpression IEvaluable
}

func (root *RootNode) BuildPositionalFieldValueLvalueNode(
	astNode *asts.ASTNode,
) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypePositionalFieldValue))
	lib.InternalCodingErrorIf(astNode == nil)
	lib.InternalCodingErrorIf(len(astNode.Children) != 1)
	lhsFieldIndexExpression, err := root.BuildEvaluableNode(astNode.Children[0])
	if err != nil {
		return nil, err
	}

	return NewPositionalFieldValueLvalueNode(lhsFieldIndexExpression), nil
}

func NewPositionalFieldValueLvalueNode(
	lhsFieldIndexExpression IEvaluable,
) *PositionalFieldValueLvalueNode {
	return &PositionalFieldValueLvalueNode{
		lhsFieldIndexExpression: lhsFieldIndexExpression,
	}
}

func (node *PositionalFieldValueLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	return node.AssignIndexed(rvalue, nil, state)
}

func (node *PositionalFieldValueLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {
	// AssignmentNode checks for absentness of the rvalue, so we just assign
	// whatever we get
	lib.InternalCodingErrorIf(rvalue.IsAbsent())

	// For normal DSL use the CST validator will prohibit this from being
	// called in places the current record is undefined (begin and end blocks).
	// However in the REPL people can read past end of stream and still try to
	// print inrec attributes. Also, a UDF/UDS invoked from begin/end could try
	// to access the inrec, and that would get past the validator.
	if state.Inrec == nil {
		return fmt.Errorf("there is no current record to assign to")
	}

	lhsFieldIndex := node.lhsFieldIndexExpression.Evaluate(state)

	if indices == nil {
		index, ok := lhsFieldIndex.GetIntValue()
		if ok {
			// TODO: incorporate error-return into this API
			//err := state.Inrec.PutCopyWithPositionalIndex(&lhsFieldIndex, rvalue)
			//if err != nil {
			//return err
			//}
			//return nil
			state.Inrec.PutCopyWithPositionalIndex(index, rvalue)
			return nil
		}
		return fmt.Errorf(
			"positional index for $[[[...]]] assignment must be integer; got %s",
			lhsFieldIndex.GetTypeName(),
		)
	} else {
		// xxx positional
		return state.Inrec.PutIndexed(
			append([]*mlrval.Mlrval{lhsFieldIndex}, indices...),
			rvalue,
		)
	}
}

// Same code as PositionalFieldNameLvalueNode.
// May as well let them do 'unset $[[[7]]]' as well as $[[7]]'.
func (node *PositionalFieldValueLvalueNode) Unassign(
	state *runtime.State,
) {
	node.UnassignIndexed(nil, state)
}

func (node *PositionalFieldValueLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {
	// For normal DSL use the CST validator will prohibit this from being
	// called in places the current record is undefined (begin and end blocks).
	// However in the REPL people can read past end of stream and still try to
	// print inrec attributes. Also, a UDF/UDS invoked from begin/end could try
	// to access the inrec, and that would get past the validator.
	if state.Inrec == nil {
		return
	}

	lhsFieldIndex := node.lhsFieldIndexExpression.Evaluate(state)
	if indices == nil {
		index, ok := lhsFieldIndex.GetIntValue()
		if ok {
			state.Inrec.RemoveWithPositionalIndex(index)
		} else {
			// TODO: incorporate error-return into this API
		}
	} else {
		// xxx positional
		state.Inrec.RemoveIndexed(
			append([]*mlrval.Mlrval{lhsFieldIndex}, indices...),
		)
	}
}

type FullSrecLvalueNode struct {
}

func (root *RootNode) BuildFullSrecLvalueNode(astNode *asts.ASTNode) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeFullSrec))
	lib.InternalCodingErrorIf(astNode == nil)
	// PGPG FullSrec has empty list as children
	lib.InternalCodingErrorIf(astNode.Children != nil && len(astNode.Children) > 0)
	return NewFullSrecLvalueNode(), nil
}

func NewFullSrecLvalueNode() *FullSrecLvalueNode {
	return &FullSrecLvalueNode{}
}

func (node *FullSrecLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	return node.AssignIndexed(rvalue, nil, state)
}

func (node *FullSrecLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {
	// For normal DSL use the CST validator will prohibit this from being
	// called in places the current record is undefined (begin and end blocks).
	// However in the REPL people can read past end of stream and still try to
	// print inrec attributes. Also, a UDF/UDS invoked from begin/end could try
	// to access the inrec, and that would get past the validator.
	if state.Inrec == nil {
		return fmt.Errorf("there is no current record to assign to")
	}

	// AssignmentNode checks for absentness of the rvalue, so we just assign
	// whatever we get
	lib.InternalCodingErrorIf(rvalue.IsAbsent())

	// The input record is a *Mlrmap so just invoke its PutIndexed.
	err := state.Inrec.PutIndexed(indices, rvalue)
	if err != nil {
		return err
	}
	return nil
}

func (node *FullSrecLvalueNode) Unassign(
	state *runtime.State,
) {
	node.UnassignIndexed(nil, state)
}

func (node *FullSrecLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {
	// For normal DSL use the CST validator will prohibit this from being
	// called in places the current record is undefined (begin and end blocks).
	// However in the REPL people can read past end of stream and still try to
	// print inrec attributes. Also, a UDF/UDS invoked from begin/end could try
	// to access the inrec, and that would get past the validator.
	if state.Inrec == nil {
		return
	}

	if indices == nil {
		state.Inrec.Clear()
	} else {
		state.Inrec.RemoveIndexed(indices)
	}
}

type DirectOosvarValueLvalueNode struct {
	lhsOosvarName *mlrval.Mlrval
}

func (root *RootNode) BuildDirectOosvarValueLvalueNode(astNode *asts.ASTNode) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeDirectOosvarValue) &&
		astNode.Type != asts.NodeType(NodeTypeBracedOosvarValue))

	var name string
	if astNode.Type == asts.NodeType(NodeTypeBracedOosvarValue) {
		name = tokenLitStripBraced(astNode)
	} else {
		name = tokenLitStripDollarOrAt(astNode)
	}
	lhsOosvarName := mlrval.FromString(name)
	return NewDirectOosvarValueLvalueNode(lhsOosvarName), nil
}

func NewDirectOosvarValueLvalueNode(lhsOosvarName *mlrval.Mlrval) *DirectOosvarValueLvalueNode {
	return &DirectOosvarValueLvalueNode{
		lhsOosvarName: lhsOosvarName,
	}
}

func (node *DirectOosvarValueLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	return node.AssignIndexed(rvalue, nil, state)
}

func (node *DirectOosvarValueLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {
	// AssignmentNode checks for absent, so we just assign whatever we get
	lib.InternalCodingErrorIf(rvalue.IsAbsent())
	if indices == nil {
		err := state.Oosvars.PutCopyWithMlrvalIndex(node.lhsOosvarName, rvalue)
		if err != nil {
			return err
		}
		return nil
	}
	return state.Oosvars.PutIndexed(
		append([]*mlrval.Mlrval{node.lhsOosvarName}, indices...),
		rvalue,
	)
}

func (node *DirectOosvarValueLvalueNode) Unassign(
	state *runtime.State,
) {
	node.UnassignIndexed(nil, state)
}

func (node *DirectOosvarValueLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {
	if indices == nil {
		name := node.lhsOosvarName.String()
		state.Oosvars.Remove(name)
	} else {
		state.Oosvars.RemoveIndexed(
			append([]*mlrval.Mlrval{node.lhsOosvarName}, indices...),
		)
	}
}

type IndirectOosvarValueLvalueNode struct {
	lhsOosvarNameExpression IEvaluable
}

func (root *RootNode) BuildIndirectOosvarValueLvalueNode(
	astNode *asts.ASTNode,
) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeIndirectOosvarValue))
	lib.InternalCodingErrorIf(astNode == nil)
	lib.InternalCodingErrorIf(len(astNode.Children) != 1)

	lhsOosvarNameExpression, err := root.BuildEvaluableNode(astNode.Children[0])
	if err != nil {
		return nil, err
	}

	return NewIndirectOosvarValueLvalueNode(lhsOosvarNameExpression), nil
}

func NewIndirectOosvarValueLvalueNode(
	lhsOosvarNameExpression IEvaluable,
) *IndirectOosvarValueLvalueNode {
	return &IndirectOosvarValueLvalueNode{
		lhsOosvarNameExpression: lhsOosvarNameExpression,
	}
}

func (node *IndirectOosvarValueLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	return node.AssignIndexed(rvalue, nil, state)
}

func (node *IndirectOosvarValueLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {
	// AssignmentNode checks for absentness of the rvalue, so we just assign
	// whatever we get
	lib.InternalCodingErrorIf(rvalue.IsAbsent())

	lhsOosvarName := node.lhsOosvarNameExpression.Evaluate(state)

	if indices == nil {
		err := state.Oosvars.PutCopyWithMlrvalIndex(lhsOosvarName, rvalue)
		if err != nil {
			return err
		}
		return nil
	}
	return state.Oosvars.PutIndexed(
		append([]*mlrval.Mlrval{lhsOosvarName.Copy()}, indices...),
		rvalue,
	)
}

func (node *IndirectOosvarValueLvalueNode) Unassign(
	state *runtime.State,
) {
	node.UnassignIndexed(nil, state)
}

func (node *IndirectOosvarValueLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {
	lhsOosvarName := node.lhsOosvarNameExpression.Evaluate(state)

	if indices == nil {
		sname := lhsOosvarName.String()
		state.Oosvars.Remove(sname)
	} else {
		state.Oosvars.RemoveIndexed(
			append([]*mlrval.Mlrval{lhsOosvarName}, indices...),
		)
	}
}

type FullOosvarLvalueNode struct {
}

func (root *RootNode) BuildFullOosvarLvalueNode(astNode *asts.ASTNode) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeFullOosvar))
	lib.InternalCodingErrorIf(astNode == nil)
	lib.InternalCodingErrorIf(astNode.Children != nil && len(astNode.Children) > 0)
	return NewFullOosvarLvalueNode(), nil
}

func NewFullOosvarLvalueNode() *FullOosvarLvalueNode {
	return &FullOosvarLvalueNode{}
}

func (node *FullOosvarLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	return node.AssignIndexed(rvalue, nil, state)
}

func (node *FullOosvarLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {
	// AssignmentNode checks for absentness of the rvalue, so we just assign
	// whatever we get
	lib.InternalCodingErrorIf(rvalue.IsAbsent())

	// The input record is a *Mlrmap so just invoke its PutIndexed.
	err := state.Oosvars.PutIndexed(indices, rvalue)
	if err != nil {
		return err
	}
	return nil
}

func (node *FullOosvarLvalueNode) Unassign(
	state *runtime.State,
) {
	node.UnassignIndexed(nil, state)
}

func (node *FullOosvarLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {
	if indices == nil {
		state.Oosvars.Clear()
	} else {
		state.Oosvars.RemoveIndexed(indices)
	}
}

type LocalVariableLvalueNode struct {
	stackVariable *runtime.StackVariable
	typeName      string

	// a = 1;
	// b = 1;
	// if (true) {
	//   a = 3;     <-- defineTypedAtScope is false; updates outer a
	//   var b = 4; <-- defineTypedAtScope is true;  creates new inner b
	// }
	defineTypedAtScope bool
}

func (root *RootNode) BuildTypedeclLocalVariableLvalueNode(astNode *asts.ASTNode) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Children == nil || len(astNode.Children) < 2)
	typeNode := astNode.Children[0]
	varNode := astNode.Children[1]
	// PGPG Typedecl produces kw_int, kw_bool, etc. (no Typedecl wrapper node)
	lib.InternalCodingErrorIf(varNode.Type != asts.NodeType(NodeTypeLocalVariable))
	typeName := tokenLit(typeNode)
	variableName := tokenLit(varNode)
	return NewLocalVariableLvalueNode(
		runtime.NewStackVariable(variableName),
		typeName,
		true, // defineTypedAtScope
	), nil
}

func (root *RootNode) BuildLocalVariableLvalueNode(astNode *asts.ASTNode) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeLocalVariable))

	// TODO require type mask in strict mode

	variableName := tokenLit(astNode)
	typeName := "any"
	defineTypedAtScope := false
	// PGPG: LocalVariable is terminal (children nil or empty). Miller had typed params with Children[0]=Typedecl.
	if astNode.Children != nil && len(astNode.Children) > 0 { // typed, like 'num x = 3'
		typeNode := astNode.Children[0]
		lib.InternalCodingErrorIf(typeNode.Type != asts.NodeType(NodeTypeTypedecl))
		typeName = tokenLit(typeNode)
		defineTypedAtScope = true
	}
	return NewLocalVariableLvalueNode(
		runtime.NewStackVariable(variableName),
		typeName,
		defineTypedAtScope,
	), nil
}

func NewLocalVariableLvalueNode(
	stackVariable *runtime.StackVariable,
	typeName string,
	defineTypedAtScope bool,
) *LocalVariableLvalueNode {
	return &LocalVariableLvalueNode{
		stackVariable:      stackVariable,
		typeName:           typeName,
		defineTypedAtScope: defineTypedAtScope,
	}
}

func (node *LocalVariableLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	return node.AssignIndexed(rvalue, nil, state)
}

func (node *LocalVariableLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {
	// AssignmentNode checks for absent, so we just assign whatever we get
	lib.InternalCodingErrorIf(rvalue.IsAbsent())
	var err error
	if indices == nil {
		if node.defineTypedAtScope {
			err = state.Stack.DefineTypedAtScope(node.stackVariable, node.typeName, rvalue)
		} else {
			err = state.Stack.Set(node.stackVariable, rvalue)
		}
	} else {
		// There is no 'map x[1] = {}' in the DSL grammar.
		lib.InternalCodingErrorIf(node.defineTypedAtScope)

		err = state.Stack.SetIndexed(node.stackVariable, indices, rvalue)
	}
	return err
}

func (node *LocalVariableLvalueNode) Unassign(
	state *runtime.State,
) {
	node.UnassignIndexed(nil, state)
}

func (node *LocalVariableLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {
	if indices == nil {
		state.Stack.Unset(node.stackVariable)
	} else {
		state.Stack.UnsetIndexed(node.stackVariable, indices)
	}
}

// IndexedValueNode is a delegator to base-lvalue types.
// * The baseLvalue is some IAssignable
// * The indexEvaluables are an array of IEvaluables
// * Each needs to evaluate to int or string
// * Assignment needs to walk each level:
//   o error if ith mlrval is int and that level isn't an array
//   o error if ith mlrval is string and that level isn't a map
//   o error for any other types -- maybe absent-handling for heterogeneity ...

type IndexedLvalueNode struct {
	baseLvalue      IAssignable
	indexEvaluables []IEvaluable
}

// Either 'mymap["attr"]' or 'mymap.attr'. Furthermore they can be mixed as in
// 'mymap["foo"].bar' or 'mymap.foo["bar"]'.
func (root *RootNode) BuildIndexedLvalueNode(astNode *asts.ASTNode) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeArrayOrMapIndexAccess) && astNode.Type != asts.NodeType(NodeTypeDotOperator))
	lib.InternalCodingErrorIf(astNode == nil)

	var baseLvalue IAssignable = nil
	indexEvaluables := []IEvaluable{}
	var err error

	// $ mlr -n put -v '$x[1][2]=3'
	// DSL EXPRESSION:
	// $x[1][2]=3
	// AST:
	// * StatementBlock
	//     * Assignment "="
	//         * ArrayOrMapIndexAccess "[]"
	//             * ArrayOrMapIndexAccess "[]"
	//                 * DirectFieldValue "x"
	//                 * IntLiteral "1"
	//             * IntLiteral "2"
	//         * IntLiteral "3"

	// In the AST, the indices come in last-shallowest, down to first-deepest,
	// then the base Lvalue.
	walkerNode := astNode
	for {
		if walkerNode.Type == asts.NodeType(NodeTypeArrayOrMapIndexAccess) {
			lib.InternalCodingErrorIf(walkerNode == nil)
			lib.InternalCodingErrorIf(len(walkerNode.Children) != 2)
			indexEvaluable, err := root.BuildEvaluableNode(walkerNode.Children[1])
			if err != nil {
				return nil, err
			}
			indexEvaluables = append([]IEvaluable{indexEvaluable}, indexEvaluables...)
			walkerNode = walkerNode.Children[0]
		} else if walkerNode.Type == asts.NodeType(NodeTypeDotOperator) {
			lib.InternalCodingErrorIf(walkerNode == nil)
			lib.InternalCodingErrorIf(len(walkerNode.Children) != 2)
			indexEvaluable := root.BuildStringLiteralNode(tokenLit(walkerNode.Children[1]))
			indexEvaluables = append([]IEvaluable{indexEvaluable}, indexEvaluables...)

			walkerNode = walkerNode.Children[0]
		} else {
			baseLvalue, err = root.BuildAssignableNode(walkerNode)
			if err != nil {
				return nil, err
			}
			break
		}
	}
	return NewIndexedLvalueNode(baseLvalue, indexEvaluables), nil
}

func NewIndexedLvalueNode(
	baseLvalue IAssignable,
	indexEvaluables []IEvaluable,
) *IndexedLvalueNode {
	return &IndexedLvalueNode{
		baseLvalue:      baseLvalue,
		indexEvaluables: indexEvaluables,
	}
}

// Computes Lvalue indices and then delegates to the baseLvalue.  E.g. for
// '$x[1][2] = 3' or '@x[1][2] = 3', the indices are [1,2], and the baseLvalue
// is '$x' or '@x' respectively.
func (node *IndexedLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	indices := make([]*mlrval.Mlrval, len(node.indexEvaluables))

	for i := range node.indexEvaluables {
		indices[i] = node.indexEvaluables[i].Evaluate(state)
		if indices[i].IsAbsent() {
			return nil
		}
	}

	// This lets the user do '$y[ ["a", "b", "c"] ] = $x' in lieu of
	// '$y["a"]["b"]["c"] = $x'.
	if len(indices) == 1 && indices[0].IsArray() {
		indices = mlrval.CopyMlrvalArray(indices[0].GetArray())
	}

	return node.baseLvalue.AssignIndexed(rvalue, indices, state)
}

func (node *IndexedLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {
	// We are the delegator, not the delegatee
	lib.InternalCodingErrorIf(true)
	return nil // not reached
}

func (node *IndexedLvalueNode) Unassign(
	state *runtime.State,
) {
	indices := make([]*mlrval.Mlrval, len(node.indexEvaluables))
	for i := range node.indexEvaluables {
		indices[i] = node.indexEvaluables[i].Evaluate(state)
	}

	node.baseLvalue.UnassignIndexed(indices, state)
}

func (node *IndexedLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {
	// We are the delegator, not the delegatee
	lib.InternalCodingErrorIf(true)
}

type EnvironmentVariableLvalueNode struct {
	nameExpression IEvaluable
}

func (root *RootNode) BuildEnvironmentVariableLvalueNode(astNode *asts.ASTNode) (IAssignable, error) {
	lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeEnvironmentVariable))
	lib.InternalCodingErrorIf(astNode == nil)
	lib.InternalCodingErrorIf(len(astNode.Children) != 1)
	child := astNode.Children[0]
	var nameExpression IEvaluable
	// ENV.FOO: child is non_sigil_name; BuildEvaluableNode doesn't handle bare identifiers
	if string(child.Type) == "non_sigil_name" {
		sval := tokenLit(child)
		nameExpression = root.BuildStringLiteralNode(sval)
	} else {
		var err error
		nameExpression, err = root.BuildEvaluableNode(child)
		if err != nil {
			return nil, err
		}
	}
	return NewEnvironmentVariableLvalueNode(nameExpression), nil
}

func NewEnvironmentVariableLvalueNode(
	nameExpression IEvaluable,
) *EnvironmentVariableLvalueNode {
	return &EnvironmentVariableLvalueNode{
		nameExpression: nameExpression,
	}
}

func (node *EnvironmentVariableLvalueNode) Assign(
	rvalue *mlrval.Mlrval,
	state *runtime.State,
) error {
	// AssignmentNode checks for absentness of the rvalue, so we just assign
	// whatever we get
	lib.InternalCodingErrorIf(rvalue.IsAbsent())

	name := node.nameExpression.Evaluate(state)
	if name.IsAbsent() {
		return nil
	}

	if !name.IsString() {
		return fmt.Errorf(
			`assignments to ENV[...] must have string names; got %s "%s"`,
			name.GetTypeName(),
			name.String(),
		)
	}

	sname := name.String()
	svalue := rvalue.String()
	os.Setenv(sname, svalue)
	if sname == "TZ" {
		err := lib.SetTZFromEnv() // affects the time library; notify it
		if err != nil {
			return err
		}
	}
	return nil
}

func (node *EnvironmentVariableLvalueNode) AssignIndexed(
	rvalue *mlrval.Mlrval,
	indices []*mlrval.Mlrval,
	state *runtime.State,
) error {
	return fmt.Errorf("ENV[...] cannot be indexed")
}

func (node *EnvironmentVariableLvalueNode) Unassign(
	state *runtime.State,
) {
	name := node.nameExpression.Evaluate(state)
	if name.IsAbsent() {
		return
	}

	if !name.IsString() {
		// TODO: needs error-return
		return
	}

	os.Unsetenv(name.String())
}

func (node *EnvironmentVariableLvalueNode) UnassignIndexed(
	indices []*mlrval.Mlrval,
	state *runtime.State,
) {
	// TODO: needs error return
	//return errors.New("mlr: ENV[...] cannot be indexed")
}
