package com.jscape.inet.proxy.socks.protocol.v5.marshaling;

import com.jscape.inet.proxy.socks.protocol.v5.messages.CommandRequest;
import com.jscape.inet.proxy.socks.protocol.v5.messages.Message;
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.HashMap;
import java.util.Map;

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

/**
 * @author Alex Usun
 */
public class CommandRequestCodec
        extends BaseCodec {

    private static final int RESERVED_FIELD_VALUE = 0;

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

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

    public CommandRequestCodec set(Entry... entries) {
        for (Entry entry : entries) {
            this.readingEntries.put(entry.code, entry);
            this.writingEntries.put(entry.messageClass, entry);
        }

        return this;
    }

    @Override
    public Message read(InputStream in)
            throws IOException {
        int version = ByteCodec.readValue(in) & 0xFF;
        assertVersionSupported(version);
        int code = ByteCodec.readValue(in) & 0xFF;
        int reserved = ByteCodec.readValue(in) & 0xFF;
        Entry entry = entryFor(code);

        return entry.codec.read(in);
    }

    @Override
    public void write(Message value, OutputStream out)
            throws IOException {
        CommandRequest message = (CommandRequest) value;
        ByteCodec.writeValue((byte) VERSION, out);
        Entry entry = entryFor(message);
        ByteCodec.writeValue((byte) entry.code, out);
        ByteCodec.writeValue((byte) RESERVED_FIELD_VALUE, out);
        entry.codec.write(message, out);
    }

    private Entry entryFor(int code)
            throws IOException {
        Entry entry = this.readingEntries.get(code);

        if (entry == null) {
            throw new IOException("Unsupported command type: " + code);
        }

        return entry;
    }

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

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

        return entry;
    }

    public static class Entry {

        public final int code;
        public final Class messageClass;
        public final DataCodec<Message> codec;

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

            assertNotNull(messageClass);
            this.messageClass = messageClass;

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

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

}
