/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */

package net.ranides.assira.io.uri;

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.maps.MapCollectors;
import net.ranides.assira.collection.maps.MultiMap;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.text.Charsets;
import net.ranides.assira.text.StringTraits;
import net.ranides.assira.text.StringUtils;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public class URIUtils {

    /**
     * Decodes an application/x-www-form-urlencoded string using UTF8 charset
     *
     * @param input input
     * @return string
     */
    public static String decodeURL(String input) {
        return decodeURL(input, Charsets.UTF8);
    }

    /**
     * Decodes an application/x-www-form-urlencoded string using provided charset
     *
     * @param input input
     * @param charset charset
     * @return string
     */
	public static String decodeURL(String input, Charset charset) {
        try {
            return URLDecoder.decode(input, charset.name());
        } catch(UnsupportedEncodingException cause) {
			throw new AssertionError("Unsupported charset: " + charset, cause);
        }
    }

    /**
     * Translates a string into application/x-www-form-urlencoded format using UTF8 charset.
     *
     * @param input input
     * @return string
     */
    public static String encodeURL(String input) {
        return encodeURL(input, Charsets.UTF8);
    }

    /**
     * Translates a string into application/x-www-form-urlencoded format using provided charset.
     *
     * @param input input
     * @param charset charset
     * @return string
     */
    public static String encodeURL(String input, Charset charset) {
        try {
            return URLEncoder.encode(input, charset.name());
        } catch(UnsupportedEncodingException cause) {
			throw new AssertionError("Unsupported charset: " + charset, cause);
        }
    }

    /**
     * Returns FILE part of URI, assuming form: "scheme://FILE?query...#anchor..."
     *
     * @param uri uri
     * @return string
     */
    public static String getFile(URI uri) {
        String p = uri.getSchemeSpecificPart();
        String q = uri.getQuery();
        int qe = (null==q ? 0 : q.length()+1);
        
        // strip repeated "/"
        int n = p.length();
        int offset = 0;
        while( offset<n && '/'==p.charAt(offset) ) { 
			offset++; 
		}
        if(offset>0) {
            offset--;
        }
        
        return p.substring(offset, p.length() - qe);
    }

    /**
     * Returns LOGIN part of URI, assuming form: "scheme://login:password@host..."
     *
     * @param uri uri
     * @return optional
     */
    public static Ref<String> getLogin(URI uri) {
        String ui = uri.getUserInfo();
        return (null != ui) ? Ref.of(ui.split(":")[0]) : Ref.empty();
    }

    /**
     * Returns PASSWORD part of URI, assuming form: "scheme://login:password@host..."
     *
     * @param uri uri
     * @return optional
     */
    public static Ref<String> getPassword(URI uri) {
        String ui = uri.getUserInfo();
        return StringUtils.contains(ui, ':') ? Ref.of(StringUtils.after(ui, ":")) : Ref.empty();
    }

    /**
     * Returns PORT part of URI, assuming form: "scheme://...@host.com:port/..."
     *
     * @param uri uri
     * @return optional
     */
    public static Ref<Integer> getPort(URI uri) {
        int port = uri.getPort();
        return -1 != port ? Ref.of(port) : Ref.empty();
    }

    /**
     * Returns MultiMap with all query parameters.
     * Returns MultiMap because URIs support adding multiple parameters with the same name.
     *
     * @param uri uri
     * @return MultiMap
     */
    public static MultiMap<String,String> getParams(URI uri) {
        String query = uri.getQuery();
        if(StringTraits.isEmpty(query)) {
            return MultiMap.empty();
        }
        return Arrays
            .stream(query.split("&"))
            .map(v -> v.split("="))
            .collect(MapCollectors.multimap(p -> paramName(p), p -> paramValue(p)));
    }

    /**
     * Returns first value of query parameter with "name".
     *
     * @param uri uri
     * @param param param
     * @return optional
     */
    public static Ref<String> getParam(URI uri, String param) {
        String query = uri.getQuery();
        if(StringTraits.isEmpty(query)) {
            return Ref.empty();
        }
        return CQuery.from()
            .array(query.split("&"))
            .map(v -> v.split("="))
            .filter(p -> param.equals(paramName(p)))
            .map(p -> paramValue(p))
            .first();
    }

    /**
     * Returns all values of query parameter with "name".
     * Because URI allows for multiple parameters with the same value, returns list.
     *
     * @param uri uri
     * @param param param
     * @return list
     */
    public static List<String> getParams(URI uri, String param) {
        String query = uri.getQuery();
        if(StringTraits.isEmpty(query)) {
            return Collections.emptyList();
        }
        return Arrays
            .stream(query.split("&"))
            .map(v -> v.split("="))
            .filter(p -> param.equals(paramName(p)))
            .map(p -> paramValue(p))
            .collect(Collectors.toList());
    }
    
    private static String paramName(String[] array) {
        return array.length > 0 ? array[0] : "";
    }
    
    private static String paramValue(String[] array) {
        return array.length > 1 ? URIUtils.decodeURL(array[1]) : "";
    }
    
}
