// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// This is a part of HexoDSP. Released under (A)GPLv3 or any later.
// See README.md and COPYING for details.

use crate::dsp::{ProcBuf, SAtom};
use triple_buffer::{Input, Output, TripleBuffer};

/// Step in a `NodeProg` that stores the to be
/// executed node and output operations.
#[derive(Debug, Clone)]
pub struct NodeOp {
    /// Stores the index of the node
    pub idx:  u8,
    /// Output index and length of the node:
    pub out_idxlen: (usize, usize),
    /// Input index and length of the node:
    pub in_idxlen: (usize, usize),
    /// Atom data index and length of the node:
    pub at_idxlen: (usize, usize),
    /// Input indices, (<out vec index>, <own node input index>)
    pub inputs: Vec<(usize, usize)>,
}

impl std::fmt::Display for NodeOp {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Op(i={} out=({}-{}) in=({}-{}) at=({}-{})",
               self.idx,
               self.out_idxlen.0,
               self.out_idxlen.1,
               self.in_idxlen.0,
               self.in_idxlen.1,
               self.at_idxlen.0,
               self.at_idxlen.1)?;

        for i in self.inputs.iter() {
            write!(f, " cpy=(o{} => i{})", i.0, i.1)?;
        }

        write!(f, ")")
    }
}

/// A node graph execution program. It comes with buffers
/// for the inputs, outputs and node parameters (knob values).
#[derive(Debug)]
pub struct NodeProg {
    /// The input vector stores the smoothed values of the params.
    /// It is not used directly, but will be merged into the `cur_inp`
    /// field together with the assigned outputs.
    pub inp:    Vec<ProcBuf>,

    /// The temporary input vector that is initialized from `inp`
    /// and is then merged with the associated outputs.
    pub cur_inp: Vec<ProcBuf>,

    /// The output vector, holding all the node outputs.
    pub out:    Vec<ProcBuf>,

    /// The param vector, holding all parameter inputs of the
    /// nodes, such as knob settings.
    pub params: Vec<f32>,

    /// The atom vector, holding all non automatable parameter inputs
    /// of the nodes, such as samples or integer settings.
    pub atoms:  Vec<SAtom>,

    /// The node operations that are executed in the order they appear in this
    /// vector.
    pub prog:   Vec<NodeOp>,

    /// A marker, that checks if we can still swap buffers with
    /// with other NodeProg instances. This is usally set if the ProcBuf pointers
    /// have been copied into `cur_inp`. You can call `unlock_buffers` to
    /// clear `locked_buffers`:
    pub locked_buffers: bool,

    /// Holds the input end of a triple buffer that is used
    /// to publish the most recent output values to the frontend.
    pub out_feedback: Input<Vec<f32>>,

    /// Temporary hold for the producer for the `out_feedback`:
    pub out_fb_cons: Option<Output<Vec<f32>>>,
}

impl Drop for NodeProg {
    fn drop(&mut self) {
        for buf in self.inp.iter_mut() {
            buf.free();
        }

        for buf in self.out.iter_mut() {
            buf.free();
        }
    }
}


impl NodeProg {
    pub fn empty() -> Self {
        let out_fb = vec![];
        let tb = TripleBuffer::new(out_fb);
        let (input_fb, output_fb) = tb.split();
        Self {
            out:     vec![],
            inp:     vec![],
            cur_inp: vec![],
            params:  vec![],
            atoms:   vec![],
            prog:    vec![],
            out_feedback:   input_fb,
            out_fb_cons:    Some(output_fb),
            locked_buffers: false,
        }
    }

    pub fn new(out_len: usize, inp_len: usize, at_len: usize) -> Self {
        let mut out = vec![];
        out.resize_with(out_len, ProcBuf::new);

        let out_fb = vec![0.0; out_len];
        let tb = TripleBuffer::new(out_fb);
        let (input_fb, output_fb) = tb.split();

        let mut inp = vec![];
        inp.resize_with(inp_len, ProcBuf::new);
        let mut cur_inp = vec![];
        cur_inp.resize_with(inp_len, ProcBuf::null);

        let mut params = vec![];
        params.resize(inp_len, 0.0);
        let mut atoms = vec![];
        atoms.resize(at_len, SAtom::setting(0));

        Self {
            out,
            inp,
            cur_inp,
            params,
            atoms,
            prog:           vec![],
            out_feedback:   input_fb,
            out_fb_cons:    Some(output_fb),
            locked_buffers: false,
        }
    }

    pub fn take_feedback_consumer(&mut self) -> Option<Output<Vec<f32>>> {
        self.out_fb_cons.take()
    }

    pub fn params_mut(&mut self) -> &mut [f32] {
        &mut self.params
    }

    pub fn atoms_mut(&mut self) -> &mut [SAtom] {
        &mut self.atoms
    }

    pub fn append_op(&mut self, node_op: NodeOp) {
        for n_op in self.prog.iter_mut() {
            if n_op.idx == node_op.idx {
                return;
            }
        }

        self.prog.push(node_op);
    }

    pub fn append_edge(
        &mut self,
        node_op: NodeOp,
        inp_index: usize,
        out_index: usize)
    {
        for n_op in self.prog.iter_mut() {
            if n_op.idx == node_op.idx {
                n_op.inputs.push((out_index, inp_index));
                return;
            }
        }
    }

    pub fn append_with_inputs(
        &mut self,
        mut node_op: NodeOp,
        inp1: Option<(usize, usize)>,
        inp2: Option<(usize, usize)>,
        inp3: Option<(usize, usize)>)
    {
        for n_op in self.prog.iter_mut() {
            if n_op.idx == node_op.idx {
                if let Some(inp1) = inp1 { n_op.inputs.push(inp1); }
                if let Some(inp2) = inp2 { n_op.inputs.push(inp2); }
                if let Some(inp3) = inp3 { n_op.inputs.push(inp3); }
                return;
            }
        }

        if let Some(inp1) = inp1 { node_op.inputs.push(inp1); }
        if let Some(inp2) = inp2 { node_op.inputs.push(inp2); }
        if let Some(inp3) = inp3 { node_op.inputs.push(inp3); }
        self.prog.push(node_op);
    }

    pub fn initialize_input_buffers(&mut self) {
        for param_idx in 0..self.params.len() {
            let param_val = self.params[param_idx];
            self.inp[param_idx].fill(param_val);
        }
    }

    pub fn swap_previous_outputs(&mut self, prev_prog: &mut NodeProg) {
        if self.locked_buffers {
            self.unlock_buffers();
        }

        if prev_prog.locked_buffers {
            prev_prog.unlock_buffers();
        }

        // XXX: Swapping is now safe, because the `cur_inp` field
        //      no longer references to the buffers in `inp` or `out`.
        for (old_inp_pb, new_inp_pb) in
            prev_prog.inp.iter_mut().zip(
                self.inp.iter_mut())
        {
            std::mem::swap(old_inp_pb, new_inp_pb);
        }
    }

    pub fn unlock_buffers(&mut self) {
        for buf in self.cur_inp.iter_mut() {
            *buf = ProcBuf::null();
        }
        self.locked_buffers = false;
    }

    pub fn assign_outputs(&mut self) {
        for op in self.prog.iter() {

            // First step is copying the ProcBufs to the `cur_inp` current
            // input buffer vector. It holds the data for smoothed paramter
            // inputs or just constant values since the last smoothing.
            //
            // Next we overwrite the input ProcBufs which have an
            // assigned output buffer.
            //
            // ProcBuf has a raw pointer inside, and this copying
            // is therefor very fast.
            //
            // XXX: This requires, that the graph is not cyclic,
            // because otherwise we would write output buffers which
            // are already accessed in the current iteration.
            // This might lead to unexpected effects inside the process()
            // call of the nodes.
            let input_bufs = &mut self.cur_inp;
            let out_bufs   = &mut self.out;

            let inp = op.in_idxlen;

            // First step (refresh inputs):
            input_bufs[inp.0..inp.1]
                .copy_from_slice(&self.inp[inp.0..inp.1]);

            // Second step (assign outputs):
            for io in op.inputs.iter() {
                input_bufs[io.1] = out_bufs[io.0];
            }
        }

        self.locked_buffers = true;
    }
}