/*
 * 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.oak.plugins.nodetype;

import javax.jcr.PropertyType;
import javax.jcr.nodetype.ItemDefinition;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.NodeTypeDefinition;
import javax.jcr.nodetype.PropertyDefinition;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A <code>NodeTypeDefDiff</code> represents the result of the comparison of
 * two node type definitions.
 * <p>
 * The result of the comparison can be categorized as one of the following types:
 * <p>
 * <b><code>NONE</code></b> indicates that there is no modification at all.
 * <p>
 * A <b><code>TRIVIAL</code></b> modification has no impact on the consistency
 * of existing content. The following modifications are considered
 * <code>TRIVIAL</code>:
 * <ul>
 * <li>changing node type <code>orderableChildNodes</code> flag
 * <li>changing node type <code>primaryItemName</code> value
 * <li>adding non-<code>mandatory</code> property/child node
 * <li>changing property/child node <code>protected</code> flag
 * <li>changing property/child node <code>onParentVersion</code> value
 * <li>changing property/child node <code>mandatory</code> flag to <code>false</code>
 * <li>changing property/child node <code>autoCreated</code> flag
 * <li>changing specific property/child node <code>name</code> to <code>*</code>
 * <li>changing child node <code>defaultPrimaryType</code>
 * <li>changing child node <code>sameNameSiblings</code> flag to <code>true</code>
 * <li>weaken child node <code>requiredPrimaryTypes</code> (e.g. by removing)
 * <li>weaken property <code>valueConstraints</code> (e.g. by removing a constraint
 * or by making a specific constraint less restrictive)
 * <li>changing property <code>defaultValues</code>
 * <li>changing specific property <code>requiredType</code> to <code>undefined</code>
 * <li>changing property <code>multiple</code> flag to <code>true</code>
 * </ul>
 * <p>
 * A <b><code>MAJOR</code></b> modification potentially <i>affects</i> the
 * consistency of existing content.
 *
 * All modifications that are not <b><code>TRIVIAL</code></b> are considered
 * <b><code>MAJOR</code></b>.
 *
 * <p>
 * <em>This class duplicates code from org.apache.jackrabbit.spi.commons.nodetype.NodeTypeDefDiff; both should be updated in sync,
 * see <a href="https://issues.apache.org/jira/browse/OAK-2802">OAK-2802</a></em>
 */
public class NodeTypeDefDiff {

    /**
     * no modification
     */
    public static final int NONE = 0;
    /**
     * trivial modification: does not affect consistency of existing content
     */
    public static final int TRIVIAL = 1;
    /**
     * major modification: <i>does</i> affect consistency of existing content
     */
    public static final int MAJOR = 2;

    private final NodeTypeDefinition oldDef;
    private final NodeTypeDefinition newDef;
    private int type;

    private final List<PropDefDiff> propDefDiffs = new ArrayList<PropDefDiff>();
    private final List<ChildNodeDefDiff> childNodeDefDiffs = new ArrayList<ChildNodeDefDiff>();

    /**
     * Constructor
     * @param oldDef old definition
     * @param newDef new definition
     */
    private NodeTypeDefDiff(NodeTypeDefinition oldDef, NodeTypeDefinition newDef) {
        this.oldDef = oldDef;
        this.newDef = newDef;
        init();
    }

    /**
     *
     */
    private void init() {
        if (oldDef.equals(newDef)) {
            // definitions are identical
            type = NONE;
        } else {
            // definitions are not identical, determine type of modification

            // assume TRIVIAL change by default
            type = TRIVIAL;

            // check supertypes
            int tmpType = supertypesDiff();
            if (tmpType > type) {
                type = tmpType;
            }

            // check mixin flag (MAJOR modification)
            tmpType = mixinFlagDiff();
            if (tmpType > type) {
                type = tmpType;
            }

            // check abstract flag (MAJOR modification)
            tmpType = abstractFlagDiff();
            if (tmpType > type) {
                type = tmpType;
            }

            // no need to check orderableChildNodes flag (TRIVIAL modification)
            // no need to check queryable flag (TRIVIAL modification)

            // check property definitions
            tmpType = buildPropDefDiffs();
            if (tmpType > type) {
                type = tmpType;
            }

            // check child node definitions
            tmpType = buildChildNodeDefDiffs();
            if (tmpType > type) {
                type = tmpType;
            }
        }
    }

    /**
     * @param oldDef old definition
     * @param newDef new definition
     * @return the diff
     */
    public static NodeTypeDefDiff create(NodeTypeDefinition oldDef, NodeTypeDefinition newDef) {
        if (oldDef == null || newDef == null) {
            throw new IllegalArgumentException("arguments can not be null");
        }
        if (!oldDef.getName().equals(newDef.getName())) {
            throw new IllegalArgumentException("at least node type names must be matching");
        }
        return new NodeTypeDefDiff(oldDef, newDef);
    }

    /**
     * @return <code>true</code> if modified
     */
    public boolean isModified() {
        return type != NONE;
    }

    /**
     * @return <code>true</code> if trivial
     */
    public boolean isTrivial() {
        return type == TRIVIAL;
    }

    /**
     * @return <code>true</code> if major
     */
    public boolean isMajor() {
        return type == MAJOR;
    }

    /**
     * Returns the type of modification as expressed by the following constants:
     * <ul>
     * <li><b><code>NONE</code></b>: no modification at all
     * <li><b><code>TRIVIAL</code></b>: does not affect consistency of
     * existing content
     * <li><b><code>MAJOR</code></b>: <i>does</i> affect consistency of existing
     * content
     * </ul>
     *
     * @return the type of modification
     */
    public int getType() {
        return type;
    }

    /**
     * @return <code>true</code> if mixin flag diff
     */
    public int mixinFlagDiff() {
        return oldDef.isMixin() != newDef.isMixin() ? MAJOR : NONE;
    }

    /**
     * @return <code>true</code> if abstract flag diff
     */
    public int abstractFlagDiff() {
        return oldDef.isAbstract() && !newDef.isAbstract() ? MAJOR : NONE;
    }

    /**
     * @return <code>true</code> if supertypes diff
     */
    public int supertypesDiff() {
        Set<String> set1 = new HashSet<String>(Arrays.asList(oldDef.getDeclaredSupertypeNames()));
        Set<String> set2 = new HashSet<String>(Arrays.asList(newDef.getDeclaredSupertypeNames()));
        return !set1.equals(set2) ? MAJOR : NONE;
    }

    /**
     * @return diff type
     */
    private int buildPropDefDiffs() {
        int maxType = NONE;
        Map<PropertyDefinitionId, PropertyDefinition> oldDefs = new HashMap<PropertyDefinitionId, PropertyDefinition>();
        for (PropertyDefinition def : oldDef.getDeclaredPropertyDefinitions()) {
            oldDefs.put(new PropertyDefinitionId(def), def);
        }

        Map<PropertyDefinitionId, PropertyDefinition> newDefs = new HashMap<PropertyDefinitionId, PropertyDefinition>();
        for (PropertyDefinition def : newDef.getDeclaredPropertyDefinitions()) {
            newDefs.put(new PropertyDefinitionId(def), def);
        }

        /**
         * walk through defs1 and process all entries found in
         * both defs1 & defs2 and those found only in defs1
         */
        for (Map.Entry<PropertyDefinitionId, PropertyDefinition> entry : oldDefs.entrySet()) {
            PropertyDefinitionId id = entry.getKey();
            PropertyDefinition def1 = entry.getValue();
            PropertyDefinition def2 = newDefs.get(id);
            PropDefDiff diff = new PropDefDiff(def1, def2);
            if (diff.getType() > maxType) {
                maxType = diff.getType();
            }
            propDefDiffs.add(diff);
            newDefs.remove(id);
        }

        /**
         * defs2 by now only contains entries found in defs2 only;
         * walk through defs2 and process all remaining entries
         */
        for (Map.Entry<PropertyDefinitionId, PropertyDefinition> entry : newDefs.entrySet()) {
            PropertyDefinition def = entry.getValue();
            PropDefDiff diff = new PropDefDiff(null, def);
            if (diff.getType() > maxType) {
                maxType = diff.getType();
            }
            propDefDiffs.add(diff);
        }

        return maxType;
    }

    /**
     * @return diff type
     */
    private int buildChildNodeDefDiffs() {
        int maxType = NONE;
        final Map<NodeDefinitionId, List<NodeDefinition>> oldDefs = collectChildNodeDefs(oldDef.getDeclaredChildNodeDefinitions());
        final Map<NodeDefinitionId, List<NodeDefinition>> newDefs = collectChildNodeDefs(newDef.getDeclaredChildNodeDefinitions());

        for (NodeDefinitionId defId : oldDefs.keySet()) {
            final ChildNodeDefDiffs childNodeDefDiffs = new ChildNodeDefDiffs(oldDefs.get(defId), newDefs.get(defId));
            this.childNodeDefDiffs.addAll(childNodeDefDiffs.getChildNodeDefDiffs());
            newDefs.remove(defId);
        }

        for (NodeDefinitionId defId : newDefs.keySet()) {
            final ChildNodeDefDiffs childNodeDefDiffs = new ChildNodeDefDiffs(null, newDefs.get(defId));
            this.childNodeDefDiffs.addAll(childNodeDefDiffs.getChildNodeDefDiffs());
        }

        for (ChildNodeDefDiff diff : childNodeDefDiffs) {
            if (diff.getType() > maxType) {
                maxType = diff.getType();
            }
        }

        return maxType;
    }

    private Map<NodeDefinitionId, List<NodeDefinition>> collectChildNodeDefs(final NodeDefinition[] cnda1) {
        Map<NodeDefinitionId, List<NodeDefinition>> defs1 = new HashMap<NodeDefinitionId, List<NodeDefinition>>();
        for (NodeDefinition def1 : cnda1) {
            final NodeDefinitionId def1Id = new NodeDefinitionId(def1);
            List<NodeDefinition> list = defs1.get(def1Id);
            if (list == null) {
                list = new ArrayList<NodeDefinition>();
                defs1.put(def1Id, list);
            }
            list.add(def1);
        }
        return defs1;
    }

    @Override
    public String toString() {
        String result = getClass().getName() + "[\n\tnodeTypeName="
                + oldDef.getName();

        result += ",\n\tmixinFlagDiff=" + modificationTypeToString(mixinFlagDiff());
        result += ",\n\tsupertypesDiff=" + modificationTypeToString(supertypesDiff());

        result += ",\n\tpropertyDifferences=[\n";
        result += toString(propDefDiffs);
        result += "\t]";

        result += ",\n\tchildNodeDifferences=[\n";
        result += toString(childNodeDefDiffs);
        result += "\t]\n";
        result += "]\n";

        return result;
    }

    private String toString(List<? extends ChildItemDefDiff> childItemDefDiffs) {
        String result = "";
        for (Iterator<? extends ChildItemDefDiff> iter = childItemDefDiffs.iterator(); iter.hasNext();) {
            ChildItemDefDiff propDefDiff = iter.next();
            result += "\t\t" + propDefDiff;
            if (iter.hasNext()) {
                result += ",";
            }
            result += "\n";
        }
        return result;
    }

    private String modificationTypeToString(int modificationType) {
        String typeString = "unknown";
        switch (modificationType) {
            case NONE:
                typeString = "NONE";
                break;
            case TRIVIAL:
                typeString = "TRIVIAL";
                break;
            case MAJOR:
                typeString = "MAJOR";
                break;
        }
        return typeString;
    }


    //--------------------------------------------------------< inner classes >

    abstract class ChildItemDefDiff {
        protected final ItemDefinition oldDef;
        protected final ItemDefinition newDef;
        protected int type;

        ChildItemDefDiff(ItemDefinition oldDef, ItemDefinition newDef) {
            this.oldDef = oldDef;
            this.newDef = newDef;
            init();
        }

        protected void init() {
            // determine type of modification
            if (isAdded()) {
                if (!newDef.isMandatory()) {
                    // adding a non-mandatory child item is a TRIVIAL change
                    type = TRIVIAL;
                } else {
                    // adding a mandatory child item is a MAJOR change
                    type = MAJOR;
                }
            } else if (isRemoved()) {
                // removing a child item is a MAJOR change
                type = MAJOR;
            } else {
                /**
                 * neither added nor removed => has to be either identical
                 * or modified
                 */
                if (oldDef.equals(newDef)) {
                    // identical
                    type = NONE;
                } else {
                    // modified
                    if (oldDef.isMandatory() != newDef.isMandatory()
                            && newDef.isMandatory()) {
                        // making a child item mandatory is a MAJOR change
                        type = MAJOR;
                    } else {
                        if (!"*".equals(oldDef.getName())
                                && "*".equals(newDef.getName())) {
                            // just making a child item residual is a TRIVIAL change
                            type = TRIVIAL;
                        } else {
                            if (!oldDef.getName().equals(newDef.getName())) {
                                // changing the name of a child item is a MAJOR change
                                type = MAJOR;
                            } else {
                                // all other changes are TRIVIAL
                                type = TRIVIAL;
                            }
                        }
                    }
                }
            }
        }

        public int getType() {
            return type;
        }

        public boolean isAdded() {
            return oldDef == null && newDef != null;
        }

        public boolean isRemoved() {
            return oldDef != null && newDef == null;
        }

        public boolean isModified() {
            return oldDef != null && newDef != null
                    && !oldDef.equals(newDef);
        }

        @Override
        public String toString() {
            String typeString = modificationTypeToString(getType());

            String operationString;
            if (isAdded()) {
                operationString = "ADDED";
            } else if (isModified()) {
                operationString = "MODIFIED";
            } else if (isRemoved()) {
                operationString = "REMOVED";
            } else {
                operationString = "NONE";
            }

            ItemDefinition itemDefinition = (oldDef != null) ? oldDef : newDef;

            return getClass().getName() + "[itemName="
                    + itemDefinition.getName() + ", type=" + typeString
                    + ", operation=" + operationString + "]";
        }

    }

    public class PropDefDiff extends ChildItemDefDiff {

        PropDefDiff(PropertyDefinition oldDef, PropertyDefinition newDef) {
            super(oldDef, newDef);
        }

        public PropertyDefinition getOldDef() {
            return (PropertyDefinition) oldDef;
        }

        public PropertyDefinition getNewDef() {
            return (PropertyDefinition) newDef;
        }

        @Override
        protected void init() {
            super.init();
            /**
             * only need to do comparison if base class implementation
             * detected a non-MAJOR (i.e. TRIVIAL) modification;
             * no need to check for additions or removals as this is already
             * handled in base class implementation.
             */
            if (isModified() && type == TRIVIAL) {
                // check if valueConstraints were made more restrictive
                String[] vca1 = getOldDef().getValueConstraints();
                Set<String> set1 = new HashSet<String>();
                for (String aVca1 : vca1) {
                    set1.add(aVca1);
                }
                String[] vca2 = getNewDef().getValueConstraints();
                Set<String> set2 = new HashSet<String>();
                for (String aVca2 : vca2) {
                    set2.add(aVca2);
                }

                if (!set1.equals(set2)) {
                    // valueConstraints have been modified
                    if (set2.isEmpty()) {
                        // all existing constraints have been cleared
                        // => TRIVIAL change
                        type = TRIVIAL;
                    } else if (set1.isEmpty()) {
                        // constraints have been set on a previously unconstrained property
                        // => MAJOR change
                        type = MAJOR;
                    } else if (set2.containsAll(set1)) {
                        // new set is a superset of old set,
                        // i.e. constraints have been weakened
                        // (since constraints are OR'ed)
                        // => TRIVIAL change
                        type = TRIVIAL;
                    } else {
                        // constraint have been removed/modified (MAJOR change);
                        // since we're unable to semantically compare
                        // value constraints (e.g. regular expressions), all
                        // such modifications are considered a MAJOR change.
                        type = MAJOR;
                    }
                }

                // no need to check defaultValues (TRIVIAL change)
                // no need to check availableQueryOperators (TRIVIAL change)
                // no need to check queryOrderable (TRIVIAL change)

                if (type == TRIVIAL) {
                    int t1 = getOldDef().getRequiredType();
                    int t2 = getNewDef().getRequiredType();
                    if (t1 != t2) {
                        if (t2 == PropertyType.UNDEFINED) {
                            // changed getRequiredType to UNDEFINED (TRIVIAL change)
                            type = TRIVIAL;
                        } else {
                            // changed getRequiredType to specific type (MAJOR change)
                            type = MAJOR;
                        }
                    }
                    boolean b1 = getOldDef().isMultiple();
                    boolean b2 = getNewDef().isMultiple();
                    if (b1 != b2) {
                        if (b2) {
                            // changed multiple flag to true (TRIVIAL change)
                            type = TRIVIAL;
                        } else {
                            // changed multiple flag to false (MAJOR change)
                            type = MAJOR;
                        }
                    }
                }
            }
        }
    }

    public class ChildNodeDefDiff extends ChildItemDefDiff {

        ChildNodeDefDiff(NodeDefinition oldDef, NodeDefinition newDef) {
            super(oldDef, newDef);
        }

        public NodeDefinition getOldDef() {
            return (NodeDefinition) oldDef;
        }

        public NodeDefinition getNewDef() {
            return (NodeDefinition) newDef;
        }

        @Override
        protected void init() {
            super.init();
            /**
             * only need to do comparison if base class implementation
             * detected a non-MAJOR (i.e. TRIVIAL) modification;
             * no need to check for additions or removals as this is already
             * handled in base class implementation.
             */
            if (isModified() && type == TRIVIAL) {

                boolean b1 = getOldDef().allowsSameNameSiblings();
                boolean b2 = getNewDef().allowsSameNameSiblings();
                if (b1 != b2 && !b2) {
                    // changed sameNameSiblings flag to false (MAJOR change)
                    type = MAJOR;
                }

                // no need to check defaultPrimaryType (TRIVIAL change)

                if (type == TRIVIAL) {
                    Set<String> s1 = new HashSet<String>(Arrays.asList(getOldDef().getRequiredPrimaryTypeNames()));
                    Set<String> s2 = new HashSet<String>(Arrays.asList(getNewDef().getRequiredPrimaryTypeNames()));
                    // normalize sets by removing nt:base (adding/removing nt:base is irrelevant for the diff)
                    s1.remove("nt:base");
                    s2.remove("nt:base");
                    if (!s1.equals(s2)) {
                        // requiredPrimaryTypes have been modified
                        if (s1.containsAll(s2)) {
                            // old list is a superset of new list
                            // => removed requiredPrimaryType (TRIVIAL change)
                            type = TRIVIAL;
                        } else {
                            // added/modified requiredPrimaryType (MAJOR change)
                            // todo check whether aggregate of old requiredTypes would include aggregate of new requiredTypes => trivial change
                            type = MAJOR;
                        }
                    }
                }
            }
        }
    }

    /**
     * Identifier used to identify corresponding property definitions
     */
    static class PropertyDefinitionId {

        String declaringNodeType;
        String name;
        boolean definesResidual;

        PropertyDefinitionId(PropertyDefinition def) {
            declaringNodeType = def.getDeclaringNodeType().getName();
            name = def.getName();
            definesResidual = "*".equals(def.getName());
        }

        //---------------------------------------< java.lang.Object overrides >
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj instanceof PropertyDefinitionId) {
                PropertyDefinitionId other = (PropertyDefinitionId) obj;
                return declaringNodeType.equals(other.declaringNodeType)
                        && name.equals(other.name)
                        && definesResidual == other.definesResidual;
            }
            return false;
        }

        @Override
        public int hashCode() {
            int h = 17;
            h = 37 * h + declaringNodeType.hashCode();
            h = 37 * h + name.hashCode();
            h = 37 * h + (definesResidual ? 11 : 43);
            return h;
        }
    }

    /**
     * Identifier used to identify corresponding node definitions
     */
    static class NodeDefinitionId {

        String declaringNodeType;
        String name;

        NodeDefinitionId(NodeDefinition def) {
            declaringNodeType = def.getDeclaringNodeType().getName();
            name = def.getName();
        }

        //---------------------------------------< java.lang.Object overrides >
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj instanceof NodeDefinitionId) {
                NodeDefinitionId other = (NodeDefinitionId) obj;
                return declaringNodeType.equals(other.declaringNodeType)
                        && name.equals(other.name);
            }
            return false;
        }

        @Override
        public int hashCode() {
            int h = 17;
            h = 37 * h + declaringNodeType.hashCode();
            h = 37 * h + name.hashCode();
            return h;
        }
    }

    private class ChildNodeDefDiffs {

        private final List<NodeDefinition> defs1;
        private final List<NodeDefinition> defs2;

        private ChildNodeDefDiffs(final List<NodeDefinition> defs1, final List<NodeDefinition> defs2) {
            this.defs1 = defs1 != null ? defs1 : Collections.<NodeDefinition>emptyList();
            this.defs2 = defs2 != null ? defs2 : Collections.<NodeDefinition>emptyList();
        }

        private Collection<ChildNodeDefDiff> getChildNodeDefDiffs() {
            // gather all possible combinations of diffs
            final List<ChildNodeDefDiff> diffs = new ArrayList<ChildNodeDefDiff>();
            for (NodeDefinition def1 : defs1) {
                for (NodeDefinition def2 : defs2) {
                    diffs.add(new ChildNodeDefDiff(def1, def2));
                }
            }
            if (defs2.size() < defs1.size()) {
                for (NodeDefinition def1 : defs1) {
                    diffs.add(new ChildNodeDefDiff(def1, null));
                }
            }
            if (defs1.size() < defs2.size()) {
                for (NodeDefinition def2 : defs2) {
                    diffs.add(new ChildNodeDefDiff(null, def2));
                }
            }
            // sort them according to decreasing compatibility
            Collections.sort(diffs, new Comparator<ChildNodeDefDiff>() {
                @Override
                public int compare(final ChildNodeDefDiff o1, final ChildNodeDefDiff o2) {
                    return o1.getType() - o2.getType();
                }
            });
            // select the most compatible ones
            final int size = defs1.size() > defs2.size() ? defs1.size() : defs2.size();
            AtomicInteger allowedNewNull = new AtomicInteger(defs1.size() - defs2.size());
            AtomicInteger allowedOldNull = new AtomicInteger(defs2.size() - defs1.size());
            final List<ChildNodeDefDiff> results = new ArrayList<ChildNodeDefDiff>();
            for (ChildNodeDefDiff diff : diffs) {
                if (!alreadyMatched(results, diff.getNewDef(), diff.getOldDef(), allowedNewNull, allowedOldNull)) {
                    results.add(diff);
                    if (diff.getNewDef() == null) {
                        allowedNewNull.decrementAndGet();
                    }
                    if (diff.getOldDef() == null) {
                        allowedOldNull.decrementAndGet();
                    }
                }
                if (results.size() == size) {
                    break;
                }
            }
            return results;
        }

        private boolean alreadyMatched(final List<ChildNodeDefDiff> result, final NodeDefinition newDef, final NodeDefinition oldDef, final AtomicInteger allowedNewNull, final AtomicInteger allowedOldNull) {
            boolean containsNewDef = false, containsOldDef = false;
            for (ChildNodeDefDiff d : result) {
                if (d.getNewDef() != null && d.getNewDef().equals(newDef)) {
                    containsNewDef = true;
                    break;
                }
                if (d.getOldDef() != null && d.getOldDef().equals(oldDef)) {
                    containsOldDef = true;
                    break;
                }
            }
            if (oldDef == null) {
                if (allowedOldNull.get() < 1) {
                    containsOldDef = true;
                }
            }
            if (newDef == null) {
                if (allowedNewNull.get() < 1) {
                    containsNewDef = true;
                }
            }

            return containsNewDef || containsOldDef;
        }
    }

}
