/*
 * Copyright (c) 2011-2024 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
 *
 * Licensed 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 com.marklogic.tree;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Writable;
import org.w3c.dom.Node;

import com.marklogic.dom.AttrImpl;
import com.marklogic.dom.CommentImpl;
import com.marklogic.dom.DocumentImpl;
import com.marklogic.dom.ElementImpl;
import com.marklogic.dom.NodeImpl;
import com.marklogic.dom.ProcessingInstructionImpl;
import com.marklogic.dom.TextImpl;

/**
 * Java equivalent of ExpandedTreeRep in Tree.h
 * 
 * @author jchen
 */
public class ExpandedTree implements Writable {
    public static final Log LOG = LogFactory.getLog(ExpandedTree.class);
	private static final Charset UTF8 = Charset.forName("UTF8");

	NodeImpl nodes[]; // NodeRep*
	
	public long ordinal;  // uint64_t
	public long uriKey;   // uint64_t
	public long uniqKey;  // uint64_t
	public long linkKey;  // uint64_t
	public long keys[];   // uint64_t*
	public byte atomData[]; // char*
	public String atomString[];
	public int atomIndex[]; // unsigned*
	
	public long nodeOrdinal[];
	public byte nodeKind[];
	public int nodeRepID[];
	public int nodeParentNodeRepID[];

	public int docNodeTextRepID[]; // unsigned DocNodeRep::textRepID
	public int docNodeChildNodeRepID[]; // unsigned DocNodeRep::childNodeRepID
	public int docNodeNumChildren[]; // unsigned DocNodeRep::numChildren

	public int elemNodeNodeNameRepID[]; // unsigned ElemNodeRep::nodeNameRepID
	public int elemNodeAttrNodeRepID[]; // unsigned ElemNodeRep::attrNodeRepID
	public int elemNodeChildNodeRepID[]; // unsigned ElemNodeRep::childNodeRepID
	public int elemNodeElemDeclRepID[]; // unsigned ElemNodeRep::elemDeclRepID
	public int elemNodeNumAttributes[]; // unsigned ElemNodeRep::numAttributes:24
	public int elemNodeNumDefaultAttrs[]; // unsigned ElemNodeRep::numDefaultAttrs:8
	public int elemNodeNumChildren[]; // unsigned ElemNodeRep::numChildren:28
	public int elemNodeFlags[]; // unsigned ElemNodeRep::flags

	public int attrNodeNodeNameRepID[];
	public int attrNodeTextRepID[];
	public int attrNodeAttrDeclRepID[];

	public int piNodeTargetAtom[];
	public int piNodeTextRepID[];

	public long linkNodeKey[];
	public long linkNodeNodeCount[];
	public int linkNodeNodeNameRepID[];
	public int linkNodeNodeRepID[];

	public int nodeNameNameAtom[];
	public int nodeNameNamespaceAtom[];

	public long nsNodeOrdinal[];
	public int nsNodePrevNSNodeRepID[];
	public int nsNodePrefixAtom[];
	public int nsNodeUriAtom[];

	public long permNodeOrdinal[];
	public int permNodePrevPermNodeRepID[];
	public Capability permNodeCapability[];
	public long permNodeRoleId[];
	
	public int arrayNodeTextRepID[]; // unsigned ArrayNodeRep.testRepID
	public int arrayNodeChildNodeRepID[]; // unsigned ArrayNodeRep.childNodeRepID
	public int arrayNodeNumChildren[]; // unsigned ArrayNodeRep.numChildren

	public double doubles[]; 

	public long binaryKey; // uint64_t BinaryNodeRep.binaryKey
	public long binaryOffset; // uint64_t BinaryNodeRep.offset
	public long binarySize; // uint64_t BinaryNodeRep.size
	public long binaryOrigLen; // uint64_t BinaryNodeRep.originalLength
	public int binaryPathAtom; // unsigned BinaryNodeRep.pathAtom

	public int numTextReps; // not in ExpandedTreeRep
	public int textReps[]; // unsigned*
	public int binaryData[]; // unsigned*
	
	public int atomLimit;  // unsigned
	public int numKeys;  // unsigned
	public int numNodeReps; // unsigned
	public int numNSNodeReps; // unsigned
	public int numPermNodeReps; // unsigned
	public int numLinkNodeReps; // unsigned
	public int uriTextRepID; // unsigned
	public int colsTextRepID; // unsigned
	public int[] metaKeys; // unsigned*
	public int[] metaVals; // unsigned*
	public int schemaRepUID; // unsigned
	public long schemaTimestamp; // uint64_t
	
	public int numMetadata; // unsigned

	private long fragmentOrdinal;
	private int quality;

	public boolean atomEquals(int atom, byte value[]) {
		int p = 0;
		int i = atomIndex[atom] + (atomData[atomIndex[atom]]>>>7) + 1;
		while (p < value.length) {
			byte b = atomData[i];
			if (LOG.isTraceEnabled()) {
			    LOG.trace(String.format("%02x %02x", b, value[p]));
			}
			if ((b == 0) || (b != value[p]))
				return false;
			p++;
			i++;
		}
		return true;
	}
	
	public String atomString(int i) {
		if (i<0 || i==Integer.MAX_VALUE) return null;
		String value = null;
		if (atomString == null) {
			atomString = new String[atomIndex.length];
		} else if (atomString.length > i){
			value = atomString[i];
		}
		if (value == null) {
            int aidx;
            if (atomData[atomIndex[i]]>=0) {
                aidx = atomIndex[i] + 1;
            } else {
                aidx = atomIndex[i] + 2;
            }
			value = atomString[i] = new String(atomData, aidx,
					atomIndex[i + 1] - aidx - 1, UTF8);
		}
		return value;
	}
	
	public String getText(int index) {
            int input = index;
	    if (textReps==null) return null;
    	    StringBuilder buf = new StringBuilder();
    	    for (int i=textReps[index++]; i > 0; --i) {
    	        if (LOG.isTraceEnabled()) {
    	            LOG.trace("atom " + textReps[index] + " [" + 
    	                atomString(textReps[index]) + "] length " + 
    	                atomString(textReps[index]).length());
    	        }
    		buf.append(atomString(textReps[index++]));
    	    }
            if (LOG.isTraceEnabled()) {
            LOG.trace("getText(" + input + ") returning [" + buf.toString() + 
                    "] length " + buf.length());
            }
            return buf.toString();
	}
	
    public String[] getCollections() {
        int index = colsTextRepID;
        int cnt = textReps[index++];
        String[] cols = new String[cnt];
        for (int i = 0; i < cnt; ++i) {
            cols[i] = atomString(textReps[index++]);
        }
        return cols;
    }
    
    public Map<String, String> getMetadata() {
        if (numMetadata == 0) return null;
        Map<String, String> metaMap = new HashMap<>(numMetadata);
        for (int i = 0; i < numMetadata; i++) {
            metaMap.put(atomString(metaKeys[i]), getText(metaVals[i]));
        }
        return metaMap;
    }
	
	public byte rootNodeKind() {
	    if (node(0) != null) {
	        return nodeKind[((DocumentImpl)node(0)).getFirstChildIndex()];
	    } else {
	        return nodeKind[0];
	    }
	}
	
	public Node node(int i) {
		if (i == Integer.MAX_VALUE) {
			return null;
		}
		else if (nodes[i] != null) {
			return nodes[i];
		}
		else {
			switch (nodeKind[i]) {
			case NodeKind.ELEM:
				nodes[i] = new ElementImpl(this, i);
				break;
            case NodeKind.ATTR:
                nodes[i] = new AttrImpl(this, i);
                break;
			case NodeKind.TEXT:
				nodes[i] = new TextImpl(this, i);
				break;
			case NodeKind.LINK:
				break;
			case NodeKind.NS:
				break;
            case NodeKind.DOC:
                nodes[i] = new DocumentImpl(this, i);
                break;
			case NodeKind.PI:
				nodes[i] = new ProcessingInstructionImpl(this, i);
				break;
			case NodeKind.COMMENT:
				nodes[i] = new CommentImpl(this, i);
				break;
			case NodeKind.PERM:
				break;
			case NodeKind.BINARY:		
				break;
			default:
				LOG.warn("Unexpected node kind: " + nodeKind[i] + " @ " + i);
				break;
			}
			return nodes[i];
		}
	}

    public String getDocumentURI() {
        return getText(uriTextRepID);
    }
    
    public Path getPathToBinary() {
        long dirKey = binaryKey >>> 54;
        String dir = String.format("%03x", dirKey);
        String fileName = String.format("%016x", binaryKey);
        return new Path(dir, fileName);
    }

    public boolean containLinks() {
        return numLinkNodeReps > 0;
    }

    public long getFragmentOrdinal() {
        return fragmentOrdinal;
    }

    public void setFragmentOrdinal(long fragmentOrdinal) {
        this.fragmentOrdinal = fragmentOrdinal;
    }

    public int getQuality() {
        return quality;
    }

    public void setQuality(int quality) {
        this.quality = quality;
    }

    @Override
    public void readFields(DataInput in) throws IOException {
        uriKey = in.readLong();
        uniqKey = in.readLong();
        linkKey = in.readLong();
        numKeys = in.readInt();
        if (numKeys > 0) {
            keys = new long[numKeys];
            for (int i = 0; i < numKeys; i++) {
                keys[i] = in.readLong();
            }
        }
        int atomDataLen = in.readInt();
        if (atomDataLen > 0) {
            atomData = new byte[atomDataLen];
            for (int i = 0; i < atomDataLen; i++) {
                atomData[i] = in.readByte();
            }
        }
        atomLimit = in.readInt();
        if (atomLimit > 0) {
            atomIndex = new int[atomLimit+1];
            for (int i = 0; i < atomLimit + 1; i++) {
                atomIndex[i] = in.readInt();               
            }
        }
        int nodeNameNameAtomLen = in.readInt();
        if (nodeNameNameAtomLen > 0) {
            nodeNameNameAtom = new int[nodeNameNameAtomLen];
            nodeNameNamespaceAtom = new int[nodeNameNameAtomLen];
            for (int i = 0; i < nodeNameNameAtomLen; i++) {
                nodeNameNameAtom[i] = in.readInt();
                nodeNameNamespaceAtom[i] = in.readInt();
            }
        }
        numNodeReps = in.readInt();
        if (numNodeReps > 0) {
            nodes = new NodeImpl[numNodeReps]; 
            nodeOrdinal = new long[numNodeReps];
            nodeKind = new byte[numNodeReps];
            nodeRepID = new int[numNodeReps];
            nodeParentNodeRepID = new int[numNodeReps];
            for (int i = 0; i < numNodeReps; i++) {
                nodeOrdinal[i] = in.readLong();
                nodeKind[i] = in.readByte();
                nodeRepID[i] = in.readInt();
                nodeParentNodeRepID[i] = in.readInt();
            }
        }
        int numElemNodeReps = in.readInt();
        if (numElemNodeReps > 0) {
            elemNodeNodeNameRepID = new int [numElemNodeReps];
            elemNodeAttrNodeRepID = new int[numElemNodeReps];
            elemNodeChildNodeRepID = new int[numElemNodeReps];
            elemNodeElemDeclRepID = new int[numElemNodeReps];
            elemNodeNumAttributes = new int[numElemNodeReps];
            elemNodeNumDefaultAttrs = new int[numElemNodeReps];
            elemNodeNumChildren = new int[numElemNodeReps];
            elemNodeFlags = new int[numElemNodeReps];
            for (int i = 0; i < numElemNodeReps; i++) {
                elemNodeNodeNameRepID[i] = in.readInt();
                elemNodeAttrNodeRepID[i] = in.readInt();
                elemNodeChildNodeRepID[i] = in.readInt();
                elemNodeElemDeclRepID[i] = in.readInt();
                elemNodeNumAttributes[i] = in.readInt();
                elemNodeNumDefaultAttrs[i] = in.readInt();
                elemNodeNumChildren[i] = in.readInt();
                elemNodeFlags[i] = in.readInt();
            }
        }
        int numAttrNodeReps = in.readInt();
        if (numAttrNodeReps > 0) {
            attrNodeNodeNameRepID = new int[numAttrNodeReps];
            attrNodeTextRepID = new int[numAttrNodeReps];
            attrNodeAttrDeclRepID = new int[numAttrNodeReps];
            for (int i = 0; i < numAttrNodeReps; i++) {
                attrNodeNodeNameRepID[i] = in.readInt();
                attrNodeTextRepID[i] = in.readInt();
                attrNodeAttrDeclRepID[i] = in.readInt();
            }
        }
        numLinkNodeReps = in.readInt();
        if (numLinkNodeReps > 0) {
            linkNodeKey = new long[numLinkNodeReps];
            linkNodeNodeCount = new long[numLinkNodeReps];
            linkNodeNodeNameRepID = new int[numLinkNodeReps];
            linkNodeNodeRepID = new int[numLinkNodeReps];
            for (int i = 0; i < numLinkNodeReps; i++) {
                linkNodeKey[i] = in.readLong();
                linkNodeNodeCount[i] = in.readLong();
                linkNodeNodeNameRepID[i] = in.readInt();
                linkNodeNodeRepID[i] = in.readInt();
            }
        }
        int numDocNodeReps = in.readInt();
        if (numDocNodeReps > 0) {
            docNodeTextRepID = new int[numDocNodeReps];
            docNodeChildNodeRepID = new int[numDocNodeReps];
            docNodeNumChildren = new int[numDocNodeReps];
            for (int i = 0; i < numDocNodeReps; i++) {
                docNodeTextRepID[i] = in.readInt();
                docNodeChildNodeRepID[i] = in.readInt();
                docNodeNumChildren[i] = in.readInt();
            }
        }
        int numPINodeReps = in.readInt();
        if (numPINodeReps > 0) {
            piNodeTargetAtom = new int[numPINodeReps];
            piNodeTextRepID = new int[numPINodeReps];
            for (int i = 0; i < numPINodeReps; i++) {
                piNodeTargetAtom[i] = in.readInt();
                piNodeTextRepID[i] = in.readInt();
            }
        }
        numNSNodeReps = in.readInt();
        if (numNSNodeReps > 0) {
            nsNodeOrdinal = new long[numNSNodeReps];
            nsNodePrevNSNodeRepID = new int[numNSNodeReps];
            nsNodePrefixAtom = new int[numNSNodeReps];
            nsNodeUriAtom = new int[numNSNodeReps];
            for (int i = 0; i < numNSNodeReps; i++) {
                nsNodeOrdinal[i] = in.readLong();
                nsNodePrevNSNodeRepID[i] = in.readInt();
                nsNodePrefixAtom[i] = in.readInt();
                nsNodeUriAtom[i] = in.readInt();
            }
        }
        // skip permission node since it's not exposed to the API
        uriTextRepID = in.readInt();
        colsTextRepID = in.readInt();
        numTextReps = in.readInt();
        if (numTextReps > 0) {
            textReps = new int[numTextReps];
            for (int i = 0; i < numTextReps; i++) {
                textReps[i] = in.readInt();
            }
        }
        int numArrayNodeReps = in.readInt();
        if (numArrayNodeReps > 0) {
            arrayNodeTextRepID = new int [numArrayNodeReps];
            arrayNodeChildNodeRepID = new int[numArrayNodeReps];
            arrayNodeNumChildren = new int[numArrayNodeReps];
            for (int i = 0; i < numArrayNodeReps; i++) {
                arrayNodeTextRepID[i] = in.readInt();
                arrayNodeChildNodeRepID[i] = in.readInt();
                arrayNodeNumChildren[i] = in.readInt();
            }
        }
        int numDoubles = in.readInt();
        if (numDoubles > 0) {
            doubles = new double [numDoubles];
            for (int i = 0; i < numDoubles; i++) {
                doubles[i] = in.readDouble();
            }
        }
    }

    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(uriKey);
        out.writeLong(uniqKey);
        out.writeLong(linkKey);
        out.writeInt(numKeys);
        if (numKeys > 0) {
           for (long key : keys) {
               out.writeLong(key);
           }
        }
        if (atomData != null && atomData.length > 0) {
            out.writeInt(atomData.length);
          for (byte atomDatum : atomData) {
            out.writeByte(atomDatum);
          }
        } else {
            out.writeInt(0);
        }
        out.writeInt(atomLimit);
        if (atomIndex != null && atomIndex.length > 0) {
          for (int index : atomIndex) {
            out.writeInt(index);
          }
        }
        if (nodeNameNameAtom != null && nodeNameNameAtom.length > 0) {
            out.writeInt(nodeNameNameAtom.length);
            for (int i = 0; i < nodeNameNameAtom.length; i++) {
                out.writeInt(nodeNameNameAtom[i]);
                out.writeInt(nodeNameNamespaceAtom[i]);
            }
        } else {
            out.writeInt(0);
        }
        out.writeInt(numNodeReps);
        if (numNodeReps > 0) {
            for (int i = 0; i < numNodeReps; i++) {
                out.writeLong(nodeOrdinal[i]);
                out.writeByte(nodeKind[i]);
                out.writeInt(nodeRepID[i]);
                out.writeInt(nodeParentNodeRepID[i]);
            }
        }
        if (elemNodeNodeNameRepID != null && 
            elemNodeNodeNameRepID.length > 0) {
            out.writeInt(elemNodeNodeNameRepID.length);
            for (int i = 0; i < elemNodeNodeNameRepID.length; i++) {
                out.writeInt(elemNodeNodeNameRepID[i]);
                out.writeInt(elemNodeAttrNodeRepID[i]);
                out.writeInt(elemNodeChildNodeRepID[i]);
                out.writeInt(elemNodeElemDeclRepID[i]);
                out.writeInt(elemNodeNumAttributes[i]);
                out.writeInt(elemNodeNumDefaultAttrs[i]);
                out.writeInt(elemNodeNumChildren[i]);
                out.writeInt(elemNodeFlags[i]);
            }
        } else {
            out.writeInt(0);
        }
        if (attrNodeNodeNameRepID != null && 
            attrNodeNodeNameRepID.length > 0) {
            out.writeInt(attrNodeNodeNameRepID.length);
            for (int i = 0; i < attrNodeNodeNameRepID.length; i++) {
                out.writeInt(attrNodeNodeNameRepID[i]);
                out.writeInt(attrNodeTextRepID[i]);
                out.writeInt(attrNodeAttrDeclRepID[i]);
            }
        } else {
            out.writeInt(0);
        }
        out.writeInt(numLinkNodeReps);
        if (numLinkNodeReps > 0) {
            for (int i = 0; i < numLinkNodeReps; i++) {
                out.writeLong(linkNodeKey[i]);
                out.writeLong(linkNodeNodeCount[i]);
                out.writeInt(linkNodeNodeNameRepID[i]);
                out.writeInt(linkNodeNodeRepID[i]);
            }
        }
        if (docNodeTextRepID != null && docNodeTextRepID.length > 0) {
            out.writeInt(docNodeTextRepID.length);
            for (int i = 0; i < docNodeTextRepID.length; i++) {
                out.writeInt(docNodeTextRepID[i]);
                out.writeInt(docNodeChildNodeRepID[i]);
                out.writeInt(docNodeNumChildren[i]);
            }
        } else {
            out.writeInt(0);
        }
        if (piNodeTargetAtom != null && piNodeTargetAtom.length > 0) {
            out.writeInt(piNodeTargetAtom.length);
            for (int i = 0; i < piNodeTargetAtom.length; i++) {
                out.writeInt(piNodeTargetAtom[i]);
                out.writeInt(piNodeTextRepID[i]);
            }
        } else {
            out.writeInt(0);
        }
        out.writeInt(numNSNodeReps);
        if (numNSNodeReps > 0) {
            for (int i = 0; i < numNSNodeReps; i++) {
                out.writeLong(nsNodeOrdinal[i]);
                out.writeInt(nsNodePrevNSNodeRepID[i]);
                out.writeInt(nsNodePrefixAtom[i]);
                out.writeInt(nsNodeUriAtom[i]);
            }
        }
        // skip permission node since it's not exposed to the API
        out.writeInt(uriTextRepID);
        out.writeInt(colsTextRepID);
        out.writeInt(numTextReps);
        if (numTextReps > 0) {
            for (int i = 0; i < numTextReps; i++) {
                out.writeInt(textReps[i]);
            }
        }
        if (arrayNodeTextRepID != null &&
            arrayNodeTextRepID.length > 0) {
            out.writeInt(arrayNodeTextRepID.length);
            for (int i = 0; i < arrayNodeTextRepID.length; i++) {
                out.writeInt(arrayNodeTextRepID[i]);
                out.writeInt(arrayNodeChildNodeRepID[i]);
                out.writeInt(arrayNodeNumChildren[i]);
            }
        } else {
            out.writeInt(0);
        }
        if (doubles != null &&
            doubles.length > 0) {
            out.writeInt(doubles.length);
          for (double aDouble : doubles) {
            out.writeDouble(aDouble);
          }
        } else {
            out.writeInt(0);
        }


    }    

    public String toString() {
        StringBuilder sb = new StringBuilder();
        putNode(0,sb);
        return sb.toString();
    }

    public void putNode(int index, StringBuilder sb) {

	if (LOG.isTraceEnabled()) {
	    LOG.trace(String.format("putNode index %d nodeKind %d", 
                index, nodeKind[index]));
	}
        switch (nodeKind[index]) {
            case NodeKind.TEXT: {
                sb.append("\"");
                int id = nodeRepID[index];
                sb.append(getText(id));
                sb.append("\"");
                break;
            }
            case NodeKind.DOC: {
                int id = nodeRepID[index];
                //sb.append(getText(docNodeTextRepID[id]));
                int limit = docNodeChildNodeRepID[id]+ docNodeNumChildren[id];
                for (int i = docNodeChildNodeRepID[id]; i < limit; i++) {
                   putNode(i, sb);
                }
                break;
            }
            case NodeKind.NULL: {
                sb.append("null");
                break;
            }
            case NodeKind.BOOLEAN: {
                if (nodeRepID[index]==1) {
                    sb.append("true");
                }
                else {
                    sb.append("false");
                }
                break;
            }
            case NodeKind.NUMBER: {
                sb.append(String.valueOf(doubles[nodeRepID[index]]));
                break;
            }
            case NodeKind.ARRAY: {
                sb.append("[");
                int id = nodeRepID[index];
                int limit = arrayNodeChildNodeRepID[id] + 
                            arrayNodeNumChildren[id];
                int i = 0;
                for (int idx = arrayNodeChildNodeRepID[id]; 
                     idx < limit; idx++,i++) {
                   if (i!=0) sb.append(", ");
                   putNode(idx, sb);
                }
                sb.append("]");
                break;
            }
            case NodeKind.OBJECT: {
                sb.append("{ ");
                int id = nodeRepID[index];
                int limit = arrayNodeChildNodeRepID[id] + 
                            arrayNodeNumChildren[id];
                int i=0;
                for (int idx = arrayNodeChildNodeRepID[id]; 
                     idx < limit; idx++, i++) {
                   if (i!=0) sb.append(", ");
                   sb.append("\"");
                   sb.append(atomString(textReps[arrayNodeTextRepID[id] + i]));
                   sb.append("\" : ");
                   putNode(idx, sb);
                }
                sb.append(" }");
                break;
            }
            default: {
                sb.append("node:UNKNOWN ");
                break;
            }
        }
    }
}
