/*
 * 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.xerces.dom;

import org.w3c.dom.DOMException;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * CharacterData is an abstract Node that can carry character data as its
 * Value.  It provides shared behavior for Text, CData, and
 * possibly other node types. All offsets are 0-based.
 * <p>
 * Since ProcessingInstructionImpl inherits from this class to reuse the
 * setNodeValue method, this class isn't declared as implementing the interface
 * CharacterData. This is done by relevant subclasses (TexImpl, CommentImpl).
 * <p>
 * This class doesn't directly support mutation events, however, it notifies
 * the document when mutations are performed so that the document class do so.
 * 
 * @xerces.internal 
 *
 * @version $Id: CharacterDataImpl.java 447266 2006-09-18 05:57:49Z mrglavas $
 * @since  PR-DOM-Level-1-19980818.
 */
public abstract class CharacterDataImpl
    extends ChildNode {

    //
    // Constants
    //

    /** Serialization version. */
    static final long serialVersionUID = 7931170150428474230L;

    //
    // Data
    //

    protected String data;

    /** Empty child nodes. */
    private static transient NodeList singletonNodeList = new NodeList() {
        public Node item(int index) { return null; }
        public int getLength() { return 0; }
    };

    //
    // Constructors
    //

    public CharacterDataImpl(){}

    /** Factory constructor. */
    protected CharacterDataImpl(CoreDocumentImpl ownerDocument, String data) {
        super(ownerDocument);
        this.data = data;
    }

    //
    // Node methods
    //

    /** Returns an empty node list. */
    public NodeList getChildNodes() {
        return singletonNodeList;
    }

    /*
     * returns the content of this node
     */
    public String getNodeValue() {
        if (needsSyncData()) {
            synchronizeData();
        }
        return data;
    }

   /** Convenience wrapper for calling setNodeValueInternal when 
     * we are not performing a replacement operation  
     */
    protected void setNodeValueInternal (String value) {
    	setNodeValueInternal(value, false);
    }
    
    /** This function added so that we can distinguish whether
     *  setNodeValue has been called from some other DOM functions.
     *  or by the client.<p>
     *  This is important, because we do one type of Range fix-up, 
     *  from the high-level functions in CharacterData, and another
     *  type if the client simply calls setNodeValue(value).
     */
    protected void setNodeValueInternal(String value, boolean replace) {
        
        CoreDocumentImpl ownerDocument = ownerDocument();
        
        if (ownerDocument.errorChecking && isReadOnly()) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
            throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
        }
        
        // revisit: may want to set the value in ownerDocument.
        // Default behavior, overridden in some subclasses
        if (needsSyncData()) {
            synchronizeData();
        }
        
        // keep old value for document notification
        String oldvalue = this.data;
        
        // notify document
        ownerDocument.modifyingCharacterData(this, replace);
        
        this.data = value;
        
        // notify document
        ownerDocument.modifiedCharacterData(this, oldvalue, value, replace);
    }

    /**
     * Sets the content, possibly firing related events,
     * and updating ranges (via notification to the document)
     */
    public void setNodeValue(String value) {

        setNodeValueInternal(value);

        // notify document
        ownerDocument().replacedText(this);
    }

    //
    // CharacterData methods
    //

    /**
     * Retrieve character data currently stored in this node.
     * 
     * @throws DOMExcpetion(DOMSTRING_SIZE_ERR) In some implementations,
     * the stored data may exceed the permitted length of strings. If so,
     * getData() will throw this DOMException advising the user to
     * instead retrieve the data in chunks via the substring() operation.  
     */
    public String getData() {
        if (needsSyncData()) {
            synchronizeData();
        }
        return data;
    }

    /** 
     * Report number of characters currently stored in this node's
     * data. It may be 0, meaning that the value is an empty string. 
     */
    public int getLength() {   
        if (needsSyncData()) {
            synchronizeData();
        }
        return data.length();
    }  

    /** 
     * Concatenate additional characters onto the end of the data
     * stored in this node. Note that this, and insert(), are the paths
     * by which a DOM could wind up accumulating more data than the
     * language's strings can easily handle. (See above discussion.)
     * 
     * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR) if node is readonly.
     */
    public void appendData(String data) {

    	if (isReadOnly()) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
            throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
        }
        if (data == null) {
            return;
        }
        if (needsSyncData()) {
            synchronizeData();
        }
        
        setNodeValue(this.data + data);

    } // appendData(String)

    /**
     * Remove a range of characters from the node's value. Throws a
     * DOMException if the offset is beyond the end of the
     * string. However, a deletion _count_ that exceeds the available
     * data is accepted as a delete-to-end request.
     * 
     * @throws DOMException(INDEX_SIZE_ERR) if offset is negative or
     * greater than length, or if count is negative.
     * 
     * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR) if node is
     * readonly.  
     */
    public void deleteData(int offset, int count) 
        throws DOMException {
    	
    	internalDeleteData(offset, count, false);
    } // deleteData(int,int)

    
    /** NON-DOM INTERNAL: Within DOM actions, we sometimes need to be able
     * to control which mutation events are spawned. This version of the
     * deleteData operation allows us to do so. It is not intended
     * for use by application programs.
     */
    void internalDeleteData (int offset, int count, boolean replace)
    throws DOMException {
        
        CoreDocumentImpl ownerDocument = ownerDocument();
        if (ownerDocument.errorChecking) {
            if (isReadOnly()) {
                String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
                throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
            }
            
            if (count < 0) {
                String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INDEX_SIZE_ERR", null);
                throw new DOMException(DOMException.INDEX_SIZE_ERR, msg);
            }
        }
        
        if (needsSyncData()) {
            synchronizeData();
        }
        int tailLength = Math.max(data.length() - count - offset, 0);
        try {
            String value = data.substring(0, offset) +
            (tailLength > 0 ? data.substring(offset + count, offset + count + tailLength) : "");            
            
            setNodeValueInternal(value, replace);      
            
            // notify document
            ownerDocument.deletedText(this, offset, count);
        }
        catch (StringIndexOutOfBoundsException e) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INDEX_SIZE_ERR", null);
            throw new DOMException(DOMException.INDEX_SIZE_ERR, msg);
        }
        
    } // internalDeleteData(int,int,boolean)

    /**
     * Insert additional characters into the data stored in this node,
     * at the offset specified.
     *
     * @throws DOMException(INDEX_SIZE_ERR) if offset is negative or
     * greater than length.
     *
     * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR) if node is readonly.  
     */
    public void insertData(int offset, String data) 
        throws DOMException {

    	internalInsertData(offset, data, false);
        
    } // insertData(int,int)
    
    
    
    /** NON-DOM INTERNAL: Within DOM actions, we sometimes need to be able
     * to control which mutation events are spawned. This version of the
     * insertData operation allows us to do so. It is not intended
     * for use by application programs.
     */
    void internalInsertData (int offset, String data, boolean replace)
    throws DOMException {
        
        CoreDocumentImpl ownerDocument = ownerDocument();
        
        if (ownerDocument.errorChecking && isReadOnly()) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
            throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
        }
        
        if (needsSyncData()) {
            synchronizeData();
        }
        try {
            String value =
                new StringBuffer(this.data).insert(offset, data).toString();
            
            
            setNodeValueInternal(value, replace);
            
            // notify document
            ownerDocument.insertedText(this, offset, data.length());
        }
        catch (StringIndexOutOfBoundsException e) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INDEX_SIZE_ERR", null);
            throw new DOMException(DOMException.INDEX_SIZE_ERR, msg);
        }
        
    } // internalInsertData(int,String,boolean)

    
    
    /**
     * Replace a series of characters at the specified (zero-based)
     * offset with a new string, NOT necessarily of the same
     * length. Convenience method, equivalent to a delete followed by an
     * insert. Throws a DOMException if the specified offset is beyond
     * the end of the existing data.
     * 
     * @param offset       The offset at which to begin replacing.
     * 
     * @param count        The number of characters to remove, 
     * interpreted as in the delete() method.
     * 
     * @param data         The new string to be inserted at offset in place of
     * the removed data. Note that the entire string will
     * be inserted -- the count parameter does not affect
     * insertion, and the new data may be longer or shorter
     * than the substring it replaces.
     * 
     * @throws DOMException(INDEX_SIZE_ERR) if offset is negative or
     * greater than length, or if count is negative.
     * 
     * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR) if node is
     * readonly.  
     */
    public void replaceData(int offset, int count, String data) 
    throws DOMException {
        
        CoreDocumentImpl ownerDocument = ownerDocument();
        
        // The read-only check is done by deleteData()
        // ***** This could be more efficient w/r/t Mutation Events,
        // specifically by aggregating DOMAttrModified and
        // DOMSubtreeModified. But mutation events are 
        // underspecified; I don't feel compelled
        // to deal with it right now.
        if (ownerDocument.errorChecking && isReadOnly()) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
            throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
        }
        
        if (needsSyncData()) {
            synchronizeData();
        }
        
        //notify document
        ownerDocument.replacingData(this);
        
        // keep old value for document notification
        String oldvalue = this.data;
        
        internalDeleteData(offset, count, true);
        internalInsertData(offset, data, true);
        
        ownerDocument.replacedCharacterData(this, oldvalue, this.data);
        
    } // replaceData(int,int,String)

    /**
     * Store character data into this node.
     * 
     * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR) if node is readonly.
     */
    public void setData(String value) 
        throws DOMException {
        setNodeValue(value);
    }

    /** 
     * Substring is more than a convenience function. In some
     * implementations of the DOM, where the stored data may exceed the
     * length that can be returned in a single string, the only way to
     * read it all is to extract it in chunks via this method.
     *
     * @param offset        Zero-based offset of first character to retrieve.
     * @param count Number of characters to retrieve. 
     *
     * If the sum of offset and count exceeds the length, all characters
     * to end of data are returned.
     *
     * @throws DOMException(INDEX_SIZE_ERR) if offset is negative or
     * greater than length, or if count is negative.
     *
     * @throws DOMException(WSTRING_SIZE_ERR) In some implementations,
     * count may exceed the permitted length of strings. If so,
     * substring() will throw this DOMException advising the user to
     * instead retrieve the data in smaller chunks.  
     */
    public String substringData(int offset, int count) 
        throws DOMException {

        if (needsSyncData()) {
            synchronizeData();
        }
        
        int length = data.length();
        if (count < 0 || offset < 0 || offset > length - 1) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INDEX_SIZE_ERR", null);
            throw new DOMException(DOMException.INDEX_SIZE_ERR, msg);
        }

        int tailIndex = Math.min(offset + count, length);

        return data.substring(offset, tailIndex);

    } // substringData(int,int):String

} // class CharacterDataImpl
