/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.jcr.value;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.net.URI;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.UUID;
import javax.jcr.RepositoryException;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.common.util.SecureHash;
import org.modeshape.jcr.GraphI18n;
import org.modeshape.jcr.api.value.DateTime;
import org.modeshape.jcr.value.basic.StringValueFactory;

/**
 * A set of {@link Comparator} objects for the different kinds of property values.
 * 
 * @see PropertyType#getComparator()
 */
@Immutable
public final class ValueComparators {

    /**
     * A comparator of string values.
     */
    public static final Comparator<String> STRING_COMPARATOR = new StringComparator();

    /**
     * A comparator of long values.
     */
    public static final Comparator<Long> LONG_COMPARATOR = new LongComparator();

    /**
     * A comparator of double values.
     */
    public static final Comparator<Double> DOUBLE_COMPARATOR = new DoubleComparator();

    /**
     * A comparator of decimal values.
     */
    public static final Comparator<BigDecimal> DECIMAL_COMPARATOR = new DecimalComparator();

    /**
     * A comparator of boolean values.
     */
    public static final Comparator<Boolean> BOOLEAN_COMPARATOR = new BooleanComparator();

    /**
     * A comparator of binary values. Although {@link BinaryValue} is {@link Comparable}, this comparator does not rely upon any
     * particular Binary implementation. Thus, Binary implementations can use this for their {@link Comparable#compareTo(Object)}
     * implementation.
     */
    public static final Comparator<BinaryValue> BINARY_COMPARATOR = new BinaryValueComparator();

    /**
     * A comparator of date-time instances.
     */
    public static final Comparator<DateTime> DATE_TIME_COMPARATOR = new DateTimeComparator();

    /**
     * A comparator of date values.
     */
    public static final Comparator<Date> DATE_COMPARATOR = new DateComparator();

    /**
     * A comparator of calendar values.
     */
    public static final Comparator<Calendar> CALENDAR_COMPARATOR = new CalendarComparator();

    /**
     * A comparator of name values.
     */
    public static final Comparator<Name> NAME_COMPARATOR = new NameComparator();
    /**
     * A comparator of path values.
     */
    public static final Comparator<Path> PATH_COMPARATOR = new PathComparator();

    /**
     * A comparator of path segment values.
     */
    public static final Comparator<Path.Segment> PATH_SEGMENT_COMPARATOR = new PathSegmentComparator();

    /**
     * A comparator of path segment names, excluding same-name-sibling indexes.
     */
    public static final Comparator<Path.Segment> PATH_SEGMENT_NAME_COMPARATOR = new PathSegmentNameComparator();

    /**
     * A comparator of URI values.
     */
    public static final Comparator<URI> URI_COMPARATOR = new UriComparator();

    /**
     * A comparator of UUID values.
     */
    public static final Comparator<UUID> UUID_COMPARATOR = new UuidComparator();

    /**
     * A comparator of reference values.
     */
    public static final Comparator<Reference> REFERENCE_COMPARATOR = new ReferenceComparator();

    /**
     * A comparator of other values.
     */
    public static final Comparator<Object> OBJECT_COMPARATOR = new ObjectComparator();

    protected static final class StringComparator implements Comparator<String>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( String o1,
                            String o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class BooleanComparator implements Comparator<Boolean>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( Boolean o1,
                            Boolean o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class LongComparator implements Comparator<Long>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( Long o1,
                            Long o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class DoubleComparator implements Comparator<Double>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( Double o1,
                            Double o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class DecimalComparator implements Comparator<BigDecimal>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( BigDecimal o1,
                            BigDecimal o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class DateTimeComparator implements Comparator<DateTime>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( DateTime o1,
                            DateTime o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class DateComparator implements Comparator<Date>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( Date o1,
                            Date o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class CalendarComparator implements Comparator<Calendar>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( Calendar o1,
                            Calendar o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class NameComparator implements Comparator<Name>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( Name o1,
                            Name o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class PathComparator implements Comparator<Path>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( Path o1,
                            Path o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class PathSegmentComparator implements Comparator<Path.Segment>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( Path.Segment o1,
                            Path.Segment o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class PathSegmentNameComparator implements Comparator<Path.Segment>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( Path.Segment o1,
                            Path.Segment o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.getName().compareTo(o2.getName());
        }
    }

    protected static final class UriComparator implements Comparator<URI>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( URI o1,
                            URI o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class UuidComparator implements Comparator<UUID>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( UUID o1,
                            UUID o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class ReferenceComparator implements Comparator<Reference>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( Reference o1,
                            Reference o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    }

    protected static final class ObjectComparator implements Comparator<Object>, Serializable {
        private static final long serialVersionUID = 1L;

        @SuppressWarnings( "unchecked" )
        @Override
        public int compare( Object o1,
                            Object o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            PropertyType type1 = PropertyType.discoverType(o1);
            PropertyType type2 = PropertyType.discoverType(o2);

            // Canonicalize the values ...
            o1 = type1.getCanonicalValue(o1);
            o2 = type2.getCanonicalValue(o2);

            if (type1 != PropertyType.OBJECT && type2 != PropertyType.OBJECT) {
                if (type1 == type2) return ((Comparator<Object>)type1.getComparator()).compare(o1, o2);

                // The types are different but the classes are the same ...
                if (type1.getValueClass().isAssignableFrom(type2.getValueClass())) {
                    return ((Comparator<Object>)type1.getComparator()).compare(o1, o2);
                }
                if (type2.getValueClass().isAssignableFrom(type1.getValueClass())) {
                    return ((Comparator<Object>)type2.getComparator()).compare(o1, o2);
                }
            }

            // The types are different. See if one is a BINARY value (because we can use the secure
            // hashes to efficiently do the comparison) ...
            ValueFactory<String> stringFactory = getStringValueFactory();
            String value1 = null;
            String value2 = null;
            if (type1 == PropertyType.BINARY || type2 == PropertyType.BINARY) {
                try {
                    byte[] hash1 = null;
                    byte[] hash2 = null;
                    // We don't have access to a binary factory, so do this brute force.
                    // Conver the non-binary value to a string, then compute the hash of the string ...
                    if (type1 == PropertyType.BINARY) {
                        value2 = stringFactory.create(o2);
                        hash2 = SecureHash.getHash(SecureHash.Algorithm.SHA_1, value2.getBytes());
                        hash1 = ((BinaryValue)o1).getHash();
                    } else {
                        assert type2 == PropertyType.BINARY;
                        value1 = stringFactory.create(o1);
                        hash1 = SecureHash.getHash(SecureHash.Algorithm.SHA_1, value1.getBytes());
                        hash2 = ((BinaryValue)o2).getHash();
                    }
                    // Compute the difference in the hashes ...
                    if (hash1.length == hash2.length) {
                        for (int i = 0; i != hash1.length; ++i) {
                            int diff = hash1[i] - hash2[i];
                            if (diff != 0) return diff;
                        }
                        return 0;
                    }
                } catch (Throwable error) {
                    // If anything went wrong, just continue with the string comparison
                }
            }

            // The types are different and must be converted ...
            if (value1 == null) value1 = stringFactory.create(o1);
            if (value2 == null) value2 = stringFactory.create(o2);
            return value1.compareTo(value2);
        }
    }

    protected static final class BinaryValueComparator implements Comparator<BinaryValue>, Serializable {
        private static final long serialVersionUID = 1L;

        @Override
        public int compare( BinaryValue o1,
                            BinaryValue o2 ) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            try {
                try {
                    // Check the lengths first ...
                    final long len1 = o1.getSize();
                    final long len2 = o2.getSize();
                    if (len1 < len2) return -1;
                    if (len1 > len2) return 1;

                    // Compare using the hashes, if available
                    String hash1 = o1.getHexHash();
                    String hash2 = o2.getHexHash();
                    if (hash1 != null && hash2 != null) {
                        // If the hashes match, then we should assume that the values match.
                        // That's the whole point of using a secure hash.
                        return hash1.compareTo(hash2);
                    }

                    // One or both of the hashes could not be generated, so we have to go compare
                    // the whole values. This is unfortunate, but should happen very rarely (if ever)
                    // as long as the BinaryValue.getHash() is always implemented

                    // Otherwise they are the same length ...
                    InputStream stream1 = null;
                    InputStream stream2 = null;
                    try {
                        stream1 = o1.getStream();
                        stream2 = o2.getStream();
                        byte[] buffer1 = new byte[1024];
                        byte[] buffer2 = new byte[1024];
                        while (true) {
                            int numRead1 = stream1.read(buffer1);
                            if (numRead1 < 0) break;
                            int numRead2 = stream2.read(buffer2);
                            if (numRead1 != numRead2) {
                                throw new IoException(GraphI18n.errorReadingPropertyValueBytes.text());
                            }
                            for (int i = 0; i != numRead1; ++i) {
                                int diff = buffer1[i] - buffer2[i];
                                if (diff != 0) return diff;
                            }
                        }
                        return 0;
                    } catch (RepositoryException e) {
                        throw new IoException(GraphI18n.errorReadingPropertyValueBytes.text());
                    } catch (IOException e) {
                        throw new IoException(GraphI18n.errorReadingPropertyValueBytes.text());
                    } finally {
                        if (stream1 != null) {
                            try {
                                stream1.close();
                            } catch (IOException e) {
                                // do nothing
                            }
                        }
                        if (stream2 != null) {
                            try {
                                stream2.close();
                            } catch (IOException e) {
                                // do nothing
                            }
                        }
                    }
                } finally {
                    o2.dispose();
                }
            } finally {
                o1.dispose();
            }
        }
    }

    // This is loaded lazily so that there is not a circular dependency between PropertyType (depends on this),
    // StringValueFactory (depends on PropertyType), and OBJECT_COMPARATOR (which depends on StringValueFactory) ...
    private static ValueFactory<String> STRING_VALUE_FACTORY;

    protected static final ValueFactory<String> getStringValueFactory() {
        // No locking is required, because it doesn't matter if we create several instances during initialization ...
        if (STRING_VALUE_FACTORY == null) {
            STRING_VALUE_FACTORY = new StringValueFactory(Path.NO_OP_DECODER, Path.NO_OP_ENCODER);
        }
        return STRING_VALUE_FACTORY;
    }
}
