/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.ignite.internal.igfs.common;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Arrays;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.igfs.IgfsPath;
import org.apache.ignite.internal.processors.igfs.IgfsUtils;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.jetbrains.annotations.Nullable;

import static org.apache.ignite.internal.igfs.common.IgfsIpcCommand.AFFINITY;
import static org.apache.ignite.internal.igfs.common.IgfsIpcCommand.OPEN_CREATE;
import static org.apache.ignite.internal.igfs.common.IgfsIpcCommand.OPEN_READ;
import static org.apache.ignite.internal.igfs.common.IgfsIpcCommand.READ_BLOCK;
import static org.apache.ignite.internal.igfs.common.IgfsIpcCommand.SET_TIMES;
import static org.apache.ignite.internal.igfs.common.IgfsIpcCommand.WRITE_BLOCK;

/**
 * Implementation of IGFS client message marshaller.
 */
public class IgfsMarshaller {
    /** Packet header size. */
    public static final int HEADER_SIZE = 24;

    /**
     * Creates new header with given request ID and command.
     *
     * @param reqId Request ID.
     * @param cmd Command.
     * @return Created header.
     */
    public static byte[] createHeader(long reqId, IgfsIpcCommand cmd) {
        assert cmd != null;

        byte[] hdr = new byte[HEADER_SIZE];

        U.longToBytes(reqId, hdr, 0);

        U.intToBytes(cmd.ordinal(), hdr, 8);

        return hdr;
    }

    /**
     * Creates new header with given request ID and command.
     *
     * @param reqId Request ID.
     * @param cmd Command.
     * @return Created header.
     */
    public static byte[] fillHeader(byte[] hdr, long reqId, IgfsIpcCommand cmd) {
        assert cmd != null;

        Arrays.fill(hdr, (byte)0);

        U.longToBytes(reqId, hdr, 0);

        U.intToBytes(cmd.ordinal(), hdr, 8);

        return hdr;
    }

    /**
     * Serializes the message and sends it into the given output stream.
     * @param msg Message.
     * @param hdr Message header.
     * @param out Output.
     * @throws IgniteCheckedException If failed.
     */
    public void marshall(IgfsMessage msg, byte[] hdr, ObjectOutput out) throws IgniteCheckedException {
        assert hdr != null;
        assert hdr.length == HEADER_SIZE;

        try {
            switch (msg.command()) {
                case HANDSHAKE: {
                    out.write(hdr);

                    IgfsHandshakeRequest req = (IgfsHandshakeRequest)msg;

                    U.writeString(out, req.igfsName());
                    U.writeString(out, req.logDirectory());

                    break;
                }
                case STATUS: {
                    out.write(hdr);

                    break;
                }

                case EXISTS:
                case INFO:
                case PATH_SUMMARY:
                case UPDATE:
                case RENAME:
                case DELETE:
                case MAKE_DIRECTORIES:
                case LIST_PATHS:
                case LIST_FILES:
                case AFFINITY:
                case SET_TIMES:
                case OPEN_READ:
                case OPEN_APPEND:
                case OPEN_CREATE: {
                    out.write(hdr);

                    IgfsPathControlRequest req = (IgfsPathControlRequest)msg;

                    U.writeString(out, req.userName());
                    writePath(out, req.path());
                    writePath(out, req.destinationPath());
                    out.writeBoolean(req.flag());
                    out.writeBoolean(req.colocate());
                    IgfsUtils.writeStringMap(out, req.properties());

                    // Minor optimization.
                    if (msg.command() == AFFINITY) {
                        out.writeLong(req.start());
                        out.writeLong(req.length());
                    }
                    else if (msg.command() == OPEN_CREATE) {
                        out.writeInt(req.replication());
                        out.writeLong(req.blockSize());
                    }
                    else if (msg.command() == SET_TIMES) {
                        out.writeLong(req.accessTime());
                        out.writeLong(req.modificationTime());
                    }
                    else if (msg.command() == OPEN_READ && req.flag())
                        out.writeInt(req.sequentialReadsBeforePrefetch());

                    break;
                }

                case CLOSE:
                case READ_BLOCK:
                case WRITE_BLOCK: {
                    assert msg.command() != WRITE_BLOCK : "WRITE_BLOCK should be marshalled manually.";

                    IgfsStreamControlRequest req = (IgfsStreamControlRequest)msg;

                    U.longToBytes(req.streamId(), hdr, 12);

                    if (msg.command() == READ_BLOCK)
                        U.intToBytes(req.length(), hdr, 20);

                    out.write(hdr);

                    if (msg.command() == READ_BLOCK)
                        out.writeLong(req.position());

                    break;
                }

                case CONTROL_RESPONSE: {
                    out.write(hdr);

                    IgfsControlResponse res = (IgfsControlResponse)msg;

                    res.writeExternal(out);

                    break;
                }

                case MODE_RESOLVER: {
                    out.write(hdr);

                    break;
                }

                default: {
                    assert false : "Invalid command: " + msg.command();

                    throw new IllegalArgumentException("Failed to marshal message (invalid command): " +
                        msg.command());
                }
            }
        }
        catch (IOException e) {
            throw new IgniteCheckedException("Failed to send message to IGFS data node (is data node up and running?)", e);
        }
    }

    /**
     * @param cmd Command.
     * @param hdr Header.
     * @param in Input.
     * @return Message.
     * @throws IgniteCheckedException If failed.
     */
    public IgfsMessage unmarshall(IgfsIpcCommand cmd, byte[] hdr, ObjectInput in) throws IgniteCheckedException {
        assert hdr != null;
        assert hdr.length == HEADER_SIZE;

        try {
            IgfsMessage msg;

            switch (cmd) {
                case HANDSHAKE: {
                    IgfsHandshakeRequest req = new IgfsHandshakeRequest();

                    req.igfsName(U.readString(in));
                    req.logDirectory(U.readString(in));

                    msg = req;

                    break;
                }

                case STATUS: {
                    msg = new IgfsStatusRequest();

                    break;
                }

                case EXISTS:
                case INFO:
                case PATH_SUMMARY:
                case UPDATE:
                case RENAME:
                case DELETE:
                case MAKE_DIRECTORIES:
                case LIST_PATHS:
                case LIST_FILES:
                case SET_TIMES:
                case AFFINITY:
                case OPEN_READ:
                case OPEN_APPEND:
                case OPEN_CREATE: {
                    IgfsPathControlRequest req = new IgfsPathControlRequest();

                    req.userName(U.readString(in));
                    req.path(readPath(in));
                    req.destinationPath(readPath(in));
                    req.flag(in.readBoolean());
                    req.colocate(in.readBoolean());
                    req.properties(IgfsUtils.readStringMap(in));

                    // Minor optimization.
                    if (cmd == AFFINITY) {
                        req.start(in.readLong());
                        req.length(in.readLong());
                    }
                    else if (cmd == OPEN_CREATE) {
                        req.replication(in.readInt());
                        req.blockSize(in.readLong());
                    }
                    else if (cmd == SET_TIMES) {
                        req.accessTime(in.readLong());
                        req.modificationTime(in.readLong());
                    }
                    else if (cmd == OPEN_READ && req.flag())
                        req.sequentialReadsBeforePrefetch(in.readInt());

                    msg = req;

                    break;
                }

                case CLOSE:
                case READ_BLOCK:
                case WRITE_BLOCK: {
                    IgfsStreamControlRequest req = new IgfsStreamControlRequest();

                    long streamId = U.bytesToLong(hdr, 12);

                    req.streamId(streamId);
                    req.length(U.bytesToInt(hdr, 20));

                    if (cmd == READ_BLOCK)
                        req.position(in.readLong());

                    msg = req;

                    break;
                }

                case CONTROL_RESPONSE: {
                    IgfsControlResponse res = new IgfsControlResponse();

                    res.readExternal(in);

                    msg = res;

                    break;
                }

                case MODE_RESOLVER: {
                    msg = new IgfsModeResolverRequest();

                    break;
                }

                default: {
                    assert false : "Invalid command: " + cmd;

                    throw new IllegalArgumentException("Failed to unmarshal message (invalid command): " + cmd);
                }
            }

            msg.command(cmd);

            return msg;
        }
        catch (IOException | ClassNotFoundException e) {
            throw new IgniteCheckedException("Failed to unmarshal client message: " + cmd, e);
        }
    }

    /**
     * Writes IGFS path to given data output. Can write {@code null} values.
     *
     * @param out Data output.
     * @param path Path to write.
     * @throws IOException If write failed.
     */
    private void writePath(ObjectOutput out, @Nullable IgfsPath path) throws IOException {
        out.writeBoolean(path != null);

        if (path != null)
            path.writeExternal(out);
    }

    /**
     * Reads IGFS path from data input that was written by {@link #writePath(ObjectOutput, IgfsPath)} method.
     *
     * @param in Data input.
     * @return Written path or {@code null}.
     */
    @Nullable private IgfsPath readPath(ObjectInput in) throws IOException {
        return in.readBoolean() ? IgfsUtils.readPath(in) : null;
    }
}