#### NullNode ####

type NullRenderer <: AudioRenderer end
typealias NullNode AudioNode{NullRenderer}
export NullNode

function render(node::NullRenderer, device_input::AudioBuf, info::DeviceInfo)
    # TODO: preallocate buffer
    return zeros(info.buf_size)
end

#### SinOsc ####

# Generates a sin tone at the given frequency

type SinOscRenderer{T<:Union(Float32, AudioNode)} <: AudioRenderer
    freq::T
    phase::Float32
    buf::AudioBuf

    function SinOscRenderer(freq)
        new(freq, 0.0, AudioSample[])
    end
end

typealias SinOsc AudioNode{SinOscRenderer}
SinOsc(freq::Real) = SinOsc(SinOscRenderer{Float32}(freq))
SinOsc(freq::AudioNode) = SinOsc(SinOscRenderer{AudioNode}(freq))
SinOsc() = SinOsc(440)
export SinOsc

function render(node::SinOscRenderer{Float32}, device_input::AudioBuf,
        info::DeviceInfo)
    if length(node.buf) != info.buf_size
        resize!(node.buf, info.buf_size)
    end
    outbuf = node.buf
    phase = node.phase
    freq = node.freq
    # make sure these are Float32s so that we don't allocate doing conversions
    # in the tight loop
    pi2::Float32 = 2pi
    phase_inc::Float32 = 2pi * freq / info.sample_rate
    i::Int = 1
    while i <= info.buf_size
        outbuf[i] = sin(phase)
        phase = (phase + phase_inc) % pi2
        i += 1
    end
    node.phase = phase
    return outbuf
end

function render(node::SinOscRenderer{AudioNode}, device_input::AudioBuf,
        info::DeviceInfo)
    freq = render(node.freq, device_input, info)::AudioBuf
    block_size = min(length(freq), info.buf_size)
    if(length(node.buf) != block_size)
        resize!(node.buf, block_size)
    end
    outbuf = node.buf

    phase::Float32 = node.phase
    pi2::Float32 = 2pi
    phase_step::Float32 = 2pi/(info.sample_rate)
    i::Int = 1
    while i <= block_size
        outbuf[i] = sin(phase)
        phase = (phase + phase_step*freq[i]) % pi2
        i += 1
    end
    node.phase = phase
    return outbuf
end

#### AudioMixer ####

# Mixes a set of inputs equally

type MixRenderer <: AudioRenderer
    inputs::Vector{AudioNode}
    buf::AudioBuf

    MixRenderer(inputs) = new(inputs, AudioSample[])
    MixRenderer() = MixRenderer(AudioNode[])
end

typealias AudioMixer AudioNode{MixRenderer}
export AudioMixer

function render(node::MixRenderer, device_input::AudioBuf, info::DeviceInfo)
    if length(node.buf) != info.buf_size
        resize!(node.buf, info.buf_size)
    end
    mix_buffer = node.buf
    n_inputs = length(node.inputs)
    i = 1
    max_samples = 0
    fill!(mix_buffer, 0)
    while i <= n_inputs
        rendered = render(node.inputs[i], device_input, info)::AudioBuf
        nsamples = length(rendered)
        max_samples = max(max_samples, nsamples)
        j::Int = 1
        while j <= nsamples
            mix_buffer[j] += rendered[j]
            j += 1
        end
        if nsamples < info.buf_size
            deleteat!(node.inputs, i)
            n_inputs -= 1
        else
            i += 1
        end
    end
    if max_samples < length(mix_buffer)
        return mix_buffer[1:max_samples]
    else
        # save the allocate and copy if we don't need to
        return mix_buffer
    end
end

Base.push!(mixer::AudioMixer, node::AudioNode) = push!(mixer.renderer.inputs, node)

#### Gain ####
type GainRenderer{T<:Union(Float32, AudioNode)} <: AudioRenderer
    in1::AudioNode
    in2::T
    buf::AudioBuf

    GainRenderer(in1, in2) = new(in1, in2, AudioSample[])
end

function render(node::GainRenderer{Float32},
                device_input::AudioBuf,
                info::DeviceInfo)
    input = render(node.in1, device_input, info)::AudioBuf
    if length(node.buf) != length(input)
        resize!(node.buf, length(input))
    end
    i = 1
    while i <= length(input)
        node.buf[i] = input[i] * node.in2
        i += 1
    end
    return node.buf
end

function render(node::GainRenderer{AudioNode},
                device_input::AudioBuf,
                info::DeviceInfo)
    in1_data = render(node.in1, device_input, info)::AudioBuf
    in2_data = render(node.in2, device_input, info)::AudioBuf
    block_size = min(length(in1_data), length(in2_data))
    if length(node.buf) != block_size
        resize!(node.buf, block_size)
    end
    i = 1
    while i <= block_size
        node.buf[i] = in1_data[i] * in2_data[i]
        i += 1
    end
    return node.buf
end

typealias Gain AudioNode{GainRenderer}
Gain(in1::AudioNode, in2::Real) = Gain(GainRenderer{Float32}(in1, in2))
Gain(in1::AudioNode, in2::AudioNode) = Gain(GainRenderer{AudioNode}(in1, in2))
export Gain

#### Offset ####
type OffsetRenderer <: AudioRenderer
    in_node::AudioNode
    offset::Float32
    buf::AudioBuf

    OffsetRenderer(in_node, offset) = new(in_node, offset, AudioSample[])
end

function render(node::OffsetRenderer, device_input::AudioBuf, info::DeviceInfo)
    input = render(node.in_node, device_input, info)::AudioBuf
    if length(node.buf) != length(input)
        resize!(node.buf, length(input))
    end
    i = 1
    while i <= length(input)
        node.buf[i] = input[i] + node.offset
        i += 1
    end
    return node.buf
end

typealias Offset AudioNode{OffsetRenderer}
export Offset


#### Array Player ####

# Plays a AudioBuf by rendering it out piece-by-piece

type ArrayRenderer <: AudioRenderer
    arr::AudioBuf
    arr_index::Int
    buf::AudioBuf

    ArrayRenderer(arr::AudioBuf) = new(arr, 1, AudioSample[])
end

typealias ArrayPlayer AudioNode{ArrayRenderer}
export ArrayPlayer

function render(node::ArrayRenderer, device_input::AudioBuf, info::DeviceInfo)
    range_end = min(node.arr_index + info.buf_size-1, length(node.arr))
    block_size = range_end - node.arr_index + 1
    if length(node.buf) != block_size
        resize!(node.buf, block_size)
    end
    copy!(node.buf, 1, node.arr, node.arr_index, block_size)
    node.arr_index = range_end + 1
    return node.buf
end

# Allow users to play a raw array by wrapping it in an ArrayPlayer
function play(arr::AudioBuf, args...)
    player = ArrayPlayer(arr)
    play(player, args...)
end

# If the array is the wrong floating type, convert it
function play{T <: FloatingPoint}(arr::Array{T}, args...)
    arr = convert(AudioBuf, arr)
    play(arr, args...)
end

# If the array is an integer type, scale to [-1, 1] floating point

# integer audio can be slightly (by 1) more negative than positive,
# so we just scale so that +/- typemax(T) becomes +/- 1
function play{T <: Signed}(arr::Array{T}, args...)
    arr = arr / typemax(T)
    play(arr, args...)
end

function play{T <: Unsigned}(arr::Array{T}, args...)
    zero = (typemax(T) + 1) / 2
    range = floor(typemax(T) / 2)
    arr = (arr .- zero) / range
    play(arr, args...)
end

#### Noise ####

type WhiteNoiseRenderer <: AudioRenderer end
typealias WhiteNoise AudioNode{WhiteNoiseRenderer}
export WhiteNoise

function render(node::WhiteNoiseRenderer, device_input::AudioBuf, info::DeviceInfo)
    return rand(AudioSample, info.buf_size) .* 2 .- 1
end


#### AudioInput ####

# Renders incoming audio input from the hardware

type InputRenderer <: AudioRenderer
    channel::Int
    InputRenderer(channel::Integer) = new(channel)
    InputRenderer() = new(1)
end

function render(node::InputRenderer, device_input::AudioBuf, info::DeviceInfo)
    @assert size(device_input, 1) == info.buf_size
    return device_input[:, node.channel]
end

typealias AudioInput AudioNode{InputRenderer}
export AudioInput

#### LinRamp ####

type LinRampRenderer <: AudioRenderer
    key_samples::Array{AudioSample}
    key_durations::Array{Float32}

    duration::Float32
    buf::AudioBuf

    LinRampRenderer(start, finish, dur) = LinRampRenderer([start,finish], [dur])

    LinRampRenderer(key_samples, key_durations) =
        LinRampRenderer(
            [convert(AudioSample,s) for s in key_samples],
            [convert(Float32,d) for d in key_durations]
        )

    function LinRampRenderer(key_samples::Array{AudioSample}, key_durations::Array{Float32})
        @assert length(key_samples) == length(key_durations) + 1
        new(key_samples, key_durations, sum(key_durations), AudioSample[])
    end
end

typealias LinRamp AudioNode{LinRampRenderer}
export LinRamp

function render(node::LinRampRenderer, device_input::AudioBuf, info::DeviceInfo)
    # Resize buffer if (1) it's too small or (2) we've hit the end of the ramp
    ramp_samples::Int = int(node.duration * info.sample_rate)
    block_samples = min(ramp_samples, info.buf_size)
    if length(node.buf) != block_samples
        resize!(node.buf, block_samples)
    end

    # Fill the buffer as long as there are more segments
    dt::Float32 = 1/info.sample_rate
    i::Int = 1
    while i <= length(node.buf) && length(node.key_samples) > 1

        # Fill as much of the buffer as we can with the current segment
        ds::Float32 = (node.key_samples[2] - node.key_samples[1]) / node.key_durations[1] / info.sample_rate
        while i <= length(node.buf)
            node.buf[i] = node.key_samples[1]
            node.key_samples[1] += ds
            node.key_durations[1] -= dt
            node.duration -= dt
            i += 1

            # Discard segment if we're finished
            if node.key_durations[1] <= 0
                if length(node.key_durations) > 1
                    node.key_durations[2] -= node.key_durations[1]
                end
                shift!(node.key_samples)
                shift!(node.key_durations)
                break
            end
        end
    end

    return node.buf
end