// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.eavadeaux.net/>
//
// Klipspringer is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Klipspringer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.

package net.avadeaux.klipspringer.alsa;

import java.io.IOException;
import java.io.PrintStream;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import javax.sound.sampled.*;
import net.avadeaux.klipspringer.codec.*;

class AlsaDevice implements Device {
    private static class FormatDetails {
        final boolean signed, bigend;
        final int ss;
        FormatDetails(boolean signed, int ss, boolean bigend) {
            this.signed = signed; this.ss = ss; this.bigend = bigend;
        }
        public String toString() {
            return (signed ? "signed, " : "unsigned, ")
                + (bigend ? "big endian, " : "little endian, ")
                + ss + " bytes/sample";
        }
    }

    private static class FormatList extends LinkedList<FormatDetails> { }
    private static class DepthMap extends TreeMap<Integer, FormatList> { }

    final String name;
    private final String description;
    private final boolean input;
    private final DepthMap depths = new DepthMap();
    private final List<String> rates = new ArrayList<String>();
    private final List<String> channels = new ArrayList<String>();
    private boolean pausable;

    private AlsaDevice(String name, String description, boolean input) {
        this.name = name;
        this.description = description;
        this.input = input;
    }

    boolean pausable() { return pausable; }

    public String name() {
        return "\"klipalsa:"+name+"\"";
    }

    public String toString() {
        return name()+" ("+description+")";
    }

    private void addFormat(boolean signed, int bips, int ss, boolean bigend) {
        FormatList formats = depths.get(bips);
        if (formats == null) { depths.put(bips, (formats = new FormatList())); }
        formats.add(new FormatDetails(signed, ss, bigend));
    }

    private void addRates(int lo, int hi) { rates.add(lo == hi ? lo+"" : lo+"-"+((long) hi & 0xffffffffL)); }

    private void addChannels(int lo, int hi) {
        if      (lo == hi) { channels.add(lo == 1 ? "mono" : "stereo"); }
        else if (hi == 2)  { channels.add("mono/stereo"); }
        else               { channels.add(lo+"-"+((long) hi & 0xffffffffL)); }
    }

    private void setPausable(boolean pausable) { this.pausable = pausable; }

    // To allow outputs to scale up bit depth.
    int scaledBips(int bips) {
        if (!input && depths.get(bips) == null) {
            if (bips > 16 && bips < 24 && depths.get(24) != null) { return 24; }
            if (bips > 16 && depths.get(32) != null) { return 32; }
            if (bips < 8 && depths.get(8) != null) { return 8; }
            if (bips < 16 && depths.get(16) != null) { return 16; }
        }
        return bips;
    }

    public Device.BitLayout layout(PcmFormat format) {
        return scaledBips(format.bips()) > format.bips() ? Device.BitLayout.MSB : Device.BitLayout.LSB;
    }

    public PcmFormat selectFormat(AudioFormat format) throws UnsupportedFormatException {
        boolean signed = format.getEncoding() == AudioFormat.Encoding.PCM_SIGNED;
        if (!signed && format.getEncoding() != AudioFormat.Encoding.PCM_UNSIGNED) {
            throw new UnsupportedFormatException("Non-PCM format not supported: "+format);
        }
        int bips = format.getSampleSizeInBits();
        int sbips = format instanceof PcmFormat ? ((PcmFormat) format).significantBips() : format.getSampleSizeInBits();
        int channels = format.getChannels();
        int ss = format.getFrameSize() / channels;
        FormatList formats = depths.get(scaledBips(bips));
        if (formats == null) { throw new UnsupportedFormatException("Format not supported: "+format); }
        FormatDetails r = null;
        for (FormatDetails f : formats) {
            if (r == null
                || r.ss != ss && f.ss == ss
                || r.signed != signed && f.signed == signed
                || r.bigend != format.isBigEndian() && f.bigend == format.isBigEndian())
                { r = f; }
        }
        if (r == null) { throw new UnsupportedFormatException("Format not supported: "+format); }
        return new PcmFormat((int) format.getSampleRate(), bips, channels, r.signed, channels*r.ss, r.bigend ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN, sbips);
    }

    public void printAvailableFormats(PrintStream out) {
        if (depths.size() > 0) {
            out.print(depths.size() == 1 ? "    Bit depth: " : "    Bit depths: ");
            out.println(depths.keySet().stream().map(i -> i+"").collect(Collectors.joining(", ")));
        }
        if (rates.size() > 0) {
            out.print(rates.size() == 1 ? "    Sample rate: " : "    Sample rates: ");
            out.print(rates.stream().collect(Collectors.joining(", ")));
            out.println(" Hz");
        }
        if (channels.size() > 0) {
            out.print("    Channels: ");
            out.println(channels.stream().collect(Collectors.joining(", ")));
        }
    }

    private static class DeviceCollector {
        final Map<String, Device> devices = new LinkedHashMap<String, Device>();
        final boolean input;

        DeviceCollector(boolean input) { this.input = input; }

        void addDevices(String cardName, String devName, int card, int dev, int subdev) {
            String devId = dev == -1
                ? ""+card
                : (subdev == -1 ? card+","+dev
                   : card+","+dev+","+subdev);
            String name = cardName+", "+devName;
            devices.put("klipalsa:hw:"+devId, new AlsaDevice("hw:"+devId, name, input));
            devices.put("klipalsa:plughw:"+devId, new AlsaDevice("plughw:"+devId, name, input));
        }
    }

    static Map<String, Device> collectDevices(boolean input) {
        if (!Library.alsaSupported()) { return Collections.emptyMap(); }
        DeviceCollector devs = new DeviceCollector(input);
        collectDevices(devs, input);
        for (var dev : devs.devices.values()) {
            AlsaDevice adev = (AlsaDevice) dev;
            byte[] devBytes = adev.name.getBytes(StandardCharsets.UTF_8);
            try {
                collectFormats(Arrays.copyOf(devBytes, devBytes.length+1), adev, input);
            } catch (IOException ex) {
                // Ignore if cannot open.
                if (System.getProperty("klipspringer.debug") != null) { ex.printStackTrace(); }
            }
        }
        return devs.devices;
    }

    private static native void collectDevices(DeviceCollector devices, boolean input);
    private static native void collectFormats(byte[] devName, AlsaDevice device, boolean input) throws IOException;
}
