package com.jscape.inet.sftp.protocol.marshaling;

import com.jscape.inet.sftp.protocol.messages.Message;
import com.jscape.inet.sftp.protocol.messages.UnknownMessage;
import com.jscape.inet.ssh.protocol.v2.marshaling.primitive.Uint32Codec;
import com.jscape.util.IOTools;
import com.jscape.util.io.ByteArrayInputStream;
import com.jscape.util.io.ByteArrayOutputStream;
import com.jscape.util.io.DataCodec;
import com.jscape.util.io.primitive.ByteCodec;

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 MessageCodec
        implements DataCodec<Message> {

    private final Map<Integer, Entry> readingEntries;
    private final Map<Class, Entry> writingEntries;
    private final ByteArrayOutputStream readingBuffer;
    private final ByteArrayInputStream readingIn;
    private final ByteArrayOutputStream writingBuffer;

    public MessageCodec(Entry... entries) {
        this.readingEntries = new HashMap<>();
        this.writingEntries = new HashMap<>();

        this.readingBuffer = new ByteArrayOutputStream();
        this.readingIn = new ByteArrayInputStream(this.readingBuffer.buffer());
        this.writingBuffer = new ByteArrayOutputStream();

        set(entries);
    }

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

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

        return this;
    }

    @Override
    public Message read(InputStream in)
            throws IOException {
        int length = Uint32Codec.readValue(in);

        this.readingBuffer.position(length);
        IOTools.read(this.readingBuffer.buffer(), 0, length, in);
        this.readingIn.put(this.readingBuffer.buffer(), 0, length);

        int messageCode = ByteCodec.readValue(this.readingIn) & 0xFF;
        
        Entry entry = entryFor(messageCode);
        
        if (entry == null) {
            int id = Uint32Codec.readValue(this.readingIn);
            byte[] data = IOTools.readFully(this.readingIn);
            return new UnknownMessage(messageCode, id, data);
        }
    
        return entry.codec.read(this.readingIn);
    }

    @Override
    public void write(Message value, OutputStream out)
            throws IOException {
        Entry entry = entryFor(value);

        this.writingBuffer.reset();

        Uint32Codec.writeValue(0, this.writingBuffer);
        ByteCodec.writeValue((byte) entry.messageCode, this.writingBuffer);
        entry.codec.write(value, this.writingBuffer);

        int position = this.writingBuffer.position();
        this.writingBuffer.reset();
        Uint32Codec.writeValue(position - 4, this.writingBuffer);
        this.writingBuffer.position(position);

        this.writingBuffer.writeTo(out);
        out.flush();
    }

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

    private Entry entryFor(int type) {
        return this.readingEntries.get(type);
    }

    private Entry entryFor(Message message) {
        Entry entry = this.writingEntries.get(message.getClass());
    
        if (entry == null) {
            throw new IllegalArgumentException(String.format("Unsupported message: %s.", message.getClass()));
        }

        return entry;
    }

    public static class Entry {

        public final int messageCode;
        public final DataCodec<Message> codec;
        public final Class[] messageClasses;

        public Entry(int messageCode, DataCodec<Message> codec, Class... messageClasses) {
            this.messageCode = messageCode;

            assertNotNull(messageClasses);
            this.messageClasses = messageClasses;

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

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

}
