/*
 * Copyright (C) 1999-2020 David Schweinsberg.
 *
 * 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 com.steadystate.css.dom;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.w3c.css.sac.InputSource;
import org.w3c.css.sac.LexicalUnit;
import org.w3c.css.sac.Locator;
import org.w3c.dom.DOMException;
import org.w3c.dom.css.CSSPrimitiveValue;
import org.w3c.dom.css.CSSValue;
import org.w3c.dom.css.CSSValueList;
import org.w3c.dom.css.Counter;
import org.w3c.dom.css.RGBColor;
import org.w3c.dom.css.Rect;

import com.steadystate.css.format.CSSFormat;
import com.steadystate.css.format.CSSFormatable;
import com.steadystate.css.parser.CSSOMParser;
import com.steadystate.css.parser.LexicalUnitImpl;
import com.steadystate.css.userdata.UserDataConstants;
import com.steadystate.css.util.LangUtils;

/**
 * The <code>CSSValueImpl</code> class can represent either a
 * <code>CSSPrimitiveValue</code> or a <code>CSSValueList</code> so that
 * the type can successfully change when using <code>setCssText</code>.
 *
 * TODO:
 * Float unit conversions,
 * A means of checking valid primitive types for properties
 *
 * @author <a href="mailto:davidsch@users.sourceforge.net">David Schweinsberg</a>
 * @author rbri
 */
public class CSSValueImpl extends CSSOMObjectImpl implements CSSPrimitiveValue, CSSValueList, CSSFormatable {

    private static final long serialVersionUID = 406281136418322579L;

    private Object value_;

    public Object getValue() {
        return value_;
    }

    public void setValue(final Object value) {
        value_ = value;
    }

    /**
     * Constructor
     */
    public CSSValueImpl(final LexicalUnit value, final boolean forcePrimitive) {
        LexicalUnit parameters = null;
        try {
            parameters = value.getParameters();
        }
        catch (final IllegalStateException e) {
            // Batik SAC parser throws IllegalStateException in some cases
        }

        if (!forcePrimitive && (value.getNextLexicalUnit() != null)) {
            value_ = getValues(value);
        }
        else if (parameters != null) {
            if (value.getLexicalUnitType() == LexicalUnit.SAC_RECT_FUNCTION) {
                // Rect
                value_ = new RectImpl(value.getParameters());
            }
            else if (value.getLexicalUnitType() == LexicalUnit.SAC_RGBCOLOR) {
                // RGBColor
                value_ = new RGBColorImpl(value.getParameters());
            }
            else if (value.getLexicalUnitType() == LexicalUnit.SAC_COUNTER_FUNCTION) {
                // Counter
                value_ = new CounterImpl(false, value.getParameters());
            }
            else if (value.getLexicalUnitType() == LexicalUnit.SAC_COUNTERS_FUNCTION) {
                // Counter
                value_ = new CounterImpl(true, value.getParameters());
            }
            else {
                value_ = value;
            }
        }
        else {
            // We need to be a CSSPrimitiveValue
            value_ = value;
        }

        if (value instanceof LexicalUnitImpl) {
            final Locator locator = ((LexicalUnitImpl) value).getLocator();
            if (locator != null) {
                setUserData(UserDataConstants.KEY_LOCATOR, locator);
            }
        }
    }

    public CSSValueImpl() {
        super();
    }

    private List<CSSValueImpl> getValues(final LexicalUnit value) {
        final List<CSSValueImpl> values = new ArrayList<CSSValueImpl>();
        LexicalUnit lu = value;
        while (lu != null) {
            values.add(new CSSValueImpl(lu, true));
            lu = lu.getNextLexicalUnit();
        }
        return values;
    }

    public CSSValueImpl(final LexicalUnit value) {
        this(value, false);
    }

    @Override
    public String getCssText() {
        return getCssText(null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getCssText(final CSSFormat format) {
        if (getCssValueType() == CSS_VALUE_LIST) {

            // Create the string from the LexicalUnits so we include the correct
            // operators in the string
            final StringBuilder sb = new StringBuilder();
            final List<?> list = (List<?>) value_;
            final Iterator<?> it = list.iterator();

            boolean separate = false;
            while (it.hasNext()) {
                final Object o = it.next();

                final CSSValueImpl cssValue = (CSSValueImpl) o;
                if (separate) {
                    if (cssValue.value_ instanceof LexicalUnit) {
                        final LexicalUnit lu = (LexicalUnit) cssValue.value_;
                        if (lu.getLexicalUnitType() != LexicalUnit.SAC_OPERATOR_COMMA) {
                            sb.append(" ");
                        }
                    }
                    else {
                        sb.append(" ");
                    }
                }

                if (cssValue.value_ instanceof CSSFormatable) {
                    sb.append(((CSSFormatable) o).getCssText(format));
                }
                else {
                    sb.append(o.toString());
                }
                separate = true;
            }
            return sb.toString();
        }
        if (value_ instanceof CSSFormatable) {
            return ((CSSFormatable) value_).getCssText(format);
        }
        return value_ != null ? value_.toString() : "";
    }

    @Override
    public void setCssText(final String cssText) throws DOMException {
        try {
            final InputSource is = new InputSource(new StringReader(cssText));
            final CSSOMParser parser = new CSSOMParser();
            final CSSValueImpl v2 = (CSSValueImpl) parser.parsePropertyValue(is);
            value_ = v2.value_;
            setUserDataMap(v2.getUserDataMap());
        }
        catch (final Exception e) {
            throw new DOMExceptionImpl(
                DOMException.SYNTAX_ERR,
                DOMExceptionImpl.SYNTAX_ERROR,
                e.getMessage());
        }
    }

    @Override
    public short getCssValueType() {
        if (value_ instanceof List) {
            return CSS_VALUE_LIST;
        }
        if ((value_ instanceof LexicalUnit)
                && (((LexicalUnit) value_).getLexicalUnitType() == LexicalUnit.SAC_INHERIT)) {
            return CSS_INHERIT;
        }
        return CSS_PRIMITIVE_VALUE;
    }

    @Override
    public short getPrimitiveType() {
        if (value_ instanceof LexicalUnit) {
            final LexicalUnit lu = (LexicalUnit) value_;
            switch (lu.getLexicalUnitType()) {
                case LexicalUnit.SAC_INHERIT:
                    return CSS_IDENT;
                case LexicalUnit.SAC_INTEGER:
                case LexicalUnit.SAC_REAL:
                    return CSS_NUMBER;
                case LexicalUnit.SAC_EM:
                    return CSS_EMS;
                case LexicalUnit.SAC_EX:
                    return CSS_EXS;
                case LexicalUnit.SAC_PIXEL:
                    return CSS_PX;
                case LexicalUnit.SAC_INCH:
                    return CSS_IN;
                case LexicalUnit.SAC_CENTIMETER:
                    return CSS_CM;
                case LexicalUnit.SAC_MILLIMETER:
                    return CSS_MM;
                case LexicalUnit.SAC_POINT:
                    return CSS_PT;
                case LexicalUnit.SAC_PICA:
                    return CSS_PC;
                case LexicalUnit.SAC_PERCENTAGE:
                    return CSS_PERCENTAGE;
                case LexicalUnit.SAC_URI:
                    return CSS_URI;
                case LexicalUnit.SAC_COUNTER_FUNCTION:
    //            case LexicalUnit.SAC_COUNTERS_FUNCTION:
                    return CSS_COUNTER;
    //            case LexicalUnit.SAC_RGBCOLOR:
    //                return CSS_RGBCOLOR;
                case LexicalUnit.SAC_DEGREE:
                    return CSS_DEG;
                case LexicalUnit.SAC_GRADIAN:
                    return CSS_GRAD;
                case LexicalUnit.SAC_RADIAN:
                    return CSS_RAD;
                case LexicalUnit.SAC_MILLISECOND:
                    return CSS_MS;
                case LexicalUnit.SAC_SECOND:
                    return CSS_S;
                case LexicalUnit.SAC_HERTZ:
                    return CSS_HZ;
                case LexicalUnit.SAC_KILOHERTZ:
                    return CSS_KHZ;
                case LexicalUnit.SAC_IDENT:
                    return CSS_IDENT;
                case LexicalUnit.SAC_STRING_VALUE:
                    return CSS_STRING;
                case LexicalUnit.SAC_ATTR:
                    return CSS_ATTR;
    //            case LexicalUnit.SAC_RECT_FUNCTION:
    //                return CSS_RECT;
                case LexicalUnit.SAC_UNICODERANGE:
                case LexicalUnit.SAC_SUB_EXPRESSION:
                case LexicalUnit.SAC_FUNCTION:
                    return CSS_STRING;
                case LexicalUnit.SAC_DIMENSION:
                    return CSS_DIMENSION;
                default:
                    return CSS_UNKNOWN;
            }
        }
        else if (value_ instanceof RectImpl) {
            return CSS_RECT;
        }
        else if (value_ instanceof RGBColorImpl) {
            return CSS_RGBCOLOR;
        }
        else if (value_ instanceof CounterImpl) {
            return CSS_COUNTER;
        }
        return CSS_UNKNOWN;
    }

    @Override
    public void setFloatValue(final short unitType, final float floatValue) throws DOMException {
        value_ = LexicalUnitImpl.createNumber(null, floatValue);
    }

    @Override
    public float getFloatValue(final short unitType) throws DOMException {
        if (value_ instanceof LexicalUnit) {
            final LexicalUnit lu = (LexicalUnit) value_;
            return lu.getFloatValue();
        }
        throw new DOMExceptionImpl(
            DOMException.INVALID_ACCESS_ERR,
            DOMExceptionImpl.FLOAT_ERROR);

        // We need to attempt a conversion
        // return 0;
    }

    @Override
    public void setStringValue(final short stringType, final String stringValue) throws DOMException {
        switch (stringType) {
            case CSS_STRING:
                value_ = LexicalUnitImpl.createString(null, stringValue);
                break;
            case CSS_URI:
                value_ = LexicalUnitImpl.createURI(null, stringValue);
                break;
            case CSS_IDENT:
                value_ = LexicalUnitImpl.createIdent(null, stringValue);
                break;
            case CSS_ATTR:
    //            _value = LexicalUnitImpl.createAttr(null, stringValue);
    //            break;
                throw new DOMExceptionImpl(
                    DOMException.NOT_SUPPORTED_ERR,
                    DOMExceptionImpl.NOT_IMPLEMENTED);
            default:
                throw new DOMExceptionImpl(
                    DOMException.INVALID_ACCESS_ERR,
                    DOMExceptionImpl.STRING_ERROR);
        }
    }

    /**
     * TODO: return a value for a list type
     */
    @Override
    public String getStringValue() throws DOMException {
        if (value_ instanceof LexicalUnit) {
            final LexicalUnit lu = (LexicalUnit) value_;
            if ((lu.getLexicalUnitType() == LexicalUnit.SAC_IDENT)
                || (lu.getLexicalUnitType() == LexicalUnit.SAC_STRING_VALUE)
                || (lu.getLexicalUnitType() == LexicalUnit.SAC_URI)
                || (lu.getLexicalUnitType() == LexicalUnit.SAC_INHERIT)
                || (lu.getLexicalUnitType() == LexicalUnit.SAC_ATTR)) {
                return lu.getStringValue();
            }

            // for rgba values we are using this type
            if (lu.getLexicalUnitType() == LexicalUnit.SAC_FUNCTION) {
                return lu.toString();
            }
        }
        else if (value_ instanceof List) {
            return null;
        }

        throw new DOMExceptionImpl(
            DOMException.INVALID_ACCESS_ERR,
            DOMExceptionImpl.STRING_ERROR);
    }

    @Override
    public Counter getCounterValue() throws DOMException {
        if (value_ instanceof Counter) {
            return (Counter) value_;
        }

        throw new DOMExceptionImpl(
                DOMException.INVALID_ACCESS_ERR,
                DOMExceptionImpl.COUNTER_ERROR);
    }

    @Override
    public Rect getRectValue() throws DOMException {
        if (value_ instanceof Rect) {
            return (Rect) value_;
        }

        throw new DOMExceptionImpl(
                DOMException.INVALID_ACCESS_ERR,
                DOMExceptionImpl.RECT_ERROR);
    }

    @Override
    public RGBColor getRGBColorValue() throws DOMException {
        if (value_ instanceof RGBColor) {
            return (RGBColor) value_;
        }

        throw new DOMExceptionImpl(
            DOMException.INVALID_ACCESS_ERR,
            DOMExceptionImpl.RGBCOLOR_ERROR);
    }

    @Override
    @SuppressWarnings("unchecked")
    public int getLength() {
        if (value_ instanceof List) {
            return ((List<CSSValue>) value_).size();
        }
        return 0;
    }

    @Override
    @SuppressWarnings("unchecked")
    public CSSValue item(final int index) {
        if (value_ instanceof List) {
            final List<CSSValue> list = (List<CSSValue>) value_;
            return list.get(index);
        }
        return null;
    }

    @Override
    public String toString() {
        return getCssText(null);
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof CSSValue)) {
            return false;
        }
        final CSSValue cv = (CSSValue) obj;
        // TODO to be improved!
        return super.equals(obj)
            && (getCssValueType() == cv.getCssValueType())
            && LangUtils.equals(getCssText(), cv.getCssText());
    }

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        hash = LangUtils.hashCode(hash, value_);
        return hash;
    }

}
