//
//  ========================================================================
//  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.server;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.servlet.http.Cookie;

import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;


/* ------------------------------------------------------------ */
/** Cookie parser
 * <p>Optimized stateful cookie parser.  Cookies fields are added with the
 * {@link #addCookieField(String)} method and parsed on the next subsequent
 * call to {@link #getCookies()}.
 * If the added fields are identical to those last added (as strings), then the 
 * cookies are not re parsed.
 * 
 *
 */
public class CookieCutter
{
    private static final Logger LOG = Log.getLogger(CookieCutter.class);

    private Cookie[] _cookies;
    private Cookie[] _lastCookies;
    private final List<String> _fieldList = new ArrayList<>();
    int _fields;
    
    public CookieCutter()
    {  
    }
    
    public Cookie[] getCookies()
    {
        if (_cookies!=null)
            return _cookies;
        
        if (_lastCookies!=null && _fields==_fieldList.size())
            _cookies=_lastCookies;
        else
            parseFields();
        _lastCookies=_cookies;
        return _cookies;
    }
    
    public void setCookies(Cookie[] cookies)
    {
        _cookies=cookies;
        _lastCookies=null;
        _fieldList.clear();
        _fields=0;
    }
    
    public void reset()
    {
        _cookies=null;
        _fields=0;
    }
    
    public void addCookieField(String f)
    {
        if (f==null)
            return;
        f=f.trim();
        if (f.length()==0)
            return;
            
        if (_fieldList.size()>_fields)
        {
            if (f.equals(_fieldList.get(_fields)))
            {
                _fields++;
                return;
            }
            
            while (_fieldList.size()>_fields)
                _fieldList.remove(_fields);
        }
        _cookies=null;
        _lastCookies=null;
        _fieldList.add(_fields++,f);
    }
    
    
    protected void parseFields()
    {
        _lastCookies=null;
        _cookies=null;
        
        List<Cookie> cookies = new ArrayList<>();

        int version = 0;

        // delete excess fields
        while (_fieldList.size()>_fields)
            _fieldList.remove(_fields);
        
        // For each cookie field
        for (String hdr : _fieldList)
        {
            // Parse the header
            String name = null;
            String value = null;

            Cookie cookie = null;

            boolean invalue=false;
            boolean quoted=false;
            boolean escaped=false;
            int tokenstart=-1;
            int tokenend=-1;
            for (int i = 0, length = hdr.length(), last=length-1; i < length; i++)
            {
                char c = hdr.charAt(i);
                
                // Handle quoted values for name or value
                if (quoted)
                {
                    if (escaped)
                    {
                        escaped=false;
                        continue;
                    }
                    
                    switch (c)
                    {
                        case '"':
                            tokenend=i;
                            quoted=false;

                            // handle quote as last character specially
                            if (i==last)
                            {
                                if (invalue)
                                    value = hdr.substring(tokenstart, tokenend+1);
                                else
                                {
                                    name = hdr.substring(tokenstart, tokenend+1);
                                    value = "";
                                }
                            }
                            break;
                            
                        case '\\':
                            escaped=true;
                            continue;
                        default:
                            continue;
                    }
                }
                else
                {
                    // Handle name and value state machines
                    if (invalue)
                    {
                        // parse the value
                        switch (c)
                        {
                            case ' ':
                            case '\t':
                                continue;
                                
                            case '"':
                                if (tokenstart<0)
                                {
                                    quoted=true;
                                    tokenstart=i;
                                }
                                tokenend=i;
                                if (i==last)
                                {
                                    value = hdr.substring(tokenstart, tokenend+1);
                                    break;
                                }
                                continue;

                            case ';':
                                if (tokenstart>=0)
                                    value = hdr.substring(tokenstart, tokenend+1);
                                else
                                    value="";
                                tokenstart = -1;
                                invalue=false;
                                break;
                                
                            default:
                                if (tokenstart<0)
                                    tokenstart=i;
                                tokenend=i;
                                if (i==last)
                                {
                                    value = hdr.substring(tokenstart, tokenend+1);
                                    break;
                                }
                                continue;
                        }
                    }
                    else
                    {
                        // parse the name
                        switch (c)
                        {
                            case ' ':
                            case '\t':
                                continue;
                                
                            case '"':
                                if (tokenstart<0)
                                {
                                    quoted=true;
                                    tokenstart=i;
                                }
                                tokenend=i;
                                if (i==last)
                                {
                                    name = hdr.substring(tokenstart, tokenend+1);
                                    value = "";
                                    break;
                                }
                                continue;

                            case ';':
                                if (tokenstart>=0)
                                {
                                    name = hdr.substring(tokenstart, tokenend+1);
                                    value = "";
                                }
                                tokenstart = -1;
                                break;

                            case '=':
                                if (tokenstart>=0)
                                    name = hdr.substring(tokenstart, tokenend+1);
                                tokenstart = -1;
                                invalue=true;
                                continue;
                                
                            default:
                                if (tokenstart<0)
                                    tokenstart=i;
                                tokenend=i;
                                if (i==last)
                                {
                                    name = hdr.substring(tokenstart, tokenend+1);
                                    value = "";
                                    break;
                                }
                                continue;
                        }
                    }
                }

                // If after processing the current character we have a value and a name, then it is a cookie
                if (value!=null && name!=null)
                {
                    name=QuotedStringTokenizer.unquoteOnly(name);
                    value=QuotedStringTokenizer.unquoteOnly(value);
                    
                    try
                    {
                        if (name.startsWith("$"))
                        {
                            String lowercaseName = name.toLowerCase(Locale.ENGLISH);
                            if ("$path".equals(lowercaseName))
                            {
                                if (cookie!=null)
                                    cookie.setPath(value);
                            }
                            else if ("$domain".equals(lowercaseName))
                            {
                                if (cookie!=null)
                                    cookie.setDomain(value);
                            }
                            else if ("$port".equals(lowercaseName))
                            {
                                if (cookie!=null)
                                    cookie.setComment("$port="+value);
                            }
                            else if ("$version".equals(lowercaseName))
                            {
                                version = Integer.parseInt(value);
                            }
                        }
                        else
                        {
                            cookie = new Cookie(name, value);
                            if (version > 0)
                                cookie.setVersion(version);
                            cookies.add(cookie);
                        }
                    }
                    catch (Exception e)
                    {
                        LOG.debug(e);
                    }

                    name = null;
                    value = null;
                }
            }
        }

        _cookies = (Cookie[]) cookies.toArray(new Cookie[cookies.size()]);
        _lastCookies=_cookies;
    }
    
}
