/*
// Licensed to Julian Hyde under one or more contributor license
// agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership.
//
// Julian Hyde 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.olap4j.impl;

import org.olap4j.metadata.NamedList;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility methods common to multiple olap4j driver implementations.
 *
 * <p>This class, and this package as a whole, are part of the olap4j library
 * but the methods are not part of the public olap4j API. The classes exist
 * for the convenience of implementers of olap4j drivers, but their
 * specification and implementation may change at any time.</p>
 *
 * <p><b>Applications which use the API in this package will not be portable
 * across multiple versions of olap4j</b>. We encourage implementors of drivers
 * to use classes in this package, but not writers of applications.</p>
 *
 * @author jhyde
 * @since Dec 12, 2007
 */
public class Olap4jUtil {
    /**
     * Whether we are running a version of Java before 1.5.
     *
     * <p>If this variable is true, we will be running retroweaver. Retroweaver
     * has some problems involving {@link java.util.EnumSet}.
     */
    public static final boolean PreJdk15 =
        System.getProperty("java.version").startsWith("1.4");

    /**
     * Whether the code base has re-engineered using retroweaver.
     * If this is the case, some functionality is not available.
     */
    public static final boolean Retrowoven =
        DummyEnum.class.getSuperclass().getName().equals(
            "com.rc.retroweaver.runtime.Enum_");

    private static final Olap4jUtilCompatible compatible;

    private static final NamedList<?> EMPTY_NAMED_LIST =
        new EmptyNamedList();

    private static final Pattern CELL_VALUE_REGEX1 =
        Pattern.compile("\\s*([a-zA-Z][\\w\\.]*)\\s*=\\s*'([^']*)'");

    private static final Pattern CELL_VALUE_REGEX2 =
        Pattern.compile("\\s*([a-zA-Z][\\w\\.]*)\\s*=\\s*([^\\s]*)");

    static {
        String className;
        if (PreJdk15 || Retrowoven) {
            className = "org.olap4j.impl.Olap4jUtilCompatibleJdk14";
        } else {
            className = "org.olap4j.impl.Olap4jUtilCompatibleJdk15";
        }
        try {
            //noinspection unchecked
            Class<Olap4jUtilCompatible> clazz =
                (Class<Olap4jUtilCompatible>) Class.forName(className);
            compatible = clazz.newInstance();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Could not load '" + className + "'", e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Could not load '" + className + "'", e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Could not load '" + className + "'", e);
        }
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public static void discard(boolean b) {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public static void discard(byte b) {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public static void discard(char c) {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public static void discard(double v) {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public static void discard(float v) {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public static void discard(int i) {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public static void discard(long l) {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public static void discard(Object o) {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public static void discard(short i) {
    }

    /**
     * Casts a Set to a Set with a different element type.
     *
     * @param set Set
     * @return Set of desired type
     */
    @SuppressWarnings({"unchecked"})
    public static <T> Set<T> cast(Set<?> set) {
        return (Set<T>) set;
    }

    /**
     * Casts a List to a List with a different element type.
     *
     * @param list List
     * @return List of desired type
     */
    @SuppressWarnings({"unchecked"})
    public static <T> List<T> cast(List<?> list) {
        return (List<T>) list;
    }

    /**
     * Casts a NamedList to a NamedList with a different element type.
     *
     * @param list Named list
     * @return Named list of desired type
     */
    @SuppressWarnings({"unchecked"})
    public static <T> NamedList<T> cast(NamedList<?> list) {
        return (NamedList<T>) list;
    }

    /**
     * Returns a hashmap with given contents.
     *
     * <p>Use this method in initializers. Type parameters are inferred from
     * context, and the contents are initialized declaratively. For example,
     *
     * <blockquote><code>Map&lt;String, Integer&gt; population =<br/>
     * &nbsp;&nbsp;Olap4jUtil.mapOf(<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;"UK", 65000000,<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;"USA", 300000000);</code></blockquote>
     *
     * @see org.olap4j.impl.UnmodifiableArrayMap#of(Object, Object, Object...)
     * @see org.olap4j.impl.ArrayMap#of(Object, Object, Object...)
     *
     * @param key First key
     * @param value First value
     * @param keyValues Second and sequent key/value pairs
     * @param <K> Key type
     * @param <V> Value type
     * @return Map with given contents
     */
    public static <K, V> Map<K, V> mapOf(K key, V value, Object... keyValues)
    {
        final Map<K, V> map = new LinkedHashMap<K, V>(1 + keyValues.length);
        map.put(key, value);
        for (int i = 0; i < keyValues.length;) {
            //noinspection unchecked
            map.put((K) keyValues[i++], (V) keyValues[i++]);
        }
        return map;
    }

    /**
     * Converts a Properties object to a Map with String keys and values.
     *
     * @param properties Properties
     * @return Map backed by the given Properties object
     */
    public static Map<String, String> toMap(final Properties properties) {
        return new AbstractMap<String, String>() {
            public Set<Entry<String, String>> entrySet() {
                return cast(properties.entrySet());
            }
        };
    }

    /**
     * Returns an exception indicating that we didn't expect to find this value
     * here.
     *
     * @param value Value
     * @return an AssertionError which can be thrown
     */
    public static AssertionError unexpected(Enum value) {
        return new AssertionError(
            "Was not expecting value '" + value
            + "' for enumeration '" + value.getClass().getName()
            + "' in this context");
    }

    /**
    * Returns true if two objects are equal, or are both null.
     *
     * @param t1 First value
     * @param t2 Second value
     * @return Whether values are both equal or both null
     */
    public static <T> boolean equal(T t1, T t2) {
        return t1 == null ? t2 == null : t1.equals(t2);
    }

    /**
     * Returns a string with every occurrence of a seek string replaced with
     * another.
     *
     * @param s String to act on
     * @param find String to find
     * @param replace String to replace it with
     * @return The modified string
     */
    public static String replace(
        String s,
        String find,
        String replace)
    {
        // let's be optimistic
        int found = s.indexOf(find);
        if (found == -1) {
            return s;
        }
        StringBuilder sb = new StringBuilder(s.length() + 20);
        int start = 0;
        char[] chars = s.toCharArray();
        final int step = find.length();
        if (step == 0) {
            // Special case where find is "".
            sb.append(s);
            replace(sb, 0, find, replace);
        } else {
            for (;;) {
                sb.append(chars, start, found - start);
                if (found == s.length()) {
                    break;
                }
                sb.append(replace);
                start = found + step;
                found = s.indexOf(find, start);
                if (found == -1) {
                    found = s.length();
                }
            }
        }
        return sb.toString();
    }

    /**
     * Replaces all occurrences of a string in a buffer with another.
     *
     * @param buf String buffer to act on
     * @param start Ordinal within <code>find</code> to start searching
     * @param find String to find
     * @param replace String to replace it with
     * @return The string buffer
     */
    public static StringBuilder replace(
        StringBuilder buf,
        int start,
        String find,
        String replace)
    {
        // Search and replace from the end towards the start, to avoid O(n ^ 2)
        // copying if the string occurs very commonly.
        int findLength = find.length();
        if (findLength == 0) {
            // Special case where the seek string is empty.
            for (int j = buf.length(); j >= 0; --j) {
                buf.insert(j, replace);
            }
            return buf;
        }
        int k = buf.length();
        while (k > 0) {
            int i = buf.lastIndexOf(find, k);
            if (i < start) {
                break;
            }
            buf.replace(i, i + find.length(), replace);
            // Step back far enough to ensure that the beginning of the section
            // we just replaced does not cause a match.
            k = i - findLength;
        }
        return buf;
    }

    /**
     * Converts a list of SQL-style patterns into a Java regular expression.
     *
     * <p>For example, {"Foo_", "Bar%BAZ"} becomes "Foo.|Bar.*BAZ".
     *
     * @param wildcards List of SQL-style wildcard expressions
     * @return Regular expression
     */
    public static String wildcardToRegexp(List<String> wildcards) {
        StringBuilder buf = new StringBuilder();
        for (String value : wildcards) {
            if (buf.length() > 0) {
                buf.append('|');
            }
            int i = 0;
            while (true) {
                int percent = value.indexOf('%', i);
                int underscore = value.indexOf('_', i);
                if (percent == -1 && underscore == -1) {
                    if (i < value.length()) {
                        buf.append(quotePattern(value.substring(i)));
                    }
                    break;
                }
                if (underscore >= 0 && (underscore < percent || percent < 0)) {
                    if (i < underscore) {
                        buf.append(
                            quotePattern(value.substring(i, underscore)));
                    }
                    buf.append('.');
                    i = underscore + 1;
                } else if (percent >= 0
                    && (percent < underscore || underscore < 0))
                {
                    if (i < percent) {
                    buf.append(
                        quotePattern(value.substring(i, percent)));
                    }
                    buf.append(".*");
                    i = percent + 1;
                } else {
                    throw new IllegalArgumentException();
                }
            }
        }
        return buf.toString();
    }

    /**
     * Returns a literal pattern String for the specified String.
     *
     * <p>Specification as for {@link java.util.regex.Pattern#quote(String)}, which was
     * introduced in JDK 1.5.
     *
     * @param s The string to be literalized
     * @return A literal string replacement
     */
    public static String quotePattern(String s) {
        return compatible.quotePattern(s);
    }

    /**
     * Converts a camel-case name to an upper-case name with underscores.
     *
     * <p>For example, <code>camelToUpper("FooBar")</code> returns "FOO_BAR".
     *
     * @param s Camel-case string
     * @return  Upper-case string
     */
    public static String camelToUpper(String s) {
        StringBuilder buf = new StringBuilder(s.length() + 10);
        int prevUpper = -1;
        for (int i = 0; i < s.length(); ++i) {
            char c = s.charAt(i);
            if (Character.isUpperCase(c)) {
                if (i > prevUpper + 1) {
                    buf.append('_');
                }
                prevUpper = i;
            } else {
                c = Character.toUpperCase(c);
            }
            buf.append(c);
        }
        return buf.toString();
    }

    /**
     * Returns an exception which indicates that a particular piece of
     * functionality should work, but a developer has not implemented it yet.
     *
     * @param o Object operation was called on, perhaps indicating the
     * subtype where a virtual method needs to be implemented
     *
     * @return an exception which can be thrown
     */
    public static RuntimeException needToImplement(Object o) {
        throw new UnsupportedOperationException("need to implement " + o);
    }

    /**
     * Parses a unique name.
     *
     * <p>For example, <tt>uniqueNameToStringArray("[foo].[bar]")</tt> returns
     * {@code ["foo", "bar"]}.
     *
     * @see org.olap4j.mdx.IdentifierNode#parseIdentifier(String)
     *
     * @param uniqueName Unique name
     * @return Parsed unique name
     */
    public static List<String> parseUniqueName(String uniqueName) {
        List<String> trail = new ArrayList<String>();
        Pattern regex = Pattern.compile("([^\\[\\]\\.]*)");
        Matcher matcher = regex.matcher(uniqueName);
        while (matcher.find()) {
            String match = matcher.group();
            if (!match.equals("")) {
                trail.add(match);
            }
        }
        return trail;
    }

    /**
     * Converts the contents of an array of strings to
     * a proper String representation.
     *
     * @param array Array of strings
     * @return string representation of the array
     */
    public static String stringArrayToString(String[] array) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < array.length; i++) {
            sb.append(array[i]);
            if (i < (array.length - 1)) {
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }

    @SuppressWarnings({"unchecked"})
    public static <T> NamedList<T> emptyNamedList() {
        return (NamedList<T>) EMPTY_NAMED_LIST;
    }

    /**
     * Returns an unmodifiable view of the specified list.  This method allows
     * modules to provide users with "read-only" access to internal
     * lists.  Query operations on the returned list "read through" to the
     * specified list, and attempts to modify the returned list, whether
     * direct or via its iterator, result in an
     * <tt>UnsupportedOperationException</tt>.<p>
     *
     * The returned list will be serializable if the specified list
     * is serializable. Similarly, the returned list will implement
     * {@link RandomAccess} if the specified list does.
     *
     * <p>The equivalent of
     * {@link java.util.Collections#unmodifiableList(java.util.List)}.
     *
     * @param  list the list for which an unmodifiable view is to be returned.
     * @return an unmodifiable view of the specified list.
     */
    public static <T> NamedList<T> unmodifiableNamedList(
        final NamedList<T> list)
    {
        return list instanceof RandomAccess
            ? new UnmodifiableNamedRandomAccessList<T>(list)
            : new UnmodifiableNamedList<T>(list);
    }

    /**
     * Equivalent to {@link java.util.EnumSet#of(Enum, Enum[])} on JDK 1.5 or
     * later. Otherwise, returns an ordinary set.
     *
     * @param first an element that the set is to contain initially
     * @param rest the remaining elements the set is to contain initially
     * @throws NullPointerException if any of the specified elements are null,
     *     or if <tt>rest</tt> is null
     * @return an enum set initially containing the specified elements
     */
    public static <E extends Enum<E>> Set<E> enumSetOf(E first, E... rest) {
        return compatible.enumSetOf(first, rest);
    }

    /**
     * Equivalent to {@link java.util.EnumSet#noneOf(Class)} on JDK 1.5 or
     * later. Otherwise, returns an ordinary set.
     *
     * @param elementType the class object of the element type for this enum
     *     set
     * @return an empty enum set
     */
    public static <E extends Enum<E>> Set<E> enumSetNoneOf(
        Class<E> elementType)
    {
        return compatible.enumSetNoneOf(elementType);
    }

    /**
     * Equivalent to {@link java.util.EnumSet#allOf(Class)} on JDK 1.5 or later.
     * Otherwise, returns an ordinary set.

     * @param elementType the class object of the element type for this enum
     *     set
     * @return an enum set containing all elements of the given enum class
     */
    public static <E extends Enum<E>> Set<E> enumSetAllOf(
        Class<E> elementType)
    {
        return compatible.enumSetAllOf(elementType);
    }

    /**
     * Parses a formatted cell values.
     *
     * <p>There is a customary way of including formatting infornation in cell
     * values (started with Microsoft OLAP Services, and continued by JPivot and
     * now Pentaho Analyzer). This method parses out the formatted value that
     * should be displayed on the screen and also any properties present.
     *
     * <p>Examples:<ul>
     * <li>"$123" no formatting information</li>
     * <li>"|$123|style=red|" print in red style</li>
     * <li>"|$123|style=red|arrow=up|" print in red style with an up arrow</li>
     * </ul>
     *
     * <h4>Properties</h4>
     *
     * <table border="1">
     * <tr>
     *     <th>Name</th>
     *     <th>Value</th>
     *     <th>Description</th>
     * </tr>
     * <tr>
     *     <td>style</td>
     *     <td>red|green|yellow</td>
     *     <td>renders the Member in that color</td>
     * </tr>
     * <tr>
     *     <td>link</td>
     *     <td>a url</td>
     *     <td>creates a hyperlink on the member</td>
     * </tr>
     * <tr>
     *     <td>arrow</td>
     *     <td>up|down|blank</td>
     *     <td>paints an arrow image</td>
     * </tr>
     * <tr>
     *     <td>image</td>
     *     <td>a uri. If the uri starts with "/" the context name will be
     *         prepended</td>
     *     <td>paints image</td>
     * </tr>
     * </table>
     *
     * @param formattedValue Formatted cell value
     * @param map Map into which to place (property, value) pairs
     * @return Formatted cell value with properties removed
     */
    public static String parseFormattedCellValue(
        String formattedValue,
        Map<String, String> map)
    {
        if (formattedValue.startsWith("|")) {
            String[] strs = formattedValue.substring(1).split("\\|");
            formattedValue = strs[0]; // original value
            for (int i = 1; i < strs.length; i++) {
                Matcher m = CELL_VALUE_REGEX1.matcher(strs[i]);
                if (m.matches()) {
                    String propName = m.group(1); // property name
                    String propValue = m.group(2); // property value
                    map.put(propName, propValue);
                    continue;
                }

                m = CELL_VALUE_REGEX2.matcher(strs[i]);
                if (m.matches()) {
                    String propName = m.group(1); // property name
                    String propValue = m.group(2); // property value
                    map.put(propName, propValue);
                    continue;
                }

                // it is not a key=value pair
                // we add the String to the formatted value
                formattedValue += strs[i];
            }
        }
        return formattedValue;
    }

    private enum DummyEnum {
    }

    /**
     * Implementation of {@link NamedList} that is immutable and empty.
     */
    private static class EmptyNamedList<T> extends AbstractNamedList<T> {
        public String getName(Object element) {
            throw new UnsupportedOperationException();
        }

        public int size() {
            return 0;
        }

        public T get(int index) {
            throw new IndexOutOfBoundsException("Index: " + index);
        }

        // Preserves singleton property
        @SuppressWarnings({"UnusedDeclaration"})
        private Object readResolve() {
            return EMPTY_NAMED_LIST;
        }
    }

    private static class UnmodifiableNamedList<T> implements NamedList<T> {
        private final NamedList<T> list;

        UnmodifiableNamedList(NamedList<T> list) {
            this.list = list;
        }

        public T get(String s) {
            return list.get(s);
        }

        public int indexOfName(String s) {
            return list.indexOfName(s);
        }

        public String getName(Object element) {
            return list.getName(element);
        }

        public Map<String, T> asMap() {
            return Collections.unmodifiableMap(list.asMap());
        }

        public int size() {
            return list.size();
        }

        public boolean isEmpty() {
            return list.isEmpty();
        }

        public boolean contains(Object o) {
            return list.contains(o);
        }

        public Iterator<T> iterator() {
            return Collections.unmodifiableList(list).iterator();
        }

        public Object[] toArray() {
            return list.toArray();
        }

        public <T2> T2[] toArray(T2[] a) {
            //noinspection SuspiciousToArrayCall
            return list.toArray(a);
        }

        public boolean add(T t) {
            throw new UnsupportedOperationException();
        }

        public boolean remove(Object o) {
            throw new UnsupportedOperationException();
        }

        public boolean containsAll(Collection<?> c) {
            throw new UnsupportedOperationException();
        }

        public boolean addAll(Collection<? extends T> c) {
            throw new UnsupportedOperationException();
        }

        public boolean addAll(int index, Collection<? extends T> c) {
            throw new UnsupportedOperationException();
        }

        public boolean removeAll(Collection<?> c) {
            throw new UnsupportedOperationException();
        }

        public boolean retainAll(Collection<?> c) {
            throw new UnsupportedOperationException();
        }

        public void clear() {
            throw new UnsupportedOperationException();
        }

        public T get(int index) {
            return list.get(index);
        }

        public T set(int index, T element) {
            throw new UnsupportedOperationException();
        }

        public void add(int index, T element) {
            throw new UnsupportedOperationException();
        }

        public T remove(int index) {
            throw new UnsupportedOperationException();
        }

        public int indexOf(Object o) {
            return list.indexOf(0);
        }

        public int lastIndexOf(Object o) {
            return list.lastIndexOf(o);
        }

        public ListIterator<T> listIterator() {
            return Collections.unmodifiableList(list).listIterator();
        }

        public ListIterator<T> listIterator(int index) {
            return Collections.unmodifiableList(list).listIterator(index);
        }

        public List<T> subList(int fromIndex, int toIndex) {
            // TODO: NamedList.subList should return NamedList.
            return Collections.unmodifiableList(
                list.subList(fromIndex, toIndex));
        }
    }

    private static class UnmodifiableNamedRandomAccessList<T>
        extends UnmodifiableNamedList<T>
        implements RandomAccess
    {
        UnmodifiableNamedRandomAccessList(NamedList<T> list) {
            super(list);
        }
    }
}

// End Olap4jUtil.java
