// 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.codec;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/** Common base class for decoders that invoke native libraries. */
public abstract class ExternalDecoder implements AudioDecoder {
    static { Library.init(); }

    private enum State { NEW, INITIALIZED, METADATA_DECODED, SEEK, SEEK_ABORT, SEEK_OK, AUDIO_DECODED, CLOSED }

    private final Target target;
    private final long handle;
    private State state = State.NEW;

    /** Called from constructor to throw runtime exception if file type not supported. */
    abstract void checkSupported();

    /** Initiates decoder, including opening the file. */
    ExternalDecoder(String fileName, Target target) throws IOException {
        checkSupported();
        this.target = target;
        byte[] fnamUtf8 = fileName.getBytes(StandardCharsets.UTF_8);
        try {
            handle = create(fileName, Arrays.copyOf(fnamUtf8, fnamUtf8.length+1));
        } catch (FileNotFoundException ex) { throw new FileNotFoundException(fileName+": "+ex.getMessage()); }
        if (handle == 0) {
            // Should be impossible, create should have thrown, but just in case.
            throw new IOException("Invalid FLAC decoder handle");
        }
        state = State.INITIALIZED;
    }

    /** Always returns true, indicating that decodeAll can be called more than once, each time
      * resetting the underlying input.
      */
    public boolean reusable() { return true; }

    public synchronized void close() throws IOException {
        if (state != State.CLOSED) {
            free(handle);
            state = State.CLOSED;
            target.close();
        }
    }

    // Rethrows exception after closing.
    private void fail(RuntimeException ex) throws IOException {
        try { close(); } catch (Throwable th) {
            if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
        }
        throw ex;
    }

    // Rethrows exception after closing.
    private void fail(IOException ex) throws IOException {
        try { close(); } catch (Throwable th) {
            if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
        }
        throw ex;
    }

    public synchronized void decodeMetadata() throws IOException {
        try {
            if (state != State.INITIALIZED) {
                throw new IllegalStateException("Attempted decodeMetadata in state "+state);
            }
            nativeDecodeMetadata(handle);
        } catch (IOException ex) { fail(ex); } catch (RuntimeException ex) { fail(ex); }
    }

    public synchronized boolean decodeAll(long fromFrame) throws IOException {
        try {
            if (state == State.INITIALIZED) {
                decodeMetadata();
            } else if (state == State.AUDIO_DECODED) {
                rewind(handle);
                state = State.METADATA_DECODED;
            } else if (state != State.METADATA_DECODED) {
                throw new IllegalStateException("Attempted decodeAll in state "+state);
            }
            if (fromFrame != 0) {
                state = State.SEEK;
                nativeSeek(handle, fromFrame);
            }
            boolean r = state == State.SEEK_ABORT ? false : nativeDecodeAll(handle);
            state = State.AUDIO_DECODED;
            return r;
        } catch (IOException ex) { fail(ex); } catch (RuntimeException ex) { fail(ex); }
        throw new IllegalStateException(); // unreachable
    }

    // Callback function.
    private void metadata(int rate, int bips, int ss, int channels, ByteOrder bo, long totalFrames)
        throws IOException
    {
        target.metadata(new PcmFormat(rate, bips, channels, true, channels*ss, bo, bips), totalFrames);
        state = State.METADATA_DECODED;
    }

    // Callback function. Handles write callback from seek specially to prevent seek failure.
    private boolean write(ByteBuffer buffer) throws IOException {
        boolean r = target.write(buffer);
        if (state == State.SEEK) {
            state = r ? State.SEEK_OK : State.SEEK_ABORT;
            return true;
        }
        return r;
    }

    // Abstract methods that invoke external API.
    abstract long create(String fnam, byte[] bfnam) throws IOException;
    abstract void rewind(long handle) throws IOException;
    abstract void free(long handle);
    abstract void nativeSeek(long handle, long samplePos) throws IOException;
    abstract void nativeDecodeMetadata(long handle) throws IOException;
    abstract boolean nativeDecodeAll(long handle) throws IOException;
}
