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

import com.jscape.inet.http.protocol.messages.Header;
import com.jscape.inet.http.protocol.messages.Message;
import com.jscape.inet.http.protocol.messages.RequestMessage;
import com.jscape.inet.http.protocol.messages.UnknownRequest;
import com.jscape.util.IOTools;
import com.jscape.util.io.DataCodec;
import com.jscape.util.reflection.ConstructorInvoker;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

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

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

    private static final int BUFFER_SIZE = 64 * 1024;

    private final Map<String, Entry> readingEntries;
    private final Map<Class, Entry> writingEntries;
    
    public RequestCodec(Entry... entries) {
        this.readingEntries = new HashMap<>();
        this.writingEntries = new HashMap<>();
        set(entries);
    }
    
    public RequestCodec set(Entry... entries) {
        this.readingEntries.clear();
        this.writingEntries.clear();
        for (Entry entry : entries) {
            this.readingEntries.put(entry.method, entry);
            this.writingEntries.put(entry.requestClass, entry);
        }
        
        return this;
    }
    
    @Override
    public Message read(InputStream in)
            throws IOException {
        String method = LineCodec.readStringSpace(StandardCharsets.US_ASCII, in);
        Entry entry = entryFor(method);
        
        String uri = LineCodec.readStringSpace(StandardCharsets.US_ASCII, in);
        String version = LineCodec.readStringCRLF(StandardCharsets.US_ASCII, in);
        Header[] headers = HeadersCodec.readValue(in);
    
        try {
            return ConstructorInvoker.invoke(entry != null ? entry.requestClass : UnknownRequest.class, uri != null ? uri : "", version, headers, in);
        } catch (Exception e) {
            throw new IOException(e);
        }
    }
    
    @Override
    public void write(Message value, OutputStream out)
            throws IOException {
        RequestMessage request = (RequestMessage) value;
        Entry entry = entryFor(request);
        
        String statusLine = String.format("%s %s %s", entry.method, request.uri, request.protocolVersion);
        LineCodec.writeStringCRLF(statusLine, StandardCharsets.US_ASCII, out);
        HeadersCodec.writeValue(request.headers, out);
        IOTools.copy(request.body, out, BUFFER_SIZE);
    }

    private Entry entryFor(RequestMessage message)
            throws IOException {
        Entry entry = this.writingEntries.get(message.getClass());
        
        if (entry == null) {
            throw new IOException(String.format("Unsupported message class: %s.", message.getClass()));
        }
        
        return entry;
    }
    
    private Entry entryFor(String method) {
        return this.readingEntries.get(method);
    }

    public static class Entry {
        
        public final String method;
        public final Class requestClass;
    
        public Entry(String method, Class requestClass) {
            assertNotNull(method);
            this.method = method;
            
            assertNotNull(requestClass);
            this.requestClass = requestClass;
        }
    
        @Override
        public String toString() {
            return "Entry {"
                   + "method='" + this.method + '\''
                   + ", requestClass=" + this.requestClass
                   + '}';
        }
    }
}
