// Copyright 2017 The Oto Authors
//
// 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
// +build !android
// +build !ios

package oto

/*
#cgo pkg-config: alsa

#include <alsa/asoundlib.h>

static void check(int *err, int newErr) {
  if (*err) {
    return;
  }
  *err = newErr;
}

static int ALSA_hw_params(
    snd_pcm_t          *pcm,
    unsigned           sampleRate,
    unsigned           numChans,
    snd_pcm_format_t   format,
    snd_pcm_uframes_t* buffer_size,
    snd_pcm_uframes_t* period_size) {
  snd_pcm_hw_params_t* params = NULL;
  int err = 0;
  snd_pcm_hw_params_alloca(&params);
  check(&err, snd_pcm_hw_params_any(pcm, params));

  check(&err, snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED));
  check(&err, snd_pcm_hw_params_set_format(pcm, params, format));
  check(&err, snd_pcm_hw_params_set_channels(pcm, params, numChans));
  check(&err, snd_pcm_hw_params_set_rate_resample(pcm, params, 1));
  check(&err, snd_pcm_hw_params_set_rate_near(pcm, params, &sampleRate, NULL));
  check(&err, snd_pcm_hw_params_set_buffer_size_near(pcm, params, buffer_size));
  check(&err, snd_pcm_hw_params_set_period_size_near(pcm, params, period_size, NULL));

  check(&err, snd_pcm_hw_params(pcm, params));

  return err;
}
*/
import "C"

import (
	"fmt"
	"unsafe"
)

type driver struct {
	handle          *C.snd_pcm_t
	buf             []byte
	bufSamples      int
	numChans        int
	bitDepthInBytes int
}

func alsaError(err C.int) error {
	return fmt.Errorf("oto: ALSA error: %s", C.GoString(C.snd_strerror(err)))
}

func newDriver(sampleRate, numChans, bitDepthInBytes, bufferSizeInBytes int) (tryWriteCloser, error) {
	p := &driver{
		numChans:        numChans,
		bitDepthInBytes: bitDepthInBytes,
	}

	// open a default ALSA audio device for blocking stream playback
	cs := C.CString("default")
	defer C.free(unsafe.Pointer(cs))
	if errCode := C.snd_pcm_open(&p.handle, cs, C.SND_PCM_STREAM_PLAYBACK, 0); errCode < 0 {
		return nil, alsaError(errCode)
	}

	// bufferSize is the total size of the main circular buffer fullness of this buffer
	// oscilates somewhere between bufferSize and bufferSize-periodSize
	bufferSize := C.snd_pcm_uframes_t(bufferSizeInBytes / (numChans * bitDepthInBytes))
	// periodSize is the number of samples that will be taken from the main circular
	// buffer at once, we leave this value to bufferSize, because ALSA will change that
	// to the maximum viable number, obviously lower than bufferSize
	periodSize := bufferSize

	// choose the correct sample format according to bitDepthInBytes
	var format C.snd_pcm_format_t
	switch bitDepthInBytes {
	case 1:
		format = C.SND_PCM_FORMAT_U8
	case 2:
		format = C.SND_PCM_FORMAT_S16_LE
	default:
		panic(fmt.Errorf("oto: bitDepthInBytes must be 1 or 2, got %d", bitDepthInBytes))
	}

	// set the device hardware parameters according to sampleRate, numChans, format, bufferSize
	// and periodSize
	//
	// bufferSize and periodSize are passed as pointers, because they may be changed according
	// to the wisdom of ALSA
	//
	// ALSA will try too keep them as close to what was requested as possible
	if errCode := C.ALSA_hw_params(p.handle, C.uint(sampleRate), C.uint(numChans), format, &bufferSize, &periodSize); errCode < 0 {
		p.Close()
		return nil, alsaError(errCode)
	}

	// Raspberry Pi 400 fix - for some reason the call to ALSA_hw_params can result in the bufferSize and periodSize
	// being the same (this could be becuase the rpi driver does not drop the periodSize below 1024, whereas desktop
	// Linux is happy to go lower). When the bufferSize and periodSize are the same it causes constant buffer underruns
	// that stutter the audio continuously.
	//
	// So in the case where an RPi (or any other driver) returns the same buffer and period size we will change the
	// buffer size to twice that of the period size and re-request hw params.
	if bufferSize == periodSize {
		bufferSize = periodSize * 2
		if errCode := C.ALSA_hw_params(p.handle, C.uint(sampleRate), C.uint(numChans), format, &bufferSize, &periodSize); errCode < 0 {
			p.Close()
			return nil, alsaError(errCode)
		}
	}

	// allocate the buffer of the size of the period, use the periodSize that we've got back
	// from ALSA after it's wise decision
	p.bufSamples = int(periodSize)
	p.buf = []byte{}

	return p, nil
}

func (p *driver) TryWrite(data []byte) (n int, err error) {
	bufSize := p.bufSamples * p.numChans * p.bitDepthInBytes
	for len(data) > 0 {
		toWrite := min(len(data), max(0, bufSize-len(p.buf)))
		p.buf = append(p.buf, data[:toWrite]...)
		data = data[toWrite:]
		n += toWrite

		// our buffer is not full and we've used up all the data, we'll keep them and finish
		if len(p.buf) < bufSize {
			break
		}

		// write samples to the main circular buffer
		wrote := C.snd_pcm_writei(p.handle, unsafe.Pointer(&p.buf[0]), C.snd_pcm_uframes_t(p.bufSamples))
		if wrote == -C.EPIPE {
			// Underrun!
			if errCode := C.snd_pcm_prepare(p.handle); errCode < 0 {
				return 0, alsaError(errCode)
			}
			continue
		}
		if wrote < 0 {
			// an error occurred while writing samples
			return 0, alsaError(C.int(wrote))
		}
		p.buf = p.buf[int(wrote)*p.numChans*p.bitDepthInBytes:]
	}
	return n, nil
}

func (p *driver) Close() error {
	// drop the remaining unprocessed samples in the main circular buffer
	if errCode := C.snd_pcm_drop(p.handle); errCode < 0 {
		return alsaError(errCode)
	}
	if errCode := C.snd_pcm_close(p.handle); errCode < 0 {
		return alsaError(errCode)
	}
	return nil
}

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