package main

import (
	"os"
	"sync"
)

const (
	DESCRIPTOR_STDIN  = 0
	DESCRIPTOR_STDOUT = 1
	DESCRIPTOR_STDERR = 2
)

type ExecutionState struct {
	Environment map[string]string // inherited by child processes
	Variables   map[string]string // not inherited by child processes
	Descriptors map[int]*os.File
}

func NewExecutionState() *ExecutionState {
	return &ExecutionState{
		Environment: make(map[string]string, 0),
		Variables:   make(map[string]string, 0),
		Descriptors: make(map[int]*os.File, 0),
	}
}

func (this *ExecutionState) Clone() ExecutionState {
	ret := *NewExecutionState()

	for k, v := range this.Environment {
		ret.Environment[k] = v
	}
	for k, v := range this.Variables {
		ret.Variables[k] = v
	}
	for k, v := range this.Descriptors {
		ret.Descriptors[k] = v
	}

	return ret
}

type ExitState struct {
	ExitCode int
}

type Statement interface {
	Execute(state *ExecutionState) ExitState
}

//

type IfElseStatement struct {
	Test, Positive, Negative Statement
}

func (this *IfElseStatement) Execute(state *ExecutionState) ExitState {
	ret := this.Test.Execute(state)
	if ret.ExitCode == 0 {
		return this.Positive.Execute(state)
	} else {
		return this.Negative.Execute(state)
	}
}

//

type IfStatement struct {
	Test, Positive Statement
}

func (this *IfStatement) Execute(state *ExecutionState) ExitState {
	ret := this.Test.Execute(state)
	if ret.ExitCode == 0 {
		return this.Positive.Execute(state)
	} else {
		return ret // What's the canonical return code for a failing if test?
	}
}

//

type SubshellStatement struct {
	Inner Statement
}

func (this *SubshellStatement) Execute(state *ExecutionState) ExitState {
	newState := *state
	return this.Inner.Execute(&newState)
}

//

type NonblockingStatement struct {
	Inner Statement
}

func (this *NonblockingStatement) Execute(state *ExecutionState) ExitState {
	newState := *state // always copy the environment
	go func() {
		this.Inner.Execute(&newState)
	}()

	// TODO add a 'wait' ptr into the parent environment, so builtin calls to 'wait' can work
	return ExitState{}
}

//

type ConditionalStatement struct {
	Precondition, Postaction Statement
}

func (this *ConditionalStatement) Execute(state *ExecutionState) ExitState {
	retn := this.Precondition.Execute(state)
	if retn.ExitCode == 0 {
		return this.Postaction.Execute(state)
	} else {
		return retn
	}
}

//

type ShortCircuitStatement struct {
	First, Second Statement
}

func (this *ShortCircuitStatement) Execute(state *ExecutionState) ExitState {
	retn := this.First.Execute(state)
	if retn.ExitCode == 0 {
		return retn
	} else {
		return this.Second.Execute(state)
	}
}

//

type PipeStatement struct {
	Source, Dest Statement
}

func (this *PipeStatement) Execute(state *ExecutionState) ExitState {

	r, w, err := os.Pipe()
	if err != nil {
		panic(err)
	}

	child1 := state.Clone()
	child2 := state.Clone()
	child1.Descriptors[DESCRIPTOR_STDOUT] = w
	child2.Descriptors[DESCRIPTOR_STDIN] = r

	// Wait for both processes to finish before continuing, return exit of the last command
	// TODO: add some 'set' flags to control the exact blocking behaviour here

	ret := ExitState{}

	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer wg.Done()
		this.Source.Execute(&child1)
		w.Close()
	}()
	go func() {
		defer wg.Done()
		ret = this.Dest.Execute(&child2)
		r.Close()
	}()
	wg.Wait()

	return ret // from the second command
}
