/*
 * Copyright (c) 2010-2025 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.core.audio.internal.javasound;

import java.io.IOException;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line.Info;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.utils.AudioWaveUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is a class that plays an AudioStream through the Java sound API
 *
 * @author Kelly Davis - Initial contribution and API
 * @author Kai Kreuzer - Refactored to use AudioStream and logging
 *
 */
@NonNullByDefault
public class AudioPlayer extends Thread {

    private final Logger logger = LoggerFactory.getLogger(AudioPlayer.class);

    /**
     * The AudioStream to play
     */
    private final AudioStream audioStream;

    /**
     * Constructs an AudioPlayer to play the passed AudioSource
     *
     * @param audioStream The AudioStream to play
     */
    public AudioPlayer(AudioStream audioStream) {
        this.audioStream = audioStream;
    }

    /**
     * This method plays the contained AudioSource
     */
    @Override
    public void run() {
        SourceDataLine line;

        org.openhab.core.audio.AudioFormat openhabAudioFormat = audioStream.getFormat();

        AudioFormat audioFormat = convertAudioFormat(openhabAudioFormat);
        if (audioFormat == null) {
            logger.warn("Audio format is unsupported or does not have enough details in order to be played");
            return;
        }
        DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
        try {
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(audioFormat);
        } catch (Exception e) {
            logger.warn("No line found: {}", e.getMessage());
            logger.info("Available lines are:");
            Mixer.Info[] mixerInfo = AudioSystem.getMixerInfo(); // get available mixers
            Mixer mixer;
            for (Mixer.Info value : mixerInfo) {
                mixer = AudioSystem.getMixer(value);
                Info[] lineInfos = mixer.getSourceLineInfo();
                for (Info lineInfo : lineInfos) {
                    logger.info("{}", lineInfo);
                }
            }
            return;
        }
        line.start();
        int nRead = 0;
        byte[] abData = new byte[65532]; // needs to be a multiple of 4 and 6, to support both 16 and 24 bit stereo
        try {
            // If this is a wav container, we should remove the header from the stream
            // to avoid a "clack" noise at the beginning
            if (org.openhab.core.audio.AudioFormat.CONTAINER_WAVE.equals(openhabAudioFormat.getContainer())) {
                AudioWaveUtils.removeFMT(audioStream);
            }
            while (-1 != nRead) {
                nRead = audioStream.read(abData, 0, abData.length);
                if (nRead >= 0) {
                    line.write(abData, 0, nRead);
                }
            }
        } catch (IOException e) {
            logger.error("Error while playing audio: {}", e.getMessage());
            return;
        } finally {
            line.drain();
            line.close();
            try {
                audioStream.close();
            } catch (IOException e) {
            }
        }
    }

    /**
     * Converts an org.openhab.core.audio.AudioFormat
     * to a javax.sound.sampled.AudioFormat
     *
     * @param audioFormat The AudioFormat to convert
     * @return The corresponding AudioFormat
     */
    protected @Nullable AudioFormat convertAudioFormat(org.openhab.core.audio.AudioFormat audioFormat) {
        String codec = audioFormat.getCodec();
        AudioFormat.Encoding encoding = new AudioFormat.Encoding(codec);
        if (org.openhab.core.audio.AudioFormat.CODEC_PCM_SIGNED.equals(codec)) {
            encoding = AudioFormat.Encoding.PCM_SIGNED;
        } else if (org.openhab.core.audio.AudioFormat.CODEC_PCM_ULAW.equals(codec)) {
            encoding = AudioFormat.Encoding.ULAW;
        } else if (org.openhab.core.audio.AudioFormat.CODEC_PCM_ALAW.equals(codec)) {
            encoding = AudioFormat.Encoding.ALAW;
        }

        final Long frequency = audioFormat.getFrequency();
        if (frequency == null) {
            return null;
        }
        final float sampleRate = frequency.floatValue();

        final Integer bitDepth = audioFormat.getBitDepth();
        if (bitDepth == null) {
            return null;
        }
        final int sampleSizeInBits = bitDepth.intValue();

        final int channels = audioFormat.getChannels() == null ? Integer.valueOf(1) : audioFormat.getChannels();

        final int frameSize = channels * sampleSizeInBits / 8;

        final float frameRate = channels * sampleRate / frameSize;

        final Boolean bigEndian = audioFormat.isBigEndian();
        if (bigEndian == null) {
            return null;
        }

        return new AudioFormat(encoding, sampleRate, sampleSizeInBits, channels, frameSize, frameRate, bigEndian);
    }
}
