/*
 * 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.tika.parser.microsoft.onenote.fsshttpb.streamobj;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.tika.exception.TikaException;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.BinaryItem;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.DataNodeObjectData;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.streamobj.basic.ExGuid;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.ByteUtil;
import org.apache.tika.parser.microsoft.onenote.fsshttpb.util.SequenceNumberGenerator;

public class LeafNodeObject extends NodeObject {
    public DataNodeObjectData dataNodeObjectData;
    public DataHashObject dataHash;

    /**
     * Initializes a new instance of the LeafNodeObjectData class.
     */
    public LeafNodeObject() {
        super(StreamObjectTypeHeaderStart.LeafNodeObject);
    }

    /**
     * Get all the content which is represented by the intermediate node object.
     *
     * @return Return the byte list of intermediate node object content.
     */
    @Override
    public List<Byte> getContent() throws TikaException {
        List<Byte> content = new ArrayList<Byte>();

        if (this.dataNodeObjectData != null) {
            ByteUtil.appendByteArrayToListOfByte(content, this.dataNodeObjectData.objectData);
        } else if (this.intermediateNodeObjectList != null) {
            for (LeafNodeObject intermediateNode : this.intermediateNodeObjectList) {
                content.addAll(intermediateNode.getContent());
            }
        } else {
            throw new TikaException(
                    "The DataNodeObjectData and IntermediateNodeObjectList properties in " +
                            "LeafNodeObjectData cannot be null at the same time.");
        }

        return content;
    }

    /**
     * Used to de-serialize the element.
     *
     * @param byteArray     A Byte array
     * @param currentIndex  Start position
     * @param lengthOfItems The length of the items
     */
    @Override
    protected void deserializeItemsFromByteArray(byte[] byteArray, AtomicInteger currentIndex,
                                                 int lengthOfItems)
            throws TikaException, IOException {
        AtomicInteger index = new AtomicInteger(currentIndex.get());
        if (lengthOfItems != 0) {
            throw new StreamObjectParseErrorException(currentIndex.get(), "LeafNodeObjectData",
                    "Stream Object over-parse error", null);
        }

        this.signature = StreamObject.getCurrent(byteArray, index, SignatureObject.class);
        this.dataSize = StreamObject.getCurrent(byteArray, index, DataSizeObject.class);

        // Try to read StreamObjectHeaderStart to see there is data hash object or not
        AtomicReference<StreamObjectHeaderStart> streamObjectHeader = new AtomicReference<>();
        if ((StreamObjectHeaderStart.tryParse(byteArray, index.get(), streamObjectHeader)) != 0) {
            if (streamObjectHeader.get().type == StreamObjectTypeHeaderStart.DataHashObject) {
                this.dataHash = StreamObject.getCurrent(byteArray, index, DataHashObject.class);
            }
        }

        currentIndex.set(index.get());
    }

    /**
     * Used to convert the element into a byte List.
     *
     * @param byteList A Byte list
     * @return A constant value
     */
    @Override
    protected int serializeItemsToByteList(List<Byte> byteList) throws TikaException, IOException {
        byteList.addAll(this.signature.serializeToByteList());
        byteList.addAll(this.dataSize.serializeToByteList());
        return 0;
    }

    /**
     * The class is used to build a intermediate node object.
     */
    public static class IntermediateNodeObjectBuilder {
        /**
         * This method is used to build intermediate node object from an list of object group data element
         *
         * @param objectGroupList  Specify the list of object group data elements.
         * @param dataObj          Specify the object group object.
         * @param intermediateGuid Specify the intermediate extended GUID.
         * @return Return the intermediate node object.
         */
        public LeafNodeObject Build(List<ObjectGroupDataElementData> objectGroupList,
                                    ObjectGroupObjectData dataObj,
                                    ExGuid intermediateGuid) throws TikaException, IOException {
            AtomicReference<LeafNodeObject> node = new AtomicReference<>();
            AtomicReference<IntermediateNodeObject> rootNode = new AtomicReference<>();

            AtomicInteger index = new AtomicInteger(0);
            if (StreamObject.tryGetCurrent(ByteUtil.toByteArray(dataObj.data.content), index, node,
                    LeafNodeObject.class)) {
                if (dataObj.objectExGUIDArray == null) {
                    throw new TikaException(
                            "Failed to build intermediate node because the object extend GUID array does not exist.");
                }

                node.get().exGuid = intermediateGuid;

                // Contain a single Data Node Object.
                if (dataObj.objectExGUIDArray.count.getDecodedValue() == 1) {
                    AtomicReference<ObjectGroupObjectDeclare> dataNodeDeclare =
                            new AtomicReference<>();
                    ObjectGroupObjectData dataNodeData = this.FindByExGuid(objectGroupList,
                            dataObj.objectExGUIDArray.content.get(0), dataNodeDeclare);
                    BinaryItem data = dataNodeData.data;

                    node.get().dataNodeObjectData =
                            new DataNodeObjectData(ByteUtil.toByteArray(data.content), 0,
                                    (int) data.length.getDecodedValue());
                    node.get().dataNodeObjectData.exGuid = dataObj.objectExGUIDArray.content.get(0);
                    node.get().intermediateNodeObjectList = null;
                } else {
                    // Contain a list of LeafNodeObjectData
                    node.get().intermediateNodeObjectList = new ArrayList<LeafNodeObject>();
                    node.get().dataNodeObjectData = null;
                    for (ExGuid extGuid : dataObj.objectExGUIDArray.content) {
                        AtomicReference<ObjectGroupObjectDeclare> intermediateDeclare =
                                new AtomicReference<>();
                        ObjectGroupObjectData intermediateData =
                                this.FindByExGuid(objectGroupList, extGuid, intermediateDeclare);
                        node.get().intermediateNodeObjectList.add(
                                new IntermediateNodeObjectBuilder().Build(objectGroupList,
                                        intermediateData, extGuid));
                    }
                }
            } else if (StreamObject.tryGetCurrent(ByteUtil.toByteArray(dataObj.data.content), index,
                    rootNode, IntermediateNodeObject.class)) {
                // In Sub chunking for larger than 1MB zip file, MOSS2010 could return IntermediateNodeObject.
                // For easy further process, the rootNode will be replaced by intermediate node instead.
                node.set(new LeafNodeObject());
                node.get().intermediateNodeObjectList = new ArrayList<LeafNodeObject>();
                node.get().dataSize = rootNode.get().dataSize;
                node.get().exGuid = rootNode.get().exGuid;
                node.get().signature = rootNode.get().signature;
                node.get().dataNodeObjectData = null;
                for (ExGuid extGuid : dataObj.objectExGUIDArray.content) {
                    AtomicReference<ObjectGroupObjectDeclare> intermediateDeclare =
                            new AtomicReference<>();
                    ObjectGroupObjectData intermediateData =
                            this.FindByExGuid(objectGroupList, extGuid, intermediateDeclare);
                    node.get().intermediateNodeObjectList.add(
                            new IntermediateNodeObjectBuilder().Build(objectGroupList,
                                    intermediateData, extGuid));
                }
            } else {
                throw new TikaException(
                        "In the ObjectGroupDataElement cannot only contain the " +
                                "IntermediateNodeObject or IntermediateNodeObject.");
            }

            return node.get();
        }

        /**
         * This method is used to build intermediate node object from a byte array with a signature
         *
         * @param array     Specify the byte array.
         * @param signature Specify the signature.
         * @return Return the intermediate node object.
         */
        public LeafNodeObject Build(byte[] array, SignatureObject signature) {
            LeafNodeObject nodeObject = new LeafNodeObject();
            nodeObject.dataSize = new DataSizeObject();
            nodeObject.dataSize.dataSize = array.length;

            nodeObject.signature = signature;
            nodeObject.exGuid =
                    new ExGuid(SequenceNumberGenerator.GetCurrentSerialNumber(), UUID.randomUUID());

            nodeObject.dataNodeObjectData = new DataNodeObjectData(array, 0, array.length);
            nodeObject.intermediateNodeObjectList = null;

            // Now in the current implementation, one intermediate node only contain one single data object node.
            return nodeObject;
        }

        /**
         * This method is used to find the object group data element using the specified extended GUID
         *
         * @param objectGroupList Specify the object group data element list.
         * @param extendedGuid    Specify the extended GUID.
         * @param declare         Specify the output of ObjectGroupObjectDeclare.
         * @return Return the object group data element if found.
         */

        private ObjectGroupObjectData FindByExGuid(List<ObjectGroupDataElementData> objectGroupList,
                                                   ExGuid extendedGuid,
                                                   AtomicReference<ObjectGroupObjectDeclare> declare)
                throws TikaException {
            for (ObjectGroupDataElementData objectGroup : objectGroupList) {

                int findIndex = -1;
                for (int i = 0;
                        i < objectGroup.objectGroupDeclarations.objectDeclarationList.size(); ++i) {
                    ObjectGroupObjectDeclare objDeclare =
                            objectGroup.objectGroupDeclarations.objectDeclarationList.get(i);
                    if (objDeclare.objectExtendedGUID.equals(extendedGuid)) {
                        findIndex = i;
                        break;
                    }
                }

                if (findIndex < 0) {
                    continue;
                }

                declare.set(
                        objectGroup.objectGroupDeclarations.objectDeclarationList.get(findIndex));
                return objectGroup.objectGroupData.objectGroupObjectDataList.get(findIndex);
            }

            throw new TikaException("Cannot find the " + extendedGuid.guid.toString());
        }
    }
}
