/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.io.uri.impl;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.nio.charset.Charset;
import java.time.Instant;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.DatatypeConverter;
import net.ranides.assira.collection.maps.GenericMap;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.credentials.UserProperty;
import net.ranides.assira.io.IOHandle;
import net.ranides.assira.io.IOStreams;
import net.ranides.assira.io.uri.URITime;
import net.ranides.assira.io.uri.URIFlags;
import net.ranides.assira.io.uri.URIHandle;
import net.ranides.assira.io.uri.URIHandler;
import net.ranides.assira.io.uri.URIResolver;
import net.ranides.assira.text.Charsets;
import net.ranides.assira.text.StringTraits;
import net.ranides.assira.trace.LoggerUtils;
import org.slf4j.Logger;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class CHTTPHandler implements URIHandler {
    
    private static final Logger LOGGER = LoggerUtils.getLogger();
    
    private static final Pattern RE_CHARSET = Pattern.compile(";[ ]*charset=([^; ]+)");

    private final URIResolver resolver;
    
    public CHTTPHandler() {
        this.resolver = URIResolver.DEFAULT;
    }

    public CHTTPHandler(URIResolver resolver) {
        this.resolver = resolver;
    }
    
    @Override
    public URIHandle resolve(URI uri) throws IOException {
        return new CHttpHandle(uri);
    }

    @Override
    public Collection<String> schemes() {
        return Arrays.asList("http","https");
    }
    
    private class CHttpHandle extends CHandle {
        
        private final URI uri;
        private final IOHandle<HttpURLConnection> head;
        private final IOHandle<HttpURLConnection> get;
        private final IOHandle<HttpURLConnection> post;

        public CHttpHandle(URI uri) throws MalformedURLException {
            this.uri = uri;
            head = IOHandle.shared(() -> connect("HEAD"));
            get = IOHandle.shared(() -> connect("GET"));
            post = IOHandle.shared(() -> connect("POST"));
        }

        @Override
        public URIResolver resolver() {
            return resolver;
        }
        
        @Override
        public URI uri() throws IOException {
            return uri;
        }
        
        @Override
        public Charset charset() throws IOException {
            return getContentCharset(head().getContentType());
        }

        @Override
        public Set<URIFlags> flags() throws IOException {
            Set<URIFlags> re = URIFlags.collect(URIFlags.FILE);
            try {
                HttpURLConnection c = head();
                if(isSuccess(c)) {
                    re.add(URIFlags.EXISTS);
                    re.add(URIFlags.READABLE);
                }

                if(StringTraits.starts(c.getContentType(), "text/")) {
                    re.add(URIFlags.TEXT);
                } else {
                    re.add(URIFlags.BINARY);
                }
                if(c.getContentLengthLong() >= 0) {
                    re.add(URIFlags.SIZE);
                }
                if(StringTraits.contains(c.getHeaderField("Accept-Ranges"), "bytes")) {
                    re.add(URIFlags.SEEK);
                }
            } catch(IOException cause) {
                LOGGER.warn("URI.flags: HTTP error", cause);
            }
            return re;
        }

        @Override
        public Instant time(URITime ut) throws IOException {
            if(ut == URITime.MODIFIED) {
                HttpURLConnection c = head();
                if(null == c.getHeaderField("Last-Modified")) {
                    return Instant.ofEpochMilli(c.getDate());
                }
                return new Date(c.getHeaderFieldDate("Last-Modified", 0)).toInstant();
            }
            return super.time(ut);
        }

        @Override
        public long size() throws IOException {
            HttpURLConnection c = head();
            if(2 != (c.getResponseCode() / 100)) {
                throw new IOException(c.getResponseMessage());
            }
            return c.getContentLengthLong();
        }

        @Override
        public InputStream istream() throws IOException {
            return get.get().getInputStream();
        }

        @Override
        public InputStream istream(long begin, long end) throws IOException {
            HttpURLConnection c = get.get();
            if(end == Long.MAX_VALUE) {
                c.setRequestProperty("Range", "bytes=" + begin + "-");
            } else {
                c.setRequestProperty("Range", "bytes=" + begin + "-" + end);
            }
            if (c.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
                return IOStreams.limit(c.getInputStream(), begin, end);
            } else {
                return c.getInputStream();
            }
        }

        @Override
        public OutputStream ostream() throws IOException {
            HttpURLConnection c = post.get();
            c.connect();
            return c.getOutputStream();
        }

        @Override
        public void create() throws IOException {
            execute("PUT");
        }

        @Override
        public void delete() throws IOException {
            execute("DELETE");
        }
        
        private HttpURLConnection head() throws IOException {
            return get.opened() ? get.get() : head.get();
        }
        
        private void execute(String method) throws IOException {
            HttpURLConnection c = connect(method);
            boolean success = isSuccess(c);
            c.disconnect();
            if(!success) {
                throw new IOException(c.getResponseCode() + ": " + c.getResponseMessage());
            }
        }
        
        private HttpURLConnection connect(String method) throws IOException {
            HttpURLConnection c = (HttpURLConnection)uri.toURL().openConnection();
            c.setRequestMethod(method);
            
            Optional<GenericMap<String>> ui = resolver.users().match(uri).first();
            if(ui.isPresent()) {
                setAuthorizationHeader(c, ui.get());
            }
            return c;
        }

    }
    
    private static boolean isSuccess(HttpURLConnection c) throws IOException {
        return 2 == (c.getResponseCode() / 100);
    }
    
    private static Charset getContentCharset(String type) throws IOException {
        if(null == type) {
            throw new IOException("Unknown MIME type");
        }
        if(!type.startsWith("text/")) {
            throw new IOException("Unsupported MIME type: " + type);
        }
        Matcher hit = RE_CHARSET.matcher(type);
        if(!hit.find()) {
            return Charsets.UTF8;
        }
        return Charset.forName(hit.group(1));
    }
    
    private static void setAuthorizationHeader(HttpURLConnection connection, GenericMap<String> user) {
        String passphrase = user.get(UserProperty.LOGIN) + ":" + user.get(UserProperty.PASSWORD);
        String header = "Basic " + DatatypeConverter.printBase64Binary(passphrase.getBytes());
        connection.setRequestProperty("Authorization", header);
    }
}
