// Copyright 2015 Hajime Hoshi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// +build js

package oto

import (
	"encoding/base64"
	"errors"
	"fmt"
	"sync"
	"syscall/js"
)

type driver struct {
	sampleRate      int
	channelNum      int
	bitDepthInBytes int
	nextPos         float64
	tmp             []byte
	bufferSize      int
	context         js.Value
	ready           bool
	callbacks       map[string]js.Func

	// For Audio Worklet
	workletNode     js.Value
	workletNodePost js.Value
	messageArray    js.Value
	transferArray   js.Value
	bufs            [][]js.Value
	cond            *sync.Cond
}

type warn struct {
	msg string
}

func (w *warn) Error() string {
	return w.msg
}

const audioBufferSamples = 3200

func tryAudioWorklet(context js.Value, channelNum int) (js.Value, error) {
	if !js.Global().Get("AudioWorkletNode").Truthy() {
		return js.Undefined(), nil
	}

	worklet := context.Get("audioWorklet")
	if !worklet.Truthy() {
		return js.Undefined(), &warn{
			msg: "AudioWorklet is not available due to the insecure context. See https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet",
		}
	}

	script := `
class EbitenAudioWorkletProcessor extends AudioWorkletProcessor {
  constructor() {
    super();

    this.buffers_ = [[], []];
    this.offsets_ = [0, 0];
    this.offsetsInArray_ = [0, 0];
    this.consumed_ = [];

    this.port.onmessage = (e) => {
      const bufs = e.data;
      for (let ch = 0; ch < bufs.length; ch++) {
        const buf = bufs[ch];
        this.buffers_[ch].push(new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4));
      }
    };
  }

  bufferTotalLength(ch) {
    const sum = this.buffers_[ch].reduce((total, buf) => total + buf.length, 0);
    return sum - this.offsetsInArray_[ch];
  }

  consume(ch, i) {
    while (this.buffers_[ch][0].length <= i - this.offsets_[ch]) {
      this.offsets_[ch] += this.buffers_[ch][0].length;
      this.offsetsInArray_[ch] = 0;
      const buf = this.buffers_[ch].shift();
      this.appendConsumedBuffer(ch, buf);
    }
    this.offsetsInArray_[ch]++;
    return this.buffers_[ch][0][i - this.offsets_[ch]];
  }

  appendConsumedBuffer(ch, buf) {
    let idx = this.consumed_.length - 1;
    if (idx < 0 || this.consumed_[idx][ch]) {
      this.consumed_.push([]);
      idx++;
    }
    this.consumed_[idx][ch] = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
  }

  process(inputs, outputs, parameters) {
    const out = outputs[0];

    if (this.bufferTotalLength(0) < out[0].length) {
      for (let ch = 0; ch < out.length; ch++) {
        for (let i = 0; i < out[ch].length; i++) {
          out[ch][i] = 0;
        }
      }
      return true;
    }

    for (let ch = 0; ch < out.length; ch++) {
      const offset = this.offsets_[ch] + this.offsetsInArray_[ch];
      for (let i = 0; i < out[ch].length; i++) {
        out[ch][i] = this.consume(ch, i + offset);
      }
    }

    for (let bufs of this.consumed_) {
      this.port.postMessage(bufs, bufs.map(buf => buf.buffer));
    }
    this.consumed_ = [];

    return true;
  }
}

registerProcessor('ebiten-audio-worklet-processor', EbitenAudioWorkletProcessor);`
	scriptURL := "data:application/javascript;base64," + base64.StdEncoding.EncodeToString([]byte(script))

	ch := make(chan error)
	worklet.Call("addModule", scriptURL).Call("then", js.FuncOf(func(js.Value, []js.Value) interface{} {
		close(ch)
		return nil
	})).Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		err := args[0]
		ch <- fmt.Errorf("oto: error at addModule: %s: %s", err.Get("name").String(), err.Get("message").String())
		close(ch)
		return nil
	}))
	if err := <-ch; err != nil {
		return js.Undefined(), err
	}

	options := js.Global().Get("Object").New()
	arr := js.Global().Get("Array").New()
	arr.Call("push", channelNum)
	options.Set("outputChannelCount", arr)

	node := js.Global().Get("AudioWorkletNode").New(context, "ebiten-audio-worklet-processor", options)
	node.Call("connect", context.Get("destination"))

	return node, nil
}

func newDriver(sampleRate, channelNum, bitDepthInBytes, bufferSize int) (tryWriteCloser, error) {
	if js.Global().Get("go2cpp").Truthy() {
		return newDriverGo2Cpp(sampleRate, channelNum, bitDepthInBytes, bufferSize)
	}

	class := js.Global().Get("AudioContext")
	if !class.Truthy() {
		class = js.Global().Get("webkitAudioContext")
	}
	if !class.Truthy() {
		return nil, errors.New("oto: audio couldn't be initialized")
	}

	options := js.Global().Get("Object").New()
	options.Set("sampleRate", sampleRate)
	context := class.New(options)

	node, err := tryAudioWorklet(context, channelNum)
	if err != nil {
		w, ok := err.(*warn)
		if !ok {
			return nil, err
		}
		js.Global().Get("console").Call("warn", w.Error())
	}

	bs := bufferSize
	if !node.Truthy() {
		bs = max(bufferSize, audioBufferSamples*channelNum*bitDepthInBytes)
	} else {
		bs = max(bufferSize, 4096)
	}

	p := &driver{
		sampleRate:      sampleRate,
		channelNum:      channelNum,
		bitDepthInBytes: bitDepthInBytes,
		context:         context,
		workletNode:     node,
		bufferSize:      bs,
	}

	if node.Truthy() {
		port := node.Get("port")
		p.workletNodePost = port.Get("postMessage").Call("bind", port)
		p.messageArray = js.Global().Get("Array").New(2)
		p.transferArray = js.Global().Get("Array").New(2)
		p.cond = sync.NewCond(&sync.Mutex{})

		s := p.bufferSize / p.channelNum / p.bitDepthInBytes * 4
		p.bufs = [][]js.Value{
			{
				js.Global().Get("Uint8Array").New(s),
				js.Global().Get("Uint8Array").New(s),
			},
			{
				js.Global().Get("Uint8Array").New(s),
				js.Global().Get("Uint8Array").New(s),
			},
		}

		node.Get("port").Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
			p.cond.L.Lock()
			defer p.cond.L.Unlock()

			bufs := args[0].Get("data")
			var arr []js.Value
			for i := 0; i < bufs.Length(); i++ {
				arr = append(arr, bufs.Index(i))
			}

			notify := len(p.bufs) == 0
			p.bufs = append(p.bufs, arr)
			if notify {
				p.cond.Signal()
			}

			return nil
		}))
	}

	setCallback := func(event string) js.Func {
		var f js.Func
		f = js.FuncOf(func(this js.Value, arguments []js.Value) interface{} {
			if !p.ready {
				p.context.Call("resume")
				p.ready = true
			}
			js.Global().Get("document").Call("removeEventListener", event, f)
			return nil
		})
		js.Global().Get("document").Call("addEventListener", event, f)
		p.callbacks[event] = f
		return f
	}

	// Browsers require user interaction to start the audio.
	// https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#webaudio
	p.callbacks = map[string]js.Func{}
	setCallback("touchend")
	setCallback("keyup")
	setCallback("mouseup")
	return p, nil
}

func toLR(data []byte) ([]float32, []float32) {
	const max = 1 << 15

	l := make([]float32, len(data)/4)
	r := make([]float32, len(data)/4)
	for i := 0; i < len(data)/4; i++ {
		l[i] = float32(int16(data[4*i])|int16(data[4*i+1])<<8) / max
		r[i] = float32(int16(data[4*i+2])|int16(data[4*i+3])<<8) / max
	}
	return l, r
}

func (p *driver) TryWrite(data []byte) (int, error) {
	if !p.ready {
		return 0, nil
	}

	if p.workletNode.Truthy() {
		p.cond.L.Lock()
		defer p.cond.L.Unlock()

		n := min(len(data), max(0, p.bufferSize-len(p.tmp)))
		p.tmp = append(p.tmp, data[:n]...)

		if len(p.tmp) < p.bufferSize {
			return n, nil
		}

		for len(p.bufs) == 0 {
			p.cond.Wait()
		}

		l, r := toLR(p.tmp[:p.bufferSize])
		tl := p.bufs[0][0]
		tr := p.bufs[0][1]
		copyFloat32sToJS(tl, l)
		copyFloat32sToJS(tr, r)
		p.tmp = p.tmp[p.bufferSize:]

		bufs := p.messageArray
		bufs.SetIndex(0, tl)
		bufs.SetIndex(1, tr)
		transfers := p.transferArray
		transfers.SetIndex(0, tl.Get("buffer"))
		transfers.SetIndex(1, tr.Get("buffer"))

		p.workletNodePost.Invoke(bufs, transfers)

		p.bufs = p.bufs[1:]

		return n, nil
	}

	n := min(len(data), max(0, p.bufferSize-len(p.tmp)))
	p.tmp = append(p.tmp, data[:n]...)

	c := p.context.Get("currentTime").Float()

	if p.nextPos < c {
		p.nextPos = c
	}

	// It's too early to enqueue a buffer.
	// Highly likely, there are two playing buffers now.
	if c+float64(p.bufferSize/p.bitDepthInBytes/p.channelNum)/float64(p.sampleRate) < p.nextPos {
		return n, nil
	}

	le := audioBufferSamples * p.bitDepthInBytes * p.channelNum
	if len(p.tmp) < le {
		return n, nil
	}

	buf := p.context.Call("createBuffer", p.channelNum, audioBufferSamples, p.sampleRate)
	l, r := toLR(p.tmp[:le])
	tl, freel := float32SliceToTypedArray(l)
	tr, freer := float32SliceToTypedArray(r)
	if buf.Get("copyToChannel").Truthy() {
		buf.Call("copyToChannel", tl, 0, 0)
		buf.Call("copyToChannel", tr, 1, 0)
	} else {
		// copyToChannel is not defined on Safari 11
		buf.Call("getChannelData", 0).Call("set", tl)
		buf.Call("getChannelData", 1).Call("set", tr)
	}
	freel()
	freer()

	s := p.context.Call("createBufferSource")
	s.Set("buffer", buf)
	s.Call("connect", p.context.Get("destination"))
	s.Call("start", p.nextPos)
	p.nextPos += buf.Get("duration").Float()

	p.tmp = p.tmp[le:]
	return n, nil
}

func (p *driver) Close() error {
	for event, f := range p.callbacks {
		// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
		// "Calling removeEventListener() with arguments that do not identify any currently registered EventListener on the EventTarget has no effect."
		js.Global().Get("document").Call("removeEventListener", event, f)
		f.Release()
	}
	p.callbacks = nil
	return nil
}

func (d *driver) tryWriteCanReturnWithoutWaiting() bool {
	return true
}
