package org.jboss.resteasy.plugins.providers.sse;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.Arrays;

import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.Providers;
import jakarta.ws.rs.sse.InboundSseEvent;

import org.jboss.resteasy.plugins.providers.RegisterBuiltin;
import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.jboss.resteasy.spi.ResteasyProviderFactory;

public class InboundSseEventImpl implements InboundSseEvent {
    private final String name;

    private final String id;

    private final String comment;

    private final byte[] data;

    private final long reconnectDelay;

    private final Annotation[] annotations;

    private final MediaType mediaType;

    private final MultivaluedMap<String, String> headers;

    private final Providers providers;

    static class Builder {
        private String name;

        private String id;

        private long reconnectDelay = -1;

        private final ByteArrayOutputStream dataStream;

        private final Annotation[] annotations;

        private final MediaType mediaType;

        private final MultivaluedMap<String, String> headers;

        private final StringBuilder commentBuilder;

        private Providers providers;

        Builder(final Annotation[] annotations, final MediaType mediaType, final MultivaluedMap<String, String> headers) {
            this.annotations = annotations;
            this.mediaType = mediaType;
            this.headers = headers;

            this.commentBuilder = new StringBuilder();
            this.dataStream = new ByteArrayOutputStream();
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder id(String id) {
            this.id = id;
            return this;
        }

        public Builder commentLine(final CharSequence commentLine) {
            if (commentLine != null) {
                commentBuilder.append(commentLine).append('\n');
            }

            return this;
        }

        public Builder reconnectDelay(long milliseconds) {
            this.reconnectDelay = milliseconds;
            return this;
        }

        public Builder write(byte[] data) {
            if (data == null || data.length == 0) {
                return this;
            }
            try {
                this.dataStream.write(data);
            } catch (IOException ex) {
                throw new ProcessingException(Messages.MESSAGES.failedToWriteDataToInboudEvent(), ex);
            }
            return this;
        }

        public Builder providers(Providers providers) {
            this.providers = providers;
            return this;
        }

        public InboundSseEvent build() {
            //from https://html.spec.whatwg.org/multipage/server-sent-events.html#processField
            //If the data buffer's last character is a U+000A LINE FEED (LF) character,
            //then remove the last character from the data buffer
            return new InboundSseEventImpl(name, id, commentBuilder.length() > 0 ? commentBuilder.substring(0,
                    commentBuilder.length() - 1) : null, reconnectDelay, dataStream.toByteArray(), annotations, mediaType,
                    headers, providers);
        }
    }

    private InboundSseEventImpl(final String name, final String id, final String comment, final long reconnectDelay,
            final byte[] data, final Annotation[] annotations, final MediaType mediaType,
            final MultivaluedMap<String, String> headers, final Providers providers) {
        this.name = name;
        this.id = id;
        this.comment = comment;
        this.reconnectDelay = reconnectDelay;
        this.data = data;
        this.annotations = annotations;
        this.mediaType = mediaType;
        this.headers = headers;
        this.providers = providers;
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }

    public String getComment() {
        return comment;
    }

    public long getReconnectDelay() {
        if (reconnectDelay < 0) {
            return -1;
        }
        return reconnectDelay;
    }

    public boolean isReconnectDelaySet() {
        return reconnectDelay > -1;
    }

    public boolean isEmpty() {
        return data.length == 0;
    }

    public String readData() {
        return readData(SseConstants.STRING_AS_GENERIC_TYPE);
    }

    public <T> T readData(Class<T> type) {
        return readData(new GenericType<T>(type), MediaType.TEXT_PLAIN_TYPE);
    }

    public <T> T readData(GenericType<T> type) {
        return readData(type, MediaType.TEXT_PLAIN_TYPE);
    }

    public <T> T readData(Class<T> messageType, MediaType mediaType) {
        return readData(new GenericType<T>(messageType), mediaType);
    }

    public <T> T readData(GenericType<T> type, MediaType mediaType) {
        //System.out.println("Thread " + Thread.currentThread().getName() + "read data");
        final MediaType effectiveMediaType = mediaType == null ? this.mediaType : mediaType;
        @SuppressWarnings("rawtypes")
        MessageBodyReader reader = null;
        if (this.providers != null) {
            reader = providers.getMessageBodyReader(type.getRawType(), type.getType(), annotations, mediaType);
        } else {
            ResteasyProviderFactory factory = ResteasyProviderFactory.getInstance();
            if (!factory.isBuiltinsRegistered()) {
                RegisterBuiltin.register(factory);
            }
            reader = factory.getClientMessageBodyReader(type.getRawType(), type.getType(), annotations, mediaType);
        }
        if (reader == null) {
            throw new IllegalStateException(Messages.MESSAGES.notFoundMBR(type.getClass().getName()));
        }
        return readAndCast(type, effectiveMediaType, reader);
    }

    @SuppressWarnings("unchecked")
    private <T> T readAndCast(GenericType<T> type, MediaType effectiveMediaType,
            @SuppressWarnings("rawtypes") MessageBodyReader reader) {
        try {
            return (T) reader.readFrom(type.getRawType(), type.getType(), annotations, effectiveMediaType, headers,
                    new ByteArrayInputStream(data));
        } catch (IOException ex) {
            throw new ProcessingException(Messages.MESSAGES.failedToReadData(), ex);
        }
    }

    public byte[] getRawData() {
        if (data.length == 0) {
            return data;
        }

        return Arrays.copyOf(data, data.length);
    }

    @Override
    public String toString() {
        String s;

        try {
            s = readData();
        } catch (ProcessingException e) {
            s = "Exception:" + e.getLocalizedMessage();
        }

        return "InboundSseEvent{id=" + id + '\'' + ", comment=" + (comment == null ? "[]" : '\'' + comment + '\'')
                + ", data=" + s + '}';
    }

    public MediaType getMediaType() {
        return mediaType;
    }
}
