/* ====================================================================
   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.poi.ddf;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import org.apache.poi.util.HexDump;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;

/**
 * Escher container records store other escher records as children.
 * The container records themselves never store any information beyond
 * the standard header used by all escher records.  This one record is
 * used to represent many different types of records.
 *
 * @author Glen Stampoultzis
 */
public final class EscherContainerRecord extends EscherRecord {
    public static final short DGG_CONTAINER    = (short)0xF000;
    public static final short BSTORE_CONTAINER = (short)0xF001;
    public static final short DG_CONTAINER     = (short)0xF002;
    public static final short SPGR_CONTAINER   = (short)0xF003;
    public static final short SP_CONTAINER     = (short)0xF004;
    public static final short SOLVER_CONTAINER = (short)0xF005;

    private static POILogger log = POILogFactory.getLogger(EscherContainerRecord.class);

    /**
     * in case if document contains any charts we have such document structure:
     * BOF
     * ...
     * DrawingRecord
     * ...
     * ObjRecord|TxtObjRecord
     * ...
     * EOF
     * ...
     * BOF(Chart begin)
     * ...
     * DrawingRecord
     * ...
     * ObjRecord|TxtObjRecord
     * ...
     * EOF
     * So, when we call EscherAggregate.createAggregate() we have not all needed data.
     * When we got warning "WARNING: " + bytesRemaining + " bytes remaining but no space left"
     * we should save value of bytesRemaining
     * and add it to container size when we serialize it
     */
    private int _remainingLength;

    private final List<EscherRecord> _childRecords = new ArrayList<EscherRecord>();

    public int fillFields(byte[] data, int pOffset, EscherRecordFactory recordFactory) {
        int bytesRemaining = readHeader(data, pOffset);
        int bytesWritten = 8;
        int offset = pOffset + 8;
        while (bytesRemaining > 0 && offset < data.length) {
            EscherRecord child = recordFactory.createRecord(data, offset);
            int childBytesWritten = child.fillFields(data, offset, recordFactory);
            bytesWritten += childBytesWritten;
            offset += childBytesWritten;
            bytesRemaining -= childBytesWritten;
            addChildRecord(child);
            if (offset >= data.length && bytesRemaining > 0) {
                _remainingLength = bytesRemaining;
                if (log.check(POILogger.WARN)) {
                    log.log(POILogger.WARN, "Not enough Escher data: " + bytesRemaining + " bytes remaining but no space left");
                }
            }
        }
        return bytesWritten;
    }

    public int serialize( int offset, byte[] data, EscherSerializationListener listener )
    {
        listener.beforeRecordSerialize( offset, getRecordId(), this );

        LittleEndian.putShort(data, offset, getOptions());
        LittleEndian.putShort(data, offset+2, getRecordId());
        int remainingBytes = 0;
        Iterator<EscherRecord> iterator = _childRecords.iterator();
        while (iterator.hasNext()) {
            EscherRecord r = iterator.next();
            remainingBytes += r.getRecordSize();
        }
        remainingBytes += _remainingLength;
        LittleEndian.putInt(data, offset+4, remainingBytes);
        int pos = offset+8;
        iterator = _childRecords.iterator();
        while (iterator.hasNext()) {
            EscherRecord r = iterator.next();
            pos += r.serialize(pos, data, listener );
        }

        listener.afterRecordSerialize( pos, getRecordId(), pos - offset, this );
        return pos - offset;
    }

    public int getRecordSize() {
        int childRecordsSize = 0;
        Iterator<EscherRecord> iterator = _childRecords.iterator();
        while (iterator.hasNext()) {
            EscherRecord r = iterator.next();
            childRecordsSize += r.getRecordSize();
        }
        return 8 + childRecordsSize;
    }

    /**
     * Do any of our (top level) children have the
     *  given recordId?
     */
    public boolean hasChildOfType(short recordId) {
        Iterator<EscherRecord> iterator = _childRecords.iterator();
        while (iterator.hasNext()) {
            EscherRecord r = iterator.next();
            if(r.getRecordId() == recordId) {
                return true;
            }
        }
        return false;
    }
    public EscherRecord getChild( int index ) {
        return _childRecords.get(index);
    }

    /**
     * @return a copy of the list of all the child records of the container.
     */
    public List<EscherRecord> getChildRecords() {
        return new ArrayList<EscherRecord>(_childRecords);
    }

    public Iterator<EscherRecord> getChildIterator() {
        return new ReadOnlyIterator(_childRecords);
    }
    private static final class ReadOnlyIterator implements Iterator<EscherRecord> {
        private final List<EscherRecord> _list;
        private int _index;

        public ReadOnlyIterator(List<EscherRecord> list) {
            _list = list;
            _index = 0;
        }

        public boolean hasNext() {
            return _index < _list.size();
        }
        public EscherRecord next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            return _list.get(_index++);
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
    /**
     * replaces the internal child list with the contents of the supplied <tt>childRecords</tt>
     */
    public void setChildRecords(List<EscherRecord> childRecords) {
        if (childRecords == _childRecords) {
            throw new IllegalStateException("Child records private data member has escaped");
        }
        _childRecords.clear();
        _childRecords.addAll(childRecords);
    }

    public boolean removeChildRecord(EscherRecord toBeRemoved) {
        return _childRecords.remove(toBeRemoved);
    }



    /**
     * Returns all of our children which are also
     *  EscherContainers (may be 0, 1, or vary rarely
     *   2 or 3)
     */
    public List<EscherContainerRecord> getChildContainers() {
        List<EscherContainerRecord> containers = new ArrayList<EscherContainerRecord>();
        Iterator<EscherRecord> iterator = _childRecords.iterator();
        while (iterator.hasNext()) {
            EscherRecord r = iterator.next();
            if(r instanceof EscherContainerRecord) {
                containers.add((EscherContainerRecord) r);
            }
        }
        return containers;
    }

    public String getRecordName() {
        switch (getRecordId()) {
            case DGG_CONTAINER:
                return "DggContainer";
            case BSTORE_CONTAINER:
                return "BStoreContainer";
            case DG_CONTAINER:
                return "DgContainer";
            case SPGR_CONTAINER:
                return "SpgrContainer";
            case SP_CONTAINER:
                return "SpContainer";
            case SOLVER_CONTAINER:
                return "SolverContainer";
            default:
                return "Container 0x" + HexDump.toHex(getRecordId());
        }
    }

    public void display(PrintWriter w, int indent) {
        super.display(w, indent);
        for (Iterator<EscherRecord> iterator = _childRecords.iterator(); iterator.hasNext();)
        {
            EscherRecord escherRecord = iterator.next();
            escherRecord.display(w, indent + 1);
        }
    }

    public void addChildRecord(EscherRecord record) {
        _childRecords.add(record);
    }

    public void addChildBefore(EscherRecord record, int insertBeforeRecordId) {
        for (int i = 0; i < _childRecords.size(); i++) {
            EscherRecord rec = _childRecords.get(i);
            if(rec.getRecordId() == insertBeforeRecordId){
                _childRecords.add(i++, record);
                // TODO - keep looping? Do we expect multiple matches?
            }
        }
    }

    public String toString()
    {
        String nl = System.getProperty( "line.separator" );

        StringBuffer children = new StringBuffer();
        if ( _childRecords.size() > 0 )
        {
            children.append( "  children: " + nl );

            int count = 0;
            for ( Iterator<EscherRecord> iterator = _childRecords.iterator(); iterator
                    .hasNext(); )
            {
                EscherRecord record = iterator.next();
                children.append( "   Child " + count + ":" + nl );
                String childResult = String.valueOf( record );
                childResult = childResult.replaceAll( "\n", "\n    " );
                children.append( "    " );
                children.append( childResult );
                children.append( nl );
                count++;
            }
        }

        return getClass().getName() + " (" + getRecordName() + "):" + nl
                + "  isContainer: " + isContainerRecord() + nl
                + "  version: 0x" + HexDump.toHex( getVersion() ) + nl
                + "  instance: 0x" + HexDump.toHex( getInstance() ) + nl
                + "  recordId: 0x" + HexDump.toHex( getRecordId() ) + nl
                + "  numchildren: " + _childRecords.size() + nl
                + children.toString();
    }

    @Override
    public String toXml(String tab) {
        StringBuilder builder = new StringBuilder();
        builder.append(tab).append(formatXmlRecordHeader(getRecordName(), HexDump.toHex(getRecordId()), HexDump.toHex(getVersion()), HexDump.toHex(getInstance())));
        for ( Iterator<EscherRecord> iterator = _childRecords.iterator(); iterator
                .hasNext(); )
        {
            EscherRecord record = iterator.next();
            builder.append(record.toXml(tab+"\t"));
        }
        builder.append(tab).append("</").append(getRecordName()).append(">\n");
        return builder.toString();
    }

    public <T extends EscherRecord> T getChildById( short recordId )
    {
        for ( EscherRecord childRecord : _childRecords )
        {
            if ( childRecord.getRecordId() == recordId )
            {
                @SuppressWarnings( "unchecked" )
                final T result = (T) childRecord;
                return result;
            }
        }
        return null;
    }

    /**
     * Recursively find records with the specified record ID
     *
     * @param out - list to store found records
     */
    public void getRecordsById(short recordId, List<EscherRecord> out){
        Iterator<EscherRecord> iterator = _childRecords.iterator();
        while (iterator.hasNext()) {
            EscherRecord r = iterator.next();
            if(r instanceof EscherContainerRecord) {
                EscherContainerRecord c = (EscherContainerRecord)r;
                c.getRecordsById(recordId, out );
            } else if (r.getRecordId() == recordId){
                out.add(r);
            }
        }
    }
}
