package allen.town.podcast.parser.media.vorbis;

import androidx.annotation.NonNull;
import org.apache.commons.io.EndianUtils;
import org.apache.commons.io.IOUtils;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Locale;

public abstract class VorbisCommentReader {
    private static final String TAG = "VorbisCommentReader";
    private static final int FIRST_OGG_PAGE_LENGTH = 58;
    private static final int FIRST_OPUS_PAGE_LENGTH = 47;
    private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024;
    private static final int PACKET_TYPE_IDENTIFICATION = 1;
    private static final int PACKET_TYPE_COMMENT = 3;

    private final InputStream input;

    VorbisCommentReader(InputStream input) {
        this.input = input;
    }

    public void readInputStream() throws VorbisCommentReaderException {
        try {
            findIdentificationHeader();
            findOggPage();
            findCommentHeader();
            VorbisCommentHeader commentHeader = readCommentHeader();
            Log.d(TAG, commentHeader.toString());
            for (int i = 0; i < commentHeader.getUserCommentLength(); i++) {
                readUserComment();
            }
        } catch (IOException e) {
            Log.d(TAG, "Vorbis parser: " + e.getMessage());
        }
    }

    private void findOggPage() throws IOException {
        // find OggS
        byte[] buffer = new byte[4];
        final byte[] oggPageHeader = {'O', 'g', 'g', 'S'};
        for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) {
            int data = input.read();
            if (data == -1) {
                throw new IOException("EOF while trying to find vorbis page");
            }
            buffer[bytesRead % buffer.length] = (byte) data;
            if (bufferMatches(buffer, oggPageHeader, bytesRead)) {
                break;
            }
        }
        // read segments
        IOUtils.skipFully(input, 22);
        int numSegments = input.read();
        IOUtils.skipFully(input, numSegments);
    }

    private void readUserComment() throws VorbisCommentReaderException {
        try {
            long vectorLength = EndianUtils.readSwappedUnsignedInteger(input);
            if (vectorLength > 20 * 1024 * 1024) {
                String keyPart = readUtf8String(10);
                throw new VorbisCommentReaderException("User comment unrealistically long. "
                        + "key=" + keyPart + ", length=" + vectorLength);
            }
            String key = readContentVectorKey(vectorLength).toLowerCase(Locale.US);
            boolean shouldReadValue = handles(key);
            Log.d(TAG, "key=" + key + ", length=" + vectorLength + ", handles=" + shouldReadValue);
            if (shouldReadValue) {
                String value = readUtf8String(vectorLength - key.length() - 1);
                onContentVectorValue(key, value);
            } else {
                IOUtils.skipFully(input, vectorLength - key.length() - 1);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String readUtf8String(long length) throws IOException {
        byte[] buffer = new byte[(int) length];
        IOUtils.readFully(input, buffer);
        Charset charset = Charset.forName("UTF-8");
        return charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString();
    }

    /**
     * Looks for an identification header in the first page of the file. If an
     * identification header is found, it will be skipped completely
     */
    private void findIdentificationHeader() throws IOException {
        byte[] buffer = new byte[FIRST_OPUS_PAGE_LENGTH];
        IOUtils.readFully(input, buffer);
        final byte[] oggIdentificationHeader = new byte[]{ PACKET_TYPE_IDENTIFICATION, 'v', 'o', 'r', 'b', 'i', 's' };
        for (int i = 6; i < buffer.length; i++) {
            if (bufferMatches(buffer, oggIdentificationHeader, i)) {
                IOUtils.skip(input, FIRST_OGG_PAGE_LENGTH - FIRST_OPUS_PAGE_LENGTH);
                return;
            } else if (bufferMatches(buffer, "OpusHead".getBytes(), i)) {
                return;
            }
        }
        throw new IOException("there is no vorbis identification header");
    }

    private void findCommentHeader() throws IOException {
        byte[] buffer = new byte[64]; // Enough space for some bytes. Used circularly.
        final byte[] oggCommentHeader = new byte[]{ PACKET_TYPE_COMMENT, 'v', 'o', 'r', 'b', 'i', 's' };
        for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) {
            buffer[bytesRead % buffer.length] = (byte) input.read();
            if (bufferMatches(buffer, oggCommentHeader, bytesRead)) {
                return;
            } else if (bufferMatches(buffer, "OpusTags".getBytes(), bytesRead)) {
                return;
            }
        }
        throw new IOException("No comment header found");
    }

    /**
     * Reads backwards in haystack, starting at position. Checks if the bytes match needle.
     * Uses haystack circularly, so when reading at (-1), it reads at (length - 1).
     */
    boolean bufferMatches(byte[] haystack, byte[] needle, int position) {
        for (int i = 0; i < needle.length; i++) {
            int posInHaystack = position - i;
            while (posInHaystack < 0) {
                posInHaystack += haystack.length;
            }
            posInHaystack = posInHaystack % haystack.length;
            if (haystack[posInHaystack] != needle[needle.length - 1 - i]) {
                return false;
            }
        }
        return true;
    }

    @NonNull
    private VorbisCommentHeader readCommentHeader() throws IOException, VorbisCommentReaderException {
        try {
            long vendorLength = EndianUtils.readSwappedUnsignedInteger(input);
            String vendorName = readUtf8String(vendorLength);
            long userCommentLength = EndianUtils.readSwappedUnsignedInteger(input);
            return new VorbisCommentHeader(vendorName, userCommentLength);
        } catch (UnsupportedEncodingException e) {
            throw new VorbisCommentReaderException(e);
        }
    }

    private String readContentVectorKey(long vectorLength) throws IOException {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < vectorLength; i++) {
            char c = (char) input.read();
            if (c == '=') {
                return builder.toString();
            } else {
                builder.append(c);
            }
        }
        return null; // no key found
    }

    /**
     * Is called every time the Reader finds a content vector. The handler
     * should return true if it wants to handle the content vector.
     */
    protected abstract boolean handles(String key);

    /**
     * Is called if onContentVectorKey returned true for the key.
     */
    protected abstract void onContentVectorValue(String key, String value) throws VorbisCommentReaderException;
}
