/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.brooklyn.util.text;

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;

/** As 'StringTokenizer' but items in quotes (single or double) are treated as single tokens
 * (cf mortbay's QuotedStringTokenizer) 
 */  
public class QuotedStringTokenizer {

    final StringTokenizer delegate;
    final String quoteChars;
    final boolean includeQuotes;
    final String delimiters;
    final boolean includeDelimiters;

    public static String DEFAULT_QUOTE_CHARS = "\"\'";
    
    
    protected String DEFAULT_QUOTE_CHARS() {
        return DEFAULT_QUOTE_CHARS;
    }
    
    public final static String DEFAULT_DELIMITERS = " \t\n\r\f";    
    
    /** default quoted tokenizer, using single and double quotes as quote chars and returning quoted results
     * (use unquoteToken to unquote), and using whitespace chars as delimeters (not included as tokens);
     * string may be null if the nothing will be tokenized and the class is used only for
     * quoteToken(String) and unquote(String).
     */
    public QuotedStringTokenizer(String stringToTokenize) {
        this(stringToTokenize, true);
    }
    public QuotedStringTokenizer(String stringToTokenize, boolean includeQuotes) {
        this(stringToTokenize, null, includeQuotes);
    }
    public QuotedStringTokenizer(String stringToTokenize, String quoteChars, boolean includeQuotes) {
        this(stringToTokenize, quoteChars, includeQuotes, null, false);
    }

    public QuotedStringTokenizer(String stringToTokenize, String quoteChars, boolean includeQuotes, String delimiters, boolean includeDelimiters) {
        delegate = new StringTokenizer(stringToTokenize==null ? "" : stringToTokenize, (delimiters==null ? DEFAULT_DELIMITERS : delimiters), true);
        this.quoteChars = quoteChars==null ? DEFAULT_QUOTE_CHARS() : quoteChars;
        this.includeQuotes = includeQuotes;
        this.delimiters = delimiters==null ? DEFAULT_DELIMITERS : delimiters;
        this.includeDelimiters = includeDelimiters;
        updateNextToken();
    }
    
    public static class Builder {
        private String quoteChars = DEFAULT_QUOTE_CHARS;
        private boolean includeQuotes=true;
        private String delimiterChars=DEFAULT_DELIMITERS;
        private boolean includeDelimiters=false;

        public QuotedStringTokenizer build(String stringToTokenize) {
            return new QuotedStringTokenizer(stringToTokenize, quoteChars, includeQuotes, delimiterChars, includeDelimiters);
        }
        public List<String> buildList(String stringToTokenize) {
            return new QuotedStringTokenizer(stringToTokenize, quoteChars, includeQuotes, delimiterChars, includeDelimiters).remainderAsList();
        }
        
        public Builder quoteChars(String quoteChars) { this.quoteChars = quoteChars; return this; }
        public Builder addQuoteChars(String quoteChars) { this.quoteChars = this.quoteChars + quoteChars; return this; }
        public Builder includeQuotes(boolean includeQuotes) { this.includeQuotes = includeQuotes; return this; } 
        public Builder delimiterChars(String delimiterChars) { this.delimiterChars = delimiterChars; return this; }
        public Builder addDelimiterChars(String delimiterChars) { this.delimiterChars = this.delimiterChars + delimiterChars; return this; }
        public Builder includeDelimiters(boolean includeDelimiters) { this.includeDelimiters = includeDelimiters; return this; } 
    }
    public static Builder builder() {
        return new Builder();
    }

    String peekedNextToken = null;
    
    public synchronized boolean hasMoreTokens() {
        return peekedNextToken!=null;
    }
    
    public synchronized String nextToken() {    
        if (peekedNextToken==null) throw new NoSuchElementException();
        String lastToken = peekedNextToken;
        updateNextToken();
        return includeQuotes ? lastToken : unquoteToken(lastToken);
    }

    /** this method removes all unescaped quote chars, i.e. quote chars preceded by no backslashes (or a larger even number of them);
     * it also unescapes '\\' as '\'.  it does no other unescaping.  */
    public String unquoteToken(String word) {
        // ( (\\A|[^\\\\]) (\\\\\\\\)* ) [ Pattern.quote(quoteChars) ]  $1
        word = word.replaceAll(
                "((\\A|[^\\\\])(\\\\\\\\)*)["+
                    //Pattern.quote(
                        quoteChars
                    //)
                        +"]+",
                "$1");
        //above pattern removes any quote preceded by even number of backslashes
        //now it is safe to replace any \c by c
        word = word.replaceAll("\\\\"+"([\\\\"+
                //Pattern.quote(
                quoteChars
                //)
                +"])", "$1");
                
        return word;
    }
    
    /** returns the input text escaped for use with unquoteTokens, and wrapped in the quoteChar[0] (usu a double quote) */
    public String quoteToken(String unescapedText) {
        String result = unescapedText;
        //replace every backslash by two backslashes
        result = result.replaceAll("\\\\", "\\\\\\\\");
        //now replace every quote char by backslash quote char
        result = result.replaceAll("(["+quoteChars+"])", "\\\\$1");
        //then wrap in quote
        result = quoteChars.charAt(0) + result + quoteChars.charAt(0);
        return result;
    }

    protected synchronized void updateNextToken() {
        peekedNextToken = null;
        String token;
        do {
            if (!delegate.hasMoreTokens()) return;
            token = delegate.nextToken();
            //skip delimeters
        } while (!includeDelimiters && token.matches("["+delimiters+"]+"));
        
        StringBuffer nextToken = new StringBuffer(token);
        pullUntilValid(nextToken);
        peekedNextToken = nextToken.toString();
    }

    private void pullUntilValid(StringBuffer nextToken) {
        while (hasOpenQuote(nextToken.toString(), quoteChars) && delegate.hasMoreTokens()) {
            //keep appending until the quote is ended or there are no more quotes
            nextToken.append(delegate.nextToken());
        }
    }

    public static boolean hasOpenQuote(String stringToCheck) {
        return hasOpenQuote(stringToCheck, DEFAULT_QUOTE_CHARS);
    }

    public static boolean hasOpenQuote(String stringToCheck, String quoteChars) {        
        String x = stringToCheck;
        if (x==null) return false;

        StringBuffer xi = new StringBuffer();
        for (int i=0; i<x.length(); i++) {
            char c = x.charAt(i);
            if (c=='\\') i++;
            else if (quoteChars.indexOf(c)>=0) {
                xi.append(c);
            }
        }
        x = xi.toString();
        
        while (x.length()>0) {
            char c = x.charAt(0);
            int match = x.indexOf(c, 1);
            if (match==-1) return true;
            x = x.substring(match+1);
        }
        return false;
    }

    public List<String> remainderAsList() {
        List<String> l = new ArrayList<String>();
        while (hasMoreTokens())
            l.add(nextToken());
        return l;
    }

}
