/*
 * 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.karaf.cave.repository.service.bundlerepository;

import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.felix.utils.version.VersionRange;

public class SimpleFilter {
    public static final int MATCH_ALL = 0;
    public static final int AND = 1;
    public static final int OR = 2;
    public static final int NOT = 3;
    public static final int EQ = 4;
    public static final int LTE = 5;
    public static final int GTE = 6;
    public static final int SUBSTRING = 7;
    public static final int PRESENT = 8;
    public static final int APPROX = 9;

    private final String name;
    private final Object value;
    private final int op;

    public SimpleFilter(String name, Object value, int op) {
        this.name = name;
        this.value = value;
        this.op = op;
    }

    public String getName() {
        return name;
    }

    public Object getValue() {
        return value;
    }

    public int getOperation() {
        return op;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        toString(sb);
        return sb.toString();
    }

    private void toString(StringBuilder sb)
    {
        switch (op)
        {
            case AND:
                sb.append("(&");
                toString(sb, (List) value);
                sb.append(")");
                break;
            case OR:
                sb.append("(|");
                toString(sb, (List) value);
                sb.append(")");
                break;
            case NOT:
                sb.append("(!");
                toString(sb, (List) value);
                sb.append(")");
                break;
            case EQ:
                sb.append("(")
                        .append(name)
                        .append("=");
                toEncodedString(sb, value);
                sb.append(")");
                break;
            case LTE:
                sb.append("(")
                        .append(name)
                        .append("<=");
                toEncodedString(sb, value);
                sb.append(")");
                break;
            case GTE:
                sb.append("(")
                        .append(name)
                        .append(">=");
                toEncodedString(sb, value);
                sb.append(")");
                break;
            case SUBSTRING:
                sb.append("(").append(name).append("=");
                unparseSubstring(sb, (List) value);
                sb.append(")");
                break;
            case PRESENT:
                sb.append("(").append(name).append("=*)");
                break;
            case APPROX:
                sb.append("(").append(name).append("~=");
                toEncodedString(sb, value);
                sb.append(")");
                break;
            case MATCH_ALL:
                sb.append("(*)");
                break;
        }
    }

    private static void toString(StringBuilder sb, List list) {
        for (Object o : list) {
            SimpleFilter sf = (SimpleFilter) o;
            sf.toString(sb);
        }
    }


    private static String toDecodedString(String s, int startIdx, int endIdx) {
        StringBuilder sb = null;
        boolean escaped = false;
        for (int i = startIdx; i < endIdx; i++) {
            char c = s.charAt(i);
            if (!escaped && (c == '\\')) {
                sb = new StringBuilder(endIdx - startIdx);
                sb.append(s, startIdx, i);
                escaped = true;
            } else {
                escaped = false;
                if (sb != null) {
                    sb.append(c);
                }
            }
        }
        return sb != null ? sb.toString() : s.substring(startIdx, endIdx);
    }

    private static void toEncodedString(StringBuilder sb, Object o) {
        if (o instanceof String) {
            String s = (String) o;
            for (int i = 0; i < s.length(); i++) {
                char c = s.charAt(i);
                if ((c == '\\') || (c == '(') || (c == ')') || (c == '*')) {
                    sb.append('\\');
                }
                sb.append(c);
            }
        } else {
            sb.append(o);
        }
    }

    @SuppressWarnings("unchecked")
    public static SimpleFilter parse(String filter) {
        int idx = skipWhitespace(filter, 0);

        if ((filter == null) || (filter.length() == 0) || (idx >= filter.length())) {
            throw new IllegalArgumentException("Null or empty filter.");
        } else if (filter.charAt(idx) != '(') {
            throw new IllegalArgumentException("Missing opening parenthesis: " + filter);
        }

        SimpleFilter sf = null;
        Deque<Object> stack = new LinkedList<>();
        boolean isEscaped = false;
        while (idx < filter.length()) {
            if (sf != null) {
                throw new IllegalArgumentException(
                        "Only one top-level operation allowed: " + filter);
            }

            if (!isEscaped && (filter.charAt(idx) == '(')) {
                // Skip paren and following whitespace.
                idx = skipWhitespace(filter, idx + 1);

                if (filter.charAt(idx) == '&') {
                    int peek = skipWhitespace(filter, idx + 1);
                    if (filter.charAt(peek) == '(') {
                        idx = peek - 1;
                        stack.addFirst(new SimpleFilter(null, new ArrayList(), SimpleFilter.AND));
                    } else {
                        stack.addFirst(idx);
                    }
                } else if (filter.charAt(idx) == '|') {
                    int peek = skipWhitespace(filter, idx + 1);
                    if (filter.charAt(peek) == '(') {
                        idx = peek - 1;
                        stack.addFirst(new SimpleFilter(null, new ArrayList(), SimpleFilter.OR));
                    } else {
                        stack.addFirst(idx);
                    }
                } else if (filter.charAt(idx) == '!') {
                    int peek = skipWhitespace(filter, idx + 1);
                    if (filter.charAt(peek) == '(') {
                        idx = peek - 1;
                        stack.addFirst(new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT));
                    } else {
                        stack.addFirst(idx);
                    }
                } else {
                    stack.addFirst(idx);
                }
            } else if (!isEscaped && (filter.charAt(idx) == ')')) {
                Object top = stack.removeFirst();
                Object next = stack.peekFirst();
                if (top instanceof SimpleFilter) {
                    if (next instanceof SimpleFilter) {
                        ((List<Object>) ((SimpleFilter) next).value).add(top);
                    } else {
                        sf = (SimpleFilter) top;
                    }
                } else if (next instanceof SimpleFilter) {
                    ((List<Object>) ((SimpleFilter) next).value).add(
                            SimpleFilter.subfilter(filter, (Integer) top, idx));
                } else {
                    sf = SimpleFilter.subfilter(filter, (Integer) top, idx);
                }
            } else {
                isEscaped = !isEscaped && (filter.charAt(idx) == '\\');
            }

            idx = skipWhitespace(filter, idx + 1);
        }

        if (sf == null) {
            throw new IllegalArgumentException("Missing closing parenthesis: " + filter);
        }

        return sf;
    }

    private static SimpleFilter subfilter(String filter, int startIdx, int endIdx) {
        final String opChars = "=<>~";

        // Determine the ending index of the attribute name.
        int attrEndIdx = startIdx;
        for (int i = 0; i < (endIdx - startIdx); i++) {
            char c = filter.charAt(startIdx + i);
            if (opChars.indexOf(c) >= 0) {
                break;
            } else if (!Character.isWhitespace(c)) {
                attrEndIdx = startIdx + i + 1;
            }
        }
        if (attrEndIdx == startIdx) {
            throw new IllegalArgumentException(
                    "Missing attribute name: " + filter.substring(startIdx, endIdx));
        }
        String attr = filter.substring(startIdx, attrEndIdx);

        // Skip the attribute name and any following whitespace.
        startIdx = skipWhitespace(filter, attrEndIdx);

        // Determine the operator type.
        int op;
        switch (filter.charAt(startIdx)) {
            case '=':
                op = EQ;
                startIdx++;
                break;
            case '<':
                if (filter.charAt(startIdx + 1) != '=') {
                    throw new IllegalArgumentException(
                            "Unknown operator: " + filter.substring(startIdx, endIdx));
                }
                op = LTE;
                startIdx += 2;
                break;
            case '>':
                if (filter.charAt(startIdx + 1) != '=') {
                    throw new IllegalArgumentException(
                            "Unknown operator: " + filter.substring(startIdx, endIdx));
                }
                op = GTE;
                startIdx += 2;
                break;
            case '~':
                if (filter.charAt(startIdx + 1) != '=') {
                    throw new IllegalArgumentException(
                            "Unknown operator: " + filter.substring(startIdx, endIdx));
                }
                op = APPROX;
                startIdx += 2;
                break;
            default:
                throw new IllegalArgumentException(
                        "Unknown operator: " + filter.substring(startIdx, endIdx));
        }

        // Parse value.
        Object value = toDecodedString(filter, startIdx, endIdx);

        // Check if the equality comparison is actually a substring
        // or present operation.
        if (op == EQ) {
            String valueStr = filter.substring(startIdx, endIdx);
            List<String> values = parseSubstring(valueStr);
            if ((values.size() == 2)
                    && (values.get(0).length() == 0)
                    && (values.get(1).length() == 0)) {
                op = PRESENT;
            } else if (values.size() > 1) {
                op = SUBSTRING;
                value = values;
            }
        }

        return new SimpleFilter(attr, value, op);
    }

    public static List<String> parseSubstring(String value) {
        List<String> pieces = new ArrayList<>();
        int length = value.length();

        boolean isSimple = true;
        for (int idx = 0; idx < length; idx++) {
            char c = value.charAt(idx);
            if (c == '*' || c == '\\') {
                isSimple = false;
                break;
            }
        }
        if (isSimple) {
            pieces.add(value);
            return pieces;
        }

        StringBuilder ss = new StringBuilder();
        // int kind = SIMPLE; // assume until proven otherwise
        boolean wasStar = false; // indicates last piece was a star
        boolean leftstar = false; // track if the initial piece is a star
        boolean rightstar = false; // track if the final piece is a star

        int idx = 0;

        // We assume (sub)strings can contain leading and trailing blanks
        boolean escaped = false;
        for (;;) {
            if (idx >= length) {
                if (wasStar) {
                    // insert last piece as "" to handle trailing star
                    rightstar = true;
                } else {
                    pieces.add(ss.toString());
                    // accumulate the last piece
                    // note that in the case of
                    // (cn=); this might be
                    // the string "" (!=null)
                }
                ss.setLength(0);
                break;
            }

            // Read the next character and account for escapes.
            char c = value.charAt(idx++);
            if (!escaped && (c == '*')) {
                // If we have successive '*' characters, then we can
                // effectively collapse them by ignoring succeeding ones.
                if (!wasStar) {
                    if (ss.length() > 0) {
                        pieces.add(ss.toString()); // accumulate the pieces
                        // between '*' occurrences
                    }
                    ss.setLength(0);
                    // if this is a leading star, then track it
                    if (pieces.isEmpty()) {
                        leftstar = true;
                    }
                    wasStar = true;
                }
            } else if (!escaped && (c == '\\')) {
                escaped = true;
            } else {
                escaped = false;
                wasStar = false;
                ss.append(c);
            }
        }
        if (leftstar || rightstar || pieces.size() > 1) {
            // insert leading and/or trailing "" to anchor ends
            if (rightstar) {
                pieces.add("");
            }
            if (leftstar) {
                pieces.add(0, "");
            }
        }
        return pieces;
    }

    public static void unparseSubstring(StringBuilder sb, List<String> pieces) {
        for (int i = 0; i < pieces.size(); i++) {
            if (i > 0) {
                sb.append("*");
            }
            toEncodedString(sb, pieces.get(i));
        }
    }

    public static boolean compareSubstring(List<String> pieces, String s) {
        // Walk the pieces to match the string
        // There are implicit stars between each piece,
        // and the first and last pieces might be "" to anchor the match.
        // assert (pieces.length > 1)
        // minimal case is <string>*<string>

        boolean result = true;
        int len = pieces.size();

        // Special case, if there is only one piece, then
        // we must perform an equality test.
        if (len == 1) {
            return s.equals(pieces.get(0));
        }

        // Otherwise, check whether the pieces match
        // the specified string.

        int index = 0;

        for (int i = 0; i < len; i++) {
            String piece = pieces.get(i);

            // If this is the first piece, then make sure the
            // string starts with it.
            if (i == 0) {
                if (!s.startsWith(piece)) {
                    result = false;
                    break;
                }
            }

            // If this is the last piece, then make sure the
            // string ends with it.
            if (i == (len - 1)) {
                result = s.endsWith(piece) && (s.length() >= (index + piece.length()));
                break;
            }

            // If this is neither the first or last piece, then
            // make sure the string contains it.
            if ((i > 0) && (i < (len - 1))) {
                index = s.indexOf(piece, index);
                if (index < 0) {
                    result = false;
                    break;
                }
            }

            // Move string index beyond the matching piece.
            index += piece.length();
        }

        return result;
    }

    private static int skipWhitespace(String s, int startIdx) {
        int len = s.length();
        while ((startIdx < len) && Character.isWhitespace(s.charAt(startIdx))) {
            startIdx++;
        }
        return startIdx;
    }

    /**
     * Converts a attribute map to a filter. The filter is created by iterating
     * over the map's entry set. If ordering of attributes is important (e.g.,
     * for hitting attribute indices), then the map's entry set should iterate
     * in the desired order. Equality testing is assumed for all attribute types
     * other than version ranges, which are handled appropriated. If the attribute
     * map is empty, then a filter that matches anything is returned.
     *
     * @param attrs Map of attributes to convert to a filter.
     * @return A filter corresponding to the attributes.
     */
    @SuppressWarnings("unchecked")
    public static SimpleFilter convert(Map<String, Object> attrs) {
        // Rather than building a filter string to be parsed into a SimpleFilter,
        // we will just create the parsed SimpleFilter directly.

        List<SimpleFilter> filters = new ArrayList<>(attrs.size());

        for (Entry<String, Object> entry : attrs.entrySet()) {
            if (entry.getValue() instanceof VersionRange) {
                VersionRange vr = (VersionRange) entry.getValue();
                if (!vr.isOpenFloor()) {
                    filters.add(
                            new SimpleFilter(
                                    entry.getKey(),
                                    vr.getFloor().toString(),
                                    SimpleFilter.GTE)
                    );
                } else {
                    SimpleFilter not =
                            new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT);
                    ((List<Object>) not.getValue()).add(
                            new SimpleFilter(
                                    entry.getKey(),
                                    vr.getFloor().toString(),
                                    SimpleFilter.LTE)
                    );
                    filters.add(not);
                }

                if (vr.getCeiling() != null) {
                    if (!vr.isOpenCeiling()) {
                        filters.add(
                                new SimpleFilter(
                                        entry.getKey(),
                                        vr.getCeiling().toString(),
                                        SimpleFilter.LTE)
                        );
                    } else if (!vr.getCeiling().equals(VersionRange.INFINITE_VERSION)) {
                        SimpleFilter not =
                                new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT);
                        ((List<Object>) not.getValue()).add(
                                new SimpleFilter(
                                        entry.getKey(),
                                        vr.getCeiling().toString(),
                                        SimpleFilter.GTE)
                        );
                        filters.add(not);
                    }
                }
            } else {
                List<String> values = SimpleFilter.parseSubstring(entry.getValue().toString());
                if (values.size() > 1) {
                    filters.add(
                            new SimpleFilter(
                                    entry.getKey(),
                                    values,
                                    SimpleFilter.SUBSTRING)
                    );
                } else {
                    filters.add(
                            new SimpleFilter(
                                    entry.getKey(),
                                    values.get(0),
                                    SimpleFilter.EQ)
                    );
                }
            }
        }

        SimpleFilter sf = null;

        if (filters.size() == 1) {
            sf = filters.get(0);
        } else if (attrs.size() > 1) {
            sf = new SimpleFilter(null, filters, SimpleFilter.AND);
        } else if (filters.isEmpty()) {
            sf = new SimpleFilter(null, null, SimpleFilter.MATCH_ALL);
        }

        return sf;
    }
}
