package org.unicode.cldr.draft;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.unicode.cldr.util.SetComparator;

/**
 * A class which represents a particular modifier combination (or combinations
 * of combinations).
 * <p>
 * For example {@code alt+cmd?} gets transformed into a native format consisting of sets of ON modifiers. In this case
 * it would get transformed into {@code altL+cmd, altR+cmd, altL+altR+cmd, altL, altR, altL+altR} .
 * <p>
 * This definition can be expanded across multiple combinations. For example {@code optR+caps? cmd+shift} gets
 * transformed into {@code optR+caps, optR,
 * cmd+shiftL, cmd+shiftR, cmd+shiftL+shiftR} .
 *
 * <h1>Usage</h1>
 * <p>
 * There is a 1 to 1 relationship between a {@link KeyboardModifierSet} and a particular key map (a mapping from
 * physical keys to their output).
 *
 * <pre>
 * {@code
 * // Create the set from the XML modifier=".." attribute
 * ModifierSet modifierSet = ModifierSet.parseSet(<modifier=".." value from XML>);
 * // Test if this set is active for a particular input combination provided by the keyboard
 * modifierSet.contains(<some combination to test>);
 * }
 * </pre>
 *
 * @author rwainman@google.com (Raymond Wainman)
 */
public class KeyboardModifierSet {
    /**
     * Enum of all possible modifier keys.
     */
    public enum Modifier {
        cmd, ctrlL, ctrlR, caps, altL, altR, optL, optR, shiftL, shiftR;
    }

    static final SetComparator<Modifier> SINGLETON_COMPARATOR = new SetComparator<>();

    /** Initial input string */
    private final String input;
    /** Internal representation of all the possible combination variants */
    private final Set<Set<Modifier>> variants;

    /**
     * Private constructor. See factory {@link #parseSet} method.
     *
     * @param variants
     *            A set containing all possible variants of the combination
     *            provided in the input string.
     */
    private KeyboardModifierSet(String input, Set<EnumSet<Modifier>> variants) {
        this.input = input;
        Set<Set<Modifier>> safe = new TreeSet<>(SINGLETON_COMPARATOR);
        for (EnumSet<Modifier> item : variants) {
            safe.add(Collections.unmodifiableSet(item));
        }
        this.variants = safe;
    }

    /**
     * Return all possible variants for this combination.
     *
     * @return Set containing all possible variants.
     */
    public Set<Set<Modifier>> getVariants() {
        return variants;
    }

    /**
     * Determines if the given combination is valid within this set.
     *
     * @param combination
     *            A combination of Modifier elements.
     * @return True if the combination is valid, false otherwise.
     */
    public boolean contains(EnumSet<Modifier> combination) {
        return variants.contains(combination);
    }

    public String getInput() {
        return input;
    }

    @Override
    public String toString() {
        return input + " => " + variants;
    }

    @Override
    public boolean equals(Object arg0) {
        return arg0 == null ? false : variants.equals(((KeyboardModifierSet) arg0).variants);
    }

    @Override
    public int hashCode() {
        return variants.hashCode();
    }

    /**
     * Parse a set containing one or more modifier sets. Each modifier set is
     * separated by a single space and modifiers within a modifier set are
     * separated by a '+'. For example {@code "ctrl+opt?+caps?+shift? alt+caps+cmd?"} has two modifier sets,
     * namely:
     * <ul>
     * <li>{@code "ctrl+opt?+caps?+shift?"}
     * <li>{@code "alt+caps+cmd?"}
     * </ul>
     * <p>
     * The '?' symbol appended to some modifiers indicates that this modifier is optional (it can be ON or OFF).
     *
     * @param input
     *            String representing the sets of modifier sets. This string
     *            must match the format defined in the LDML Keyboard Standard.
     * @return A {@link KeyboardModifierSet} containing all possible variants of
     *         the specified combinations.
     * @throws IllegalArgumentException
     *             if the input string is incorrectly formatted.
     */
    public static KeyboardModifierSet parseSet(String input) {
        if (input == null) {
            throw new IllegalArgumentException("Input string cannot be null");
        }

        String modifierSetInputs[] = input.trim().split(" ");
        Set<EnumSet<Modifier>> variants = new HashSet<>();
        for (String modifierSetInput : modifierSetInputs) {
            variants.addAll(parseSingleSet(modifierSetInput));
        }
        return new KeyboardModifierSet(input, variants);
    }

    /**
     * Parse a modifier set. The set typically looks something like {@code ctrl+opt?+caps?+shift?} or
     * {@code alt+caps+cmd?} and return a set
     * containing all possible variants for that particular modifier set.
     * <p>
     * For example {@code alt+caps+cmd?} gets expanded into {@code alt+caps+cmd?, alt+caps} .
     *
     * @param input
     *            The input string representing the modifiers. This String must
     *            match the format defined in the LDML Keyboard Standard.
     * @return {@link KeyboardModifierSet}.
     * @throws IllegalArgumentException
     *             if the input string is incorrectly formatted.
     */
    private static Set<EnumSet<Modifier>> parseSingleSet(String input) {
        if (input == null) {
            throw new IllegalArgumentException("Input string cannot be null");
        }
        if (input.contains(" ")) {
            throw new IllegalArgumentException("Input string contains more than one combination");
        }

        String modifiers[] = input.trim().split("\\+");

        List<EnumSet<Modifier>> variants = new ArrayList<>();
        variants.add(EnumSet.noneOf(Modifier.class)); // Add an initial set
                                                      // which is empty

        // Trivial case
        if (input.isEmpty()) {
            return new HashSet<>(variants);
        }

        for (String modifier : modifiers) {
            String modifierElementString = modifier.replace("?", "");

            // Attempt to parse the modifier as a parent
            if (ModifierParent.isParentModifier(modifierElementString)) {
                ModifierParent parentModifier = ModifierParent.valueOf(modifierElementString);

                // Keep a collection of the new variants that need to be added
                // while iterating over the
                // existing ones
                Set<EnumSet<Modifier>> newVariants = new HashSet<>();
                for (EnumSet<Modifier> variant : variants) {
                    // A parent key gets exploded into {Left, Right, Left+Right}
                    // or {Left, Right, Left+Right,
                    // (empty)} if it is a don't care

                    // {Left}
                    EnumSet<Modifier> leftVariant = EnumSet.copyOf(variant);
                    leftVariant.add(parentModifier.leftChild);
                    newVariants.add(leftVariant);

                    // {Right}
                    EnumSet<Modifier> rightVariant = EnumSet.copyOf(variant);
                    rightVariant.add(parentModifier.rightChild);
                    newVariants.add(rightVariant);

                    // {Left+Right}
                    // If it is a don't care, we need to leave the empty case
                    // {(empty)}
                    if (modifier.contains("?")) {
                        EnumSet<Modifier> bothChildrenVariant = EnumSet.copyOf(variant);
                        bothChildrenVariant.add(parentModifier.rightChild);
                        bothChildrenVariant.add(parentModifier.leftChild);
                        newVariants.add(bothChildrenVariant);
                    }
                    // No empty case, it is safe to add to the existing variants
                    else {
                        variant.add(parentModifier.rightChild);
                        variant.add(parentModifier.leftChild);
                    }
                }
                variants.addAll(newVariants);
            }
            // Otherwise, parse as a regular modifier
            else {
                Modifier modifierElement = Modifier.valueOf(modifierElementString);
                // On case, add the modifier to all existing variants
                if (!modifier.contains("?")) {
                    for (EnumSet<Modifier> variant : variants) {
                        variant.add(modifierElement);
                    }
                }
                // Don't care case, make a copy of the existing variants and add
                // the new key to it.
                else {
                    List<EnumSet<Modifier>> newVariants = new ArrayList<>();
                    for (EnumSet<Modifier> variant : variants) {
                        EnumSet<Modifier> newVariant = EnumSet.copyOf(variant);
                        newVariant.add(modifierElement);
                        newVariants.add(newVariant);
                    }
                    variants.addAll(newVariants);
                }
            }
        }

        return new HashSet<>(variants);
    }

    /**
     * Enum of all parent modifier keys. Defines the relationships with their
     * children.
     */
    private enum ModifierParent {
        ctrl(Modifier.ctrlL, Modifier.ctrlR), alt(Modifier.altL, Modifier.altR), opt(
            Modifier.optL, Modifier.optR), shift(Modifier.shiftL, Modifier.shiftR);

        private final Modifier leftChild;
        private final Modifier rightChild;

        private ModifierParent(Modifier leftChild, Modifier rightChild) {
            this.leftChild = leftChild;
            this.rightChild = rightChild;
        }

        /**
         * Determines if the String passed in is a valid parent key.
         *
         * @param modifier
         *            The modifier string to verify.
         * @return True if it is a parent key, false otherwise.
         */
        private static boolean isParentModifier(String modifier) {
            try {
                ModifierParent.valueOf(modifier);
                return true;
            } catch (IllegalArgumentException e) {
                return false;
            }
        }
    }

    public boolean containsSome(KeyboardModifierSet keyMapModifiers) {
        for (Set<Modifier> item : keyMapModifiers.variants) {
            if (variants.contains(item)) {
                return true;
            }
        }
        return false;
    }

    public String getShortInput() {
        int pos = input.indexOf(' ');
        if (pos < 0) return input;
        return input.substring(0, pos) + "…";
    }
}
