/*
 * 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.jackrabbit.test.api.nodetype;

import org.apache.jackrabbit.test.ISO8601;

import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeIterator;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.nodetype.PropertyDefinition;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility class to locate item definitions in the NodeTyeManager.
 */
public class NodeTypeUtil {

    public static final int ANY_PROPERTY_TYPE = -1;

    /**
     * Locate a non-protected child node def declared by a non-abstract node type
     * parsing all node types
     *
     * @param session                  the session to access the node types
     * @param regardDefaultPrimaryType if true, the default primary type of the
     *                                 returned <code>NodeDef</code> is
     *                                 according to param <code>defaultPrimaryType</code>.
     *                                 If false, the returned <code>NodeDef</code>
     *                                 might have a default primary type or
     *                                 not.
     * @param defaultPrimaryType       if <code>regardDefaultPrimaryType</code>
     *                                 is true: if true, the returned
     *                                 <code>NodeDef</code> has a default
     *                                 primary type, else not
     * @param residual                 if true, the returned <code>NodeDef</code>
     *                                 is of the residual name "*", else not
     * @return
     * @throws RepositoryException
     */
    public static NodeDefinition locateChildNodeDef(Session session,
                                                    boolean regardDefaultPrimaryType,
                                                    boolean defaultPrimaryType,
                                                    boolean residual)
            throws RepositoryException {

        NodeTypeManager manager = session.getWorkspace().getNodeTypeManager();
        NodeTypeIterator types = manager.getAllNodeTypes();

        boolean skip = false;

        while (types.hasNext()) {
            NodeType type = types.nextNodeType();

            // node types with more than one residual child node definition
            // will cause trouble in test cases. the implementation
            // might pick another definition than the definition returned by
            // this method, when a child node is set.
            NodeDefinition[] childDefs = type.getChildNodeDefinitions();
            int residuals = 0;
            for (int i = 0; i < childDefs.length; i++) {
                if (childDefs[i].getName().equals("*")) {
                    residuals++;
                }
            }
            if (residuals > 1) {
                // more than one residual, not suitable for tests
                continue;
            }

            NodeDefinition nodeDefs[] = type.getDeclaredChildNodeDefinitions();

            for (int i = 0; i < nodeDefs.length; i++) {
                NodeDefinition nodeDef = nodeDefs[i];

                if (nodeDef.getDeclaringNodeType().isAbstract()) {
                    continue;
                }

                if (nodeDef.isProtected()) {
                    continue;
                }

                if (nodeDef.getRequiredPrimaryTypes().length > 1) {
                    // behaviour of implementations that support multiple multiple inheritance
                    // of primary node types is not specified
                    continue;
                }

                if (regardDefaultPrimaryType) {

                    if (defaultPrimaryType && nodeDef.getDefaultPrimaryType() == null) {
                        continue;
                    }

                    if (!defaultPrimaryType && nodeDef.getDefaultPrimaryType() != null) {
                        continue;
                    }
                }

                if (residual && !nodeDef.getName().equals("*")) {
                    continue;
                }

                if (!residual) {
                    // if another child node def is a residual definition
                    // skip the current node type
                    NodeDefinition nodeDefsAll[] = type.getChildNodeDefinitions();
                    for (int j = 0; j < nodeDefsAll.length; j++) {
                        if (nodeDefsAll[j].getName().equals("*")) {
                            skip = true;
                            break;
                        }
                    }
                    if (skip) {
                        // break the loop of the current child not defs
                        skip = false;
                        break;
                    }
                }

                return nodeDef;
            }
        }
        return null;
    }

    /**
     * Locate all non-protected child node def declared by a non-abstract node type
     * parsing all node types
     *
     * @param session                  the session to access the node types
     * @param regardDefaultPrimaryType if true, the default primary type of the
     *                                 returned <code>NodeDef</code> is
     *                                 according to param <code>defaultPrimaryType</code>.
     *                                 If false, the returned <code>NodeDef</code>
     *                                 might have a default primary type or
     *                                 not.
     * @param defaultPrimaryType       if <code>regardDefaultPrimaryType</code>
     *                                 is true: if true, the returned
     *                                 <code>NodeDef</code> has a default
     *                                 primary type, else not
     * @param residual                 if true, the returned <code>NodeDef</code>
     *                                 is of the residual name "*", else not
     * @return
     * @throws RepositoryException
     */
    public static List<NodeDefinition> locateAllChildNodeDef(Session session,
                                                    boolean regardDefaultPrimaryType,
                                                    boolean defaultPrimaryType,
                                                    boolean residual)
            throws RepositoryException {
        List<NodeDefinition> nodeTypes = new ArrayList<NodeDefinition>();

        NodeTypeManager manager = session.getWorkspace().getNodeTypeManager();
        NodeTypeIterator types = manager.getAllNodeTypes();

        boolean skip = false;

        while (types.hasNext()) {
            NodeType type = types.nextNodeType();

            // node types with more than one residual child node definition
            // will cause trouble in test cases. the implementation
            // might pick another definition than the definition returned by
            // this method, when a child node is set.
            NodeDefinition[] childDefs = type.getChildNodeDefinitions();
            int residuals = 0;
            for (int i = 0; i < childDefs.length; i++) {
                if (childDefs[i].getName().equals("*")) {
                    residuals++;
                }
            }
            if (residuals > 1) {
                // more than one residual, not suitable for tests
                continue;
            }

            NodeDefinition nodeDefs[] = type.getDeclaredChildNodeDefinitions();

            for (int i = 0; i < nodeDefs.length; i++) {
                NodeDefinition nodeDef = nodeDefs[i];

                if (nodeDef.getDeclaringNodeType().isAbstract()) {
                    continue;
                }

                if (nodeDef.isProtected()) {
                    continue;
                }

                if (nodeDef.getRequiredPrimaryTypes().length > 1) {
                    // behaviour of implementations that support multiple multiple inheritance
                    // of primary node types is not specified
                    continue;
                }

                if (regardDefaultPrimaryType) {

                    if (defaultPrimaryType && nodeDef.getDefaultPrimaryType() == null) {
                        continue;
                    }

                    if (!defaultPrimaryType && nodeDef.getDefaultPrimaryType() != null) {
                        continue;
                    }
                }

                if (residual && !nodeDef.getName().equals("*")) {
                    continue;
                }

                if (!residual) {
                    // if another child node def is a residual definition
                    // skip the current node type
                    NodeDefinition nodeDefsAll[] = type.getChildNodeDefinitions();
                    for (int j = 0; j < nodeDefsAll.length; j++) {
                        if (nodeDefsAll[j].getName().equals("*")) {
                            skip = true;
                            break;
                        }
                    }
                    if (skip) {
                        // break the loop of the current child not defs
                        skip = false;
                        break;
                    }
                }

                nodeTypes.add(nodeDef);
            }
        }
        return nodeTypes;
    }

    /**
     * Locate a child node def parsing all node types
     *
     * @param session     the session to access the node types
     * @param isProtected if true, the returned <code>NodeDef</code> is
     *                    protected, else not
     * @param mandatory   if true, the returned <code>NodeDef</code> is
     *                    mandatory, else not
     * @return the first <code>NodeDef</code> found fitting the requirements
     */
    public static NodeDefinition locateChildNodeDef(Session session,
                                                    boolean isProtected,
                                                    boolean mandatory)
            throws RepositoryException {

        NodeTypeManager manager = session.getWorkspace().getNodeTypeManager();
        NodeTypeIterator types = manager.getAllNodeTypes();

        while (types.hasNext()) {
            NodeType type = types.nextNodeType();
            NodeDefinition nodeDefs[] = type.getDeclaredChildNodeDefinitions();

            for (int i = 0; i < nodeDefs.length; i++) {
                NodeDefinition nodeDef = nodeDefs[i];

                if (nodeDef.getName().equals("*")) {
                    continue;
                }

                if (isProtected && !nodeDef.isProtected()) {
                    continue;
                }
                if (!isProtected && nodeDef.isProtected()) {
                    continue;
                }

                if (mandatory && !nodeDef.isMandatory()) {
                    continue;
                }
                if (!mandatory && nodeDef.isMandatory()) {
                    continue;
                }

                return nodeDef;
            }
        }
        return null;
    }

    /**
     * Locate a property def parsing all node types
     *
     * @param session      the session to access the node types
     * @param propertyType the type of the returned property. -1 indicates to
     *                     return a property of any type but not UNDEFIEND
     * @param multiple     if true, the returned <code>PropertyDef</code> is
     *                     multiple, else not
     * @param isProtected  if true, the returned <code>PropertyDef</code> is
     *                     protected, else not
     * @param residual     if true, the returned <code>PropertyDef</code> is of
     *                     the residual name "*", else not
     * @return the first <code>PropertyDef</code> found fitting the
     *         requirements
     */
    public static PropertyDefinition locatePropertyDef(Session session,
                                                       int propertyType,
                                                       boolean multiple,
                                                       boolean isProtected,
                                                       boolean constraints,
                                                       boolean residual)
            throws RepositoryException {

        NodeTypeManager manager = session.getWorkspace().getNodeTypeManager();
        NodeTypeIterator types = manager.getAllNodeTypes();

        while (types.hasNext()) {
            NodeType type = types.nextNodeType();
            PropertyDefinition propDefs[] = type.getDeclaredPropertyDefinitions();
            for (int i = 0; i < propDefs.length; i++) {
                PropertyDefinition propDef = propDefs[i];

                if (propertyType != ANY_PROPERTY_TYPE &&
                        propDef.getRequiredType() != propertyType) {
                    continue;
                }

                if (propertyType == ANY_PROPERTY_TYPE &&
                        propDef.getRequiredType() == PropertyType.UNDEFINED) {
                    continue;
                }

                if (multiple && !propDef.isMultiple()) {
                    continue;
                }
                if (!multiple && propDef.isMultiple()) {
                    continue;
                }

                if (isProtected && !propDef.isProtected()) {
                    continue;
                }
                if (!isProtected && propDef.isProtected()) {
                    continue;
                }

                String vc[] = propDef.getValueConstraints();
                if (!constraints && vc != null && vc.length > 0) {
                    continue;
                }
                if (constraints) {
                    // property def with constraints requested
                    if (vc == null || vc.length == 0) {
                        // property def has no constraints
                        continue;
                    }
                }

                if (!residual && propDef.getName().equals("*")) {
                    continue;
                }

                if (residual && !propDef.getName().equals("*")) {
                    continue;
                }

                // also skip property residual property definition if there
                // is another residual definition
                if (residual) {
                    // check if there is another residual property def
                    if (getNumResidualPropDefs(type) > 1) {
                        continue;
                    }
                }

                if (!residual) {
                    // if not looking for a residual property def then there
                    // must not be any residual definition at all on the node
                    // type
                    if (getNumResidualPropDefs(type) > 0) {
                        continue;
                    }
                }

                return propDef;
            }
        }
        return null;
    }

    /**
     * Returns the number of residual property definitions of <code>type</code>
     * including its base types.
     * @param type the node type
     * @return the number of residual property definitions.
     */
    private static int getNumResidualPropDefs(NodeType type) {
        PropertyDefinition[] pDefs = type.getPropertyDefinitions();
        int residuals = 0;
        for (int j = 0; j < pDefs.length; j++) {
            PropertyDefinition pDef = pDefs[j];
            if (pDef.getName().equals("*")) {
                residuals++;
            }
        }
        return residuals;
    }

    /**
     * Locate a property def parsing all node types
     *
     * @param session     the session to access the node types
     * @param isProtected if true, the returned <code>PropertyDef</code> is
     *                    protected, else not
     * @param mandatory   if true, the returned <code>PropertyDef</code> is
     *                    mandatory, else not
     * @return the first <code>PropertyDef</code> found fitting the
     *         requirements
     */
    public static PropertyDefinition locatePropertyDef(Session session,
                                                       boolean isProtected,
                                                       boolean mandatory)
            throws RepositoryException {

        NodeTypeManager manager = session.getWorkspace().getNodeTypeManager();
        NodeTypeIterator types = manager.getAllNodeTypes();

        while (types.hasNext()) {
            NodeType type = types.nextNodeType();
            PropertyDefinition propDefs[] = type.getDeclaredPropertyDefinitions();
            for (int i = 0; i < propDefs.length; i++) {
                PropertyDefinition propDef = propDefs[i];

                if (propDef.getName().equals("*")) {
                    continue;
                }

                if (isProtected && !propDef.isProtected()) {
                    continue;
                }
                if (!isProtected && propDef.isProtected()) {
                    continue;
                }

                if (mandatory && !propDef.isMandatory()) {
                    continue;
                }
                if (!mandatory && propDef.isMandatory()) {
                    continue;
                }

                return propDef;
            }
        }
        return null;
    }

    /**
     * Returns a name that is not defined by the nodeType's child node def
     */
    public static String getUndefinedChildNodeName(NodeType nodeType) {

        NodeDefinition nodeDefs[] = nodeType.getChildNodeDefinitions();
        StringBuffer s = new StringBuffer("X");

        for (int i = 0; i < nodeDefs.length; i++) {
            s.append(nodeDefs[i].getName());
        }
        String undefinedName = s.toString();
        undefinedName = undefinedName.replaceAll("\\*", "");
        undefinedName = undefinedName.replaceAll(":", "");
        return undefinedName;
    }

    /**
     * Returns a node type that is nor legalType nor a sub type of of
     */
    public static String getIllegalChildNodeType(NodeTypeManager manager,
                                                 String legalType)
            throws RepositoryException {

        NodeTypeIterator types = manager.getAllNodeTypes();
        while (types.hasNext()) {
            NodeType type = types.nextNodeType();
            if (!type.getName().equals(legalType)) {
                NodeType superTypes[] = type.getSupertypes();
                boolean isSubType = false;
                for (int i = 0; i < superTypes.length; i++) {
                    String name = superTypes[i].getName();
                    if (name.equals(legalType)) {
                        isSubType = true;
                        break;
                    }
                }
                if (!isSubType) {
                    return type.getName();
                }
            }
        }
        return null;
    }

    /**
     * Returns any value of the requested type
     */
    public static Value getValueOfType(Session session, int type)
            throws ValueFormatException, UnsupportedOperationException, RepositoryException {
        switch (type) {
            case (PropertyType.BINARY):
                // note: If binary is not UTF-8 behavior is implementation-specific
                return session.getValueFactory().createValue("abc", PropertyType.BINARY);
            case (PropertyType.BOOLEAN):
                return session.getValueFactory().createValue(true);
            case (PropertyType.DATE):
                return session.getValueFactory().createValue(Calendar.getInstance());
            case (PropertyType.DOUBLE):
                return session.getValueFactory().createValue(1.0);
            case (PropertyType.LONG):
                return session.getValueFactory().createValue(1);
            case (PropertyType.NAME):
                return session.getValueFactory().createValue("abc", PropertyType.NAME);
            case (PropertyType.PATH):
                return session.getValueFactory().createValue("/abc", PropertyType.PATH);
            default:
                // STRING and UNDEFINED
                // note: REFERENCE is not testable since its format is implementation-specific
                return session.getValueFactory().createValue("abc");
        }
    }

    /**
     * Returns a value according to the value contraints of a
     * <code>PropertyDefinition</code>
     *
     * @param propDef   The <code>PropertyDefinition</code> whose constraints
     *                  will be regarded
     * @param satisfied If true, the returned <code>Value</code> will satisfying
     *                  the constraints - If false, the returned
     *                  <code>Value</code> will not satisfying the constraints.
     * @return Depending on param <code>satisfied</code> a <code>Value</code>
     *         satisfying or not satistying the constraints of
     *         <code>propDef</code> will be returned. Null will be returned if
     *         no accordant <code>Value</code> could be build.
     */
    public static Value getValueAccordingToValueConstraints(Session session,
                                                            PropertyDefinition propDef,
                                                            boolean satisfied)
            throws ValueFormatException, RepositoryException {

        int type = propDef.getRequiredType();
        String constraints[] = propDef.getValueConstraints();

        if (constraints == null || constraints.length == 0) {
            return null;
        }

        switch (type) {
            case (PropertyType.BINARY):
                {
                    long absMin = 0;
                    long absMax = 0;

                    // indicate if absMin and absMax are already set
                    boolean absMinSet = false;
                    boolean absMaxSet = false;

                    // boundless vars indicate min/max without bounds,
                    // if constraint is e.g.(min,) or [,max]
                    boolean maxBoundless = false;
                    boolean minBoundless = false;

                    // find smallest min and largest max value
                    for (int i = 0; i < constraints.length; i++) {
                        if (!minBoundless) {
                            String minStr = getConstraintMin(constraints[i]);
                            if (minStr == null) {
                                minBoundless = true;
                            } else {
                                long min = Long.valueOf(minStr).longValue();
                                if (!absMinSet) {
                                    absMin = min;
                                    absMinSet = true;
                                } else if (min < absMin) {
                                    absMin = min;
                                }
                            }
                        }
                        if (!maxBoundless) {
                            String maxStr = getConstraintMax(constraints[i]);
                            if (maxStr == null) {
                                maxBoundless = true;
                            } else {
                                long max = Long.valueOf(maxStr).longValue();
                                if (!absMaxSet) {
                                    absMax = max;
                                    absMaxSet = true;
                                } else if (max > absMax) {
                                    absMin = max;
                                }
                            }
                        }
                    }
                    if (satisfied) {
                        // build a binary value absMin < size > absMax
                        StringBuffer content = new StringBuffer();
                        for (int i = 0; i <= absMin + 1; i++) {
                            content.append("X");
                        }
                        if (!maxBoundless && content.length() >= absMax) {
                            return null;
                        } else {
                            return session.getValueFactory().createValue(content.toString(), PropertyType.BINARY);
                        }
                    } else {
                        if (!minBoundless && absMin > 1) {
                            // return a value of size < absMin
                            return session.getValueFactory().createValue("0", PropertyType.BINARY);
                        } else if (!maxBoundless) {
                            // build a binary value of size > absMax
                            StringBuffer content = new StringBuffer();
                            for (int i = 0; i <= absMax; i = i + 10) {
                                content.append("0123456789");
                            }
                            return session.getValueFactory().createValue(content.toString(), PropertyType.BINARY);
                        } else {
                            return null;
                        }
                    }
                }

            case (PropertyType.BOOLEAN):
                {
                    if (constraints.length > 1) {
                        return null; // silly constraint
                    }
                    boolean value = Boolean.valueOf(constraints[0]).booleanValue();
                    if (satisfied) {
                        return session.getValueFactory().createValue(value);
                    } else {
                        return session.getValueFactory().createValue(!value);
                    }
                }

            case (PropertyType.DATE):
                {
                    Calendar absMin = null;
                    Calendar absMax = null;

                    // boundless vars indicate min/max without bounds,
                    // if constraint is e.g.(min,) or [,max]
                    boolean maxBoundless = false;
                    boolean minBoundless = false;

                    // find smallest min and largest max value
                    for (int i = 0; i < constraints.length; i++) {
                        if (!minBoundless) {
                            String minStr = getConstraintMin(constraints[i]);
                            if (minStr == null) {
                                minBoundless = true;
                            } else {
                                Calendar min = ISO8601.parse(minStr);
                                if (absMin == null || min.before(absMin)) {
                                    absMin = min;
                                }
                            }
                        }
                        if (!maxBoundless) {
                            String maxStr = getConstraintMax(constraints[i]);
                            if (maxStr == null) {
                                maxBoundless = true;
                            } else {
                                Calendar max = ISO8601.parse(maxStr);
                                if (absMax == null || max.after(absMax)) {
                                    absMax = max;
                                }
                            }
                        }
                    }
                    if (satisfied) {
                        if (absMin != null) {
                            absMin.setTimeInMillis(absMin.getTimeInMillis() + 1);
                            if (absMin.after(absMax)) {
                                return null;
                            }
                            return session.getValueFactory().createValue(absMin);
                        } else if (absMax != null) {
                            absMax.setTimeInMillis(absMax.getTimeInMillis() - 1);
                            if (absMax.before(absMin)) {
                                return null;
                            }
                            return session.getValueFactory().createValue(absMax);
                        } else {
                            // neither min nor max set: return "now"
                            return session.getValueFactory().createValue(Calendar.getInstance());
                        }
                    } else {
                        if (!minBoundless) {
                            absMin.setTimeInMillis(absMin.getTimeInMillis() - 1);
                            return session.getValueFactory().createValue(absMin);
                        } else if (!maxBoundless) {
                            absMax.setTimeInMillis(absMax.getTimeInMillis() + 1);
                            return session.getValueFactory().createValue(absMax);
                        } else {
                            return null;
                        }
                    }
                }

            case (PropertyType.DOUBLE):
                {
                    double absMin = 0;
                    double absMax = 0;

                    // indicate if absMin and absMax are already set
                    boolean absMinSet = false;
                    boolean absMaxSet = false;

                    // boundless vars indicate min/max without bounds,
                    // if constraint is e.g.(min,) or [,max]
                    boolean maxBoundless = false;
                    boolean minBoundless = false;

                    // find smallest min and largest max value
                    for (int i = 0; i < constraints.length; i++) {
                        if (!minBoundless) {
                            String minStr = getConstraintMin(constraints[i]);
                            if (minStr == null) {
                                minBoundless = true;
                            } else {
                                double min = Double.valueOf(minStr).doubleValue();
                                if (!absMinSet) {
                                    absMin = min;
                                    absMinSet = true;
                                } else if (min < absMin) {
                                    absMin = min;
                                }
                            }
                        }
                        if (!maxBoundless) {
                            String maxStr = getConstraintMax(constraints[i]);
                            if (maxStr == null) {
                                maxBoundless = true;
                            } else {
                                double max = Double.valueOf(maxStr).doubleValue();
                                if (!absMaxSet) {
                                    absMax = max;
                                    absMaxSet = true;
                                } else if (max > absMax) {
                                    absMax = max;
                                }
                            }
                        }
                    }
                    if (satisfied) {
                        if (minBoundless) {
                            return session.getValueFactory().createValue(absMax - 1.0);
                        } else if (maxBoundless) {
                            return session.getValueFactory().createValue(absMin + 1.0);
                        } else if (absMin < absMax) {
                            double d = (absMin + absMax) / 2;
                            return session.getValueFactory().createValue(d);
                        } else {
                            return null;
                        }
                    } else {
                        if (!minBoundless) {
                            return session.getValueFactory().createValue(absMin - 1.0);
                        } else if (!maxBoundless) {
                            return session.getValueFactory().createValue(absMax + 1.0);
                        } else {
                            return null;
                        }
                    }
                }

            case (PropertyType.LONG):
                {
                    long absMin = 0;
                    long absMax = 0;

                    // indicate if absMin and absMax are already set
                    boolean absMinSet = false;
                    boolean absMaxSet = false;

                    // boundless vars indicate min/max without bounds,
                    // if constraint is e.g.(min,) or [,max]
                    boolean maxBoundless = false;
                    boolean minBoundless = false;

                    // find smallest min and largest max value
                    for (int i = 0; i < constraints.length; i++) {
                        if (!minBoundless) {
                            String minStr = getConstraintMin(constraints[i]);
                            if (minStr == null) {
                                minBoundless = true;
                            } else {
                                long min = Long.valueOf(minStr).longValue();
                                if (!absMinSet) {
                                    absMin = min;
                                    absMinSet = true;
                                } else if (min < absMin) {
                                    absMin = min;
                                }
                            }
                        }
                        if (!maxBoundless) {
                            String maxStr = getConstraintMax(constraints[i]);
                            if (maxStr == null) {
                                maxBoundless = true;
                            } else {
                                long max = Long.valueOf(maxStr).longValue();
                                if (!absMaxSet) {
                                    absMax = max;
                                    absMaxSet = true;
                                } else if (max > absMax) {
                                    absMax = max;
                                }
                            }
                        }
                    }
                    if (satisfied) {
                        if (minBoundless) {
                            return session.getValueFactory().createValue(absMax - 1);
                        } else if (maxBoundless) {
                            return session.getValueFactory().createValue(absMin + 1);
                        } else if (absMin < absMax - 1) {
                            long x = (absMin + absMax) / 2;
                            return session.getValueFactory().createValue(x);
                        } else {
                            return null;
                        }
                    } else {
                        if (!minBoundless) {
                            return session.getValueFactory().createValue(absMin - 1);
                        } else if (!maxBoundless) {
                            return session.getValueFactory().createValue(absMax + 1);
                        } else {
                            return null;
                        }
                    }
                }

            case (PropertyType.NAME):
                {
                    if (satisfied) {
                        // not in use so far
                        return null;
                    } else {
                        // build a name that is for sure not part of the constraints
                        StringBuffer name = new StringBuffer("X");
                        for (int i = 0; i < constraints.length; i++) {
                            name.append(constraints[i].replaceAll(":", ""));
                        }
                        return session.getValueFactory().createValue(name.toString(), PropertyType.NAME);
                    }
                }

            case (PropertyType.PATH):
                {
                    if (satisfied) {
                        // not in use so far
                        return null;
                    } else {
                        // build a path that is for sure not part of the constraints
                        StringBuffer path = new StringBuffer("X");
                        for (int i = 0; i < constraints.length; i++) {
                            path.append(constraints[i]);
                        }
                        String pathStr = path.toString();

                        // replace colon to avoid /a/x:b + y:c => /a/x:b:y:c
                        // where x:b:y:c is not a legal path element
                        pathStr = pathStr.replaceAll(":", "");
                        pathStr = pathStr.replaceAll("\\*", "");
                        pathStr = pathStr.replaceAll("//", "/");

                        return session.getValueFactory().createValue(pathStr, PropertyType.PATH);
                    }
                }

            case (PropertyType.UNDEFINED):
                {
                    return null;
                }

            default:
                {
                    if (satisfied) {
                        // not in use so far
                        return null;
                    } else {
                        // build a string that will probably not satisfy the constraints
                        StringBuffer value = new StringBuffer("X");
                        for (int i = 0; i < constraints.length; i++) {
                            value.append(constraints[i]);
                        }

                        // test if value does not match any of the constraints
                        for (int i = 0; i < constraints.length; i++) {
                            Pattern pattern = Pattern.compile(constraints[i]);
                            Matcher matcher = pattern.matcher(value);
                            if (matcher.matches()) {
                                return null;
                            }
                        }
                        return session.getValueFactory().createValue(value.toString());
                    }
                }
        }
    }

    // ------------------------< internal >-------------------------------------

    /**
     * Get the min value (as string) of a numeric/date constraint string
     */
    private static String getConstraintMin(String constraint) {
        String min = constraint.substring(0, constraint.indexOf(","));
        min = min.replaceAll("\\(", "");
        min = min.replaceAll("\\[", "");
        min = min.replaceAll(" ", "");
        if (min.equals("")) {
            min = null;
        }
        return min;
    }

    /**
     * Get the max value (as string) of a numeric/date constraint string
     */
    private static String getConstraintMax(String constraint) {
        String max = constraint.substring(constraint.indexOf(",") + 1);
        max = max.replaceAll("\\)", "");
        max = max.replaceAll("\\]", "");
        max = max.replaceAll(" ", "");
        if (max.equals("")) {
            max = null;
        }
        return max;
    }

}
