package com.ctc.wstx.dtd;

import java.util.*;

import com.ctc.wstx.util.ExceptionUtil;

/**
 * Content specification that defines content model that has
 * multiple alternative elements; including mixed content model.
 */
public class ChoiceContentSpec
    extends ContentSpec
{
    final boolean mNsAware;

    /**
     * Whether this is a mixed content model; mostly affects String
     * representation
     */
    final boolean mHasMixed;

    final ContentSpec[] mContentSpecs;

    /*
    ///////////////////////////////////////////////////
    // Life-cycle
    ///////////////////////////////////////////////////
     */

    private ChoiceContentSpec(boolean nsAware, char arity, boolean mixed,
                              ContentSpec[] specs)
    {
        super(arity);
        mNsAware = nsAware;
        mHasMixed = mixed;
        mContentSpecs = specs;
    }

    private ChoiceContentSpec(boolean nsAware, char arity, boolean mixed, Collection specs)
    {
        super(arity);
        mNsAware = nsAware;
        mHasMixed = mixed;
        mContentSpecs = new ContentSpec[specs.size()];
        specs.toArray(mContentSpecs);
    }

    public static ChoiceContentSpec constructChoice(boolean nsAware, char arity,
                                                    Collection specs)
    {
        return new ChoiceContentSpec(nsAware, arity, false, specs);
    }

    public static ChoiceContentSpec constructMixed(boolean nsAware, Collection specs)
    {
        return new ChoiceContentSpec(nsAware, '*', true, specs);
    }

    /*
    ///////////////////////////////////////////////////
    // Public API
    ///////////////////////////////////////////////////
     */

    public StructValidator getSimpleValidator()
    {
        /* Can we create a simple validator? Yes, if the sub-specs are
         * all simple (leaves == element tokens with no arity modifier);
         * this is always true for mixed.
         */
        ContentSpec[] specs = mContentSpecs;
        int len = specs.length;
        int i;

        if (mHasMixed) {
            i = len;
        } else {
            i = 0;
            for (; i < len; ++i) {
                if (!specs[i].isLeaf()) {
                    break;
                }
            }
        }

        if (i == len) { // all leaves, kewl
            NameKeySet keyset = namesetFromSpecs(mNsAware, specs);
            return new Validator(mArity, keyset);
        }

        // Nah, need a DFA...
        return null;
    }

    public ModelNode rewrite()
    {
        // First, need to convert sub-specs:
        ContentSpec[] specs = mContentSpecs;
        int len = specs.length;
        ModelNode[] models = new ModelNode[len];
        for (int i = 0; i < len; ++i) {
            models[i] = specs[i].rewrite();
        }
        ChoiceModel model = new ChoiceModel(models);

        // and then resolve arity modifiers, if necessary:
        if (mArity == '*') {
            return new StarModel(model);
        }
        if (mArity == '?') {
            return new OptionalModel(model);
        }
        if (mArity == '+') {
            return new ConcatModel(model,
                                   new StarModel(model.cloneModel()));
        }
        return model;
    }

    public String toString()
    {
        StringBuffer sb = new StringBuffer();

        if (mHasMixed) {
            sb.append("(#PCDATA | ");
        } else {
            sb.append('(');
        }
        for (int i = 0; i < mContentSpecs.length; ++i) {
            if (i > 0) {
                sb.append(" | ");
            }
            sb.append(mContentSpecs[i].toString());
        }
        sb.append(')');

        if (mArity != ' ') {
            sb.append(mArity);
        }
        return sb.toString();
    }

    /*
    ///////////////////////////////////////////////////
    // Internal methods
    ///////////////////////////////////////////////////
     */

    /*
    ///////////////////////////////////////////////////
    // Package methods
    ///////////////////////////////////////////////////
     */

    protected static NameKeySet namesetFromSpecs(boolean nsAware, ContentSpec[] specs)
    {
        int len = specs.length;
        NameKey[] nameArray = new NameKey[len];
        for (int i = 0; i < len; ++i) {
            nameArray[i] = ((TokenContentSpec)specs[i]).getName();
        }

        if (len < 5) { // 4 or fewer elements -> small
            return new SmallNameKeySet(nsAware, nameArray);
        }
        return new LargeNameKeySet(nsAware, nameArray);
    }

    /*
    ///////////////////////////////////////////////////
    // Validator class that can be used for simple
    // choices (including mixed content)
    ///////////////////////////////////////////////////
     */

    final static class Validator
        extends StructValidator
    {
        final char mArity;
        final NameKeySet mNames;

        int mCount = 0;

        public Validator(char arity, NameKeySet names)
        {
            mArity = arity;
            mNames = names;
        }

        /**
         * Rules for reuse are simple: if we can have any number of
         * repetitions, we can just use a shared root instance. Although
         * its count variable will get updated this doesn't really
         * matter as it won't be used. Otherwise a new instance has to
         * be created always, to keep track of instance counts.
         */
        public StructValidator newInstance() {
            return (mArity == '*') ? this : new Validator(mArity, mNames);
        }

        public String tryToValidate(NameKey elemName)
        {
            if (!mNames.contains(elemName)) {
                if (mNames.hasMultiple()) {
                    return "Expected one of ("+mNames.toString(" | ")+")";
                }
                return "Expected <"+mNames.toString("")+">";
            }
            if (++mCount > 1 && (mArity == '?' || mArity == ' ')) {
                if (mNames.hasMultiple()) {
                    return "Expected $END (already had one of ["
                        +mNames.toString(" | ")+"]";
                }
                return "Expected $END (already had one <"
                    +mNames.toString("")+">]";
            }
            return null;
        }
        
        public String fullyValid()
        {
            switch (mArity) {
            case '*':
            case '?':
                return null;
            case '+': // need at least one (and multiples checked earlier)
            case ' ':
                if (mCount > 0) {
                    return null;
                }
                return "Expected "+(mArity == '+' ? "at least" : "")
                    +" one of elements ("+mNames+")";
            }
            // should never happen:
            ExceptionUtil.throwGenericInternal();
            return null;
        }
    }
}
