/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.jena.datatypes.xsd.impl;

import java.math.BigDecimal;
import java.math.BigInteger;

import org.apache.jena.datatypes.* ;
import org.apache.jena.datatypes.xsd.* ;
import org.apache.jena.graph.impl.LiteralLabel ;
import org.apache.jena.shared.impl.JenaParameters ;

/**
 * Base implementation for all numeric datatypes derived from
 * xsd:decimal. The only purpose of this place holder is
 * to support the isValidLiteral tests across numeric types.
 *  * <p>Note that float and double are not included in this set.
 */
public class XSDBaseNumericType extends XSDDatatype {

    /**
     * Constructor.
     * @param typeName the name of the XSD type to be instantiated, this is
     * used to lookup a type definition from the Xerces schema factory.
     */
    public XSDBaseNumericType(String typeName) {
        super(typeName);
    }

    /**
     * Constructor.
     * @param typeName the name of the XSD type to be instantiated, this is
     * used to lookup a type definition from the Xerces schema factory.
     * @param javaClass the java class for which this xsd type is to be
     * treated as the cannonical representation
     */
    public XSDBaseNumericType(String typeName, Class<?> javaClass) {
        super(typeName, javaClass);
    }


    /**
     * Test whether the given LiteralLabel is a valid instance
     * of this datatype. This takes into account typing information
     * as well as lexical form - for example an xsd:string is
     * never considered valid as an xsd:integer (even if it is
     * lexically legal like "1").
     */
    @Override
    public boolean isValidLiteral(LiteralLabel lit) {
        if (isBaseTypeCompatible(lit)) {
            String lex = lit.getLexicalForm();
            if (JenaParameters.enableWhitespaceCheckingOfTypedLiterals) {
                if (lex.trim().equals(lex)) {
                    return isValid(lit.getLexicalForm());
                } else {
                    return false;
                }
            } else {
                return isValid(lit.getLexicalForm());
            }
        } else {
            return false;
        }
    }

    /**
     * Test whether the given object is a legal value form
     * of this datatype. Brute force implementation.
     */
    @Override
    public boolean isValidValue(Object valueForm) {
        if (valueForm instanceof Number) {
            return isValid(unparse(valueForm));
        } else {
            return false;
        }
    }

    /**
     * Cannonicalise a java Object value to a normal form.
     * Primarily used in cases such as xsd:integer to reduce
     * the Java object representation to the narrowest of the Number
     * subclasses to ensure that indexing of typed literals works.
     */
    @Override
    public Object cannonicalise( Object value ) {

        if (value instanceof BigInteger) {
            return cannonicalizeInteger( (BigInteger)value );
        } else if (value instanceof BigDecimal) {
            return cannonicalizeDecimal( (BigDecimal)value );
        }
        return suitableInteger( ((Number)value).longValue() );
    }

    private static final BigInteger ten = new BigInteger("10");
    private static final int QUOT = 0;
    private static final int REM = 1;
    /**
     * Cannonicalize a big decimal
     */
    private Object cannonicalizeDecimal(BigDecimal value) {
        // This could can be simplified by using toBigIntegerExact
        if (value.scale() > 0) {
            // Check if we can strip off any trailing zeros after decimal point
            BigInteger i = value.unscaledValue();
            int limit = value.scale();
            int nshift = 0;
            for (nshift = 0; nshift < limit; nshift++) {
                BigInteger[] quotRem = i.divideAndRemainder(ten);
                if (quotRem[REM].intValue() != 0) break;
                i = quotRem[QUOT];
            }
            if (nshift > 0) {
               value = new BigDecimal(i, limit - nshift);
               if (value.scale() <= 0) {
                   return cannonicalizeInteger( value.toBigInteger() );
               }
            }
            return value;
        } else {
            return cannonicalizeInteger( value.toBigInteger() );
        }
    }

    /**
     * Cannonicalize a big integer
     */
    private Object cannonicalizeInteger( BigInteger value) {
        if (value.bitLength() > 63) {
            return value;
        } else {
            return suitableInteger( value.longValue() );
        }
    }

    /**
     * Parse a lexical form of this datatype to a value
     * @throws DatatypeFormatException if the lexical form is not legal
     */
    @Override
    public Object parse(String lexicalForm) throws DatatypeFormatException {
        checkWhitespace(lexicalForm);
        return super.parse(lexicalForm);
    }

    /**
     * Convert a value of this datatype to lexical form.
     * Certain forms are not a simple matter of java's toString on the Number object.
     */
    @Override
    public String unparse(Object value) {
        if ( value instanceof BigDecimal )
            // Avoid exponent usage.
            return ((BigDecimal)value).toPlainString() ;
        // See also for XSDfloat and XSDdouble.
        // Integer hierarchy is OK.
        return value.toString();
    }

    /**
     * Check for whitespace violations.
     * Turned off by default.
     */
    protected void checkWhitespace(String lexicalForm) {
        if (JenaParameters.enableWhitespaceCheckingOfTypedLiterals) {
            if ( ! lexicalForm.trim().equals(lexicalForm)) {
                throw new DatatypeFormatException(lexicalForm, this, "whitespace violation");
            }
        }
    }

    /**
     * Compares two instances of values of the given datatype.
     */
    @Override
    public boolean isEqual(LiteralLabel value1, LiteralLabel value2) {
        if (value1.getDatatype() instanceof XSDBaseNumericType && value2.getDatatype() instanceof XSDBaseNumericType) {
            Number n1 = (Number)value1.getValue();
            Number n2 = (Number)value2.getValue();
            // The cannonicalization step should take care of all cross-type cases, leaving
            // us just that equals doesn't work on BigDecimals in the way you expect
            if (n1 instanceof BigDecimal && n2 instanceof BigDecimal) {
                return  ((BigDecimal)n1).compareTo((BigDecimal)n2) == 0;
            }
            return n1.equals(n2);
        } else {
            // At least one arg is not part of the integer hierarchy
            return false;
        }
    }
}
