'use strict'

let gen = {
  memo: [].
  graph:null,
  accum:0,
  
  isNotNumber( v ) {
    return (typeof v)[0] !== 'n'  
  },
  
  getUID() {
    return this.accum++
  },
  
  out() {
    this.memo.length = 0
    return this.graph.out()
  },
  
  codegen() {
    let code
    
    if( !gen.memocode[ this.uid ] ) { // not memooutized
      code = this._name + '('
    
      this.inputs.map( ( propertyValue, idx, array ) => {
        if( typeof propertyValue === 'function' ) {
          propertyValue = propertyValue.codegen()
        }
      
        code += propertyValue
        code += idx < array.length - 1 ? ',' : ')'
      })
      
      gen.memocode[ this.uid ] = code
    } else { // use memoized code
      code = gen.memocode[ this.uid ]
    }

    return code
  }
}

let table = function( frequency ) {
  if( ! (this instanceof table) ) return new table( frequency )
    
  let tableSize = 1024
    
  Object.assign( this, {
    frequency,
    uid: gen.getUID()
    phase:0,
    tableFreq: 44100 / 1024,
    table: new Float32Array( tableSize ),
    codegen: gen.codegen,
    
    sample() {
      let index, frac, base
            
      this.phase += this.frequency / this.tableFreq
      while( this.phase >= 1024 ) this.phase -= 1024
        
      index   = this.phase | 0
      frac    = this.phase - index
      base    = this.table[ index ]
        
      return base + ( frac * ( this.table[ (index+1) & 1023 ] - base ) ) * 1
    },
    
    initTable() {
      for( let i = 0; i < this.table.length; i++ ) {
        this.table[ i ] = Math.sin( ( i / this.tableSize ) * ( Math.PI * 2 ) )
      }
    }
    
  })
  
  this.initTable()
}

let mul = function( x,y ) {
  if( ! (this instanceof mul) ) return new mul( x,y )

  Object.assign( this, {
    x,y,
    sample() {
      let x = this.x, y = this.y
    
      if( xIsUgen ) x = x.sample()
      if( yIsUgen ) y = y.sample()  
      
      return x*y
    }
  })
  
  let xIsUgen = gen.isNotNumber( x ),
      yIsUgen = gen.isNotNumber( y )
}

let add = function( x,y ) {
  if( ! (this instanceof add) ) return new add( x,y );

  Object.assign( this, {
    x,y,
    sample() {
      let x = this.x, y = this.y
    
      if( xIsUgen ) x = x.sample()
      if( yIsUgen ) y = y.sample()  
      
      return x+y
    }
  })
  
  let xIsUgen = gen.isNotNumber( x ),
      yIsUgen = gen.isNotNumber( y )
}

let abs = x => {
  let ugen = function() {
    if( this.uid in this.memo ) return this.memo[ this.uid ]
    
    let out = this.inputs[ 0 ]
    
    if( this.isNotNumber( out ) ) out = out()
    
    out = this.abs( out )
      
    this.memo.set( this.uid, out )
      
    return out
  }
  
  return {
    uid:          gen.getUID(),
    inputs:       [ x ],
    abs:          Math.abs,
    memo:         gen.memo,
    isNotNumber:  gen.isNotNumber,
    out:ugen
  }
}

let sin = x => {
  let ugen = function() {
    if( this.uid in this.memo ) return this.memo[ this.uid ]
    
    let out = this.inputs[ 0 ]
    
    if( this.isNotNumber( out ) ) out = out()
    
    out = this.sin( out )
      
    this.memo.set( this.uid, out )
      
    return out
  }
  
  return ugen.bind({
    uid:          gen.getUID(),
    inputs:       [ x ],
    sin:          Math.sin,
    memo:         gen.memo,
    isNotNumber:  gen.isNotNumber
  })
}

let accum = ( incr, reset=0, min=0, max=1 ) => {
  let ugen = function() {
    if( this.uid in this.memo ) return this.memo[ this.uid ]
    
    let incr = this.inputs[ 0 ], reset = this.inputs[ 1 ]
    
    if( this.isNotNumber( incr ) ) incr = incr()
    if( this.isNotNumber( reset ) ) reset = reset()      
    
    this.out += incr
      
    if( reset ) {
      this.out = this.min
    }else{
      if( this.out > this.max ) this.out = this.min
    }
    
    this.memo.set( this.uid, this.out )
      
    return this.out
  }
  
  return ugen.bind({
    min, 
    max,
    out:          0,
    uid:          gen.getUID(),
    inputs:       [ incr, reset ],
    memo:         gen.memo,
    isNotNumber:  gen.isNotNumber
  })
}

let bus = function( ...args ) {
  if( !( this instanceof bus ) ) return new bus( ...args )

  Object.assign( this, {
    inputs: args,
    sample() {
      let out = 0
    
      for( let i = 0, l = this.inputs.length; i < l; i++ ) {
        let input = this.inputs[ i ]
        out += input.sample()
      }
      
      return out
    }
  })
}

let param = value => {
  let p = function( v ) {
    if( v !== undefined ) this.value = v
    return this.value
  }
  
  return p.bind({ value })
}

let PI = Math.PI,
    SR = 44100

module.exports = { gen, add, mul, accum, abs, sin, bus, param, table, PI, SR, table }