/*
 * #%L
 * RegularExpressionElement.java - mongodb-async-driver - Allanbank Consulting, Inc.
 * %%
 * Copyright (C) 2011 - 2014 Allanbank Consulting, Inc.
 * %%
 * 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.
 * #L%
 */
package com.allanbank.mongodb.bson.element;

import static com.allanbank.mongodb.util.Assertions.assertNotNull;

import java.util.regex.Pattern;

import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;

import com.allanbank.mongodb.bson.Element;
import com.allanbank.mongodb.bson.ElementType;
import com.allanbank.mongodb.bson.Visitor;
import com.allanbank.mongodb.bson.io.StringEncoder;

/**
 * A wrapper for a BSON regular expression.
 *
 * @api.yes This class is part of the driver's API. Public and protected members
 *          will be deprecated for at least 1 non-bugfix release (version
 *          numbers are <major>.<minor>.<bugfix>) before being
 *          removed or modified.
 * @copyright 2011-2013, Allanbank Consulting, Inc., All Rights Reserved
 */
@Immutable
@ThreadSafe
public class RegularExpressionElement
        extends AbstractElement {

    /**
     * The {@link RegularExpressionElement}'s class to avoid the
     * {@link Class#forName(String) Class.forName(...)} overhead.
     */
    public static final Class<RegularExpressionElement> REGEX_CLASS = RegularExpressionElement.class;

    /** Option for case insensitive matching. */
    public static final int CASE_INSENSITIVE;

    /** Option for dotall mode ('.' matches everything). */
    public static final int DOT_ALL;

    /** Option to make \w, \W, etc. locale dependent. */
    public static final int LOCALE_DEPENDENT;

    /** Option for multiline matching. */
    public static final int MULTILINE;

    /** Option for case insensitive matching. */
    public static final int OPTION_I;

    /** Option to make \w, \W, etc. locale dependent. */
    public static final int OPTION_L;

    /** Option for multiline matching. */
    public static final int OPTION_M;

    /** Option for verbose mode. */
    public static final int OPTION_MASK;

    /** Option for dotall mode ('.' matches everything). */
    public static final int OPTION_S;

    /** Option to make \w, \W, etc. match unicode. */
    public static final int OPTION_U;

    /** Option for verbose mode. */
    public static final int OPTION_X;

    /** The BSON type for a string. */
    public static final ElementType TYPE = ElementType.REGEX;

    /** Option to make \w, \W, etc. match unicode. */
    public static final int UNICODE;

    /** Option for verbose mode. */
    public static final int VERBOSE;

    /**
     * Option to make \w, \W, etc. match unicode from the pattern class. Added
     * in Java7
     */
    protected static final int PATTERN_UNICODE;

    /** The options for each possible bit field. */
    private static final String[] OPTIONS;

    /** Serialization version for the class. */
    private static final long serialVersionUID = 7842839168833403380L;

    static {

        OPTION_I = 0x01;
        OPTION_L = 0x02;
        OPTION_M = 0x04;
        OPTION_S = 0x08;
        OPTION_U = 0x10;
        OPTION_X = 0x20;
        OPTION_MASK = 0x3F;

        CASE_INSENSITIVE = OPTION_I;
        LOCALE_DEPENDENT = OPTION_L;
        MULTILINE = OPTION_M;
        DOT_ALL = OPTION_S;
        UNICODE = OPTION_U;
        VERBOSE = OPTION_X;

        final String[] options = new String[OPTION_MASK + 1];

        final StringBuilder builder = new StringBuilder();
        for (int i = 0; i < (OPTION_MASK + 1); ++i) {
            builder.setLength(0);

            // Options must be in alphabetic order.
            if ((i & OPTION_I) == OPTION_I) {
                builder.append('i');
            }
            if ((i & OPTION_L) == OPTION_L) {
                builder.append('l');
            }
            if ((i & OPTION_M) == OPTION_M) {
                builder.append('m');
            }
            if ((i & OPTION_S) == OPTION_S) {
                builder.append('s');
            }
            if ((i & OPTION_U) == OPTION_U) {
                builder.append('u');
            }
            if ((i & OPTION_X) == OPTION_X) {
                builder.append('x');
            }
            options[i] = builder.toString();
        }

        OPTIONS = options;

        // New in Java7
        PATTERN_UNICODE = 0x100;
    }

    /**
     * Converts the {@link Pattern#flags() pattern flags} into a options value.
     * <p>
     * Note that the {@link #VERBOSE} and {@link #LOCALE_DEPENDENT} do not have
     * {@link Pattern} equivalent flags.
     * </p>
     * <p>
     * <blockquote>
     *
     * <pre>
     * {@link Pattern#CASE_INSENSITIVE} ==> {@link #CASE_INSENSITIVE}
     * {@link Pattern#MULTILINE} ==> {@link #MULTILINE}
     * {@link Pattern#DOTALL} ==> {@link #DOT_ALL}
     * {@link Pattern#UNICODE_CHARACTER_CLASS} ==> {@link #UNICODE}
     * </pre>
     *
     * </blockquote>
     *
     * @param pattern
     *            The pattern to extract the options from.
     * @return The options integer value.
     */
    protected static int optionsAsInt(final Pattern pattern) {
        int optInt = 0;

        if (pattern != null) {
            final int flags = pattern.flags();
            if ((flags & Pattern.CASE_INSENSITIVE) == Pattern.CASE_INSENSITIVE) {
                optInt |= CASE_INSENSITIVE;
            }
            if ((flags & Pattern.MULTILINE) == Pattern.MULTILINE) {
                optInt |= MULTILINE;
            }
            if ((flags & Pattern.DOTALL) == Pattern.DOTALL) {
                optInt |= DOT_ALL;
            }
            if ((flags & PATTERN_UNICODE) == PATTERN_UNICODE) {
                optInt |= UNICODE;
            }
        }

        return optInt;
    }

    /**
     * Converts the options string into a options value.
     *
     * @param options
     *            The possibly non-normalized options string.
     * @return The options integer value.
     */
    protected static int optionsAsInt(final String options) {
        int optInt = 0;

        if (options != null) {
            for (final char c : options.toCharArray()) {
                if ((c == 'i') || (c == 'I')) {
                    optInt |= OPTION_I;
                }
                else if ((c == 'l') || (c == 'L')) {
                    optInt |= OPTION_L;
                }
                else if ((c == 'm') || (c == 'M')) {
                    optInt |= OPTION_M;
                }
                else if ((c == 's') || (c == 'S')) {
                    optInt |= OPTION_S;
                }
                else if ((c == 'u') || (c == 'U')) {
                    optInt |= OPTION_U;
                }
                else if ((c == 'x') || (c == 'X')) {
                    optInt |= OPTION_X;
                }
                else {
                    throw new IllegalArgumentException(
                            "Invalid regular expression option '" + c
                                    + "' in options '" + options + "'.");
                }
            }
        }

        return optInt;
    }

    /**
     * Computes and returns the number of bytes that are used to encode the
     * element.
     *
     * @param name
     *            The name for the element.
     * @param pattern
     *            The BSON regular expression pattern.
     * @param options
     *            The BSON regular expression options.
     * @return The size of the element when encoded in bytes.
     */
    private static long computeSize(final String name, final String pattern,
            final int options) {
        long result = 4; // type (1) + name null byte (1) +
        // pattern null byte (1) + options null byte (1).
        result += StringEncoder.utf8Size(name);
        result += StringEncoder.utf8Size(pattern);
        result += OPTIONS[options & OPTION_MASK].length(); // ASCII

        return result;
    }

    /** The BSON regular expression options. */
    private final int myOptions;

    /** The BSON regular expression pattern. */
    private final String myPattern;

    /**
     * Constructs a new {@link RegularExpressionElement}.
     *
     * @param name
     *            The name for the BSON string.
     * @param pattern
     *            The regular expression {@link Pattern}.
     * @throws IllegalArgumentException
     *             If the {@code name} or {@code pattern} is <code>null</code>.
     */
    public RegularExpressionElement(final String name, final Pattern pattern) {
        this(name, (pattern != null) ? pattern.pattern() : null,
                optionsAsInt(pattern));
    }

    /**
     * Constructs a new {@link RegularExpressionElement}.
     *
     * @param name
     *            The name for the BSON string.
     * @param pattern
     *            The BSON regular expression pattern.
     * @param options
     *            The BSON regular expression options.
     * @throws IllegalArgumentException
     *             If the {@code name} or {@code pattern} is <code>null</code>.
     */
    public RegularExpressionElement(final String name, final String pattern,
            final int options) {
        super(name, computeSize(name, pattern, options));

        assertNotNull(pattern,
                "Regular Expression element's pattern cannot be null.");

        myPattern = pattern;
        myOptions = options;
    }

    /**
     * Constructs a new {@link RegularExpressionElement}.
     *
     * @param name
     *            The name for the BSON string.
     * @param pattern
     *            The BSON regular expression pattern.
     * @param options
     *            The BSON regular expression options.
     * @param size
     *            The size of the element when encoded in bytes. If not known
     *            then use the
     *            {@link RegularExpressionElement#RegularExpressionElement(String, String, int)}
     *            constructor instead.
     * @throws IllegalArgumentException
     *             If the {@code name} or {@code pattern} is <code>null</code>.
     */
    public RegularExpressionElement(final String name, final String pattern,
            final int options, final long size) {
        super(name, size);

        assertNotNull(pattern,
                "Regular Expression element's pattern cannot be null.");

        myPattern = pattern;
        myOptions = options;
    }

    /**
     * Constructs a new {@link RegularExpressionElement}.
     *
     * @param name
     *            The name for the BSON string.
     * @param pattern
     *            The BSON regular expression pattern.
     * @param options
     *            The BSON regular expression options.
     * @throws IllegalArgumentException
     *             If the {@code name} or {@code pattern} is <code>null</code>.
     */
    public RegularExpressionElement(final String name, final String pattern,
            final String options) {
        this(name, pattern, optionsAsInt(options));
    }

    /**
     * Constructs a new {@link RegularExpressionElement}.
     *
     * @param name
     *            The name for the BSON string.
     * @param pattern
     *            The BSON regular expression pattern.
     * @param options
     *            The BSON regular expression options.
     * @param size
     *            The size of the element when encoded in bytes. If not known
     *            then use the
     *            {@link RegularExpressionElement#RegularExpressionElement(String, String, String)}
     *            constructor instead.
     * @throws IllegalArgumentException
     *             If the {@code name} or {@code pattern} is <code>null</code>.
     */
    public RegularExpressionElement(final String name, final String pattern,
            final String options, final long size) {
        this(name, pattern, optionsAsInt(options), size);
    }

    /**
     * Accepts the visitor and calls the {@link Visitor#visitRegularExpression}
     * method.
     *
     * @see Element#accept(Visitor)
     */
    @Override
    public void accept(final Visitor visitor) {
        visitor.visitRegularExpression(getName(), getPattern(),
                OPTIONS[getOptions() & OPTION_MASK]);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Overridden to compare the expressions (as strings) if the base class
     * comparison is equals.
     * </p>
     */
    @Override
    public int compareTo(final Element otherElement) {
        int result = super.compareTo(otherElement);

        if (result == 0) {
            final RegularExpressionElement other = (RegularExpressionElement) otherElement;

            result = myPattern.compareTo(other.myPattern);
            if (result == 0) {
                result = compare(myOptions, other.myOptions);
            }
        }

        return result;
    }

    /**
     * Determines if the passed object is of this same type as this object and
     * if so that its fields are equal.
     *
     * @param object
     *            The object to compare to.
     *
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(final Object object) {
        boolean result = false;
        if (this == object) {
            result = true;
        }
        else if ((object != null) && (getClass() == object.getClass())) {
            final RegularExpressionElement other = (RegularExpressionElement) object;

            result = (myOptions == other.myOptions) && super.equals(object)
                    && nullSafeEquals(myPattern, other.myPattern);
        }
        return result;
    }

    /**
     * Returns the regular expression options.
     *
     * @return The regular expression options.
     */
    public int getOptions() {
        return myOptions;
    }

    /**
     * Returns the regular expression pattern.
     *
     * @return The regular expression pattern.
     */
    public String getPattern() {
        return myPattern;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ElementType getType() {
        return TYPE;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Returns the {@link Pattern}.
     * </p>
     */
    @Override
    public Pattern getValueAsObject() {

        int options = 0;
        if ((myOptions & CASE_INSENSITIVE) == CASE_INSENSITIVE) {
            options |= Pattern.CASE_INSENSITIVE;
        }
        if ((myOptions & MULTILINE) == MULTILINE) {
            options |= Pattern.MULTILINE;
        }
        if ((myOptions & DOT_ALL) == DOT_ALL) {
            options |= Pattern.DOTALL;
        }
        if ((myOptions & UNICODE) == UNICODE) {
            options |= PATTERN_UNICODE;
        }

        return Pattern.compile(myPattern, options);
    }

    /**
     * Computes a reasonable hash code.
     *
     * @return The hash code value.
     */
    @Override
    public int hashCode() {
        int result = 1;
        result = (31 * result) + super.hashCode();
        result = (31 * result)
                + ((myPattern != null) ? myPattern.hashCode() : 3);
        result = (31 * result) + myOptions;
        return result;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Returns a new {@link RegularExpressionElement}.
     * </p>
     */
    @Override
    public RegularExpressionElement withName(final String name) {
        if (getName().equals(name)) {
            return this;
        }
        return new RegularExpressionElement(name, myPattern, myOptions);
    }
}