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

import java.net.URI;
import java.net.URISyntaxException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import net.ranides.assira.collection.maps.AMap;
import net.ranides.assira.collection.maps.MapBuilder;
import net.ranides.assira.collection.maps.MultiMap;
import net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.text.ResolveFormatUtils;
import net.ranides.assira.text.StrBuilder;
import net.ranides.assira.text.StringTraits;
import net.ranides.assira.text.StringUtils;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class URIBuilder {
    
    private String scheme;
    private String host;
    private String port;
    private String path;
    
    private String fragment;
    private ParamMap params;
    
    private String login;
    private String password;
    
    public URIBuilder() {
        params = new ParamMap();
    }
    
    public URIBuilder(String uri) {
        this(parse(uri));
    }
    
    public URIBuilder(URI uri) {
        scheme = uri.getScheme();
        host = uri.getHost();
        port = URIUtils.getPort(uri).map(String::valueOf).orElse(null);
        path = uri.getPath();
        
        fragment = uri.getFragment();
        params = new ParamMap(uri.getQuery());
        
        login = URIUtils.getLogin(uri).orElse(null);
        password = URIUtils.getPassword(uri).orElse(null);
    }
    
    public Optional<String> login() {
        return Optional.ofNullable(login);
    }
    
    public URIBuilder login(String value) {
        this.login = value;
        return this;
    }
    
    public Optional<String> password() {
        return Optional.ofNullable(password);
    }
    
    public URIBuilder password(String value) {
        this.password = value;
        return this;
    }
    
    public String host() {
        return host;
    }
    
    public URIBuilder host(String value) {
        this.host = value;
        return this;
    }
    
    public Optional<String> fragment() {
        return Optional.ofNullable(fragment);
    }
    
    public URIBuilder fragment(String value) {
        this.fragment = value;
        return this;
    }
    
    public Optional<String> path() {
        return StringTraits.isEmpty(path) ? Optional.empty() : Optional.of(path);
    }
    
    public URIBuilder path(String value) {
        path = value;
        return this;
    }
    
    public URIBuilder path(String... values) {
        path = StringUtils.join(values, "/");
        return this;
    }

    public URIBuilder append(String value) {
        path = path + "/" + value;
        return this;
    }
    
    public URIBuilder append(String... values) {
        path = new StrBuilder(path.length()).append(path).append("/").open("", "", "/").append(values).toString();
        return this;
    }
    
    public Optional<String> port() {
        return Optional.ofNullable(port);
    }
    
    public URIBuilder port(String value) {
        this.port = value;
        return this;
    }
    
    public URIBuilder port(int value) {
        this.port = String.valueOf(value);
        return this;
    }
    
    public List<String> param(String name) {
        return params.getAll(name);
    }
    
    public URIBuilder param(String name, String value) {
        params.put(name, value);
        return this;
    }
    
    public URIBuilder param(String name, List<String> value) {
        params.putAll(name, value);
        return this;
    }
    
    public MapBuilder<String,String> params() {
        return new MapBuilder<>(params);
    }
    
    public URIBuilder params(Map<String,String> value) {
        params.putAll(value);
        return this;
    }
    
    public String scheme() {
        return scheme;
    }
    
    public URIBuilder scheme(String value) {
        this.scheme = value;
        return this;
    }
    
    public URIBuilder resolve(Object context) {
        Function<String,String> r = resolver(context);

        URIBuilder out = new URIBuilder();
        out.scheme = r.apply(scheme);
        out.host = r.apply(host);
        out.port = r.apply(port);
        out.path = r.apply(path);
        out.fragment = r.apply(fragment);
        out.params = resolve(r, params);
        out.login = r.apply(login);
        out.password = r.apply(password);
        
        return out;
    }
    
    public URI build() {
        try {
            return new URI(scheme, userinfo(), host, intport(), abspath(), query(), fragment);
        } catch(URISyntaxException cause) {
            throw new IllegalArgumentException(cause);
        }
    }
    
    public URI build(Object context) {
        return resolve(context).build();
    }
    
    private String abspath() {
        return StringTraits.starts(path, "/") ? path : StringTraits.isEmpty(path) ? path : "/"+path;
    }

    private int intport() {
        return port!=null ? Integer.parseInt(port) : -1;
    }

    private String query() {
        if(params.set.isEmpty()) {
            return null;
        }
        return StringUtils.join(params.set, "&", e -> {
            if(StringTraits.isEmpty(e.getValue())) {
                return URIUtils.encodeURL(e.getKey());
            } else {
                return URIUtils.encodeURL(e.getKey()) + "=" + URIUtils.encodeURL(e.getValue());
            }
        });
    }

    private String userinfo() throws IllegalArgumentException {
        if(login!=null) {
            if (password!=null) {
                return login + ":" + password;
            } else {
                return login;
            }
        }
        if(password!=null) {
            throw new IllegalArgumentException("password without login");
        }
        return null;
    }
    
    private static URI parse(String uri) {
        try {
            return new URI(uri);
        } catch (URISyntaxException cause) {
            throw new IllegalArgumentException(cause);
        }
    }
    private static String paramName(String[] array) {
        return array.length > 0 ? array[0] : "";
    }
    
    private static String paramValue(String[] array) {
        return array.length > 1 ? array[1] : "";
    }
    
    private static Function<String,String> resolver(Object context) {
        return value -> null == value ? null : ResolveFormatUtils.format(value, context);
    }
    
    private static ParamMap resolve(Function<String,String> resolver, ParamMap map) {
        ParamMap out = new ParamMap();
        for(Entry<String,String> entry : map.set) {
            out.set.add( newEntry(entry, resolver.apply(entry.getKey()), resolver.apply(entry.getValue())) );
        }
        return out;
    }

    private static Entry<String, String> newEntry(Entry<String, String> e, String key, String value) {
        return value.equals(e.getValue()) && key.equals(e.getKey()) ? e :new ParamEntry(key, value);
    }
    
    
    
    private static final class ParamEntry extends AbstractMap.SimpleEntry<String,String> {
        
        
        public ParamEntry(String[] p) {
            super(paramName(p), paramValue(p));
        }
        
        public ParamEntry(String key, String value) {
            super(key, value);
        }
        
    }
    
    private static final class ParamSet extends ArrayList<Entry<String,String>> implements Set<Entry<String,String>> {
        
    }
    
    private static final class ParamMap extends AMap<String,String> implements MultiMap<String,String> {
        
        private final ParamSet set = new ParamSet();
        
        public ParamMap() {
            // do nothing
        }
        
        public ParamMap(String query) {
            if(StringTraits.isEmpty(query)) {
                return;
            }
            Arrays
                .stream(query.split("&"))
                .map(v -> v.split("="))
                .forEachOrdered(p -> set.add(new ParamEntry(p)) );
        }

        @Override
        public Set<Entry<String, String>> entrySet() {
            return set;
        }

        @Override
        public int size() {
            return set.size();
        }

        @Override
        public boolean containsKey(Object key) {
            return set.stream().anyMatch(e -> e.getKey().equals(key));
        }

        @Override
        public boolean containsValue(Object value) {
            return set.stream().anyMatch(e -> e.getValue().equals(value));
        }

        @Override
        public String get(Object key) {
            return set.stream().filter(e -> e.getKey().equals(key)).findAny().map(e -> e.getValue()).orElse(null);
        }

        @Override
        public List<String> getAll(Object key) {
            return set.stream().filter(e -> e.getKey().equals(key)).map(e -> e.getValue()).collect(Collectors.toList());
        }

        @Override
        public String remove(Object key) {
            Iterator<Entry<String, String>> i = set.iterator();
            while(i.hasNext()) {
                Entry<String, String> e = i.next();
                if(e.getKey().equals(key)) {
                    i.remove();
                    return e.getValue();
                }
            }
            return null;
        }

        @Override
        public Collection<String> removeAll(Object key) {
            List<String> out = new ArrayList<>();
            Iterator<Entry<String, String>> i = set.iterator();
            while(i.hasNext()) {
                Entry<String, String> e = i.next();
                if(e.getKey().equals(key)) {
                    i.remove();
                    out.add(e.getValue());
                }
            }
            return out;
        }
        
        @Override
        public String put(String key, String value) {
            String prev = get(key);
            set.add(new ParamEntry(key, value));
            return prev;
        }

        @Override
        public boolean containsEntry(Object key, Object value) {
            return set.stream().anyMatch(e -> e.getKey().equals(key) && CompareUtils.equals(e.getValue(),value));
        }
        
        
    }
    
}
