/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.stack.core.channel.messages;

import com.google.common.base.Charsets;
import com.google.common.base.MoreObjects;
import io.netty.buffer.ByteBuf;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.util.annotations.UInt32Primitive;
import org.jetbrains.annotations.Nullable;

import static com.google.common.base.Preconditions.checkArgument;

public class HelloMessage {

    static final int MAX_ENDPOINT_URL_LENGTH = 4096;

    @UInt32Primitive
    private final long protocolVersion;

    @UInt32Primitive
    private final long receiveBufferSize;

    @UInt32Primitive
    private final long sendBufferSize;

    @UInt32Primitive
    private final long maxMessageSize;

    @UInt32Primitive
    private final long maxChunkCount;

    private final String endpointUrl;

    /**
     * @param protocolVersion   the latest version of the OPC UA TCP protocol supported by the Client.
     * @param receiveBufferSize the largest MessageChunk that the sender (Client) can receive. This value shall be
     *                          greater than or equal to 8192 bytes.
     * @param sendBufferSize    the largest MessageChunk that the sender (Client) will send. This value shall be
     *                          greater than or equal to 8192 bytes.
     * @param maxMessageSize    the maximum size for any response Message. The Message size is calculated using the
     *                          unencrypted Message body. A value of zero indicates that the Client has no limit.
     * @param maxChunkCount     the maximum number of chunks in any response Message. A value of zero indicates that
     *                          the Client has no limit.
     * @param endpointUrl       the URL of the Endpoint which the Client wished to connect to. The encoded value shall
     *                          be less than 4096 bytes.
     */
    public HelloMessage(@UInt32Primitive long protocolVersion,
                        @UInt32Primitive long receiveBufferSize,
                        @UInt32Primitive long sendBufferSize,
                        @UInt32Primitive long maxMessageSize,
                        @UInt32Primitive long maxChunkCount,
                        @Nullable String endpointUrl) {

        checkArgument(receiveBufferSize >= 8192, "receiverBufferSize must be at least 8192 bytes");
        checkArgument(sendBufferSize >= 8192, "sendBufferSize must be at least 8192 bytes");
        checkArgument(
            endpointUrl == null || endpointUrl.length() <= MAX_ENDPOINT_URL_LENGTH,
            "endpointUrl length cannot be greater than 4096 bytes");

        this.protocolVersion = protocolVersion;
        this.receiveBufferSize = receiveBufferSize;
        this.sendBufferSize = sendBufferSize;
        this.maxMessageSize = maxMessageSize;
        this.maxChunkCount = maxChunkCount;
        this.endpointUrl = endpointUrl;
    }

    @UInt32Primitive
    public long getProtocolVersion() {
        return protocolVersion;
    }

    @UInt32Primitive
    public long getReceiveBufferSize() {
        return receiveBufferSize;
    }

    @UInt32Primitive
    public long getSendBufferSize() {
        return sendBufferSize;
    }

    @UInt32Primitive
    public long getMaxMessageSize() {
        return maxMessageSize;
    }

    @UInt32Primitive
    public long getMaxChunkCount() {
        return maxChunkCount;
    }

    @Nullable
    public String getEndpointUrl() {
        return endpointUrl;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        HelloMessage that = (HelloMessage) o;

        return maxChunkCount == that.maxChunkCount &&
            maxMessageSize == that.maxMessageSize &&
            protocolVersion == that.protocolVersion &&
            receiveBufferSize == that.receiveBufferSize &&
            sendBufferSize == that.sendBufferSize &&
            endpointUrl.equals(that.endpointUrl);

    }

    @Override
    public int hashCode() {
        int result = (int) (protocolVersion ^ (protocolVersion >>> 32));
        result = 31 * result + (int) (receiveBufferSize ^ (receiveBufferSize >>> 32));
        result = 31 * result + (int) (sendBufferSize ^ (sendBufferSize >>> 32));
        result = 31 * result + (int) (maxMessageSize ^ (maxMessageSize >>> 32));
        result = 31 * result + (int) (maxChunkCount ^ (maxChunkCount >>> 32));
        result = 31 * result + endpointUrl.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("protocolVersion", protocolVersion)
            .add("receiverBufferSize", receiveBufferSize)
            .add("sendBufferSize", sendBufferSize)
            .add("maxMessageSize", maxMessageSize)
            .add("maxChunkCount", maxChunkCount)
            .add("endpointUrl", endpointUrl)
            .toString();
    }

    public static void encode(HelloMessage message, ByteBuf buffer) {
        buffer.writeIntLE((int) message.getProtocolVersion());
        buffer.writeIntLE((int) message.getReceiveBufferSize());
        buffer.writeIntLE((int) message.getSendBufferSize());
        buffer.writeIntLE((int) message.getMaxMessageSize());
        buffer.writeIntLE((int) message.getMaxChunkCount());
        encodeString(message.getEndpointUrl(), buffer);
    }

    public static HelloMessage decode(ByteBuf buffer) throws UaException {
        return new HelloMessage(
            buffer.readUnsignedIntLE(), /*    ProtocolVersion    */
            buffer.readUnsignedIntLE(), /*    ReceiveBufferSize  */
            buffer.readUnsignedIntLE(), /*    SendBufferSize     */
            buffer.readUnsignedIntLE(), /*    MaxMessageSize     */
            buffer.readUnsignedIntLE(), /*    MaxChunkCount      */
            decodeString(buffer)        /*    EndpointUrl        */
        );
    }

    private static void encodeString(String s, ByteBuf buffer) {
        if (s == null) {
            buffer.writeIntLE(-1);
        } else {
            buffer.writeIntLE(s.length());
            buffer.writeBytes(s.getBytes(Charsets.UTF_8));
        }
    }

    private static String decodeString(ByteBuf buffer) throws UaException {
        int length = buffer.readIntLE();
        if (length < 0) {
            if (length == -1) {
                return null;
            } else {
                throw new UaException(
                    StatusCodes.Bad_DecodingError,
                    "invalid endpoint URL length: " + length
                );
            }
        } else {
            if (length > MAX_ENDPOINT_URL_LENGTH) {
                throw new UaException(
                    StatusCodes.Bad_EncodingLimitsExceeded,
                    "endpoint URL length exceeds 4096: " + length
                );
            }
            byte[] bs = new byte[length];
            buffer.readBytes(bs);
            return new String(bs, Charsets.UTF_8);
        }
    }

}
