package com.jscape.inet.ssh.protocol.v2.marshaling;

import com.jscape.inet.ssh.protocol.messages.Message;
import com.jscape.inet.ssh.protocol.v2.marshaling.primitive.StringCodec;
import com.jscape.inet.ssh.protocol.v2.marshaling.primitive.Uint32Codec;
import com.jscape.inet.ssh.protocol.v2.messages.SshMsgChannelOpen;
import com.jscape.inet.ssh.protocol.v2.messages.SshMsgChannelOpenUnknown;
import com.jscape.util.IOTools;
import com.jscape.util.io.DataCodec;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import static com.jscape.util.Parameters.assertNotNull;

/**
 * @author Alex Usun
 */
public class SshMsgChannelOpenCodec
        implements DataCodec<Message> {

    private final Map<String, Entry> readingEntries;
    private final Map<Class, Entry> writingEntries;

    public SshMsgChannelOpenCodec(Entry... entries) {
        this.readingEntries = new HashMap<String, Entry>();
        this.writingEntries = new HashMap<Class, Entry>();
        set(entries);
    }

    public SshMsgChannelOpenCodec set(Entry... entries) {
        for (Entry entry : entries) {
            this.readingEntries.put(entry.channelType, entry);

            for (Class messageClass : entry.messageClasses) {
                this.writingEntries.put(messageClass, entry);
            }
        }

        return this;
    }

    @Override
    public Message read(InputStream in)
            throws IOException {
        String channelType = StringCodec.readUsAsciiValue(in);
        int senderChannel = Uint32Codec.readValue(in);
        long initialWindowSize = Uint32Codec.readLongValue(in);
        int maxPacketSize = Uint32Codec.readValue(in);

        try {
            Entry entry = entryFor(channelType);

            return entry.codec.read(in, senderChannel, initialWindowSize, maxPacketSize);
        } catch (Exception e) {
            byte[] data = IOTools.readFully(in);
            return new SshMsgChannelOpenUnknown(channelType, senderChannel, initialWindowSize, maxPacketSize, data);
        }
    }

    @Override
    public void write(Message value, OutputStream out)
            throws IOException {
        SshMsgChannelOpen message = (SshMsgChannelOpen) value;

        Entry entry = entryFor(message);

        StringCodec.writeUsAsciiValue(entry.channelType, out);
        Uint32Codec.writeValue(message.senderChannel, out);
        Uint32Codec.writeValue(message.initialWindowSize, out);
        Uint32Codec.writeValue(message.maxPacketSize, out);
        entry.codec.write(message, out);
    }

    @Override
    public String toString() {
        return "SshMsgChannelOpenCodec {"
                + "readingEntries=" + this.readingEntries
                + ", writingEntries=" + this.writingEntries
                + '}';
    }

    private Entry entryFor(String channelType) {
        Entry entry = this.readingEntries.get(channelType);

        if (entry == null) {
            throw new IllegalArgumentException("Unsupported channel type: " + channelType);
        }

        return entry;
    }

    private Entry entryFor(SshMsgChannelOpen message) {
        Entry entry = this.writingEntries.get(message.getClass());

        if (entry == null) {
            throw new IllegalArgumentException("Unsupported message: " + message.getClass());
        }

        return entry;
    }

    public interface TypeCodec {

        SshMsgChannelOpen read(InputStream in, int senderChannel, long initialWindowSize, int maxPacketSize)
                throws IOException;

        void write(SshMsgChannelOpen value, OutputStream out)
                throws IOException;

    }

    public static class Entry {

        public final String channelType;
        public final TypeCodec codec;
        public final Class[] messageClasses;

        public Entry(String channelType, TypeCodec codec, Class... messageClasses) {
            assertNotNull(channelType);
            this.channelType = channelType;

            assertNotNull(codec);
            this.codec = codec;

            assertNotNull(messageClasses);
            this.messageClasses = messageClasses;
        }

        @Override
        public String toString() {
            return "Entry {"
                    + "channelType='" + this.channelType + '\''
                    + ", codec=" + this.codec
                    + ", messageClasses=" + Arrays.toString(this.messageClasses)
                    + '}';
        }
    }
}
