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

import java.io.IOException;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;

/* ------------------------------------------------------------ */
/** StringTokenizer with Quoting support.
 *
 * This class is a copy of the java.util.StringTokenizer API and
 * the behaviour is the same, except that single and double quoted
 * string values are recognised.
 * Delimiters within quotes are not considered delimiters.
 * Quotes can be escaped with '\'.
 *
 * @see java.util.StringTokenizer
 *
 */
public class QuotedStringTokenizer
    extends StringTokenizer
{
    private final static String __delim="\t\n\r";
    private String _string;
    private String _delim = __delim;
    private boolean _returnQuotes=false;
    private boolean _returnDelimiters=false;
    private StringBuffer _token;
    private boolean _hasToken=false;
    private int _i=0;
    private int _lastStart=0;
    private boolean _double=true;
    private boolean _single=true;

    /* ------------------------------------------------------------ */
    public QuotedStringTokenizer(String str,
                                 String delim,
                                 boolean returnDelimiters,
                                 boolean returnQuotes)
    {
        super("");
        _string=str;
        if (delim!=null)
            _delim=delim;
        _returnDelimiters=returnDelimiters;
        _returnQuotes=returnQuotes;

        if (_delim.indexOf('\'')>=0 ||
            _delim.indexOf('"')>=0)
            throw new Error("Can't use quotes as delimiters: "+_delim);

        _token=new StringBuffer(_string.length()>1024?512:_string.length()/2);
    }

    /* ------------------------------------------------------------ */
    public QuotedStringTokenizer(String str,
                                 String delim,
                                 boolean returnDelimiters)
    {
        this(str,delim,returnDelimiters,false);
    }

    /* ------------------------------------------------------------ */
    public QuotedStringTokenizer(String str,
                                 String delim)
    {
        this(str,delim,false,false);
    }

    /* ------------------------------------------------------------ */
    public QuotedStringTokenizer(String str)
    {
        this(str,null,false,false);
    }

    /* ------------------------------------------------------------ */
    @Override
    public boolean hasMoreTokens()
    {
        // Already found a token
        if (_hasToken)
            return true;

        _lastStart=_i;

        int state=0;
        boolean escape=false;
        while (_i<_string.length())
        {
            char c=_string.charAt(_i++);

            switch (state)
            {
              case 0: // Start
                  if(_delim.indexOf(c)>=0)
                  {
                      if (_returnDelimiters)
                      {
                          _token.append(c);
                          return _hasToken=true;
                      }
                  }
                  else if (c=='\'' && _single)
                  {
                      if (_returnQuotes)
                          _token.append(c);
                      state=2;
                  }
                  else if (c=='\"' && _double)
                  {
                      if (_returnQuotes)
                          _token.append(c);
                      state=3;
                  }
                  else
                  {
                      _token.append(c);
                      _hasToken=true;
                      state=1;
                  }
                  break;

              case 1: // Token
                  _hasToken=true;
                  if(_delim.indexOf(c)>=0)
                  {
                      if (_returnDelimiters)
                          _i--;
                      return _hasToken;
                  }
                  else if (c=='\'' && _single)
                  {
                      if (_returnQuotes)
                          _token.append(c);
                      state=2;
                  }
                  else if (c=='\"' && _double)
                  {
                      if (_returnQuotes)
                          _token.append(c);
                      state=3;
                  }
                  else
                  {
                      _token.append(c);
                  }
                  break;

              case 2: // Single Quote
                  _hasToken=true;
                  if (escape)
                  {
                      escape=false;
                      _token.append(c);
                  }
                  else if (c=='\'')
                  {
                      if (_returnQuotes)
                          _token.append(c);
                      state=1;
                  }
                  else if (c=='\\')
                  {
                      if (_returnQuotes)
                          _token.append(c);
                      escape=true;
                  }
                  else
                  {
                      _token.append(c);
                  }
                  break;

              case 3: // Double Quote
                  _hasToken=true;
                  if (escape)
                  {
                      escape=false;
                      _token.append(c);
                  }
                  else if (c=='\"')
                  {
                      if (_returnQuotes)
                          _token.append(c);
                      state=1;
                  }
                  else if (c=='\\')
                  {
                      if (_returnQuotes)
                          _token.append(c);
                      escape=true;
                  }
                  else
                  {
                      _token.append(c);
                  }
                  break;
            }
        }

        return _hasToken;
    }

    /* ------------------------------------------------------------ */
    @Override
    public String nextToken()
        throws NoSuchElementException
    {
        if (!hasMoreTokens() || _token==null)
            throw new NoSuchElementException();
        String t=_token.toString();
        _token.setLength(0);
        _hasToken=false;
        return t;
    }

    /* ------------------------------------------------------------ */
    @Override
    public String nextToken(String delim)
        throws NoSuchElementException
    {
        _delim=delim;
        _i=_lastStart;
        _token.setLength(0);
        _hasToken=false;
        return nextToken();
    }

    /* ------------------------------------------------------------ */
    @Override
    public boolean hasMoreElements()
    {
        return hasMoreTokens();
    }

    /* ------------------------------------------------------------ */
    @Override
    public Object nextElement()
        throws NoSuchElementException
    {
        return nextToken();
    }

    /* ------------------------------------------------------------ */
    /** Not implemented.
     */
    @Override
    public int countTokens()
    {
        return -1;
    }


    /* ------------------------------------------------------------ */
    /** Quote a string.
     * The string is quoted only if quoting is required due to
     * embedded delimiters, quote characters or the
     * empty string.
     * @param s The string to quote.
     * @param delim the delimiter to use to quote the string
     * @return quoted string
     */
    public static String quoteIfNeeded(String s, String delim)
    {
        if (s==null)
            return null;
        if (s.length()==0)
            return "\"\"";


        for (int i=0;i<s.length();i++)
        {
            char c = s.charAt(i);
            if (c=='\\' || c=='"' || c=='\'' || Character.isWhitespace(c) || delim.indexOf(c)>=0)
            {
                StringBuffer b=new StringBuffer(s.length()+8);
                quote(b,s);
                return b.toString();
            }
        }

        return s;
    }

    /* ------------------------------------------------------------ */
    /** Quote a string.
     * The string is quoted only if quoting is required due to
     * embeded delimiters, quote characters or the
     * empty string.
     * @param s The string to quote.
     * @return quoted string
     */
    public static String quote(String s)
    {
        if (s==null)
            return null;
        if (s.length()==0)
            return "\"\"";

        StringBuffer b=new StringBuffer(s.length()+8);
        quote(b,s);
        return b.toString();

    }

    private static final char[] escapes = new char[32];
    static
    {
        Arrays.fill(escapes, (char)0xFFFF);
        escapes['\b'] = 'b';
        escapes['\t'] = 't';
        escapes['\n'] = 'n';
        escapes['\f'] = 'f';
        escapes['\r'] = 'r';
    }

    /* ------------------------------------------------------------ */
    /** Quote a string into an Appendable.
     * Only quotes and backslash are escaped.
     * @param buffer The Appendable
     * @param input The String to quote.
     */
    public static void quoteOnly(Appendable buffer, String input)
    {
        if(input==null)
            return;

        try
        {
            buffer.append('"');
            for (int i = 0; i < input.length(); ++i)
            {
                char c = input.charAt(i);
                if (c == '"' || c == '\\')
                    buffer.append('\\');
                buffer.append(c);
            }
            buffer.append('"');
        }
        catch (IOException x)
        {
            throw new RuntimeException(x);
        }
    }

    /* ------------------------------------------------------------ */
    /** Quote a string into an Appendable.
     * The characters ", \, \n, \r, \t, \f and \b are escaped
     * @param buffer The Appendable
     * @param input The String to quote.
     */
    public static void quote(Appendable buffer, String input)
    {
        if(input==null)
            return;

        try
        {
            buffer.append('"');
            for (int i = 0; i < input.length(); ++i)
            {
                char c = input.charAt(i);
                if (c >= 32)
                {
                    if (c == '"' || c == '\\')
                        buffer.append('\\');
                    buffer.append(c);
                }
                else
                {
                    char escape = escapes[c];
                    if (escape == 0xFFFF)
                    {
                        // Unicode escape
                        buffer.append('\\').append('u').append('0').append('0');
                        if (c < 0x10)
                            buffer.append('0');
                        buffer.append(Integer.toString(c, 16));
                    }
                    else
                    {
                        buffer.append('\\').append(escape);
                    }
                }
            }
            buffer.append('"');
        }
        catch (IOException x)
        {
            throw new RuntimeException(x);
        }
    }


    /* ------------------------------------------------------------ */
    public static String unquoteOnly(String s)
    {
        return unquoteOnly(s, false);
    }


    /* ------------------------------------------------------------ */
    /** Unquote a string, NOT converting unicode sequences
     * @param s The string to unquote.
     * @param lenient if true, will leave in backslashes that aren't valid escapes
     * @return quoted string
     */
    public static String unquoteOnly(String s, boolean lenient)
    {
        if (s==null)
            return null;
        if (s.length()<2)
            return s;

        char first=s.charAt(0);
        char last=s.charAt(s.length()-1);
        if (first!=last || (first!='"' && first!='\''))
            return s;

        StringBuilder b = new StringBuilder(s.length() - 2);
        boolean escape=false;
        for (int i=1;i<s.length()-1;i++)
        {
            char c = s.charAt(i);

            if (escape)
            {
                escape=false;
                if (lenient && !isValidEscaping(c))
                {
                    b.append('\\');
                }
                b.append(c);
            }
            else if (c=='\\')
            {
                escape=true;
            }
            else
            {
                b.append(c);
            }
        }

        return b.toString();
    }

    /* ------------------------------------------------------------ */
    public static String unquote(String s)
    {
        return unquote(s,false);
    }

    /* ------------------------------------------------------------ */
    /** Unquote a string.
     * @param s The string to unquote.
     * @return quoted string
     */
    public static String unquote(String s, boolean lenient)
    {
        if (s==null)
            return null;
        if (s.length()<2)
            return s;

        char first=s.charAt(0);
        char last=s.charAt(s.length()-1);
        if (first!=last || (first!='"' && first!='\''))
            return s;

        StringBuilder b = new StringBuilder(s.length() - 2);
        boolean escape=false;
        for (int i=1;i<s.length()-1;i++)
        {
            char c = s.charAt(i);

            if (escape)
            {
                escape=false;
                switch (c)
                {
                    case 'n':
                        b.append('\n');
                        break;
                    case 'r':
                        b.append('\r');
                        break;
                    case 't':
                        b.append('\t');
                        break;
                    case 'f':
                        b.append('\f');
                        break;
                    case 'b':
                        b.append('\b');
                        break;
                    case '\\':
                        b.append('\\');
                        break;
                    case '/':
                        b.append('/');
                        break;
                    case '"':
                        b.append('"');
                        break;
                    case 'u':
                        b.append((char)(
                                (TypeUtil.convertHexDigit((byte)s.charAt(i++))<<24)+
                                (TypeUtil.convertHexDigit((byte)s.charAt(i++))<<16)+
                                (TypeUtil.convertHexDigit((byte)s.charAt(i++))<<8)+
                                (TypeUtil.convertHexDigit((byte)s.charAt(i++)))
                                )
                        );
                        break;
                    default:
                        if (lenient && !isValidEscaping(c))
                        {
                            b.append('\\');
                        }
                        b.append(c);
                }
            }
            else if (c=='\\')
            {
                escape=true;
            }
            else
            {
                b.append(c);
            }
        }

        return b.toString();
    }


    /* ------------------------------------------------------------ */
    /** Check that char c (which is preceded by a backslash) is a valid
     * escape sequence.
     * @param c
     * @return
     */
    private static boolean isValidEscaping(char c)
    {
        return ((c == 'n') || (c == 'r') || (c == 't') ||
                 (c == 'f') || (c == 'b') || (c == '\\') ||
                 (c == '/') || (c == '"') || (c == 'u'));
    }

    /* ------------------------------------------------------------ */
    public static boolean isQuoted(String s)
    {
        return s!=null && s.length()>0 && s.charAt(0)=='"' && s.charAt(s.length()-1)=='"';
    }

    /* ------------------------------------------------------------ */
    /**
     * @return handle double quotes if true
     */
    public boolean getDouble()
    {
        return _double;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param d handle double quotes if true
     */
    public void setDouble(boolean d)
    {
        _double=d;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return handle single quotes if true
     */
    public boolean getSingle()
    {
        return _single;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param single handle single quotes if true
     */
    public void setSingle(boolean single)
    {
        _single=single;
    }
}
