/*
 * 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.felix.utils.manifest;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class Parser
{
    private Parser() { }

    private static final char EOF = (char) -1;

    private static char charAt(int pos, String headers, int length)
    {
        if (pos >= length)
        {
            return EOF;
        }
        return headers.charAt(pos);
    }

    private static final int CLAUSE_START = 0;
    private static final int PARAMETER_START = 1;
    private static final int KEY = 2;
    private static final int DIRECTIVE_OR_TYPEDATTRIBUTE = 4;
    private static final int ARGUMENT = 8;
    private static final int VALUE = 16;

    public static Clause[] parseHeader(String header) throws IllegalArgumentException
    {
        List<Clause> clauses = new ArrayList<Clause>();
        if (header != null)
        {
            if (header.length() == 0)
            {
                throw new IllegalArgumentException("The header cannot be an empty string.");
            }
            String key = null;
            String attr = null;
            String value = null;
            String type = null;
            boolean isAttribute = true;
            List<String> paths = new ArrayList<String>();
            List<Attribute> attrs = new ArrayList<Attribute>();
            List<Directive> dirs = new ArrayList<Directive>();
            int state = CLAUSE_START;
            int currentPosition = 0;
            int startPosition = 0;
            int length = header.length();
            boolean quoted = false;
            boolean escaped = false;

            char currentChar = EOF;
            do
            {
                currentChar = charAt(currentPosition, header, length);
                switch (state)
                {
                    case CLAUSE_START:
                        paths.clear();
                        attrs.clear();
                        dirs.clear();
                        state = PARAMETER_START;
                    case PARAMETER_START:
                        startPosition = currentPosition;
                        type = null;
                        state = KEY;
                    case KEY:
                        switch (currentChar)
                        {
                            case ':':
                            case '=':
                                key = header.substring(startPosition, currentPosition).trim();
                                startPosition = currentPosition + 1;
                                isAttribute = true;
                                state = currentChar == ':' ? DIRECTIVE_OR_TYPEDATTRIBUTE : ARGUMENT;
                                break;
                            case EOF:
                            case ',':
                                paths.add(header.substring(startPosition, currentPosition).trim());
                                for (String path : paths) {
                                    clauses.add(new Clause(path, new Directive[0], new Attribute[0]));
                                }
                                state = CLAUSE_START;
                                break;
                            case ';':
                                paths.add(header.substring(startPosition, currentPosition).trim());
                                state = PARAMETER_START;
                                break;
                            default:
                                break;
                        }
                        currentPosition++;
                        break;
                    case DIRECTIVE_OR_TYPEDATTRIBUTE:
                        switch(currentChar)
                        {
                            case '=':
                                if (startPosition != currentPosition)
                                {
                                    type = header.substring(startPosition, currentPosition).trim();
                                }
                                else
                                {
                                    isAttribute = false;
                                }
                                state = ARGUMENT;
                                startPosition = currentPosition + 1;
                                break;
                            default:
                                break;
                        }
                        currentPosition++;
                        break;
                    case ARGUMENT:
                        if (currentChar == '\"')
                        {
                            quoted = true;
                            currentPosition++;
                        }
                        else
                        {
                            quoted = false;
                        }
                        if (!Character.isWhitespace(currentChar)) {
                            state = VALUE;
                        }
                        else {
                            currentPosition++;
                        }
                        break;
                    case VALUE:
                        if (escaped)
                        {
                            escaped = false;
                        }
                        else
                        {
                            if (currentChar == '\\' )
                            {
                                escaped = true;
                            }
                            else if (quoted && currentChar == '\"')
                            {
                                quoted = false;
                            }
                            else if (!quoted)
                            {
                                switch(currentChar)
                                {
                                    case EOF:
                                    case ';':
                                    case ',':
                                        value = header.substring(startPosition, currentPosition).trim();
                                        if (value.startsWith("\"") && value.endsWith("\""))
                                        {
                                            value = value.substring(1, value.length() - 1);
                                        }
                                        if (isAttribute) {
                                            for (Attribute a : attrs) {
                                                if (a.getName().equals(key)) {
                                                    throw new IllegalArgumentException("Duplicate '" + key + "' in: " + header);
                                                }
                                            }
                                            attrs.add(new Attribute(key, value, type));
                                        } else {
                                            for (Directive d : dirs) {
                                                if (d.getName().equals(key)) {
                                                    throw new IllegalArgumentException("Duplicate '" + key + "' in: " + header);
                                                }
                                            }
                                            dirs.add(new Directive(key, value));
                                        }
                                        state = currentChar == ';' ? PARAMETER_START : CLAUSE_START;
                                        if (state == CLAUSE_START) {
                                            Directive[] d = dirs.toArray(new Directive[dirs.size()]);
                                            Attribute[] a = attrs.toArray(new Attribute[attrs.size()]);
                                            for (String path : paths) {
                                                clauses.add(new Clause(path, d, a));
                                            }
                                        }
                                        break;
                                    default:
                                        break;
                                }
                            }
                        }
                        currentPosition++;
                        break;
                    default:
                        break;
                }
            } while ( currentChar != EOF);

            if (state > PARAMETER_START)
            {
                throw new IllegalArgumentException("Unable to parse header: " + header);
            }
            /*
            List<String> ss = new ArrayList<String>();
            StringBuilder sb = new StringBuilder();
            boolean escaped = false;
            boolean quoted = false;
            for (int i = 0; i < header.length(); i++) {
                char c = header.charAt(i);
                if (escaped) {
                    sb.append(c);
                    escaped = false;
                } else if (quoted && c == '\\') {
                    sb.append(c);
                    escaped = true;
                } else if (c == '\"') {
                    if (quoted) {
                        sb.append(c);
                        quoted = false;
                    } else {
                        sb.append(c);
                        quoted = true;
                    }
                } else if (!quoted && c == ',') {
                    ss.add(sb.toString());
                    sb.setLength(0);
                } else {
                    sb.append(c);
                }
            }
            if (sb.length() > 0) {
                ss.add(sb.toString());
            }
            clauses = parseClauses(ss.toArray(new String[ss.size()]));
            */
        }
//        return (clauses == null) ? new Clause[0] : clauses;
        return clauses.toArray(new Clause[clauses.size()]);
    }

    public static Clause[] parseClauses(String[] ss) throws IllegalArgumentException
    {
        if (ss == null)
        {
            return null;
        }

        List<Clause> completeList = new ArrayList<Clause>();
        for (int ssIdx = 0; ssIdx < ss.length; ssIdx++)
        {
            // Break string into semi-colon delimited pieces.
            String[] pieces = parseDelimitedString(ss[ssIdx], ";");

            // Count the number of different clauses; clauses
            // will not have an '=' in their string. This assumes
            // that clauses come first, before directives and
            // attributes.
            int pathCount = 0;
            for (int pieceIdx = 0; pieceIdx < pieces.length; pieceIdx++)
            {
                if (pieces[pieceIdx].indexOf('=') >= 0)
                {
                    break;
                }
                pathCount++;
            }

            // Error if no packages were specified.
            if (pathCount == 0)
            {
                throw new IllegalArgumentException("No path specified on clause: " + ss[ssIdx]);
            }

            // Parse the directives/attributes.
            Directive[] dirs = new Directive[pieces.length - pathCount];
            Attribute[] attrs = new Attribute[pieces.length - pathCount];
            int dirCount = 0, attrCount = 0;
            int idx = -1;
            String sep = null;
            for (int pieceIdx = pathCount; pieceIdx < pieces.length; pieceIdx++)
            {
                // Check if it is a directive.
                if ((idx = pieces[pieceIdx].indexOf(":=")) >= 0)
                {
                    sep = ":=";
                }
                // Check if it is an attribute.
                else if ((idx = pieces[pieceIdx].indexOf("=")) >= 0)
                {
                    sep = "=";
                }
                // It is an error.
                else
                {
                    throw new IllegalArgumentException("Not a directive/attribute: " + ss[ssIdx]);
                }

                String key = pieces[pieceIdx].substring(0, idx).trim();
                String value = pieces[pieceIdx].substring(idx + sep.length()).trim();

                // Remove quotes, if value is quoted.
                if (value.startsWith("\"") && value.endsWith("\""))
                {
                    value = value.substring(1, value.length() - 1);
                }

                // Save the directive/attribute in the appropriate array.
                if (sep.equals(":="))
                {
                    dirs[dirCount++] = new Directive(key, value);
                }
                else
                {
                    int columnIdx = key.indexOf(':');
                    if (columnIdx > 1)
                    {
                        attrs[attrCount++] = new Attribute(key.substring(0, columnIdx), value, key.substring(columnIdx + 1));
                    }
                    else
                    {
                        attrs[attrCount++] = new Attribute(key, value);
                    }
                }
            }

            // Shrink directive array.
            Directive[] dirsFinal = new Directive[dirCount];
            System.arraycopy(dirs, 0, dirsFinal, 0, dirCount);
            // Shrink attribute array.
            Attribute[] attrsFinal = new Attribute[attrCount];
            System.arraycopy(attrs, 0, attrsFinal, 0, attrCount);

            // Create package attributes for each package and
            // set directives/attributes. Add each package to
            // completel list of packages.
            Clause[] pkgs = new Clause[pathCount];
            for (int pkgIdx = 0; pkgIdx < pathCount; pkgIdx++)
            {
                pkgs[pkgIdx] = new Clause(pieces[pkgIdx], dirsFinal, attrsFinal);
                completeList.add(pkgs[pkgIdx]);
            }
        }

        Clause[] pkgs = completeList.toArray(new Clause[completeList.size()]);
        return pkgs;
    }

    public static String[] parseDelimitedString(String value, String delim) {
        return parseDelimitedString(value, delim, true);
    }

    /**
     * Parses delimited string and returns an array containing the tokens. This
     * parser obeys quotes, so the delimiter character will be ignored if it is
     * inside of a quote. This method assumes that the quote character is not
     * included in the set of delimiter characters.
     * @param value the delimited string to parse.
     * @param delim the characters delimiting the tokens.
     * @return an array of string tokens or null if there were no tokens.
    **/
    public static String[] parseDelimitedString(String value, String delim, boolean trim)
    {
        if (value == null)
        {
           value = "";
        }

        List<String> list = new ArrayList<String>();

        int CHAR = 1;
        int DELIMITER = 2;
        int STARTQUOTE = 4;
        int ENDQUOTE = 8;

        StringBuffer sb = new StringBuffer();

        int expecting = (CHAR | DELIMITER | STARTQUOTE);

        boolean isEscaped = false;
        for (int i = 0; i < value.length(); i++)
        {
            char c = value.charAt(i);

            boolean isDelimiter = (delim.indexOf(c) >= 0);

            if (!isEscaped && (c == '\\'))
            {
                isEscaped = true;
                continue;
            }

            if (isEscaped)
            {
                sb.append(c);
            }
            else if (isDelimiter && ((expecting & DELIMITER) > 0))
            {
                list.add(trim ? sb.toString().trim() : sb.toString());
                sb.delete(0, sb.length());
                expecting = (CHAR | DELIMITER | STARTQUOTE);
            }
            else if ((c == '"') && ((expecting & STARTQUOTE) > 0))
            {
                sb.append(c);
                expecting = CHAR | ENDQUOTE;
            }
            else if ((c == '"') && ((expecting & ENDQUOTE) > 0))
            {
                sb.append(c);
                expecting = (CHAR | STARTQUOTE | DELIMITER);
            }
            else if ((expecting & CHAR) > 0)
            {
                sb.append(c);
            }
            else
            {
                throw new IllegalArgumentException("Invalid delimited string: " + value);
            }

            isEscaped = false;
        }

        String s = trim ? sb.toString().trim() : sb.toString();
        if (s.length() > 0)
        {
            list.add(s);
        }

        return list.toArray(new String[list.size()]);
    }
}