/*   Copyright 2004 The Apache Software Foundation
 *
 *   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.
 */

package org.apache.xmlbeans.impl.values;

import org.apache.xmlbeans.*;
import org.apache.xmlbeans.impl.common.QNameHelper;
import org.apache.xmlbeans.impl.common.ValidationContext;
import org.apache.xmlbeans.impl.schema.SchemaTypeImpl;

import javax.xml.namespace.QName;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Date;
import java.util.List;


/**
 * This class implements simple union types.
 */
public class XmlUnionImpl extends XmlObjectBase implements XmlAnySimpleType {
    public XmlUnionImpl(SchemaType type, boolean complex) {
        _schemaType = type;
        initComplexType(complex, false);
    }

    public SchemaType schemaType() {
        return _schemaType;
    }

    public SchemaType instanceType() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).instanceType();
    }

    private final SchemaType _schemaType;
    private XmlAnySimpleType _value; // underlying value
    private String _textvalue = ""; // textual value


    // SIMPLE VALUE ACCESSORS BELOW -------------------------------------------
    // gets raw text value

    protected String compute_text(NamespaceManager nsm) {
        return _textvalue;
    }

    protected boolean is_defaultable_ws(String v) {
        try {
            XmlAnySimpleType savedValue = _value;
            set_text(v);

            // restore the saved value
            _value = savedValue;

            return false;
        } catch (XmlValueOutOfRangeException e) {
            return true;
        }
    }

    protected void set_text(String s) {
        // first check against any patterns...
        if (!_schemaType.matchPatternFacet(s) && _validateOnSet()) {
            throw new XmlValueOutOfRangeException(XmlErrorCodes.DATATYPE_VALID$PATTERN_VALID,
                new Object[]{"string", s, QNameHelper.readable(_schemaType)});
        }


        // save state for rollback
        String original = _textvalue;
        _textvalue = s;

        // iterate through the types, trying to create a type
        SchemaType[] members = _schemaType.getUnionConstituentTypes();
        assert (members != null);

        boolean pushed = false;
        // boolean wasstrict = set_strict(true); // tell types to complain ferverently about errors
        if (has_store()) {
            NamespaceContext.push(new NamespaceContext(get_store()));
            pushed = true;
        }
        try {
            for (boolean validate = true; validate || !_validateOnSet(); validate = false) {
                for (SchemaType member : members) {
                    // From the point of view of the following call, "this" is a generic
                    // XmlAnySimpleType implementation, for which only getText can be called.
                    // (Note that "this" is not wrapped in the proxy object.)
                    try {
                        XmlAnySimpleType newval = ((SchemaTypeImpl) member).newValue(s, validate);

                        // now we need to check against (enuemration) restrictions
                        if (!check(newval, _schemaType)) {
                            continue;
                        }

                        // found one that works!
                        _value = newval;
                        return;
                    } catch (XmlValueOutOfRangeException ignored) {
                    } catch (Exception e) {
                        throw new RuntimeException("Troublesome union exception caused by unexpected " + e, e);
                        // assert(false) : "Unexpected " + e;
                        // continue;
                    }
                }
                if (!validate) {
                    break;
                }
            }
        } finally {
            if (pushed) {
                NamespaceContext.pop();
            }
            // set_strict(wasstrict);
        }


        // doesn't match any of the members; rollback and throw
        _textvalue = original;
        throw new XmlValueOutOfRangeException(XmlErrorCodes.DATATYPE_VALID$UNION,
            new Object[]{s, QNameHelper.readable(_schemaType)});
    }

    protected void set_nil() {
        _value = null;
        _textvalue = null;
    }

    protected int get_wscanon_rule() {
        return SchemaType.WS_PRESERVE;
    }


    // numerics
    public float getFloatValue() {
        check_dated();
        return _value == null ? 0.0f : ((SimpleValue) _value).getFloatValue();
    }

    public double getDoubleValue() {
        check_dated();
        return _value == null ? 0.0 : ((SimpleValue) _value).getDoubleValue();
    }

    public BigDecimal getBigDecimalValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).getBigDecimalValue();
    }

    public BigInteger getBigIntegerValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).getBigIntegerValue();
    }

    public byte getByteValue() {
        check_dated();
        return _value == null ? 0 : ((SimpleValue) _value).getByteValue();
    }

    public short getShortValue() {
        check_dated();
        return _value == null ? 0 : ((SimpleValue) _value).getShortValue();
    }

    public int getIntValue() {
        check_dated();
        return _value == null ? 0 : ((SimpleValue) _value).getIntValue();
    }

    public long getLongValue() {
        check_dated();
        return _value == null ? 0 : ((SimpleValue) _value).getLongValue();
    }


    // various
    public byte[] getByteArrayValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).getByteArrayValue();
    }

    public boolean getBooleanValue() {
        check_dated();
        return _value != null && ((SimpleValue) _value).getBooleanValue();
    }

    public Calendar getCalendarValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).getCalendarValue();
    }

    public Date getDateValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).getDateValue();
    }

    public GDate getGDateValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).getGDateValue();
    }

    public GDuration getGDurationValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).getGDurationValue();
    }

    public QName getQNameValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).getQNameValue();
    }

    public List<?> getListValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).getListValue();
    }

    public List<? extends XmlAnySimpleType> xgetListValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).xgetListValue();
    }

    public StringEnumAbstractBase getEnumValue() {
        check_dated();
        return _value == null ? null : ((SimpleValue) _value).getEnumValue();
    }

    public String getStringValue() {
        check_dated();
        return _value == null ? null : _value.getStringValue();
    }

    /**
     * True if the given schema type's logical type is a match for
     * the given category of java concepts.
     */

    private static final int JAVA_NUMBER = SchemaType.BTC_LAST_BUILTIN + 1;
    private static final int JAVA_DATE = SchemaType.BTC_LAST_BUILTIN + 2;
    private static final int JAVA_CALENDAR = SchemaType.BTC_LAST_BUILTIN + 3;
    private static final int JAVA_BYTEARRAY = SchemaType.BTC_LAST_BUILTIN + 4;
    private static final int JAVA_LIST = SchemaType.BTC_LAST_BUILTIN + 5;

    private static boolean logical_overlap(SchemaType type, int javacode) {
        // non-union types because it's being applied on irreducible union members!
        assert (type.getSimpleVariety() != SchemaType.UNION);

        if (javacode <= SchemaType.BTC_LAST_BUILTIN) {
            if (type.getSimpleVariety() != SchemaType.ATOMIC) {
                return false;
            }

            return (type.getPrimitiveType().getBuiltinTypeCode() == javacode);
        }

        switch (javacode) {
            case JAVA_NUMBER: {
                if (type.getSimpleVariety() != SchemaType.ATOMIC) {
                    return false;
                }

                switch (type.getPrimitiveType().getBuiltinTypeCode()) {
                    case SchemaType.BTC_FLOAT:
                    case SchemaType.BTC_DOUBLE:
                    case SchemaType.BTC_DECIMAL:
                    case SchemaType.BTC_G_YEAR:
                    case SchemaType.BTC_G_MONTH:
                    case SchemaType.BTC_G_DAY:
                        return true;
                }
                return false;
            }
            case JAVA_DATE: {
                if (type.getSimpleVariety() != SchemaType.ATOMIC) {
                    return false;
                }

                switch (type.getPrimitiveType().getBuiltinTypeCode()) {
                    case SchemaType.BTC_DATE_TIME:
                    case SchemaType.BTC_DATE:
                        return true;
                }
                return false;
            }
            case JAVA_CALENDAR: {
                if (type.getSimpleVariety() != SchemaType.ATOMIC) {
                    return false;
                }

                switch (type.getPrimitiveType().getBuiltinTypeCode()) {
                    case SchemaType.BTC_DATE_TIME:
                    case SchemaType.BTC_DATE:
                    case SchemaType.BTC_TIME:
                    case SchemaType.BTC_G_YEAR_MONTH:
                    case SchemaType.BTC_G_MONTH_DAY:
                    case SchemaType.BTC_G_YEAR:
                    case SchemaType.BTC_G_MONTH:
                    case SchemaType.BTC_G_DAY:
                        return true;
                }
                return false;
            }

            case JAVA_BYTEARRAY: {
                if (type.getSimpleVariety() != SchemaType.ATOMIC) {
                    return false;
                }

                switch (type.getPrimitiveType().getBuiltinTypeCode()) {
                    case SchemaType.BTC_BASE_64_BINARY:
                    case SchemaType.BTC_HEX_BINARY:
                        return true;
                }
                return false;
            }
            case JAVA_LIST: {
                return (type.getSimpleVariety() == SchemaType.LIST);
            }
        }

        assert (false) : "missing case";
        return false;
    }

    /**
     * Grabs a chained value of type st, creating and attaching
     * one if not present.
     */
    private void set_primitive(int typecode, Object val) {
        SchemaType[] members = _schemaType.getUnionConstituentTypes();
        assert (members != null);

        boolean pushed = false;
        if (has_store()) {
            NamespaceContext.push(new NamespaceContext(get_store()));
            pushed = true;
        }
        try {
            for (boolean validate = true; validate || !_validateOnSet(); validate = false) {
                for (SchemaType member : members) {
                    // candidates must be a logical match for the desired typecode
                    if (logical_overlap(member, typecode)) {
                        XmlAnySimpleType newval;

                        try {
                            newval = ((SchemaTypeImpl) member).newValue(val, validate);
                        } catch (XmlValueOutOfRangeException ignored) {
                            // doesn't match this type even though logical categories
                            // line up (probably because of restriciton); try the next type.
                            continue;
                        } catch (Exception e) {
                            assert (false) : "Unexpected " + e;
                            continue;
                        }

                    /* TODO: rethink this - disabling for now.

                    // OK, now we've got a newval... We have to verify
                    // that lexically it doesn't overlap with previous types

                    String newvaltext = null;

                    inner: for (int j = 0; j < i; j++)
                    {
                        if (members[j].getSimpleVariety() == SchemaType.LIST ||
                            lexical_overlap(members[j].getPrimitiveType().getBuiltinTypeCode(),
                                            newval.schemaType().getPrimitiveType().getBuiltinTypeCode()))
                        {
                            // there is a preceding type that may lexically overlap with ours...
                            // if it lexically contains the string representation of our new
                            // proposed value, then it's impossible for the union to have our
                            // logical value (because it would have been masked) and throw an
                            // error.
                            if (newvaltext == null)
                                newvaltext = newval.stringValue();
                            try
                            {
                                // discard return value
                                members[i].newValue(newvaltext);

                                // oh bad, we succeeded. Our instance lexically looks like a
                                // previous type, and this isn't a valid value. Keep on hunting.
                                continue outer;
                            }
                            catch (XmlValueOutOfRangeException e)
                            {
                                // this is good: this error means that our value doesn't look like
                                // the other type.
                                continue inner;
                            }
                        }
                    }

                    */

                        // No lexical masking: we're OK
                        _value = newval;
                        _textvalue = _value.getStringValue();
                        return;
                    }
                }
                if (!validate) {
                    break;
                }
            }
        } finally {
            if (pushed) {
                NamespaceContext.pop();
            }
        }

        // doesn't match any of the members; throw
        throw new XmlValueOutOfRangeException(XmlErrorCodes.DATATYPE_VALID$UNION,
            new Object[]{val.toString(), QNameHelper.readable(_schemaType)});
    }

    // here are the setters

    protected void set_boolean(boolean v) {
        set_primitive(SchemaType.BTC_BOOLEAN, v);
    }

    protected void set_byte(byte v) {
        set_primitive(JAVA_NUMBER, v);
    }

    protected void set_short(short v) {
        set_primitive(JAVA_NUMBER, v);
    }

    protected void set_int(int v) {
        set_primitive(JAVA_NUMBER, v);
    }

    protected void set_long(long v) {
        set_primitive(JAVA_NUMBER, v);
    }

    protected void set_float(float v) {
        set_primitive(JAVA_NUMBER, v);
    }

    protected void set_double(double v) {
        set_primitive(JAVA_NUMBER, v);
    }

    protected void set_ByteArray(byte[] b) {
        set_primitive(JAVA_BYTEARRAY, b);
    }

    protected void set_hex(byte[] b) {
        set_primitive(JAVA_BYTEARRAY, b);
    }

    protected void set_b64(byte[] b) {
        set_primitive(JAVA_BYTEARRAY, b);
    }

    protected void set_BigInteger(BigInteger v) {
        set_primitive(JAVA_NUMBER, v);
    }

    protected void set_BigDecimal(BigDecimal v) {
        set_primitive(JAVA_NUMBER, v);
    }

    protected void set_QName(QName v) {
        set_primitive(SchemaType.BTC_QNAME, v);
    }

    protected void set_Calendar(Calendar c) {
        set_primitive(JAVA_CALENDAR, c);
    }

    protected void set_Date(Date d) {
        set_primitive(JAVA_DATE, d);
    }

    protected void set_GDate(GDateSpecification d) {
        int btc = d.getBuiltinTypeCode();
        if (btc <= 0) {
            throw new XmlValueOutOfRangeException();
        }
        set_primitive(btc, d);
    }

    protected void set_GDuration(GDurationSpecification d) {
        set_primitive(SchemaType.BTC_DURATION, d);
    }

    protected void set_enum(StringEnumAbstractBase e) {
        set_primitive(SchemaType.BTC_STRING, e);
    }

    protected void set_list(List<?> v) {
        set_primitive(JAVA_LIST, v);
    }


    protected void set_xmlfloat(XmlObject v) {
        set_primitive(SchemaType.BTC_FLOAT, v);
    }

    protected void set_xmldouble(XmlObject v) {
        set_primitive(SchemaType.BTC_DOUBLE, v);
    }

    protected void set_xmldecimal(XmlObject v) {
        set_primitive(SchemaType.BTC_DECIMAL, v);
    }

    protected void set_xmlduration(XmlObject v) {
        set_primitive(SchemaType.BTC_DURATION, v);
    }

    protected void set_xmldatetime(XmlObject v) {
        set_primitive(SchemaType.BTC_DATE_TIME, v);
    }

    protected void set_xmltime(XmlObject v) {
        set_primitive(SchemaType.BTC_TIME, v);
    }

    protected void set_xmldate(XmlObject v) {
        set_primitive(SchemaType.BTC_DATE, v);
    }

    protected void set_xmlgyearmonth(XmlObject v) {
        set_primitive(SchemaType.BTC_G_YEAR_MONTH, v);
    }

    protected void set_xmlgyear(XmlObject v) {
        set_primitive(SchemaType.BTC_G_YEAR, v);
    }

    protected void set_xmlgmonthday(XmlObject v) {
        set_primitive(SchemaType.BTC_G_MONTH_DAY, v);
    }

    protected void set_xmlgday(XmlObject v) {
        set_primitive(SchemaType.BTC_G_DAY, v);
    }

    protected void set_xmlgmonth(XmlObject v) {
        set_primitive(SchemaType.BTC_G_MONTH, v);
    }


    private static boolean check(XmlObject v, SchemaType sType) {
        XmlObject[] vals = sType.getEnumerationValues();
        if (vals != null) {
            for (XmlObject val : vals) {
                if (val.valueEquals(v)) {
                    return true;
                }
            }
            return false;
        }

        return true;
    }

    protected boolean equal_to(XmlObject xmlobj) {
        return _value.valueEquals(xmlobj);
    }

    protected int value_hash_code() {
        return _value.hashCode();
    }

    protected void validate_simpleval(String lexical, ValidationContext ctx) {
        try {
            check_dated();
        } catch (Exception e) {
            ctx.invalid(XmlErrorCodes.UNION, new Object[]{"'" + lexical + "' does not match any of the member types for " + QNameHelper.readable(schemaType())});
            return;
        }
        if (_value == null) {
            ctx.invalid(XmlErrorCodes.UNION, new Object[]{"'" + lexical + "' does not match any of the member types for " + QNameHelper.readable(schemaType())});
            return;
        }

        ((XmlObjectBase) _value).validate_simpleval(lexical, ctx);
    }


}

