use crate::{bind, receive, result, right, run_step, send, suspend, Coroutine};

/// Generated by chain, represents feeding
/// inputs from one routine into another
///
/// This is useful to allow 'communication' between
/// coroutines
pub enum ChainResult<'a, IA, OA, OB, RA, RB> {
    OutOfInputs {
        first: RA,
        second: Coroutine<'a, OA, OB, RB>,
    },
    Finished {
        second: RB,
        first: Coroutine<'a, IA, OA, RA>,
    },
}

/// Feeds the outputs of the first routine
/// into the inputs of the second
///
/// As coroutines as more pull based, second is really
/// in charge here, first will 'run' whenever second
/// is after more inputs. In the case that first finishes
/// the return result will be provided, and the second coroutine
/// so it can be further ran.
///
/// If second finishes, the remaining 'input' routine is also
/// returned, allowing it to be used elsewhere
pub fn chain<'a, IA, OA, OB, RA, RB>(
    first: Coroutine<'a, IA, OA, RA>,
    second: Coroutine<'a, OA, OB, RB>,
) -> Coroutine<'a, IA, OB, ChainResult<'a, IA, OA, OB, RA, RB>>
where
    OA: Send,
    OB: Send,
    RA: Send,
    RB: Send,
{
    match run_step(second) {
        crate::StepResult::Done(second) => {
            let r = ChainResult::Finished { second, first };
            result(r)
        }
        crate::StepResult::Yield { output, next } => {
            let msg = send(output);
            let next = chain(first, *next);
            right(msg, next)
        }
        crate::StepResult::Next(next_b) => match run_step(first) {
            crate::StepResult::Done(first) => {
                let second = suspend(next_b);
                let r = ChainResult::OutOfInputs { second, first };
                result(r)
            }
            crate::StepResult::Yield { output, next } => {
                let first = *next;
                let second = next_b(output);
                chain(first, second)
            }
            crate::StepResult::Next(next_a) => {
                let next = move |input: IA| {
                    let first = next_a(input);
                    let second = suspend(next_b);
                    chain(first, second)
                };
                bind(receive(), next)
            }
        },
    }
}
