/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.commons.dto;

import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.retrieveIntValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.retrieveStringValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.storeIntValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.storeStringValueWithNullCheck;

import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;

import org.msgpack.core.MessagePacker;
import org.msgpack.core.MessageUnpacker;

import com.scytl.products.ov.commons.beans.domain.model.messaging.SafeStreamDeserializationException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.StreamSerializableClassType;
import com.scytl.products.ov.commons.dto.mixing.MixingPayload;
import com.scytl.products.ov.commons.dto.mixing.MixingPayloadImpl;

/**
 * Default implementation of a message that carries a mixing and decryption
 * payload.
 */
public class MixingDTOImpl implements MixingDTO {

    private String requestId;

    private MixingPayload payload;

    private Set<String> queuesToVisit;

    private String lastQueueName;

    private String lastNodeName;

    private MixingError error;

    public MixingDTOImpl() {

    }

    /**
     * Creates the initial mixing DTO.
     *
     * @param requestId
     *            an external tracking ID.
     * @param allQueues
     *            all the queues this payload should be sent to
     * @param payload
     *            the data to be processed
     */
    public MixingDTOImpl(String requestId, Set<String> allQueues, MixingPayload payload) {
        this.requestId = Objects.requireNonNull(requestId, "Missing the request ID");
        this.payload = Objects.requireNonNull(payload, "Missing the signed payload");

        Objects.requireNonNull(allQueues, "Missing the queues to visit");
        queuesToVisit = new LinkedHashSet<>(allQueues);
        if (queuesToVisit.isEmpty()) {
            throw new IllegalArgumentException(String.format("Payload %s has no place to go", payload.getVoteSetId()));
        }
    }

    /**
     * Updates the DTO with a new payload and its metadata.
     *
     * @param visitedQueue
     *            the queue the DTO was received from
     * @param visitedNode
     *            the node that processed the payload in the DTO
     * @param newPayload
     *            the processed data and the corresponding signature.
     */
    @Override
    public void update(String visitedQueue, String visitedNode, MixingPayload newPayload) {
        // If the DTO is successful, the last queue should not be re-visited.
        if (null == error) {
            if (!queuesToVisit.remove(visitedQueue)) {
                throw new IllegalArgumentException(
                    String.format("The recently visited queue was not expected: %s", visitedQueue));
            }
        }

        lastQueueName = visitedQueue;
        lastNodeName = visitedNode;
        payload = newPayload;
    }

    @Override
    public MixingPayload getPayload() {
        return payload;
    }

    @Override
    public String getRequestId() {
        return requestId;
    }

    @Override
    public Set<String> getQueuesToVisit() {
        return (null == queuesToVisit) ? null : Collections.unmodifiableSet(queuesToVisit);
    }

    @Override
    public String getLastQueueName() {
        return lastQueueName;
    }

    @Override
    public String getLastNodeName() {
        return lastNodeName;
    }

    @Override
    public void setError(Throwable error) {
        if (null == this.error) {
            // Create a new error condition.
            this.error = new MixingError(error);
        } else {
            // Update the internal mixing error object.
            this.error.update(error);
        }
    }

    @Override
    public MixingError getError() {
        return error;
    }

    @Override
    public void serialize(MessagePacker packer) throws IOException {
        storeStringValueWithNullCheck(packer, requestId);
        payload.serialize(packer);
        if (queuesToVisit == null) {
            packer.packNil();
        } else {
            packer.packArrayHeader(queuesToVisit.size());
            for (String queue : queuesToVisit) {
                storeStringValueWithNullCheck(packer, queue);
            }
        }
        storeStringValueWithNullCheck(packer, lastQueueName);
        storeStringValueWithNullCheck(packer, lastNodeName);
        if (error == null) {
            packer.packNil();
        } else {
            storeStringValueWithNullCheck(packer, error.getErrorMessage());
            storeStringValueWithNullCheck(packer, error.getErrorClass());
            storeStringValueWithNullCheck(packer, error.getStackTrace());
            storeIntValueWithNullCheck(packer, error.getRetriesLeft());
        }

    }

    @Override
    public void deserialize(MessageUnpacker unpacker) throws SafeStreamDeserializationException {
        try {
            this.requestId = retrieveStringValueWithNullCheck(unpacker);
            this.payload = new MixingPayloadImpl();
            this.payload.deserialize(unpacker);
            if (unpacker.tryUnpackNil()) {
                this.queuesToVisit = null;
            } else {
                int setSize = unpacker.unpackArrayHeader();
                this.queuesToVisit = new HashSet<>(setSize);
                for (int i = 0; i < setSize; i++) {
                    queuesToVisit.add(retrieveStringValueWithNullCheck(unpacker));
                }
            }

            this.lastQueueName = retrieveStringValueWithNullCheck(unpacker);
            this.lastNodeName = retrieveStringValueWithNullCheck(unpacker);
            if (unpacker.tryUnpackNil()) {
                this.error = null;
            } else {
                String errorMessage = retrieveStringValueWithNullCheck(unpacker);
                String errorClass = retrieveStringValueWithNullCheck(unpacker);
                String stackTrace = retrieveStringValueWithNullCheck(unpacker);
                int retriesLeft = retrieveIntValueWithNullCheck(unpacker);
                this.error = new MixingError(errorMessage, errorClass, stackTrace, retriesLeft);
            }
        } catch (IOException e) {
            throw new SafeStreamDeserializationException(e);
        }

    }

    @Override
    public StreamSerializableClassType type() {
        return StreamSerializableClassType.MIXING_DTO_IMPL;
    }
}
