/*
 * @(#) ObjectComparatorManager.java
 *
 * Copyright 2002 - 2003 JIDE Software. All rights reserved.
 */
package com.jidesoft.comparator;

import com.jidesoft.utils.CacheMap;
import com.jidesoft.utils.RegistrationListener;

import java.text.Collator;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;

/**
 * A global object that can register comparator with a type and a ComparatorContext.
 */
public class ObjectComparatorManager {

    private static final CacheMap<Comparator<?>, ComparatorContext> _cache = new CacheMap<Comparator<?>, ComparatorContext>(new ComparatorContext(""));

    private static final Comparator<Object> _defaultComparator = new DefaultComparator();

    public static void registerComparator(Class<?> clazz, Comparator comparator) {
        registerComparator(clazz, comparator, ComparatorContext.DEFAULT_CONTEXT);
    }

    /**
     * Registers a comparator with the type specified as class and a comparator context specified as context.
     *
     * @param clazz      type.
     * @param comparator the comparator to be registered.
     * @param context    the comparator context.
     */
    public static void registerComparator(Class<?> clazz, Comparator comparator, ComparatorContext context) {
        if (clazz == null) {
            throw new IllegalArgumentException("Parameter clazz cannot be null");
        }
        if (context == null) {
            context = ComparatorContext.DEFAULT_CONTEXT;
        }

        if (isAutoInit() && !_inited && !_initing) {
            initDefaultComparator();
        }

        _cache.register(clazz, comparator, context);
    }

    /**
     * Unregisters comparator associated with the class and context.
     *
     * @param clazz the data type.
     */
    public static void unregisterComparator(Class<?> clazz) {
        unregisterComparator(clazz, ComparatorContext.DEFAULT_CONTEXT);
    }

    /**
     * Unregisters comparator associated with the class and context.
     *
     * @param clazz   the data type.
     * @param context the comparator context.
     */
    public static void unregisterComparator(Class<?> clazz, ComparatorContext context) {
        if (context == null) {
            context = ComparatorContext.DEFAULT_CONTEXT;
        }

        if (isAutoInit() && !_inited && !_initing) {
            initDefaultComparator();
        }

        _cache.unregister(clazz, context);
    }

    /**
     * Unregisters all comparators associated with the class.
     *
     * @param clazz the data type.
     */
    public static void unregisterAllComparators(Class<?> clazz) {
        _cache.remove(clazz);
    }

    /**
     * Unregisters all the comparators which registered before.
     */
    public static void unregisterAllComparators() {
        _cache.clear();
    }

    /**
     * Gets the registered comparator associated with class and default context.
     *
     * @param clazz the data type.
     *
     * @return the registered comparator.
     */
    public static Comparator getComparator(Class<?> clazz) {
        return getComparator(clazz, ComparatorContext.DEFAULT_CONTEXT);
    }

    /**
     * Gets the comparator.
     *
     * @param clazz   the data type.
     * @param context the comparator context.
     *
     * @return the comparator.
     */
    public static Comparator getComparator(Class<?> clazz, ComparatorContext context) {
        if (isAutoInit() && !_inited && !_initing) {
            initDefaultComparator();
        }

        if (context == null) {
            context = ComparatorContext.DEFAULT_CONTEXT;
        }
        Comparator object = _cache.getRegisteredObject(clazz, context);
        if (object != null) {
            return object;
        }
        else {
            return _defaultComparator;
        }
    }

    /**
     * Compares the two objects. It will look up in <code>ObjectComparatorManager</code> to find the comparator and
     * compare.
     *
     * @param o1 the first object to be compared.
     * @param o2 the second object to be compared.
     *
     * @return the compare result as defined in {@link Comparator#compare(Object, Object)}
     */

    public static int compare(Object o1, Object o2) {
        return compare(o1, o2, ComparatorContext.DEFAULT_CONTEXT);
    }

    /**
     * Compares the two objects. It will look up in <code>ObjectComparatorManager</code> to find the comparator and
     * compare.
     *
     * @param o1      the first object to be compared.
     * @param o2      the second object to be compared.
     * @param context the comparator context
     *
     * @return the compare result as defined in {@link Comparator#compare(Object, Object)}
     */

    public static int compare(Object o1, Object o2, ComparatorContext context) {
        if (o1 == null && o2 == null) {
            return 0;
        }
        else if (o1 == null) {
            return -1;
        }
        else if (o2 == null) {
            return 1;
        }

        // both not null

        Class<?> clazz;
        Class<?> clazz1 = o1.getClass();
        Class<?> clazz2 = o2.getClass();
        if (clazz1 == clazz2) {
            clazz = clazz1;
        }
        else if (clazz1.isAssignableFrom(clazz2)) {
            clazz = clazz1;
        }
        else if (clazz2.isAssignableFrom(clazz1)) {
            clazz = clazz2;
        }
        else if (clazz1.isAssignableFrom(Comparable.class) && clazz2.isAssignableFrom(Comparable.class)) {
            clazz = Comparable.class;
        }
        else {
            clazz = Object.class;
        }

        return compare(o1, o2, clazz, context);
    }

    /**
     * Compares the two objects. It will look up in <code>ObjectComparatorManager</code> to find the comparator and
     * compare. This method needs a third parameter which is the data type. This is useful when you have two objects
     * that have different data types but both extend the same super class. In this case, you may want the super class
     * as the key to look up in <code>ObjectComparatorManager</code>.
     *
     * @param o1    the first object to be compared.
     * @param o2    the second object to be compared.
     * @param clazz the data type of the two objects. If your two objects have the same type, you may just use {@link
     *              #compare(Object, Object)} methods.
     *
     * @return the compare result as defined in {@link Comparator#compare(Object, Object)}
     */
    public static int compare(Object o1, Object o2, Class<?> clazz) {
        return compare(o1, o2, clazz, ComparatorContext.DEFAULT_CONTEXT);
    }

    /**
     * Compares the two objects. It will look up in <code>ObjectComparatorManager</code> to find the comparator and
     * compare. If it is not found, we will convert the object to string and compare the two strings.
     *
     * @param o1      the first object to be compared.
     * @param o2      the second object to be compared.
     * @param clazz   the data type of the two objects. If your two objects have the same type, you may just use {@link
     *                #compare(Object, Object)} methods.
     * @param context the comparator context
     *
     * @return the compare result as defined in {@link Comparator#compare(Object, Object)}
     */
    public static int compare(Object o1, Object o2, Class<?> clazz, ComparatorContext context) {
        Comparator comparator = getComparator(clazz, context);
        if (comparator != null) {
            try {
                return comparator.compare(o1, o2);
            }
            catch (Exception e) {
                // ignore and let the code below handles it.
            }
        }
        if (o1 == o2) {
            return 0;
        }
        else {
            if (o1 == null) {
                return -1;
            }
            else if (o2 == null) {
                return 1;
            }
            else { // otherwise, compare as string
                return o1.toString().compareTo(o2.toString());
            }
        }
    }

    private static boolean _inited = false;
    private static boolean _initing = false;
    private static boolean _autoInit = true;

    /**
     * Checks the value of autoInit.
     *
     * @return true or false.
     *
     * @see #setAutoInit(boolean)
     */
    public static boolean isAutoInit() {
        return _autoInit;
    }

    /**
     * Sets autoInit to true or false. If autoInit is true, whenever someone tries to call methods like as toString or
     * fromString, {@link #initDefaultComparator()} will be called if it has never be called. By default, autoInit is
     * true.
     * <p/>
     * This might affect the behavior if users provide their own comparators and want to overwrite default comparators.
     * In this case, instead of depending on autoInit to initialize default comparators, you should call {@link
     * #initDefaultComparator()} first, then call registerComparator to add your own comparators.
     *
     * @param autoInit false if you want to disable autoInit which means you either don't want those default comparators
     *                 registered or you will call {@link #initDefaultComparator()} yourself.
     */
    public static void setAutoInit(boolean autoInit) {
        _autoInit = autoInit;
    }

    /**
     * Adds a listener to the list that's notified each time a change to the manager occurs.
     *
     * @param l the RegistrationListener
     */
    public static void addRegistrationListener(RegistrationListener l) {
        _cache.addRegistrationListener(l);
    }

    /**
     * Removes a listener from the list that's notified each time a change to the manager occurs.
     *
     * @param l the RegistrationListener
     */
    public static void removeRegistrationListener(RegistrationListener l) {
        _cache.removeRegistrationListener(l);
    }

    /**
     * Returns an array of all the registration listeners registered on this manager.
     *
     * @return all of this registration's <code>RegistrationListener</code>s or an empty array if no registration
     *         listeners are currently registered
     *
     * @see #addRegistrationListener
     * @see #removeRegistrationListener
     */
    public static RegistrationListener[] getRegistrationListeners() {
        return _cache.getRegistrationListeners();
    }

    /**
     * Gets the available ComparatorContexts registered with the class.
     *
     * @param clazz the class.
     *
     * @return the available ComparatorContext.
     */
    public static ComparatorContext[] getComparatorContexts(Class<?> clazz) {
        return _cache.getKeys(clazz, new ComparatorContext[0]);
    }

    /**
     * Initialize default comparator. Please make sure you call this method before you use any comparator related
     * classes such as SortableTableModel.
     */
    public static void initDefaultComparator() {
        if (_inited) {
            return;
        }

        _initing = true;

        try {
            registerComparator(Object.class, new DefaultComparator());
            registerComparator(Boolean.class, new BooleanComparator());
            registerComparator(Calendar.class, new CalendarComparator());
            registerComparator(Date.class, new DateComparator());

            NumberComparator numberComparator = new NumberComparator();
            registerComparator(Number.class, numberComparator);
            registerComparator(double.class, numberComparator);
            registerComparator(float.class, numberComparator);
            registerComparator(long.class, numberComparator);
            registerComparator(int.class, numberComparator);
            registerComparator(short.class, numberComparator);
            registerComparator(byte.class, numberComparator);

            NumberComparator absoluteNumberComparator = new NumberComparator();
            absoluteNumberComparator.setAbsolute(true);
            registerComparator(Number.class, absoluteNumberComparator, NumberComparator.CONTEXT_ABSOLUTE);
            registerComparator(double.class, absoluteNumberComparator, NumberComparator.CONTEXT_ABSOLUTE);
            registerComparator(float.class, absoluteNumberComparator, NumberComparator.CONTEXT_ABSOLUTE);
            registerComparator(long.class, absoluteNumberComparator, NumberComparator.CONTEXT_ABSOLUTE);
            registerComparator(int.class, absoluteNumberComparator, NumberComparator.CONTEXT_ABSOLUTE);
            registerComparator(short.class, absoluteNumberComparator, NumberComparator.CONTEXT_ABSOLUTE);
            registerComparator(byte.class, absoluteNumberComparator, NumberComparator.CONTEXT_ABSOLUTE);

            registerComparator(Comparable.class, new FastComparableComparator());
            registerComparator(String.class, Collator.getInstance());
            Collator caseInsensitiveCollator = Collator.getInstance();
            caseInsensitiveCollator.setStrength(Collator.PRIMARY);
            registerComparator(String.class, caseInsensitiveCollator, new ComparatorContext("Ignorecase"));
            Collator secondaryCollator = Collator.getInstance();
            secondaryCollator.setStrength(Collator.SECONDARY);
            registerComparator(String.class, secondaryCollator, new ComparatorContext("Secondary"));
            registerComparator(CharSequence.class, new CharSequenceComparator(), CharSequenceComparator.CONTEXT);
            registerComparator(CharSequence.class, new CharSequenceComparator(false), CharSequenceComparator.CONTEXT_IGNORE_CASE);
            registerComparator(CharSequence.class, new AlphanumComparator(), AlphanumComparator.CONTEXT);
            registerComparator(CharSequence.class, new AlphanumComparator(false), AlphanumComparator.CONTEXT_IGNORE_CASE);
        }
        finally {
            _initing = false;
            _inited = true;
        }

    }

    /**
     * If {@link #initDefaultComparator()} is called once, calling it again will have no effect because an internal flag
     * is set. This method will reset the internal flag so that you can call  {@link #initDefaultComparator()} in case
     * you unregister all comparators using {@link #unregisterAllComparators()}.
     */
    public static void resetInit() {
        _inited = false;
    }

    public static void clear() {
        resetInit();
        _cache.clear();
    }
}
