//
//  ========================================================================
//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.eclipse.jetty.util.ArrayTrie;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.Trie;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;


/* ------------------------------------------------------------ */
/** MIME Type enum and utilities
 *
 */
public class MimeTypes
{
    /* ------------------------------------------------------------ */
    private static final Logger LOG = Log.getLogger(MimeTypes.class);
    private static final  Trie<ByteBuffer> TYPES= new ArrayTrie<ByteBuffer>(512);
    private static final  Map<String,String> __dftMimeMap = new HashMap<String,String>();
    private static final  Map<String,String> __inferredEncodings = new HashMap<String,String>();
    private static final  Map<String,String> __assumedEncodings = new HashMap<String,String>();
    
    public enum Type
    {
        FORM_ENCODED("application/x-www-form-urlencoded"),
        MESSAGE_HTTP("message/http"),
        MULTIPART_BYTERANGES("multipart/byteranges"),
        MULTIPART_FORM_DATA("multipart/form-data"),

        TEXT_HTML("text/html"),
        TEXT_PLAIN("text/plain"),
        TEXT_XML("text/xml"),
        TEXT_JSON("text/json",StandardCharsets.UTF_8),
        APPLICATION_JSON("application/json",StandardCharsets.UTF_8),

        TEXT_HTML_8859_1("text/html;charset=iso-8859-1",TEXT_HTML),
        TEXT_HTML_UTF_8("text/html;charset=utf-8",TEXT_HTML),

        TEXT_PLAIN_8859_1("text/plain;charset=iso-8859-1",TEXT_PLAIN),
        TEXT_PLAIN_UTF_8("text/plain;charset=utf-8",TEXT_PLAIN),

        TEXT_XML_8859_1("text/xml;charset=iso-8859-1",TEXT_XML),
        TEXT_XML_UTF_8("text/xml;charset=utf-8",TEXT_XML),

        TEXT_JSON_8859_1("text/json;charset=iso-8859-1",TEXT_JSON),
        TEXT_JSON_UTF_8("text/json;charset=utf-8",TEXT_JSON),

        APPLICATION_JSON_8859_1("application/json;charset=iso-8859-1",APPLICATION_JSON),
        APPLICATION_JSON_UTF_8("application/json;charset=utf-8",APPLICATION_JSON);


        /* ------------------------------------------------------------ */
        private final String _string;
        private final Type _base;
        private final ByteBuffer _buffer;
        private final Charset _charset;
        private final String _charsetString;
        private final boolean _assumedCharset;
        private final HttpField _field;

        /* ------------------------------------------------------------ */
        Type(String s)
        {
            _string=s;
            _buffer=BufferUtil.toBuffer(s);
            _base=this;
            _charset=null;
            _charsetString=null;
            _assumedCharset=false;
            _field=new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,_string);
        }

        /* ------------------------------------------------------------ */
        Type(String s,Type base)
        {
            _string=s;
            _buffer=BufferUtil.toBuffer(s);
            _base=base;
            int i=s.indexOf(";charset=");
            _charset=Charset.forName(s.substring(i+9));
            _charsetString=_charset.toString().toLowerCase(Locale.ENGLISH);
            _assumedCharset=false;
            _field=new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,_string);
        }

        /* ------------------------------------------------------------ */
        Type(String s,Charset cs)
        {
            _string=s;
            _base=this;
            _buffer=BufferUtil.toBuffer(s);
            _charset=cs;
            _charsetString=_charset==null?null:_charset.toString().toLowerCase(Locale.ENGLISH);
            _assumedCharset=true;
            _field=new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,_string);
        }

        /* ------------------------------------------------------------ */
        public ByteBuffer asBuffer()
        {
            return _buffer.asReadOnlyBuffer();
        }

        /* ------------------------------------------------------------ */
        public Charset getCharset()
        {
            return _charset;
        }

        /* ------------------------------------------------------------ */
        public String getCharsetString()
        {
            return _charsetString;
        }

        /* ------------------------------------------------------------ */
        public boolean is(String s)
        {
            return _string.equalsIgnoreCase(s);
        }

        /* ------------------------------------------------------------ */
        public String asString()
        {
            return _string;
        }

        /* ------------------------------------------------------------ */
        @Override
        public String toString()
        {
            return _string;
        }

        /* ------------------------------------------------------------ */
        public boolean isCharsetAssumed()
        {
            return _assumedCharset;
        }

        /* ------------------------------------------------------------ */
        public HttpField getContentTypeField()
        {
            return _field;
        }

        /* ------------------------------------------------------------ */
        public Type getBaseType()
        {
            return _base;
        }
    }

    /* ------------------------------------------------------------ */
    public  static final Trie<MimeTypes.Type> CACHE= new ArrayTrie<>(512);
    static
    {
        for (MimeTypes.Type type : MimeTypes.Type.values())
        {
            CACHE.put(type.toString(),type);
            TYPES.put(type.toString(),type.asBuffer());

            int charset=type.toString().indexOf(";charset=");
            if (charset>0)
            {
                String alt=type.toString().replace(";charset=","; charset=");
                CACHE.put(alt,type);
                TYPES.put(alt,type.asBuffer());
            }
            
            if (type.isCharsetAssumed())
                __assumedEncodings.put(type.asString(),type.getCharsetString());
        }

        String resourceName = "org/eclipse/jetty/http/mime.properties";
        try (InputStream stream = MimeTypes.class.getClassLoader().getResourceAsStream(resourceName))
        {
            if (stream == null)
            {
                LOG.warn("Missing mime-type resource: {}", resourceName);
            }
            else
            {
                try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8))
                {
                    Properties props = new Properties();
                    props.load(reader);
                    props.stringPropertyNames().stream()
                    .filter(x->x!=null)
                    .forEach(x->
                    __dftMimeMap.put(StringUtil.asciiToLowerCase(x), normalizeMimeType(props.getProperty(x))));

                    if (__dftMimeMap.size()==0)
                    {
                        LOG.warn("Empty mime types at {}", resourceName);
                    }
                    else if (__dftMimeMap.size()<props.keySet().size())
                    {
                        LOG.warn("Duplicate or null mime-type extension in resource: {}", resourceName);
                    }  
                }
                catch (IOException e)
                {
                    LOG.warn(e.toString());
                    LOG.debug(e);
                }

            }
        }
        catch (IOException e)
        {
            LOG.warn(e.toString());
            LOG.debug(e);
        }
        
        resourceName = "org/eclipse/jetty/http/encoding.properties";
        try (InputStream stream = MimeTypes.class.getClassLoader().getResourceAsStream(resourceName))
        {
            if (stream == null)
                LOG.warn("Missing encoding resource: {}", resourceName);
            else
            {
                try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8))
                {
                    Properties props = new Properties();
                    props.load(reader);
                    props.stringPropertyNames().stream()
                    .filter(t->t!=null)
                    .forEach(t->
                    {
                        String charset = props.getProperty(t);
                        if (charset.startsWith("-"))
                            __assumedEncodings.put(t, charset.substring(1));
                        else
                            __inferredEncodings.put(t, props.getProperty(t));
                    });

                    if (__inferredEncodings.size()==0)
                    {
                        LOG.warn("Empty encodings at {}", resourceName);
                    }
                    else if ((__inferredEncodings.size()+__assumedEncodings.size())<props.keySet().size())
                    {
                        LOG.warn("Null or duplicate encodings in resource: {}", resourceName);
                    }
                }
                catch (IOException e)
                {
                    LOG.warn(e.toString());
                    LOG.debug(e);
                }
            }
        }
        catch (IOException e)
        {
            LOG.warn(e.toString());
            LOG.debug(e);
        }
    }


    /* ------------------------------------------------------------ */
    private final Map<String,String> _mimeMap=new HashMap<String,String>();

    /* ------------------------------------------------------------ */
    /** Constructor.
     */
    public MimeTypes()
    {
    }

    /* ------------------------------------------------------------ */
    public synchronized Map<String,String> getMimeMap()
    {
        return _mimeMap;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param mimeMap A Map of file extension to mime-type.
     */
    public void setMimeMap(Map<String,String> mimeMap)
    {
        _mimeMap.clear();
        if (mimeMap!=null)
        {
            for (Entry<String, String> ext : mimeMap.entrySet())
                _mimeMap.put(StringUtil.asciiToLowerCase(ext.getKey()),normalizeMimeType(ext.getValue()));
        }
    }

    /* ------------------------------------------------------------ */
    /** Get the MIME type by filename extension.
     * Lookup only the static default mime map.
     * @param filename A file name
     * @return MIME type matching the longest dot extension of the
     * file name.
     */
    public static String getDefaultMimeByExtension(String filename)
    {
        String type=null;

        if (filename!=null)
        {
            int i=-1;
            while(type==null)
            {
                i=filename.indexOf(".",i+1);

                if (i<0 || i>=filename.length())
                    break;

                String ext=StringUtil.asciiToLowerCase(filename.substring(i+1));
                if (type==null)
                    type=__dftMimeMap.get(ext);
            }
        }

        if (type==null)
        {
            type=__dftMimeMap.get("*");
        }

        return type;
    }

    /* ------------------------------------------------------------ */
    /** Get the MIME type by filename extension.
     * Lookup the content and static default mime maps.
     * @param filename A file name
     * @return MIME type matching the longest dot extension of the
     * file name.
     */
    public String getMimeByExtension(String filename)
    {
        String type=null;

        if (filename!=null)
        {
            int i=-1;
            while(type==null)
            {
                i=filename.indexOf(".",i+1);

                if (i<0 || i>=filename.length())
                    break;

                String ext=StringUtil.asciiToLowerCase(filename.substring(i+1));
                if (_mimeMap!=null)
                    type=_mimeMap.get(ext);
                if (type==null)
                    type=__dftMimeMap.get(ext);
            }
        }

        if (type==null)
        {
            if (_mimeMap!=null)
                type=_mimeMap.get("*");
            if (type==null)
                type=__dftMimeMap.get("*");
        }

        return type;
    }

    /* ------------------------------------------------------------ */
    /** Set a mime mapping
     * @param extension the extension
     * @param type the mime type
     */
    public void addMimeMapping(String extension,String type)
    {
        _mimeMap.put(StringUtil.asciiToLowerCase(extension),normalizeMimeType(type));
    }

    /* ------------------------------------------------------------ */
    public static Set<String> getKnownMimeTypes()
    {
        return new HashSet<>(__dftMimeMap.values());
    }

    /* ------------------------------------------------------------ */
    private static String normalizeMimeType(String type)
    {
        MimeTypes.Type t =CACHE.get(type);
        if (t!=null)
            return t.asString();

        return StringUtil.asciiToLowerCase(type);
    }

    /* ------------------------------------------------------------ */
    public static String getCharsetFromContentType(String value)
    {
        if (value==null)
            return null;
        int end=value.length();
        int state=0;
        int start=0;
        boolean quote=false;
        int i=0;
        for (;i<end;i++)
        {
            char b = value.charAt(i);

            if (quote && state!=10)
            {
                if ('"'==b)
                    quote=false;
                continue;
            }
            
            if(';'==b && state<=8)
            {
                state = 1;
                continue;
            }

            switch(state)
            {
                case 0:
                    if ('"'==b)
                    {
                        quote=true;
                        break;
                    }
                    break;

                case 1: if ('c'==b) state=2; else if (' '!=b) state=0; break;
                case 2: if ('h'==b) state=3; else state=0;break;
                case 3: if ('a'==b) state=4; else state=0;break;
                case 4: if ('r'==b) state=5; else state=0;break;
                case 5: if ('s'==b) state=6; else state=0;break;
                case 6: if ('e'==b) state=7; else state=0;break;
                case 7: if ('t'==b) state=8; else state=0;break;

                case 8: if ('='==b) state=9; else if (' '!=b) state=0; break;

                case 9:
                    if (' '==b)
                        break;
                    if ('"'==b)
                    {
                        quote=true;
                        start=i+1;
                        state=10;
                        break;
                    }
                    start=i;
                    state=10;
                    break;

                case 10:
                    if (!quote && (';'==b || ' '==b )||
                            (quote && '"'==b ))
                        return StringUtil.normalizeCharset(value,start,i-start);
            }
        }

        if (state==10)
            return StringUtil.normalizeCharset(value,start,i-start);

        return null;
    }

    /**
     * Access a mutable map of mime type to the charset inferred from that content type.
     * An inferred encoding is used by when encoding/decoding a stream and is 
     * explicitly set in any metadata (eg Content-Type).
     * @return Map of mime type to charset
     */
    public static Map<String,String> getInferredEncodings()
    {
        return __inferredEncodings;
    }
    
    /**
     * Access a mutable map of mime type to the charset assumed for that content type.
     * An assumed encoding is used by when encoding/decoding a stream, but is not 
     * explicitly set in any metadata (eg Content-Type).
     * @return Map of mime type to charset
     */
    public static Map<String,String> getAssumedEncodings()
    {
        return __assumedEncodings;
    }

    @Deprecated
    public static String inferCharsetFromContentType(String contentType)
    {
        return getCharsetAssumedFromContentType(contentType);
    }
    
    public static String getCharsetInferredFromContentType(String contentType)
    {
        return __inferredEncodings.get(contentType);
    }

    public static String getCharsetAssumedFromContentType(String contentType)
    {
        return __assumedEncodings.get(contentType);
    }
    
    public static String getContentTypeWithoutCharset(String value)
    {
        int end=value.length();
        int state=0;
        int start=0;
        boolean quote=false;
        int i=0;
        StringBuilder builder=null;
        for (;i<end;i++)
        {
            char b = value.charAt(i);

            if ('"'==b)
            {
                if (quote)
                {
                    quote=false;
                }
                else
                {
                    quote=true;
                }

                switch(state)
                {
                    case 11:
                        builder.append(b);break;
                    case 10:
                        break;
                    case 9:
                        builder=new StringBuilder();
                        builder.append(value,0,start+1);
                        state=10;
                        break;
                    default:
                        start=i;
                        state=0;
                }
                continue;
            }

            if (quote)
            {
                if (builder!=null && state!=10)
                    builder.append(b);
                continue;
            }

            switch(state)
            {
                case 0:
                    if (';'==b)
                        state=1;
                    else if (' '!=b)
                        start=i;
                    break;

                case 1: if ('c'==b) state=2; else if (' '!=b) state=0; break;
                case 2: if ('h'==b) state=3; else state=0;break;
                case 3: if ('a'==b) state=4; else state=0;break;
                case 4: if ('r'==b) state=5; else state=0;break;
                case 5: if ('s'==b) state=6; else state=0;break;
                case 6: if ('e'==b) state=7; else state=0;break;
                case 7: if ('t'==b) state=8; else state=0;break;
                case 8: if ('='==b) state=9; else if (' '!=b) state=0; break;

                case 9:
                    if (' '==b)
                        break;
                    builder=new StringBuilder();
                    builder.append(value,0,start+1);
                    state=10;
                    break;

                case 10:
                    if (';'==b)
                    {
                        builder.append(b);
                        state=11;
                    }
                    break;
                case 11:
                    if (' '!=b)
                        builder.append(b);
            }
        }
        if (builder==null)
            return value;
        return builder.toString();

    }

}
