/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.brooklyn.util.net;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.List;

import javax.annotation.Nullable;

import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.text.Strings;

import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.io.BaseEncoding;
import com.google.common.net.MediaType;

public class Urls {

    public static Function<String,URI> stringToUriFunction() {
        return StringToUri.INSTANCE;
    }
    
    public static Function<String,URL> stringToUrlFunction() {
        return StringToUrl.INSTANCE;
    }
    
    private static enum StringToUri implements Function<String,URI> {
        INSTANCE;
        @Override public URI apply(@Nullable String input) {
            return toUri(input);
        }
        @Override
        public String toString() {
            return "StringToUri";
        }
    }

    private static enum StringToUrl implements Function<String,URL> {
        INSTANCE;
        @Override public URL apply(@Nullable String input) {
            return toUrl(input);
        }
        @Override
        public String toString() {
            return "StringToUrl";
        }
    }

    /** creates a URL, preserving null and propagating exceptions *unchecked* */
    public static final URL toUrl(@Nullable String url) {
        if (url==null) return null;
        try {
            return new URL(url);
        } catch (MalformedURLException e) {
            // FOAD
            throw Throwables.propagate(e);
        }
    }
    
    /** creates a URL, preserving null and propagating exceptions *unchecked* */
    public static final URL toUrl(@Nullable URI uri) {
        if (uri==null) return null;
        try {
            return uri.toURL();
        } catch (MalformedURLException e) {
            // FOAD
            throw Throwables.propagate(e);
        }
    }

    /** creates a URI, preserving null and propagating exceptions *unchecked* */
    public static final URI toUri(@Nullable String uri) {
        if (uri==null) return null;
        return URI.create(uri);
    }
    
    /** creates a URI, preserving null and propagating exceptions *unchecked* */
    public static final URI toUri(@Nullable URL url) {
        if (url==null) return null;
        try {
            return url.toURI();
        } catch (URISyntaxException e) {
            // FOAD
            throw Throwables.propagate(e);
        }
    }

    /** returns true if the string begins with a non-empty string of letters followed by a colon,
     * i.e. "protocol:" returns true, but "/" returns false,
     * followed by a non-space character. thereafter it is not strict, it can have spaces, but not newlines;
     * see {@link #isUrlWithProtocol(String, boolean, boolean)} */
    public static boolean isUrlWithProtocol(String x) {
        return isUrlWithProtocol(x, true, false);
    }
    
    /** as {@link #isUrlWithProtocol(String)} but configurable to be strict (false, false) or allow newline chars (if e.g. in an unescaped argument) */
    public static boolean isUrlWithProtocol(String x, boolean allowSpacesAfterCharAfterColon, boolean allowMultiline) {
        if (x==null) return false;
        for (int i=0; i<x.length(); i++) {
            char c = x.charAt(i);
            if (c==':') {
                if (i==0 || i+1>=x.length()) return false;
                char c2 = x.charAt(i+1);
                // never allow a whitespace or quote mark right after the ':', that is too similar to json/yaml!
                if (Character.isWhitespace(c2) || c2=='\'' || c2=='\"') return false;
                if (!allowMultiline) {
                    if (x.indexOf('\n')>=0) return false;
                    if (x.indexOf('\r')>=0) return false;
                }
                if (!allowSpacesAfterCharAfterColon) {
                    if (x.indexOf(' ')>=0) return false;
                    if (x.indexOf('\t')>=0) return false;
                }
                return true;
            }
            
            // protocol schema as per https://en.wikipedia.org/wiki/Uniform_Resource_Locator
            if (i==0) {
                if (!Character.isLetter(c)) return false;
            } else if (!Character.isAlphabetic(c) && !Character.isDigit(c) && c!='+' && c!='.' && c!='-') {
                return false; 
            }
        }
        // no colon found
        return false;
    }
    
    /** returns the items with exactly one "/" between items (whether or not the individual items start or end with /),
     * except where character before the / is a : (url syntax) in which case it will permit multiple (will not remove any).
     * Throws a NullPointerException if any elements of 'items' is null.
     *  */
    public static String mergePaths(String ...items) {
        List<String> parts = Arrays.asList(items);

        if (parts.contains(null)) {
            throw new NullPointerException(String.format("Unable to reliably merge path from parts: %s; input contains null values", parts));
        }

        StringBuilder result = new StringBuilder();
        for (String part: parts) {
            boolean trimThisMerge = result.length()>0 && !result.toString().endsWith("://") && !result.toString().endsWith(":///") && !result.toString().endsWith(":");
            if (trimThisMerge) {
                while (result.length()>0 && result.charAt(result.length()-1)=='/')
                    result.deleteCharAt(result.length()-1);
                result.append('/');
            }
            int i = result.length();
            result.append(part);
            if (trimThisMerge) {
                while (result.length()>i && result.charAt(i)=='/')
                    result.deleteCharAt(i);
            }
        }
        return result.toString();
    }

    /**
     * Encodes the string suitable for use in a URL, using default UTF-8 
     * (version of URLEncoder.encode that does not throw checked exception)
     */
    public static String encode(String text) {
        /*
         * TODO URLEncoder.encode is a HTML encoder, to make strings safe to use in 
         * x-www-form-urlencoded. There is huge overlap between that and url-encoding, but
         * they are not the same (e.g. space is converted to "+" rather than "%20", and
         * "*" is not converted to "%2A").
         * 
         * Correct URL-encoding depends on which part of the URL it is. For example, guava's 
         * UrlEscapers has separate escapers for path and fragment. Neither of these is 
         * appropriate for escaping the password in "http://myusername@mypassword:myhost".
         * 
         * org.apache.commons.codec.net.URLCodec behaves the same as java.net.URLEncoder: it 
         * is for "www-form-urlencoded".
         */
        try {
            return URLEncoder.encode(text, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw Exceptions.propagate(e);
        }
    }
    
    /** As {@link #encode(String)} */
    public static String decode(String text) {
        try {
            return URLDecoder.decode(text, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw Exceptions.propagate(e);
        }
    }

    /** returns the protocol (e.g. http) if one appears to be specified, or else null;
     * 'protocol' here should consist of 2 or more _letters_ only followed by a colon
     * (2 required to prevent {@code c:\xxx} being treated as a url)
     */
    public static String getProtocol(String url) {
        if (url==null) return null;
        int i=0;
        StringBuilder result = new StringBuilder();
        while (true) {
            if (url.length()<=i) return null;
            char c = url.charAt(i);
            if (Character.isLetter(c)) result.append(c);
            else if (c==':') {
                if (i>=2) return result.toString().toLowerCase();
                return null;
            } else return null;
            i++;
        }
    }

    /** return the last segment of the given url before any '?', e.g. the filename or last directory name in the case of directories
     * (cf unix `basename`) */
    public static String getBasename(String url) {
        if (url==null) return null;
        if (getProtocol(url)!=null) {
            int firstQ = url.indexOf('?');
            if (firstQ>=0)
                url = url.substring(0, firstQ);
        }
        url = Strings.removeAllFromEnd(url, "/");
        return url.substring(url.lastIndexOf('/')+1);
    }

    public static boolean isDirectory(String fileUrl) {
        File file;
        if (isUrlWithProtocol(fileUrl)) {
            if (getProtocol(fileUrl).equals("file")) {
                file = new File(URI.create(fileUrl));
            } else {
                return false;
            }
        } else {
            file = new File(fileUrl);
        }
        return file.isDirectory();
    }

    public static File toFile(String fileUrl) {
        if (isUrlWithProtocol(fileUrl)) {
            if (getProtocol(fileUrl).equals("file")) {
                return new File(URI.create(fileUrl));
            } else {
                throw new IllegalArgumentException("Not a file protocol URL: " + fileUrl);
            }
        } else {
            return new File(fileUrl);
        }
    }

    /** as {@link #asDataUrlBase64(String)} with plain text */
    public static String asDataUrlBase64(String data) {
        return asDataUrlBase64(MediaType.PLAIN_TEXT_UTF_8, data.getBytes());
    }
    
    /** 
     * Creates a "data:..." scheme URL for use with supported parsers, using Base64 encoding.
     * (But note, by default Java's URL is not one of them, although Brooklyn's ResourceUtils does support it.)
     * <p>
     * It is not necessary (at least for Brookyn's routines) to base64 encode it, but recommended as that is likely more
     * portable and easier to work with if odd characters are included.
     * <p>
     * It is worth noting that Base64 uses '+' which can be replaced by ' ' in some URL parsing.  
     * But in practice it does not seem to cause issues.
     * An alternative is to use {@link BaseEncoding#base64Url()} but it is not clear how widely that is supported
     * (nor what parameter should be given to indicate that type of encoding, as the spec calls for 'base64'!)
     * <p>
     * null type means no type info will be included in the URL. */
    public static String asDataUrlBase64(MediaType type, byte[] bytes) {
        return "data:"+(type!=null ? type.withoutParameters().toString() : "")+";base64,"+new String(BaseEncoding.base64().encode(bytes));
    }

}
