package transformers

import (
	"fmt"
	"os"

	"github.com/johnkerl/miller/v6/pkg/cli"
	"github.com/johnkerl/miller/v6/pkg/types"
)

// ChainTransformer is a refinement of Miller's high-level sketch in stream.go.
// As far as stream.go is concerned, the transformer-chain is a box which reads
// from an input-record channel (from the record-reader object) and writes to
// an output-record channel (to the record-writer object). Inside that box is a
// bit more complexity, including channels between transformers in the chain.
//
// Channel structure from outside the box:
// * readerInputRecordChannel "ichan" passes records from the record-reader
//   to the transformer chain.
// * writerOutputRecordChannel "ochan" passes records from the transformer chain
//   to the record-writer.
// * readerDownstreamDoneChannel "dchan" signifies back to the record-reader
//   that it can stop reading files, since all downstream consumers aren't
//   interested in any more data.
//
//   Record-reader
//     | ichan  ^ dchan
//     v        |
// +---------------+
// | Transformer 0 |
// | Transformer 1 |
// | Transformer 2 |
// | Transformer 3 |
// +---------------+
//     | ochan
//     v
//   Record-writer
//
// Channel structure from inside the box:
//
//   Record-reader
//     | ichan ^ dchan
//     v       |
// +---------------+
// | Transformer 0 |
// |   |       ^   |
// |   v       |   |
// | Transformer 1 |
// |   |       ^   |
// |   v       |   |
// | Transformer 2 |
// |   |       ^   |
// |   v       |   |
// | Transformer 3 |
// +---------------+
//     | ochan
//     v
//   Record-writer
//
// Each transformer has four channels from its point of view:
//
// | irchan  odchan |
// |   |       ^    |
// |   v       |    |
// | Transformer i  |
// |   |       ^    |
// |   v       |    |
// | orchan  idchan |
//
// * inputRecordChannel "irchan" is where it reads records from.
//   o If the transformer is the first in the chain, this is the
//     readerInputRecordChannel passed into ChainTransformer.
//   o Otherwise it's an intermediary channel from transformer i-1's output,
//     created and connected by ChainTransformer.
//
// * outputRecordChannel "orchan" is where it writes records to.
//   o If the transformer is the last in the chain, this is the
//     writerOutputRecordChannel passed into ChainTransformer.
//   o Otherwise it's an intermediary channel to transformer i+1's input,
//     created and connected by ChainTransformer.
//
// * inputDownstreamDoneChannel "idchan" is where it reads a downstream-done flag from.
//   o These are all created internally by ChainTransformer.
//   o If the transform is the last in the chain, nothing writes to its idchan.
//   o Otherwise transformer i's idchan is connected to transformer i+1's odchan,
//     so transformer i can accept a downstream-done flag.
//
// * outputDownstreamDoneChannel "odchan" is where it writes a downstream-done flag to.
//   o If the transformer is the first in the chain, this is the
//     readerDownstreamDoneChannel passed into ChainTransformer.
//   o Otherwise it's an intermediary channel connected to transformer i-1's idchan,
//     so transformer i can produce a downstream-done flag.
//
// Handling in practice:
//
// * Most verbs pass a downstrem-done flag on their idchan upstream to their odchan.
//   The HandleDefaultDownstreamDone function is for this purpose: most verbs use it.
//
// * The exceptional verbs are HEAD, TEE, and SEQGEN.
//
// * mlr head is the reason this exists. The problem to solve is that people
//   want to do 'mlr head -n 10 myhugefile.dat' and have mlr exit as soon as those
//   10 records have been read.
//
// * However, if someone does 'mlr cut -f foo then tee bar.dat then head -n 10',
//   they want bar.dat to have all the records seen by tee; head -n 10 should produce
//   only the first 10 but bar.dat should have them all.
//
// * Likewise, 'mlr seqgen --stop 1000000000 then head -n 10' should result
//   in seqgen breaking out of its data-production loop.
//
// * In head.go, tee.go, and seqgen.go you will see specific handling of
//   reading idchan and writing odchan.
//
// TESTING
//
// * This is a bit awkward with regard to regression-test -- we don't want a
//   multi-GB data file in our repo for the continuous integration job to check
//   that the processing finishes quickly.
//
// * Nonetheless: all these should finish quickly/
//
//   mlr head -n 10 ~/tmp/huge
//   mlr cat then head -n 10 ~/tmp/huge
//   mlr head -n 100 then tee foo.txt then head -n 10 ~/tmp/huge
//     check `wc -l foo.txt` is 100
//   mlr head -n 100 then tee foo.txt then head -n 10 then tee bar.txt ~/tmp/huge
//     check `wc -l foo.txt` is 100 and `wc -l bar.txt` is 10
//   mlr seqgen --stop 100000000 then head -n 10
//

// ChainTransformer is a refinement of Miller's high-level sketch in stream.go.
// While stream.go sees goroutines for record reader, transformer chain, and
// record writer, with input channel from record-reader to transformer chain
// and output channel from transformer chain to record-writer, ChainTransformer
// subdivides goroutines for each transformer in the chain, with intermediary
// channels between them.
func ChainTransformer(
	readerRecordChannel <-chan []*types.RecordAndContext, // list of *types.RecordAndContext
	readerDownstreamDoneChannel chan<- bool, // for mlr head -- see also stream.go
	recordTransformers []RecordTransformer, // not *recordTransformer since this is an interface
	writerRecordChannel chan<- []*types.RecordAndContext, // list of *types.RecordAndContext
	dataProcessingErrorChannel chan<- error, // for mid-stream transformer errors -- see stream.go
	options *cli.TOptions,
) {
	i := 0
	n := len(recordTransformers)

	intermediateRecordChannels := make([]chan []*types.RecordAndContext, n-1) // list of *types.RecordAndContext
	for i = 0; i < n-1; i++ {
		intermediateRecordChannels[i] = make(chan []*types.RecordAndContext, 1) // list of *types.RecordAndContext
	}

	intermediateDownstreamDoneChannels := make([]chan bool, n)
	for i = range n {
		intermediateDownstreamDoneChannels[i] = make(chan bool, 1)
	}

	for i, recordTransformer := range recordTransformers {
		// Downstream flow: channel a given transformer reads records from
		irchan := readerRecordChannel
		// Downstream flow: channel a given transformer writes transformed
		// records to
		orchan := writerRecordChannel
		// Upstream signaling: channel a given transformer reads to see if
		// downstream transformers are done (e.g. mlr head)
		idchan := intermediateDownstreamDoneChannels[i]
		// Upstream signaling: channel a given transformer (e.g. mlr head)
		// writes to signal to upstream transformers that it will ignore
		// further input.
		odchan := readerDownstreamDoneChannel

		if i > 0 {
			irchan = intermediateRecordChannels[i-1]
			odchan = intermediateDownstreamDoneChannels[i-1]
		}
		if i < n-1 {
			orchan = intermediateRecordChannels[i]
		}

		go runSingleTransformer(
			recordTransformer,
			i == 0,
			irchan,
			orchan,
			idchan,
			odchan,
			dataProcessingErrorChannel,
			options,
		)
	}
}

func runSingleTransformer(
	recordTransformer RecordTransformer,
	isFirstInChain bool,
	inputRecordChannel <-chan []*types.RecordAndContext, // list of *types.RecordAndContext
	outputRecordChannel chan<- []*types.RecordAndContext, // list of *types.RecordAndContext
	inputDownstreamDoneChannel <-chan bool,
	outputDownstreamDoneChannel chan<- bool,
	dataProcessingErrorChannel chan<- error,
	options *cli.TOptions,
) {

	if streamingProducer, ok := recordTransformer.(StreamingProducer); ok {
		// e.g. seqgen: drain the single upstream batch (the lone
		// end-of-stream marker sent by the NoInput reader) so the reader
		// goroutine isn't blocked, then let the producer generate and flush
		// its own output directly. See StreamingProducer.
		<-inputRecordChannel
		streamingProducer.ProduceStream(outputRecordChannel, inputDownstreamDoneChannel, outputDownstreamDoneChannel)
		return
	}

	done := false
	for !done {
		recordsAndContexts := <-inputRecordChannel
		var err error
		done, err = runSingleTransformerBatch(
			recordsAndContexts,
			recordTransformer,
			isFirstInChain,
			outputRecordChannel,
			inputDownstreamDoneChannel,
			outputDownstreamDoneChannel,
			dataProcessingErrorChannel,
			options,
		)
		if err != nil {
			// runSingleTransformerBatch has already sent the error to
			// dataProcessingErrorChannel and then forwarded an end-of-stream
			// marker downstream, so the record-writer drains and finishes,
			// upon which stream.Stream returns the error.
			//
			// Tell upstream (transformers and ultimately the record-reader,
			// via the mlr-head mechanism) that we'll ignore further input.
			select {
			case outputDownstreamDoneChannel <- true:
			default:
			}
			return
		}
	}
}

// runSingleTransformerBatch passes one batch of records through the
// transformer. The boolean return is true on end of record stream. A non-nil
// error is a mid-stream transformer failure: the error has already been sent
// to dataProcessingErrorChannel, and any output produced before the failure,
// plus an end-of-stream marker, has already been forwarded downstream so the
// rest of the chain and the record-writer can drain and finish cleanly.
func runSingleTransformerBatch(
	inputRecordsAndContexts []*types.RecordAndContext, // list of types.RecordAndContext
	recordTransformer RecordTransformer,
	isFirstInChain bool,
	outputRecordChannel chan<- []*types.RecordAndContext, // list of *types.RecordAndContext
	inputDownstreamDoneChannel <-chan bool,
	outputDownstreamDoneChannel chan<- bool,
	dataProcessingErrorChannel chan<- error,
	options *cli.TOptions,
) (bool, error) {
	outputRecordsAndContexts := make([]*types.RecordAndContext, 0, len(inputRecordsAndContexts))
	done := false

	for _, inputRecordAndContext := range inputRecordsAndContexts {
		// --nr-progress-mod
		// TODO: function-pointer this away to reduce instruction count in the
		// normal case which it isn't used at all. No need to test if {static thing} != 0
		// on every record.
		if options.NRProgressMod != 0 {
			if isFirstInChain && inputRecordAndContext.Record != nil {
				context := &inputRecordAndContext.Context
				if context.NR%options.NRProgressMod == 0 {
					fmt.Fprintf(os.Stderr, "NR=%d FNR=%d FILENAME=%s\n", context.NR, context.FNR, context.FILENAME)
				}
			}
		}

		// Three things can come through:
		//
		// * End-of-stream marker
		// * Non-nil records to be printed
		// * Strings to be printed from put/filter DSL print/dump/etc
		//   statements. They are handled here rather than fmt.Println directly
		//   in the put/filter handlers since we want all print statements and
		//   record-output to be in the same goroutine, for deterministic
		//   output ordering.
		//
		// The first two are passed to the transformer. The third we send along
		// the output channel without involving the record-transformer, since
		// there is no record to be transformed.

		if inputRecordAndContext.EndOfStream || inputRecordAndContext.Record != nil {
			err := recordTransformer.Transform(
				inputRecordAndContext,
				&outputRecordsAndContexts,
				// TODO: maybe refactor these out of each transformer.
				// And/or maybe poll them once per batch not once per record.
				inputDownstreamDoneChannel,
				outputDownstreamDoneChannel,
			)
			if err != nil {
				// Surface the error to stream.Stream's select loop.
				// Non-blocking send: if another goroutine errored first, that
				// error wins. This must happen before the end-of-stream
				// marker is forwarded below: the marker lets the
				// record-writer finish and signal done-writing, and the error
				// must already be buffered by then or stream.Stream could
				// return nil, losing the nonzero exit code.
				select {
				case dataProcessingErrorChannel <- err:
				default:
				}
				// Forward what was produced before the failure, plus an
				// end-of-stream marker so downstream drains and finishes.
				outputRecordsAndContexts = append(outputRecordsAndContexts,
					types.NewEndOfStreamMarker(&inputRecordAndContext.Context))
				outputRecordChannel <- outputRecordsAndContexts
				return true, err
			}
		} else {
			outputRecordsAndContexts = append(outputRecordsAndContexts, inputRecordAndContext)
		}

		if inputRecordAndContext.EndOfStream {
			done = true
			break
		}

		if done {
			break
		}
	}

	outputRecordChannel <- outputRecordsAndContexts

	return done, nil
}
