/*
 * Copyright (c) 2006 Henri Sivonen
 * Copyright (c) 2007-2009 Mozilla Foundation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a 
 * copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the 
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 */

package nu.validator.datatype;

import java.util.ArrayList;
import java.util.List;

import nu.validator.vendor.relaxng.datatype.Datatype;
import nu.validator.vendor.relaxng.datatype.DatatypeException;
import nu.validator.vendor.relaxng.datatype.DatatypeStreamingValidator;
import nu.validator.vendor.relaxng.datatype.ValidationContext;

/**
 * Common superclass for HTML5 datatypes. Implements all methods of the 
 * <code>Datatype</code> interface and leaves a new <code>checkValid</code> for 
 * subclasses to implement.
 * 
 * @version $Id$
 * @author hsivonen
 */
public abstract class AbstractDatatype implements Datatype {

    /**
     * Constructor
     */
    AbstractDatatype() {
        super();
    }

    /**
     * Calls <code>checkValid(CharSequence literal)</code>.
     * @param literal the value
     * @param context the validation context (ignored by subclasses)
     * @return <code>true</code> if valid and <code>false</code> if not
     * @see org.relaxng.datatype.Datatype#isValid(java.lang.String, org.relaxng.datatype.ValidationContext)
     */
    @Override
    public final boolean isValid(String literal, ValidationContext context) {
        try {
            checkValid(literal);
        } catch (DatatypeException e) {
            return false;
        }
        return true;
    }

    /**
     * Delegates to <code>checkValid(CharSequence literal)</code>.
     * @param literal the value
     * @param context the validation context (ignored by subclasses)
     * @throws DatatypeException if the literal does not conform to the datatype definition
     * @see org.relaxng.datatype.Datatype#checkValid(java.lang.String, org.relaxng.datatype.ValidationContext)
     */
    @Override
    public void checkValid(String literal, ValidationContext context) throws DatatypeException {
        checkValid(literal);
    }

    public abstract void checkValid(CharSequence literal) throws DatatypeException;
    
    /**
     * Merely returns a <code>DatatypeStreamingValidatorImpl</code>.
     * @param context the validation context (ignored by subclasses)
     * @return An unoptimized <code>DatatypeStreamingValidator</code>
     * @see org.relaxng.datatype.Datatype#createStreamingValidator(org.relaxng.datatype.ValidationContext)
     */
    @Override
    public DatatypeStreamingValidator createStreamingValidator(
            ValidationContext context) {
        return new DatatypeStreamingValidatorImpl(this);
    }

    /**
     * Implements strict string equality semantics by returning <code>literal</code> 
     * itself.
     * @param literal the value (get returned)
     * @param context ignored
     * @return the <code>literal</code> that was passed in
     * @see org.relaxng.datatype.Datatype#createValue(java.lang.String, org.relaxng.datatype.ValidationContext)
     */
    @Override
    public Object createValue(String literal, ValidationContext context) {
        return literal;
    }

    /**
     * Implements strict string equality semantics by performing a standard 
     * <code>equals</code> check on the arguments.
     * @param value1 an object returned by <code>createValue</code>
     * @param value2 another object returned by <code>createValue</code>
     * @return <code>true</code> if the values are equal, <code>false</code> otherwise
     * @see org.relaxng.datatype.Datatype#sameValue(java.lang.Object, java.lang.Object)
     */
    @Override
    public final boolean sameValue(Object value1, Object value2) {
        if (value1 == null) {
            return (value2 == null);
        }
        return value1.equals(value2);
    }

    /**
     * Implements strict stirng equality semantics by returning the 
     * <code>java.lang.Object</code>-level <code>hashCode</code> of 
     * the object.
     * @param value an object returned by <code>createValue</code>
     * @return the hash code
     * @see org.relaxng.datatype.Datatype#valueHashCode(java.lang.Object)
     */
    @Override
    public final int valueHashCode(Object value) {
        return value.hashCode();
    }

    /**
     * Always returns <code>Datatype.ID_TYPE_NULL</code>.
     * @return <code>Datatype.ID_TYPE_NULL</code>
     * @see org.relaxng.datatype.Datatype#getIdType()
     */
    @Override
    public final int getIdType() {
        return Datatype.ID_TYPE_NULL;
    }

    /**
     * Returns <code>false</code>
     * @return <code>false</code>
     * @see org.relaxng.datatype.Datatype#isContextDependent()
     */
    @Override
    public boolean isContextDependent() {
        return false;
    }

    protected final String trimWhitespace(String s) {
        int len = s.length();
        int st = 0;
        char[] val = s.toCharArray();
        while ((st < len) && (isWhitespace(val[st]))) {
            st++;
        }
        while ((st < len) && (isWhitespace(val[len - 1]))) {
            len--;
        }
        return ((st > 0) || (len < s.length())) ? s.substring(st, len) : s;
    }

    /**
     * Checks if a UTF-16 code unit represents a whitespace character (U+0020, 
     * U+0009, U+000C, U+000D or U+000A).
     * @param c the code unit
     * @return <code>true</code> if whitespace, <code>false</code> otherwise
     */
    protected final boolean isWhitespace(char c) {
        return c == ' ' || c == '\t' || c == '\f' || c == '\n' || c == '\r';
    }
    
    protected final boolean isAsciiDigit(char c) {
        return c >= '0' && c <= '9';
    }

    protected static final char toAsciiLowerCase(char c) {
        if (c >= 'A' && c <= 'Z') {
            c += 0x20;
        }
        return c;
    }

    protected static final String toAsciiLowerCase(CharSequence str) {
        if (str == null) {
            return null;
        }
        char[] buf = new char[str.length()];
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (c >= 'A' && c <= 'Z') {
                c += 0x20;
            }
            buf[i] = c;
        }
        return new String(buf);
    }

    protected static final char toAsciiUpperCase(char c) {
        if (c >= 'a' && c <= 'z') {
            c -= 0x20;
        }
        return c;
    }

    protected static final String toAsciiUpperCase(CharSequence str) {
        if (str == null) {
            return null;
        }
        char[] buf = new char[str.length()];
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (c >= 'a' && c <= 'z') {
                c -= 0x20;
            }
            buf[i] = c;
        }
        return new String(buf);
    }

    protected DatatypeException newDatatypeException(String message) {
        return new Html5DatatypeException(this.getClass(), this.getName(), message);
    }

    protected DatatypeException newDatatypeException(String head, String literal, String tail) {
        return new Html5DatatypeException(this.getClass(), this.getName(), head, literal, tail);
    }

    protected DatatypeException newDatatypeException(String head, char literal, String tail) {
        return new Html5DatatypeException(this.getClass(), this.getName(), head, String.valueOf(literal), tail);
    }
    
    protected DatatypeException newDatatypeException(int position, String message) {
        return new Html5DatatypeException(position, this.getClass(), this.getName(), message);
    }

    protected DatatypeException newDatatypeException(int position, String head, String literal, String tail) {
        return new Html5DatatypeException(position, this.getClass(), this.getName(), head, literal, tail);
    }

    protected DatatypeException newDatatypeException(int position, String head, char literal, String tail) {
        return new Html5DatatypeException(position, this.getClass(), this.getName(), head, String.valueOf(literal), tail);
    }    
    
    /* for datatype exceptions that are handled as warnings, the following are
     * alternative forms of all the above, with an additional "warning" parameter */

    protected DatatypeException newDatatypeException(String message, boolean warning) {
        return new Html5DatatypeException(this.getClass(), this.getName(), message, warning);
    }

    protected DatatypeException newDatatypeException(String head, String literal, String tail, boolean warning) {
        return new Html5DatatypeException(this.getClass(), this.getName(), head, literal, tail, warning);
    }

    protected DatatypeException newDatatypeException(String head, char literal, String tail, boolean warning) {
        return new Html5DatatypeException(this.getClass(), this.getName(), head, String.valueOf(literal), tail, warning);
    }
    
    protected DatatypeException newDatatypeException(int position, String message, boolean warning) {
        return new Html5DatatypeException(position, this.getClass(), this.getName(), message, warning);
    }

    protected DatatypeException newDatatypeException(int position, String head, String literal, String tail, boolean warning) {
        return new Html5DatatypeException(position, this.getClass(), this.getName(), head, literal, tail, warning);
    }

    protected DatatypeException newDatatypeException(int position, String head, char literal, String tail, boolean warning) {
        return new Html5DatatypeException(position, this.getClass(), this.getName(), head, String.valueOf(literal), tail, warning);
    }    
    
    public abstract String getName();
    
    protected List<CharSequenceWithOffset> split(CharSequence sequence, char delimiter) {
        List<CharSequenceWithOffset> rv = new ArrayList<>();
        int offset = 0;
        for (int i = 0; i < sequence.length(); i++) {
            char c = sequence.charAt(i);
            if (c == delimiter) {
                rv.add(new CharSequenceWithOffset(sequence.subSequence(offset, i), offset));
                offset = i + 1;
            }
        }
        rv.add(new CharSequenceWithOffset(sequence.subSequence(offset, sequence.length()), offset));
        return rv;
    }
    
    protected class CharSequenceWithOffset {
        private final CharSequence sequence;
        private final int offset;
        /**
         * @param sequence
         * @param offset
         */
        public CharSequenceWithOffset(final CharSequence sequence, final int offset) {
            this.sequence = sequence;
            this.offset = offset;
        }
        /**
         * Returns the offset.
         * 
         * @return the offset
         */
        public int getOffset() {
            return offset;
        }
        /**
         * Returns the sequence.
         * 
         * @return the sequence
         */
        public CharSequence getSequence() {
            return sequence;
        }
    }
}
