//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  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.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
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;


/* ------------------------------------------------------------ */
/**
 * 
 */
public class MimeTypes
{
    public enum Type
    {
        FORM_ENCODED("application/x-www-form-urlencoded"),
        MESSAGE_HTTP("message/http"),
        MULTIPART_BYTERANGES("multipart/byteranges"),

        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("text/json; charset=ISO-8859-1",APPLICATION_JSON),
        APPLICATION_JSON_UTF_8("text/json; charset=UTF-8",APPLICATION_JSON);


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

        /* ------------------------------------------------------------ */
        Type(String s)
        {
            _string=s;
            _buffer=BufferUtil.toBuffer(s);
            _base=this;
            _charset=null;
            _assumedCharset=false;
            _field=new HttpGenerator.CachedHttpField(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+10));
            _assumedCharset=false;
            _field=new HttpGenerator.CachedHttpField(HttpHeader.CONTENT_TYPE,_string);
        }

        /* ------------------------------------------------------------ */
        Type(String s,Charset cs)
        {
            _string=s;
            _base=this;
            _buffer=BufferUtil.toBuffer(s);
            _charset=cs;
            _assumedCharset=true;
            _field=new HttpGenerator.CachedHttpField(HttpHeader.CONTENT_TYPE,_string);
        }

        /* ------------------------------------------------------------ */
        public ByteBuffer asBuffer()
        {
            return _buffer.asReadOnlyBuffer();
        }
        
        /* ------------------------------------------------------------ */
        public Charset getCharset()
        {
            return _charset;
        }
        
        /* ------------------------------------------------------------ */
        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;
        }
    }

    /* ------------------------------------------------------------ */
    private static final Logger LOG = Log.getLogger(MimeTypes.class);
    public  final static Trie<MimeTypes.Type> CACHE= new ArrayTrie<>(512);
    private final static Trie<ByteBuffer> TYPES= new ArrayTrie<ByteBuffer>(512);
    private final static Map<String,String> __dftMimeMap = new HashMap<String,String>();
    private final static Map<String,String> __encodings = new HashMap<String,String>();

    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)
            {
                CACHE.put(type.toString().replace(";charset=","; charset="),type);
                TYPES.put(type.toString().replace(";charset=","; charset="),type.asBuffer());
            }
        }

        try
        {
            ResourceBundle mime = ResourceBundle.getBundle("org/eclipse/jetty/http/mime");
            Enumeration<String> i = mime.getKeys();
            while(i.hasMoreElements())
            {
                String ext = i.nextElement();
                String m = mime.getString(ext);
                __dftMimeMap.put(StringUtil.asciiToLowerCase(ext),normalizeMimeType(m));
            }
        }
        catch(MissingResourceException e)
        {
            LOG.warn(e.toString());
            LOG.debug(e);
        }

        try
        {
            ResourceBundle encoding = ResourceBundle.getBundle("org/eclipse/jetty/http/encoding");
            Enumeration<String> i = encoding.getKeys();
            while(i.hasMoreElements())
            {
                String type = i.nextElement();
                __encodings.put(type,encoding.getString(type));
            }
        }
        catch(MissingResourceException 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.
     * @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
     * @param 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;
            }

            switch(state)
            {
                case 0:
                    if ('"'==b)
                    {
                        quote=true;
                        break;
                    }
                    if (';'==b)
                        state=1;
                    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;
    }

    public static String inferCharsetFromContentType(String value)
    {
        return __encodings.get(value);
    }
    
    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();

    }
}
