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

import net.ranides.assira.collection.sets.MaskSet;
import net.ranides.assira.generic.ValueUtils;
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 java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This URIHandler loads files from URI data scheme.
 *
 * Supported schemes: data
 *
 *
 * Please note:
 * As every URIHandler, it should not be constructed directly, but connected with some instance of URIResolver.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public class CDataHandler implements URIHandler {

    static final MaskSet<URIFlags> TXT_FLAGS = URIFlags.constant(
        URIFlags.READABLE,
        URIFlags.EXISTS,
        URIFlags.FILE,
        URIFlags.SIZE,
        URIFlags.SEEK,
        URIFlags.TEXT
    );

    static final MaskSet<URIFlags> BIN_FLAGS = URIFlags.constant(
        URIFlags.READABLE,
        URIFlags.EXISTS,
        URIFlags.FILE,
        URIFlags.SIZE,
        URIFlags.SEEK,
        URIFlags.BINARY
    );

    static final Pattern RE_DATA_SCHEME = Pattern.compile("^([^;,]+)?((?:;(?:[^;,]+))*?)(;base64)?,(.*)$");

    private final URIResolver resolver;

    /**
     * Default constructor
     */
    public CDataHandler() {
        this.resolver = URIResolver.DEFAULT;
    }

    /**
     * Constructor used by ServiceLoader inside static initialization of {@link URIResolver}
     * @param resolver owner
     */
    public CDataHandler(URIResolver resolver) {
        this.resolver = resolver;
    }

    @Override
    public URIHandle resolve(URI uri) throws IOException {
        return new CDataHandle(uri);
    }

    /**
     * Helper method, tries to resolve URI given in string
     *
     * @param uri uri
     * @return CDataHandle
     *
     * @throws IOException if cdata section is invalid
     * @throws URISyntaxException if URI is invalid
     */
    public URIHandle resolve(String uri) throws IOException, URISyntaxException {
        return new CDataHandle(new URI(uri));
    }

    @Override
    public Collection<String> schemes() {
        return Arrays.asList("data");
    }

    /**
     * dataurl    := "data:" [ mediatype ] [ ";base64" ] "," data
     * mediatype  := [ type "/" subtype ] *( ";" parameter )
     * data       := *urlchar
     * parameter  := attribute "=" value
     */
    private class CDataHandle extends CHandle {

        private final URI uri;
        private final MaskSet<URIFlags> flags;
        private final Charset charset;
        private final byte[] data;
        private final String text;

        public CDataHandle(URI uri) throws IOException {
            Matcher hit = RE_DATA_SCHEME.matcher(uri.getRawSchemeSpecificPart());
            if(!hit.matches()) {
                throw new IOException("Invalid data URI: " + uri);
            }

            boolean isBase64 = hit.group(3) != null;
            String mime = ValueUtils.or(hit.group(1), "text/plain");

            this.uri = uri;
            this.charset = getCharset(hit.group(2));
            this.flags = mime.startsWith("text/") ? TXT_FLAGS : BIN_FLAGS;

            if (isBase64) {
                data = Base64.getDecoder().decode(hit.group(4));
                text = new String(data, charset);
            } else {
                text = URLDecoder.decode(hit.group(4), charset.name());
                data = text.getBytes(charset);
            }
        }

        private Charset getCharset(String x) {
            Matcher hit = Pattern.compile(";[ ]*charset=([^; ]+)").matcher(x);
            if(hit.find()) {
                return Charset.forName(hit.group(1));
            } else {
                return Charsets.UTF8;
            }
        }

        @Override
        public URIResolver resolver() {
            return CDataHandler.this.resolver;
        }

        @Override
        public boolean exists() {
            return true;
        }

        @Override
        public Charset charset() {
            return charset;
        }

        @Override
        public URI uri() {
            return uri;
        }

        @Override
        public String scheme() {
            return "data";
        }
        
        @Override
        public Set<URIFlags> flags() {
            return flags;
        }

        @Override
        public long size() throws IOException {
            return data.length;
        }

        @Override
        public InputStream istream() throws IOException {
            return new ByteArrayInputStream(data);
        }

        @Override
        public Reader reader() throws IOException {
            return new StringReader(text);
        }
    }
    
}
