// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.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;

import java.io.IOException;
import java.io.Writer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.Executor;
import javax.sound.sampled.*;
import net.avadeaux.klipspringer.codec.*;

public abstract class CommandInterface {
    @FunctionalInterface
    protected interface Command {
        void exec(Query q, Writer w) throws IOException;
    }

    protected final String NO_PLAYER_ID = "";

    protected final Map<String, Command> cmds = new HashMap<String, Command>();
    protected final Map<String, AudioStream> streamHandles = new HashMap<String, AudioStream>();

    private final Executor streamKeepAliveExecutor;
    private final double streamKeepAliveTimeoutSecs;

    public CommandInterface(Executor streamKeepAliveExecutor, double streamKeepAliveTimeoutSecs) {
        this.streamKeepAliveExecutor = streamKeepAliveExecutor;
        this.streamKeepAliveTimeoutSecs = streamKeepAliveTimeoutSecs;
    }

    private Device.Player.Factory streamFactory(Query q, WritableByteChannel out) {
        String type = q.value("type", "flac");
        double writeaheadSecs = q.doubleValue("writeahead", 10.0);
        double timeoutSecs = q.doubleValue("timeout", 0);
        int bufferFrames = q.intValue("buf_frames", 4096);
        String id = q.value("player_id", NO_PLAYER_ID);
        if ("oga".equals(type) || "flac".equals(type)) {
            int holdingBytes = q.intValue("clientbuf", 49152);
            boolean oga = "oga".equals(type);
            int comprLevel = q.intValue("compr_level", 5);
            return new AudioStream.Factory() {
                public AudioStream open(PcmFormat fmt) throws IOException {
                    FlacStream stream = new FlacStream(fmt, oga, comprLevel, bufferFrames, writeaheadSecs, timeoutSecs, holdingBytes, out, streamKeepAliveExecutor, streamKeepAliveTimeoutSecs);
                    streamHandles.put(id, stream);
                    return stream;
                }
                public Device.BitLayout layout(PcmFormat fmt) { return FlacStream.BIT_LAYOUT; }
                public PcmFormat selectFormat(AudioFormat fmt) throws UnsupportedFormatException {
                    return FlacStream.selectFormat(fmt);
                }
            };
        } else if ("ogg".equals(type)) {
            int holdingBytes = q.intValue("clientbuf", 49152);
            float quality = (float) q.doubleValue("quality", 0.9);
            return new AudioStream.Factory() {
                public AudioStream open(PcmFormat fmt) throws IOException {
                    VorbisStream stream = new VorbisStream(fmt, quality, writeaheadSecs, timeoutSecs, holdingBytes, out, streamKeepAliveExecutor, streamKeepAliveTimeoutSecs);
                    streamHandles.put(id, stream);
                    return stream;
                }
                public Device.BitLayout layout(PcmFormat fmt) { return VorbisStream.BIT_LAYOUT; }
                public PcmFormat selectFormat(AudioFormat fmt) throws UnsupportedFormatException {
                    return VorbisStream.selectFormat(fmt);
                }
            };
        } else if ("mp3".equals(type)) {
            int bitrate = q.intValue("rate", 320);
            int quality = q.intValue("quality", 2);
            return new AudioStream.Factory() {
                public AudioStream open(PcmFormat fmt) throws IOException {
                    LameStream stream = new LameStream(fmt, bitrate, quality, bufferFrames, writeaheadSecs, timeoutSecs, out, streamKeepAliveExecutor, streamKeepAliveTimeoutSecs);
                    streamHandles.put(id, stream);
                    return stream;
                }
                public Device.BitLayout layout(PcmFormat fmt) { return LameStream.BIT_LAYOUT; }
                public PcmFormat selectFormat(AudioFormat fmt) throws UnsupportedFormatException {
                    return LameStream.selectFormat(fmt);
                }
            };
        } else {
            throw new IllegalArgumentException("Unrecognized stream type: "+type);
        }
    }

    public void exec(Query q, Writer w) throws IOException {
        String cmd = q.value("cmd", null);
        if (cmd == null) {
            w.append("{ \"err\": \"No cmd in query string\" }");
            return;
        }
        Command c = cmds.get(cmd);
        if (c == null) {
            w.append("{ \"err\": \"unknown command: "+cmd+"\" }");
            return;
        }
        c.exec(q, w);
    }

    public void process(Query q, WritableByteChannel out) throws IOException {
        if ("fetchstream".equals(q.value("cmd", null))) {
            fetchStream(q, streamFactory(q, out));
        } else {
            Writer w = Channels.newWriter(out, StandardCharsets.UTF_8);
            try {
                exec(q, w);
            } finally {
                w.append("\n").flush();
                w.close();
            }
        }
    }

    protected abstract void fetchStream(Query q, Device.Player.Factory streamFact) throws IOException;
}
