/*
 * Copyright 2015-2020 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.dv8tion.jda.internal.requests;

import gnu.trove.map.TLongObjectMap;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.managers.AudioManager;
import net.dv8tion.jda.api.utils.data.DataObject;
import net.dv8tion.jda.internal.JDAImpl;
import net.dv8tion.jda.internal.audio.ConnectionRequest;
import net.dv8tion.jda.internal.audio.ConnectionStage;
import org.slf4j.Logger;

import java.util.Queue;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

//Helper class delegated to WebSocketClient
class WebSocketSendingThread implements Runnable
{
    private static final Logger LOG = WebSocketClient.LOG;

    private final WebSocketClient client;
    private final JDAImpl api;
    private final ReentrantLock queueLock;
    private final Queue<DataObject> chunkQueue;
    private final Queue<String> ratelimitQueue;
    private final TLongObjectMap<ConnectionRequest> queuedAudioConnections;
    private final ScheduledExecutorService executor;
    private Future<?> handle;

    private boolean needRateLimit = false;
    private boolean attemptedToSend = false;
    private boolean shutdown = false;

    WebSocketSendingThread(WebSocketClient client)
    {
        this.client = client;
        this.api = client.api;
        this.queueLock = client.queueLock;
        this.chunkQueue = client.chunkSyncQueue;
        this.ratelimitQueue = client.ratelimitQueue;
        this.queuedAudioConnections = client.queuedAudioConnections;
        this.executor = client.executor;
    }

    public void shutdown()
    {
        shutdown = true;
        if (handle != null)
            handle.cancel(false);
    }

    public void start()
    {
        shutdown = false;
        handle = executor.submit(this);
    }

    private void scheduleIdle()
    {
        if (shutdown)
            return;
        handle = executor.schedule(this, 500, TimeUnit.MILLISECONDS);
    }

    private void scheduleSentMessage()
    {
        if (shutdown)
            return;
        handle = executor.schedule(this, 10, TimeUnit.MILLISECONDS);
    }

    private void scheduleRateLimit()
    {
        if (shutdown)
            return;
        handle = executor.schedule(this, 1, TimeUnit.MINUTES);
    }

    @Override
    public void run()
    {
        //Make sure that we don't send any packets before sending auth info.
        if (!client.sentAuthInfo)
        {
            scheduleIdle();
            return;
        }

        ConnectionRequest audioRequest = null;
        DataObject chunkRequest = null;
        try
        {
            api.setContext();
            attemptedToSend = false;
            needRateLimit = false;
            // We do this outside of the lock because otherwise we could potentially deadlock here
            audioRequest = client.getNextAudioConnectRequest();
            queueLock.lockInterruptibly();

            chunkRequest = chunkQueue.peek();
            if (chunkRequest != null)
                handleChunkSync(chunkRequest);
            else if (audioRequest != null)
                handleAudioRequest(audioRequest);
            else
                handleNormalRequest();
        }
        catch (InterruptedException ignored)
        {
            LOG.debug("Main WS send thread interrupted. Most likely JDA is disconnecting the websocket.");
            return;
        }
        catch (Throwable ex)
        {
            // Log error
            LOG.error("Encountered error in gateway worker", ex);

            if (!attemptedToSend)
            {
                // Try to remove the failed request
                if (chunkRequest != null)
                    client.chunkSyncQueue.remove(chunkRequest);
                else if (audioRequest != null)
                    client.removeAudioConnection(audioRequest.getGuildIdLong());
            }

            // Rethrow if error to kill thread
            if (ex instanceof Error)
                throw (Error) ex;
        }
        finally
        {
            // on any exception that might cause this lock to not release
            client.maybeUnlock();
        }

        scheduleNext();
    }

    private void scheduleNext()
    {
        try
        {
            if (needRateLimit)
                scheduleRateLimit();
            else if (!attemptedToSend)
                scheduleIdle();
            else
                scheduleSentMessage();
        }
        catch (RejectedExecutionException ex)
        {
            LOG.error("Was unable to schedule next packet due to rejected execution by threadpool", ex);
        }
    }

    private void handleChunkSync(DataObject chunkOrSyncRequest)
    {
        LOG.debug("Sending chunk/sync request {}", chunkOrSyncRequest);
        boolean success = send(
            DataObject.empty()
                .put("op", WebSocketCode.MEMBER_CHUNK_REQUEST)
                .put("d", chunkOrSyncRequest)
                .toString()
        );

        if (success)
            chunkQueue.remove();
    }

    private void handleAudioRequest(ConnectionRequest audioRequest)
    {
        long channelId = audioRequest.getChannelId();
        long guildId = audioRequest.getGuildIdLong();
        Guild guild = api.getGuildById(guildId);
        if (guild == null)
        {
            LOG.debug("Discarding voice request due to null guild {}", guildId);
            // race condition on guild delete, avoid NPE on DISCONNECT requests
            queuedAudioConnections.remove(guildId);
            return;
        }
        ConnectionStage stage = audioRequest.getStage();
        AudioManager audioManager = guild.getAudioManager();
        DataObject packet;
        switch (stage)
        {
            case RECONNECT:
            case DISCONNECT:
                packet = newVoiceClose(guildId);
                break;
            default:
            case CONNECT:
                packet = newVoiceOpen(audioManager, channelId, guild.getIdLong());
        }
        LOG.debug("Sending voice request {}", packet);
        if (send(packet.toString()))
        {
            //If we didn't get RateLimited, Next request attempt will be 2 seconds from now
            // we remove it in VoiceStateUpdateHandler once we hear that it has updated our status
            // in 2 seconds we will attempt again in case we did not receive an update
            audioRequest.setNextAttemptEpoch(System.currentTimeMillis() + 2000);
            //If we are already in the correct state according to voice state
            // we will not receive a VOICE_STATE_UPDATE that would remove it
            // thus we update it here
            final GuildVoiceState voiceState = guild.getSelfMember().getVoiceState();
            client.updateAudioConnection0(guild.getIdLong(), voiceState.getChannel());
        }
    }

    private void handleNormalRequest()
    {
        String message = ratelimitQueue.peek();
        if (message != null)
        {
            LOG.debug("Sending normal message {}", message);
            if (send(message))
                ratelimitQueue.remove();
        }
    }

    //returns true if send was successful
    private boolean send(String request)
    {
        needRateLimit = !client.send(request, false);
        attemptedToSend = true;
        return !needRateLimit;
    }

    protected DataObject newVoiceClose(long guildId)
    {
        return DataObject.empty()
            .put("op", WebSocketCode.VOICE_STATE)
            .put("d", DataObject.empty()
                .put("guild_id", Long.toUnsignedString(guildId))
                .putNull("channel_id")
                .put("self_mute", false)
                .put("self_deaf", false));
    }

    protected DataObject newVoiceOpen(AudioManager manager, long channel, long guild)
    {
        return DataObject.empty()
            .put("op", WebSocketCode.VOICE_STATE)
            .put("d", DataObject.empty()
                .put("guild_id", guild)
                .put("channel_id", channel)
                .put("self_mute", manager.isSelfMuted())
                .put("self_deaf", manager.isSelfDeafened()));
    }
}