/*
 * 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.core.text;

import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Map;

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

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.BaseEncoding;
//import com.sun.jersey.core.util.Base64;

/** implementation (currently hokey) of RFC-2397 data: URI scheme.
 * see: http://stackoverflow.com/questions/12353552/any-rfc-2397-data-uri-parser-for-java */
public class DataUriSchemeParser {

    public static final String PROTOCOL_PREFIX = "data:";
    public static final String DEFAULT_MIME_TYPE = "text/plain";
    public static final String DEFAULT_CHARSET = "US-ASCII";
    
    private final String url;
    private int parseIndex = 0;
    private boolean isParsed = false;
    private boolean allowMissingComma = false;
    private boolean allowSlashesAfterColon = false;
    private boolean allowOtherLaxities = false;
    
    private String mimeType;
    private byte[] data;
    private Map<String,String> parameters = new LinkedHashMap<String,String>();

    public DataUriSchemeParser(String url) {
        this.url = Preconditions.checkNotNull(url, "url");
    }

    // ---- static conveniences -----
    
    public static String toString(String url) {
        return new DataUriSchemeParser(url).lax().parse().getDataAsString();
    }

    public static byte[] toBytes(String url) {
        return new DataUriSchemeParser(url).lax().parse().getData();
    }

    // ---- accessors (once it is parsed) -----------
    
    public String getCharset() {
        String charset = parameters.get("charset");
        if (charset!=null) return charset;
        return DEFAULT_CHARSET;
    }

    public String getMimeType() {
        assertParsed();
        if (mimeType!=null) return mimeType;
        return DEFAULT_MIME_TYPE;
    }
    
    public Map<String, String> getParameters() {
        return ImmutableMap.<String, String>copyOf(parameters);
    }

    public byte[] getData() {
        assertParsed();
        return data;
    }
    
    public ByteArrayInputStream getDataAsInputStream() {
        return new ByteArrayInputStream(getData());
    }

    public String getDataAsString() {
        return new String(getData(), Charset.forName(getCharset()));
    }

    // ---- config ------------------
    
    public synchronized DataUriSchemeParser lax() {
        return allowMissingComma(true).allowSlashesAfterColon(true).allowOtherLaxities(true);
    }
        
    public synchronized DataUriSchemeParser allowMissingComma(boolean allowMissingComma) {
        assertNotParsed();
        this.allowMissingComma = allowMissingComma;
        return this;
    }
    
    public synchronized DataUriSchemeParser allowSlashesAfterColon(boolean allowSlashesAfterColon) {
        assertNotParsed();
        this.allowSlashesAfterColon = allowSlashesAfterColon;
        return this;
    }
    
    private synchronized DataUriSchemeParser allowOtherLaxities(boolean allowOtherLaxities) {
        assertNotParsed();
        this.allowOtherLaxities = allowOtherLaxities;
        return this;
    }
    
    private void assertNotParsed() {
        if (isParsed) throw new IllegalStateException("Operation not permitted after parsing");
    }

    private void assertParsed() {
        if (!isParsed) throw new IllegalStateException("Operation not permitted before parsing");
    }

    public synchronized DataUriSchemeParser parse() {
        try {
            return parseChecked();
        } catch (Exception e) {
            throw Exceptions.propagate(e);
        }
    }
    
    public synchronized DataUriSchemeParser parseChecked() throws UnsupportedEncodingException, MalformedURLException {
        if (isParsed) return this;
        
        skipOptional(PROTOCOL_PREFIX);
        if (allowSlashesAfterColon)
            while (skipOptional("/")) ;
        
        if (allowMissingComma && remainder().indexOf(',')==-1) {
            mimeType = DEFAULT_MIME_TYPE;
            parameters.put("charset", DEFAULT_CHARSET);
        } else {        
            parseMediaType();
            parseParameterOrParameterValues();
            skipRequired(",");
        }
        
        parseData();
        
        isParsed = true;
        return this;
    }

    private void parseMediaType() throws MalformedURLException {
        if (remainder().startsWith(";") || remainder().startsWith(","))
            return;
        int slash = remainder().indexOf("/");
        if (slash==-1) throw new MalformedURLException("Missing required '/' in MIME type of data: URL");
        String type = read(slash);
        skipRequired("/");
        int next = nextSemiOrComma();
        String subtype = read(next);
        mimeType = type+"/"+subtype;
    }

    private String read(int next) {
        String result = remainder().substring(0, next);
        parseIndex += next;
        return result;
    }

    private int nextSemiOrComma() throws MalformedURLException {
        int semi = remainder().indexOf(';');
        int comma = remainder().indexOf(',');
        if (semi<0 && comma<0) throw new MalformedURLException("Missing required ',' in data: URL");
        if (semi<0) return comma;
        if (comma<0) return semi;
        return Math.min(semi, comma);
    }

    private void parseParameterOrParameterValues() throws MalformedURLException {
        while (true) {
            if (!remainder().startsWith(";")) return;
            parseIndex++;
            int eq = remainder().indexOf('=');
            String word, value;
            int nextSemiOrComma = nextSemiOrComma();
            if (eq==-1 || eq>nextSemiOrComma) {
                word = read(nextSemiOrComma);
                value = null;
            } else {
                word = read(eq);
                if (remainder().startsWith("\"")) {
                    // is quoted
                    parseIndex++;
                    int nextUnescapedQuote = nextUnescapedQuote();
                    value = "\"" + read(nextUnescapedQuote);
                } else {
                    value = read(nextSemiOrComma());
                }
            }
            parameters.put(word, value);
        }
    }

    private int nextUnescapedQuote() throws MalformedURLException {
        int i=0;
        String r = remainder();
        boolean escaped = false;
        while (i<r.length()) {
            if (escaped) {
                escaped = false;
            } else {
                if (r.charAt(i)=='"') return i;
                if (r.charAt(i)=='\\') escaped = true;
            }
            i++;
        }
        throw new MalformedURLException("Unclosed double-quote in data: URL");
    }

    private void parseData() throws UnsupportedEncodingException, MalformedURLException {
        if (parameters.containsKey("base64")) {
            checkNoParamValue("base64");
            data = BaseEncoding.base64().decode(remainder());
        } else if (parameters.containsKey("base64url")) {
            checkNoParamValue("base64url");
            data = BaseEncoding.base64Url().decode(remainder());
        } else {
            data = URLDecoder.decode(remainder(), getCharset()).getBytes(Charset.forName(getCharset()));
        }
    }

    private void checkNoParamValue(String param) throws MalformedURLException {
        if (allowOtherLaxities) return; 
        String value = parameters.get(param);
        if (value!=null)
            throw new MalformedURLException(param+" parameter must not take a value ("+value+") in data: URL");
    }

    private String remainder() {
        return url.substring(parseIndex);
    }

    private boolean skipOptional(String word) {
        if (remainder().startsWith(word)) {
            parseIndex += word.length();
            return true;
        }
        return false;
    }

    private void skipRequired(String word) throws MalformedURLException {
        if (!remainder().startsWith(word))
            throw new MalformedURLException("Missing required '"+word+"' at position "+parseIndex+" of data: URL");
        parseIndex += word.length();
    }

}
