/*
 * 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.resource;

import org.apache.felix.utils.version.VersionTable;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.resource.Capability;
import org.osgi.resource.Requirement;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;

@SuppressWarnings("rawtypes")
public class CapabilitySet {

    private static final Class<?>[] STRING_CLASS = new Class[] {String.class};

    private final Map<String, Map<Object, Set<Capability>>> indices;
    private final Set<Capability> capSet = new HashSet<>();

    public CapabilitySet(List<String> indexProps) {
        indices = new TreeMap<>();
        for (int i = 0; (indexProps != null) && (i < indexProps.size()); i++) {
            indices.put(indexProps.get(i), new HashMap<Object, Set<Capability>>());
        }
    }

    public void dump() {
        for (Entry<String, Map<Object, Set<Capability>>> entry : indices.entrySet()) {
            boolean header1 = false;
            for (Entry<Object, Set<Capability>> entry2 : entry.getValue().entrySet()) {
                boolean header2 = false;
                for (Capability cap : entry2.getValue()) {
                    if (!header1) {
                        System.out.println(entry.getKey() + ":");
                        header1 = true;
                    }
                    if (!header2) {
                        System.out.println("   " + entry2.getKey());
                        header2 = true;
                    }
                    System.out.println("      " + cap);
                }
            }
        }
    }

    public void addCapability(Capability cap) {
        capSet.add(cap);

        // Index capability.
        for (Entry<String, Map<Object, Set<Capability>>> entry : indices.entrySet()) {
            Object value = cap.getAttributes().get(entry.getKey());
            if (value != null) {
                if (value.getClass().isArray()) {
                    value = convertArrayToList(value);
                }

                Map<Object, Set<Capability>> index = entry.getValue();

                if (value instanceof Collection) {
                    Collection c = (Collection) value;
                    for (Object o : c) {
                        indexCapability(index, cap, o);
                    }
                } else {
                    indexCapability(index, cap, value);
                }
            }
        }
    }

    private void indexCapability(Map<Object, Set<Capability>> index, Capability cap, Object capValue) {
        // TODO: when JDK8, should be:
        // TODO: index.computeIfAbsent(capValue, k -> new HashSet<>()).add(cap);
        Set<Capability> set = index.get(capValue);
        if (set == null) {
            set = new HashSet<>();
            index.put(capValue, set);
        }
        set.add(cap);
    }

    public void removeCapability(Capability cap) {
        if (capSet.remove(cap)) {
            for (Entry<String, Map<Object, Set<Capability>>> entry : indices.entrySet()) {
                Object value = cap.getAttributes().get(entry.getKey());
                if (value != null) {
                    if (value.getClass().isArray()) {
                        value = convertArrayToList(value);
                    }

                    Map<Object, Set<Capability>> index = entry.getValue();

                    if (value instanceof Collection) {
                        Collection c = (Collection) value;
                        for (Object o : c) {
                            deindexCapability(index, cap, o);
                        }
                    } else {
                        deindexCapability(index, cap, value);
                    }
                }
            }
        }
    }

    private void deindexCapability(
            Map<Object, Set<Capability>> index, Capability cap, Object value) {
        Set<Capability> caps = index.get(value);
        if (caps != null) {
            caps.remove(cap);
            if (caps.isEmpty()) {
                index.remove(value);
            }
        }
    }

    public Set<Capability> match(SimpleFilter sf, boolean obeyMandatory) {
        Set<Capability> matches = match(capSet, sf);
        return obeyMandatory
                ? matchMandatory(matches, sf)
                : matches;
    }

    @SuppressWarnings("unchecked")
    private Set<Capability> match(Set<Capability> caps, SimpleFilter sf) {
        Set<Capability> matches = new HashSet<>();

        if (sf.getOperation() == SimpleFilter.MATCH_ALL) {
            matches.addAll(caps);
        } else if (sf.getOperation() == SimpleFilter.AND) {
            // Evaluate each subfilter against the remaining capabilities.
            // For AND we calculate the intersection of each subfilter.
            // We can short-circuit the AND operation if there are no
            // remaining capabilities.
            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
            for (int i = 0; (caps.size() > 0) && (i < sfs.size()); i++) {
                matches = match(caps, sfs.get(i));
                caps = matches;
            }
        } else if (sf.getOperation() == SimpleFilter.OR) {
            // Evaluate each subfilter against the remaining capabilities.
            // For OR we calculate the union of each subfilter.
            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
            for (SimpleFilter sf1 : sfs) {
                matches.addAll(match(caps, sf1));
            }
        } else if (sf.getOperation() == SimpleFilter.NOT) {
            // Evaluate each subfilter against the remaining capabilities.
            // For OR we calculate the union of each subfilter.
            matches.addAll(caps);
            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
            for (SimpleFilter sf1 : sfs) {
                matches.removeAll(match(caps, sf1));
            }
        } else {
            Map<Object, Set<Capability>> index = indices.get(sf.getName());
            if ((sf.getOperation() == SimpleFilter.EQ) && (index != null)) {
                Set<Capability> existingCaps = index.get(sf.getValue());
                if (existingCaps != null) {
                    matches.addAll(existingCaps);
                    matches.retainAll(caps);
                }
            } else {
                for (Capability cap : caps) {
                    Object lhs = cap.getAttributes().get(sf.getName());
                    if (lhs != null) {
                        if (compare(lhs, sf.getValue(), sf.getOperation())) {
                            matches.add(cap);
                        }
                    }
                }
            }
        }

        return matches;
    }

    public static boolean matches(Capability capability, Requirement requirement) {
        return Objects.equals(capability.getNamespace(), requirement.getNamespace())
                && matches(capability, RequirementImpl.getFilter(requirement));
    }

    public static boolean matches(Capability cap, SimpleFilter sf) {
        return matchesInternal(cap, sf) && matchMandatory(cap, sf);
    }

    @SuppressWarnings("unchecked")
    private static boolean matchesInternal(Capability cap, SimpleFilter sf) {
        boolean matched = true;

        if (sf.getOperation() == SimpleFilter.MATCH_ALL) {
            matched = true;
        } else if (sf.getOperation() == SimpleFilter.AND) {
            // Evaluate each subfilter against the remaining capabilities.
            // For AND we calculate the intersection of each subfilter.
            // We can short-circuit the AND operation if there are no
            // remaining capabilities.
            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
            for (int i = 0; matched && (i < sfs.size()); i++) {
                matched = matchesInternal(cap, sfs.get(i));
            }
        } else if (sf.getOperation() == SimpleFilter.OR) {
            // Evaluate each subfilter against the remaining capabilities.
            // For OR we calculate the union of each subfilter.
            matched = false;
            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
            for (int i = 0; !matched && (i < sfs.size()); i++) {
                matched = matchesInternal(cap, sfs.get(i));
            }
        } else if (sf.getOperation() == SimpleFilter.NOT) {
            // Evaluate each subfilter against the remaining capabilities.
            // For OR we calculate the union of each subfilter.
            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
            for (SimpleFilter sf1 : sfs) {
                matched = !(matchesInternal(cap, sf1));
            }
        } else {
            matched = false;
            Object lhs = cap.getAttributes().get(sf.getName());
            if (lhs != null) {
                matched = compare(lhs, sf.getValue(), sf.getOperation());
            }
        }

        return matched;
    }

    private static Set<Capability> matchMandatory(
            Set<Capability> caps, SimpleFilter sf) {
        for (Iterator<Capability> it = caps.iterator(); it.hasNext();) {
            Capability cap = it.next();
            if (!matchMandatory(cap, sf)) {
                it.remove();
            }
        }
        return caps;
    }

    private static boolean matchMandatory(Capability cap, SimpleFilter sf) {
        if (cap instanceof CapabilityImpl) {
            for (Entry<String, Object> entry : cap.getAttributes().entrySet()) {
                if (((CapabilityImpl) cap).isAttributeMandatory(entry.getKey())
                        && !matchMandatoryAttribute(entry.getKey(), sf)) {
                    return false;
                }
            }
        } else {
            String value = cap.getDirectives().get(Constants.MANDATORY_DIRECTIVE);
            if (value != null) {
                List<String> names = ResourceBuilder.parseDelimitedString(value, ",");
                for (Entry<String, Object> entry : cap.getAttributes().entrySet()) {
                    if (names.contains(entry.getKey())
                            && !matchMandatoryAttribute(entry.getKey(), sf)) {
                        return false;
                    }
                }
            }

        }
        return true;
    }

    private static boolean matchMandatoryAttribute(String attrName, SimpleFilter sf) {
        if ((sf.getName() != null) && sf.getName().equals(attrName)) {
            return true;
        } else if (sf.getOperation() == SimpleFilter.AND) {
            List list = (List) sf.getValue();
            for (Object aList : list) {
                SimpleFilter sf2 = (SimpleFilter) aList;
                if ((sf2.getName() != null)
                        && sf2.getName().equals(attrName)) {
                    return true;
                }
            }
        }
        return false;
    }

    @SuppressWarnings("unchecked")
    private static boolean compare(Object lhs, Object rhsUnknown, int op) {
        if (lhs == null) {
            return false;
        }

        // If this is a PRESENT operation, then just return true immediately
        // since we wouldn't be here if the attribute wasn't present.
        if (op == SimpleFilter.PRESENT) {
            return true;
        }

        // If the type is comparable, then we can just return the
        // result immediately.
        if (lhs instanceof Comparable) {
            // Spec says SUBSTRING is false for all types other than string.
            if ((op == SimpleFilter.SUBSTRING) && !(lhs instanceof String)) {
                return false;
            }

            Object rhs;
            if (op == SimpleFilter.SUBSTRING) {
                rhs = rhsUnknown;
            } else {
                try {
                    rhs = coerceType(lhs, (String) rhsUnknown);
                } catch (Exception ex) {
                    return false;
                }
            }

            switch (op) {
            case SimpleFilter.EQ:
                try {
                    return ((Comparable) lhs).compareTo(rhs) == 0;
                } catch (Exception ex) {
                    return false;
                }
            case SimpleFilter.GTE:
                try {
                    return ((Comparable) lhs).compareTo(rhs) >= 0;
                } catch (Exception ex) {
                    return false;
                }
            case SimpleFilter.LTE:
                try {
                    return ((Comparable) lhs).compareTo(rhs) <= 0;
                } catch (Exception ex) {
                    return false;
                }
            case SimpleFilter.APPROX:
                return compareApproximate(lhs, rhs);
            case SimpleFilter.SUBSTRING:
                return SimpleFilter.compareSubstring((List<String>) rhs, (String) lhs);
            default:
                throw new RuntimeException("Unknown comparison operator: " + op);
            }
        }

        // If the LHS is not a comparable or boolean, check if it is an
        // array. If so, convert it to a list so we can treat it as a
        // collection.
        if (lhs.getClass().isArray()) {
            lhs = convertArrayToList(lhs);
        }

        // If LHS is a collection, then call compare() on each element
        // of the collection until a match is found.
        if (lhs instanceof Collection) {
            for (Object o : (Collection) lhs) {
                if (compare(o, rhsUnknown, op)) {
                    return true;
                }
            }

            return false;
        }

        // Spec says SUBSTRING is false for all types other than string.
        if (op == SimpleFilter.SUBSTRING) {
            return false;
        }

        // Since we cannot identify the LHS type, then we can only perform
        // equality comparison.
        try {
            return lhs.equals(coerceType(lhs, (String) rhsUnknown));
        } catch (Exception ex) {
            return false;
        }
    }

    private static boolean compareApproximate(Object lhs, Object rhs) {
        if (rhs instanceof String) {
            return removeWhitespace((String) lhs)
                    .equalsIgnoreCase(removeWhitespace((String) rhs));
        } else if (rhs instanceof Character) {
            return Character.toLowerCase((Character) lhs)
                    == Character.toLowerCase((Character) rhs);
        }
        return lhs.equals(rhs);
    }

    private static String removeWhitespace(String s) {
        StringBuilder sb = new StringBuilder(s.length());
        for (int i = 0; i < s.length(); i++) {
            if (!Character.isWhitespace(s.charAt(i))) {
                sb.append(s.charAt(i));
            }
        }
        return sb.toString();
    }

    private static Object coerceType(Object lhs, String rhsString) throws Exception {
        // If the LHS expects a string, then we can just return
        // the RHS since it is a string.
        if (lhs.getClass() == rhsString.getClass()) {
            return rhsString;
        }

        // Try to convert the RHS type to the LHS type by using
        // the string constructor of the LHS class, if it has one.
        Object rhs;
        try {
            if (lhs instanceof Version) {
                rhs = VersionTable.getVersion(rhsString, false);
            } else
            // The Character class is a special case, since its constructor
            // does not take a string, so handle it separately.
            if (lhs instanceof Character) {
                rhs = rhsString.charAt(0);
            } else {
                // Spec says we should trim number types.
                if ((lhs instanceof Number) || (lhs instanceof Boolean)) {
                    rhsString = rhsString.trim();
                }
                Constructor ctor = lhs.getClass().getConstructor(STRING_CLASS);
                ctor.setAccessible(true);
                rhs = ctor.newInstance(rhsString);
            }
        } catch (Exception ex) {
            throw new Exception(
                    "Could not instantiate class "
                            + lhs.getClass().getName()
                            + " from string constructor with argument '"
                            + rhsString + "' because " + ex
            );
        }

        return rhs;
    }

    /**
     * This is an ugly utility method to convert an array of primitives
     * to an array of primitive wrapper objects. This method simplifies
     * processing LDAP filters since the special case of primitive arrays
     * can be ignored.
     *
     * @param array An array of primitive types.
     * @return An corresponding array using pritive wrapper objects.
     */
    private static List<Object> convertArrayToList(Object array) {
        int len = Array.getLength(array);
        List<Object> list = new ArrayList<>(len);
        for (int i = 0; i < len; i++) {
            list.add(Array.get(array, i));
        }
        return list;
    }
}
