/* global
  Audio,
  AudioContext,
  FileReader,
  Plotly,
  Vue,
  cancelAnimationFrame,
  fetch,
  math,
  requestAnimationFrame
*/
Vue.config.devtools = true

const figureIn = {
  data: [
    {
      line: {
        dash: 'solid',
        color: 'rgb(0,113.985,188.955)',
        width: 1
      },
      mode: 'lines+markers',
      name: '',
      type: 'scatter',
      x: [],
      y: [],
      xaxis: 'x',
      yaxis: 'y',
      marker: {
        line: {
          width: 1,
          color: []
        },
        size: 8,
        symbol: 'circle',
        color: []
      },
      visible: true,
      showlegend: true
    }
  ],
  layout: {
    title: '',
    width: 840,
    xaxis: {
      side: 'bottom',
      type: 'linear',
      range: [],
      ticks: 'inside',
      anchor: 'y',
      domain: [0.13, 0.905],
      mirror: 'ticks',
      nticks: 9,
      ticklen: 6.51,
      showgrid: false,
      showline: true,
      tickfont: {
        size: 10,
        color: 'rgb(38.25,38.25,38.25)',
        family: 'Arial, sans-serif'
      },
      tickmode: 'auto',
      zeroline: false,
      autorange: true,
      gridcolor: 'rgb(38.25,38.25,38.25)',
      gridwidth: 1,
      linecolor: 'rgb(38.25,38.25,38.25)',
      linewidth: 1,
      tickcolor: 'rgb(38.25,38.25,38.25)',
      tickwidth: 1,
      titlefont: {
        size: 11,
        color: 'rgb(38.25,38.25,38.25)',
        family: 'Arial, sans-serif'
      },
      exponentformat: 'none'
    },
    yaxis: {
      side: 'left',
      type: 'linear',
      range: [],
      ticks: 'inside',
      anchor: 'x',
      domain: [0.11, 0.925],
      mirror: 'ticks',
      nticks: 12,
      ticklen: 6.51,
      showgrid: false,
      showline: true,
      tickfont: {
        size: 10,
        color: 'rgb(38.25,38.25,38.25)',
        family: 'Arial, sans-serif'
      },
      tickmode: 'auto',
      zeroline: true,
      autorange: false,
      gridcolor: 'rgb(38.25,38.25,38.25)',
      gridwidth: 1,
      linecolor: 'rgb(38.25,38.25,38.25)',
      linewidth: 1,
      tickcolor: 'rgb(38.25,38.25,38.25)',
      tickwidth: 1,
      titlefont: {
        size: 11,
        color: 'rgb(38.25,38.25,38.25)',
        family: 'Arial, sans-serif'
      },
      exponentformat: 'none',
      showticklabels: true
    },
    height: 480,
    margin: {
      b: 0,
      l: 0,
      r: 0,
      t: 0,
      pad: 0
    },
    autosize: false,
    hovermode: 'closest',
    titlefont: {
      color: 'rgba(0,0,0,0)'
    },
    showlegend: false,
    annotations: [
      {
        x: 0.5175,
        y: 0.935,
        font: {
          size: 11,
          color: 'rgb(0,0,0)',
          family: 'Arial, sans-serif'
        },
        text: '',
        xref: 'paper',
        yref: 'paper',
        align: 'center',
        xanchor: 'center',
        yanchor: 'bottom',
        borderpad: 3,
        showarrow: false,
        textangle: 0,
        bordercolor: 'rgba(0,0,0,0)',
        borderwidth: 0.5
      }
    ],
    plot_bgcolor: 'rgba(0,0,0,0)',
    paper_bgcolor: 'rgb(255,255,255)'
  },
  frames: []
}

const figureMag = {
  data: [
    {
      line: {
        dash: 'solid',
        color: 'rgb(0,113.985,188.955)',
        width: 1
      },
      mode: 'lines',
      name: '',
      type: 'scatter',
      x: [],
      y: [],
      xaxis: 'x',
      yaxis: 'y',
      marker: {
        line: {
          width: 1,
          color: []
        },
        size: 8,
        symbol: 'circle',
        color: []
      },
      visible: true,
      showlegend: true
    }
  ],
  layout: {
    title: '',
    width: 840,
    xaxis: {
      title: 'Normalized Frequency (× π rad/sample)',
      side: 'bottom',
      type: 'linear',
      range: [0, 2],
      ticks: 'inside',
      anchor: 'y',
      domain: [0.13, 0.905],
      mirror: 'ticks',
      nticks: 9,
      ticklen: 6.51,
      showgrid: true,
      showline: true,
      tickfont: {
        size: 10,
        color: 'rgb(38.25,38.25,38.25)',
        family: 'Arial, sans-serif'
      },
      tickmode: 'array',
      tickvals: [0, 0.2, 0.4, 0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8, 2],
      zeroline: false,
      autorange: true,
      gridcolor: 'rgba(38.25,38.25,38.25,0.25)',
      gridwidth: 1,
      linecolor: 'rgb(38.25,38.25,38.25)',
      linewidth: 1,
      tickcolor: 'rgb(38.25,38.25,38.25)',
      tickwidth: 1,
      titlefont: {
        size: 18,
        color: 'rgb(38.25,38.25,38.25)',
        family: 'serif'
      },
      exponentformat: 'none'
    },
    yaxis: {
      title: 'Magnitude (dB)',
      side: 'left',
      type: 'linear',
      range: [],
      ticks: 'inside',
      anchor: 'x',
      domain: [0.11, 0.925],
      mirror: 'ticks',
      nticks: 12,
      ticklen: 6.51,
      showgrid: true,
      showline: true,
      tickfont: {
        size: 10,
        color: 'rgb(38.25,38.25,38.25)',
        family: 'Arial, sans-serif'
      },
      tickmode: 'auto',
      zeroline: false,
      autorange: true,
      gridcolor: 'rgba(38.25,38.25,38.25,0.25)',
      gridwidth: 1,
      linecolor: 'rgb(38.25,38.25,38.25)',
      linewidth: 1,
      tickcolor: 'rgb(38.25,38.25,38.25)',
      tickwidth: 1,
      titlefont: {
        size: 18,
        color: 'rgb(38.25,38.25,38.25)',
        family: 'serif'
      },
      exponentformat: 'none',
      showticklabels: true
    },
    height: 480,
    margin: {
      b: 0,
      l: 0,
      r: 0,
      t: 0,
      pad: 0
    },
    autosize: false,
    hovermode: 'closest',
    titlefont: {
      color: 'rgba(38.25,38.25,38.25,1)'
    },
    showlegend: false,
    annotations: [
      {
        x: 0.5175,
        y: 0.935,
        font: {
          size: 11,
          color: 'rgb(0,0,0)',
          family: 'Arial, sans-serif'
        },
        text: '',
        xref: 'paper',
        yref: 'paper',
        align: 'center',
        xanchor: 'center',
        yanchor: 'bottom',
        borderpad: 3,
        showarrow: false,
        textangle: 0,
        bordercolor: 'rgba(0,0,0,0)',
        borderwidth: 0.5
      }
    ],
    plot_bgcolor: 'rgba(0,0,0,0)',
    paper_bgcolor: 'rgb(255,255,255)'
  },
  frames: []
}

const figureOut = JSON.parse(JSON.stringify(figureIn))
const figurePhase = JSON.parse(JSON.stringify(figureMag))

window.app = new Vue({
  el: '#root',
  data: {
    figureIn: figureIn,
    figureOut: figureOut,
    figureMag: figureMag,
    figurePhase: figurePhase,
    p: 0,
    q: 0,
    a: [1],
    b: [1],
    w: [],
    x: [1].concat(Array(19).fill(0)),
    y: [1].concat(Array(19).fill(0)),
    z: [],
    preset: 'd',
    length: 20,
    isTrig: false,
    A: 1,
    T: 19,
    tao: 0,
    scaler: 1,
    reverse: false,
    toPower: false,
    readonly: true,
    stem: true,
    systemFile: '',
    fileReader: new FileReader()
  },
  methods: {
    systemFileHandler () {
      const file = document.querySelector('#systemFile').files[0]
      if (file) {
        this.systemFile = file
        this.fileReader.readAsText(this.systemFile)
      }
    },
    getDegreeKey (s) {
      // convert 'a' -> 'p', 'b' -> 'q'
      // 'p' - 'a' == 15
      return String.fromCharCode(s.charCodeAt(0) + 15)
    },
    system (s) {
      this.truncate()
      const old = this[s].slice(0)
      const len = this[this.getDegreeKey(s)] + 1
      if (len > old.length) {
        this[s] = Array(len).fill(1)
        Array.prototype.splice.apply(this[s], [0, old.length].concat(old))
      } else {
        this[s] = old.slice(0, len)
      }
      this.output()
    },
    input (name, length) {
      const omega = (2 * Math.PI) / this.T
      const fn = {
        d: [1].concat(Array(length - 1).fill(0)),
        u: Array(length).fill(1),
        sin: [...Array(length).keys()].map(
          x => this.A * Math.sin(omega * (x + this.tao))
        ),
        cos: [...Array(length).keys()].map(
          x => this.A * Math.cos(omega * (x + this.tao))
        ),
        exp: [...Array(length).keys()].map(x => Math.exp(x)),
        log: [...Array(length).keys()].map(x => Math.log(x + 1)),
        n: [...Array(length).keys()],
        c: this.x
      }
      return fn[name]
    },
    output () {
      const d = this.input('d', this.length)
      const h = this.filter(this.b, this.a, d)
      const x = this.input(this.preset, this.length)
      this.x = x.map((x, n) => this.scale(x, n))
      if (this.reverse) this.x = this.x.reverse()
      this.y = this.conv(this.x, h).slice(0, this.length)
      this.redrawIO(this.x, this.figureIn)
      this.redrawIO(this.y, this.figureOut)
    },
    scale (x, n) {
      const s = Math.pow(this.scaler, this.toPower ? n : 1)
      return s * x
    },
    presetHandler () {
      const s = this.preset
      if (s === 'c') {
        this.scaler = 1
        this.toPower = false
        this.readonly = false
      } else {
        this.length = 20
        this.x = this.input(s, this.length)
        this.readonly = true
        if (s === 'sin' || s === 'cos') {
          this.isTrig = true
        } else {
          this.isTrig = false
        }
      }
      this.output()
    },
    lengthHandler () {
      this.truncate()
      if (this.length > this.x.length) {
        const s = this.preset
        if (s === 'c') {
          const diff = this.length - this.x.length
          this.x = this.x.concat(Array(diff).fill(0))
        } else {
          this.x = this.input(s, this.length)
        }
      } else {
        this.x.length = this.length
      }
      this.output()
    },
    inputHandler () {
      this.x = this.x.split(',')
      this.length = this.x.length
      this.output()
    },
    trigHandler () {
      this.truncate()
      this.output()
    },
    truncate () {
      const keys = ['p', 'q', 'length', 'T', 'tao']
      keys.forEach(key => {
        this[key] = Math.trunc(this[key])
      })
    },
    // view helper function
    firstTerm (a, i) {
      for (let j = 0; j < i; j += 1) {
        if (a[j] !== 0) return false
      }
      return true
    },
    // view helper function
    allZero (a) {
      for (const i of a) {
        if (i !== 0) return false
      }
      return true
    },
    filter (b, a, x) {
      if (a[0] !== 0) {
        const n = a.length - 1
        const m = b.length - 1
        const y = []
        for (let i = 0; i < x.length; i += 1) {
          let lhs = 0
          let rhs = 0
          for (let j = 1; j <= n && j <= i; j += 1) {
            lhs = lhs + a[j] * y[i - j]
          }
          for (let j = 0; j <= m && j <= i; j += 1) {
            rhs = rhs + b[j] * x[i - j]
          }
          y[i] = (rhs - lhs) / a[0]
        }
        return y
      }
      return x
    },
    conv (x, h) {
      let disp = 0
      const y = []
      for (let k = 0; k < h.length; k += 1) {
        y.push(x[0] * h[k])
      }
      disp = disp + 1
      for (let j = 1; j < x.length; j += 1) {
        for (let k = 0; k < h.length; k += 1) {
          if (disp + k !== y.length) {
            y[disp + k] = y[disp + k] + x[j] * h[k]
          } else {
            y.push(x[j] * h[k])
          }
        }
        disp = disp + 1
      }
      return y
    },
    redrawIO (data, figure) {
      const x = figure.data[0].x
      const y = figure.data[0].y
      const lineColor = figure.data[0].marker.line.color
      const color = figure.data[0].marker.color
      x.length = 0
      y.length = 0
      lineColor.length = 0
      color.length = 0
      for (let i = 0; i < this.length; i += 1) {
        if (this.stem) {
          figure.data[0].mode = 'lines+markers'
          x.push(i, i, null)
          y.push(0, data[i], null)
          lineColor.push(
            'rgba(0,0,0,0)',
            'rgb(0,113.985,188.955)',
            'rgba(0,0,0,0)'
          )
          color.push('rgba(0,0,0,0)', 'rgba(0,0,0,0)', 'rgba(0,0,0,0)')
        } else {
          figure.data[0].mode = 'lines'
          x.push(i)
          y.push(data[i])
        }
      }
      Plotly.redraw(document.getElementById('input'))
      Plotly.redraw(document.getElementById('output'))
    },
    freqz (ob, oa, oz) {
      let b = ob.slice()
      let a = oa.slice()
      if (b.length > a.length) {
        a = a.slice().concat(Array(b.length - a.length).fill(0))
      } else if (b.length < a.length) {
        b = b.slice().concat(Array(a.length - b.length).fill(0))
      }
      const H = []
      for (let i = 0; i < oz.length; i += 1) {
        const z = math.conj(oz[i])
        let B = math.complex(0, 0)
        let A = math.complex(0, 0)
        for (let i = 0; i < b.length; i += 1) {
          B = math.add(B, math.multiply(b[i], math.pow(z, i)))
          A = math.add(A, math.multiply(a[i], math.pow(z, i)))
        }
        H.push(math.round(math.divide(B, A), 5))
      }
      return H
    },
    unwrap (op) {
      const p = op.slice()
      const dp = []
      const dps = []
      const dpcorr = []
      const cumsum = []
      const cutoff = Math.PI
      for (let i = 0; i < p.length - 1; i += 1) {
        dp[i] = p[i + 1] - p[i]
      }
      for (let i = 0; i < p.length - 1; i += 1) {
        dps[i] =
          dp[i] +
          Math.PI -
          Math.floor((dp[i] + Math.PI) / (2 * Math.PI)) * (2 * Math.PI) -
          Math.PI
      }
      for (let i = 0; i < p.length - 1; i += 1) {
        if (dps[i] === -Math.PI && dp[i] > 0) {
          dps[i] = Math.PI
        }
      }
      for (let i = 0; i < p.length - 1; i += 1) {
        dpcorr[i] = dps[i] - dp[i]
      }
      for (let i = 0; i < p.length - 1; i += 1) {
        if (Math.abs(dp[i]) < cutoff) {
          dpcorr[i] = 0
        }
      }
      cumsum[0] = dpcorr[0]
      for (let i = 1; i < p.length - 1; i += 1) {
        cumsum[i] = cumsum[i - 1] + dpcorr[i]
      }
      for (let i = 1; i < p.length; i += 1) {
        p[i] += cumsum[i - 1]
      }
      return p
    },
    redrawFR () {
      const mags1 = []
      const mags2 = []
      const phases1 = []
      const phases2 = []
      const H = this.freqz(this.b, this.a, this.z)
      for (let i = 0; i < H.length; i += 1) {
        const mag = Math.sqrt(Math.pow(H[i].re, 2) + Math.pow(H[i].im, 2))
        const phase = atan2(H[i].im, H[i].re)
        if (i < 512) {
          mags1.push(20 * Math.log10(mag))
          phases1.push(phase)
        } else {
          mags2.push(20 * Math.log10(mag))
          phases2.push(phase)
        }
      }
      const unwrap1 = this.unwrap(phases1).map(x => (x * 180) / Math.PI)
      const unwrap2 = this.unwrap(phases2).map(x => (x * 180) / Math.PI)
      mags1[511] = null
      unwrap1[511] = null
      mags2[0] = null
      unwrap2[0] = null
      figureMag.data[0].y = mags1.concat(mags2)
      figurePhase.data[0].y = unwrap1.concat(unwrap2)
      Plotly.redraw(document.getElementById('magnitude'))
      Plotly.redraw(document.getElementById('phase'))
      function atan2 (im, re) {
        if (im === 0 && re === 0) {
          return 0
        }
        return Math.atan2(im, re)
      }
    },
    drawBuffer (width, height, context, buffer) {
      context.clearRect(0, 0, width, height)
      const data = buffer
      const step = Math.ceil(data.length / width)
      const amp = height / 2
      for (let i = 0; i < width; i++) {
        let min = 1.0
        let max = -1.0
        for (let j = 0; j < step; j++) {
          const datum = data[i * step + j]
          if (datum < min) min = datum
          if (datum > max) max = datum
        }
        context.fillStyle = '#0072BD'
        context.fillRect(i, (1 + min) * amp, 1, Math.max(1, (max - min) * amp))
      }
    },
    reverb () {
      const canvas = document.getElementById('waveform3')
      const audioSource = new Audio('asset/sample.mp3')
      const audioCtx = new AudioContext()
      const audioInput = audioCtx.createMediaElementSource(audioSource)
      const convolver = audioCtx.createConvolver()
      const gain = audioCtx.createGain()
      const analyser = audioCtx.createAnalyser()
      const waveform = new Float32Array(analyser.frequencyBinCount)
      let request
      let chunks = new Float32Array(0)
      gain.gain.value = 3.5
      audioSource.onended = e => {
        cancelAnimationFrame(request)
        audioCtx.close()
        this.drawBuffer(
          canvas.width,
          canvas.height,
          canvas.getContext('2d'),
          chunks
        )
      }
      fetch('asset/clap.mp3')
        .then(response => response.arrayBuffer())
        .then(arrayBuffer => audioCtx.decodeAudioData(arrayBuffer))
        .then(buffer => {
          convolver.buffer = buffer
          audioInput
            .connect(convolver)
            .connect(gain)
            .connect(analyser)
            .connect(audioCtx.destination)
          audioSource.play()
          updateWaveform()
        })
      function updateWaveform () {
        request = requestAnimationFrame(updateWaveform)
        analyser.getFloatTimeDomainData(waveform)
        chunks = Float32Concat(chunks, waveform)
      }
      function Float32Concat (first, second) {
        const firstLength = first.length
        const result = new Float32Array(firstLength + second.length)
        result.set(first)
        result.set(second, firstLength)
        return result
      }
    }
  },
  mounted () {
    const input = document.getElementById('input')
    const output = document.getElementById('output')
    const magnitude = document.getElementById('magnitude')
    const phase = document.getElementById('phase')

    for (let i = 0; i <= 1023; i += 1) {
      this.w.push((i * (2 * Math.PI)) / 1023)
      this.z.push(math.exp(math.complex(0, this.w[i])))
    }

    figurePhase.layout.yaxis.title = 'Phase (degrees)'
    figureMag.data[0].x = this.w.map(x => x / Math.PI)
    figurePhase.data[0].x = this.w.map(x => x / Math.PI)
    Plotly.plot(input, { data: figureIn.data, layout: figureIn.layout })
    Plotly.plot(output, { data: figureOut.data, layout: figureOut.layout })
    Plotly.plot(magnitude, { data: figureMag.data, layout: figureMag.layout })
    Plotly.plot(phase, { data: figurePhase.data, layout: figurePhase.layout })
    this.redrawIO(this.x, this.figureIn)
    this.redrawIO(this.y, this.figureOut)
    this.redrawFR()
    this.fileReader.onload = e => {
      const json = JSON.parse(this.fileReader.result)
      this.a = json.a
      this.b = json.b
      this.p = this.a.length - 1
      this.q = this.b.length - 1
      this.output()
    }
  }
})
