public class Handle
{

    public final int code;

    public final int offset;

    public Handle(int code, int[] handle)
    {
        this.code = code;
        
        int value = 0;
        int shift = 0;
        for (int i = handle.length-1; i >= 0; i--) {
        	value |= handle[i] << shift;
        	shift += 8;
        }
        
        this.offset = value;
    }

    public Handle(int code, int value)
    {
        this.code = code;
        this.offset = value;
    }

    public String toString() {
    	return Integer.toString(code) + ": " + offset + "(" + Integer.toHexString(offset) + ")";
    }
} public class Point3D {

	public final double x;
	public final double y;
	public final double z;
	
	public Point3D(double x, double y, double z) {
		this.x = x;
		this.y = y;
		this.z = z;
	}

    @Override
    public String toString() {
    	return "(" + x + ", " + y + ", " + z + ")";
    }
} public class AcdbDictionaryWithDefault extends Dictionary {

    private Handle defaultEntryHandle;

    public AcdbDictionaryWithDefault(ObjectMap objectMap) {
        super(objectMap);
    }
    
    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.43 ACDBDICTIONARYWDFLT page 146

        super.readCommonDictionaryData(dataStream, stringStream, handleStream);

        defaultEntryHandle = handleStream.getHandle();
        
        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public CadObject getDefaultEntry() {
        CadObject result = objectMap.parseObject(defaultEntryHandle);
        return (CadObject) result;
    }

	public String toString() {
		return "ACDBDICTIONARYWDFLT";
	}

} public class Appid extends NonEntityObject {

    public String entryName;

    private Handle externalRefBlockHandle;

    public Appid(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.64 APPID (67) page 173

        entryName = stringStream.getTU();
        boolean sixtyFourFlag = dataStream.getB();

        // This is documented in the spec but does not appear to
        // be in the data.
//      int xRefOrdinal = dataStream.getBS();
        boolean xDep = dataStream.getB();
        int unknown = dataStream.getRC();

        externalRefBlockHandle = handleStream.getHandle(handleOfThisObject);

        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
        return "APPID";
    }

    public CadObject getExternalRefBlock() {
        CadObject result = objectMap.parseObjectPossiblyNull(externalRefBlockHandle);
        return (CadObject) result;
    }

} public class AppidControlObj extends NonEntityObject {

    public Handle[] appidObjectHandles;

    public AppidControlObj(ObjectMap objectMap) {
	super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
	// 19.4.63 APPID CONTROL (66) page 172

	int numentries = dataStream.getBL();

	// The handles

	appidObjectHandles = new Handle[numentries];
	for (int i = 0; i < numentries; i++) {
	    appidObjectHandles[i] = handleStream.getHandle(handleOfThisObject);
	}

	handleStream.advanceToByteBoundary();

	dataStream.assertEndOfStream();
	stringStream.assertEndOfStream();
	handleStream.assertEndOfStream();
    }

    @Override
    public String toString() {
	return "APPID CONTROL OBJ";
    }

    public List<Appid> getAppids()
    {
	return new AbstractList<Appid>() {
	    @Override
	    public Appid get(int index)
	    {
		CadObject result = objectMap.parseObjectPossiblyNull(appidObjectHandles[index]);
		return (Appid) result;
	    }

	    @Override
	    public int size()
	    {
		return appidObjectHandles.length;
	    }
	};
    }

} public class Arc extends EntityObject {

    public Point3D center;
    public double radius;
    public double thickness;
    public Point3D extrusion;
    public double startAngle;
    public double endAngle;

    public Arc(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.18 ARC (17) page 117    

        center = dataStream.get3BD();
        radius = dataStream.getBD();
        thickness = dataStream.getBT();
        extrusion = dataStream.getBE();
        startAngle = dataStream.getBD();
        endAngle = dataStream.getBD();

        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
        return "ARC";
    }
} public class Attdef extends EntityObject {

    public double elevation;

    public Point2D insertionPoint;

    public Point2D alignPoint;

    public Point3D extrusion;

    public double thickness;

    public double obliqueAngle;

    public double rotationAngle;

    public double height;

    public double widthFactor;

    public String defaultValue;

    public int generation;

    public int horizontalAlignment;

    public int verticalAlignment;

    public String tag;

    public int fieldLength;

    public int flags;

    public boolean lockPosition;

    public int version;

    public String prompt;

    private Handle styleHandle;

    public Attdef(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.5 ATTDEF (3) page 108

        int dataFlags = dataStream.getUnsignedRC();
        if ((dataFlags & 0x01) == 0) {
            elevation = dataStream.getRD();
        }
        insertionPoint = dataStream.get2RD();
        if ((dataFlags & 0x02) == 0) {
            alignPoint = dataStream.get2DD(insertionPoint);
        }
        extrusion = dataStream.getBE();
        thickness = dataStream.getBT();
        if ((dataFlags & 0x04) == 0) {
            obliqueAngle = dataStream.getRD();
        }
        if ((dataFlags & 0x08) == 0) {
            rotationAngle = dataStream.getRD();
        }
        height = dataStream.getRD();
        if ((dataFlags & 0x10) == 0) {
            widthFactor = dataStream.getRD();
        }
        defaultValue = stringStream.getTU();
        if ((dataFlags & 0x20) == 0) {
            generation = dataStream.getBS();
        }
        if ((dataFlags & 0x40) == 0) {
            horizontalAlignment = dataStream.getBS();
        }
        if ((dataFlags & 0x80) == 0) {
            verticalAlignment = dataStream.getBS();
        }
        tag = stringStream.getTU();
        fieldLength = dataStream.getBS();
        flags = dataStream.getRC();
        lockPosition = dataStream.getB();
        
        // Version appears to be missing.
        // Version makes sense for ATTRIB but not ATTDEF anyway.
//        version = dataStream.getRC();
        
        prompt = stringStream.getTU();
        styleHandle = handleStream.getHandle(handleOfThisObject);
        
        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

	public String toString() {
		return "ATTDEF";
	}

    public CadObject getStyle() {
        CadObject result = objectMap.parseObject(styleHandle);
        return (CadObject) result;
    }

} public class Attrib extends EntityObject {

    public double elevation;

    public Point2D insertionPoint;

    public Point2D alignPoint;

    public Point3D extrusion;

    public double thickness;

    public double obliqueAngle;

    public double rotationAngle;

    public double height;

    public double widthFactor;

    public String value;

    public int generation;

    public int horizontalAlignment;

    public int verticalAlignment;

    public String tag;

    public int fieldLength;

    public int flags;

    public boolean lockPosition;

    public int version;

    private Handle styleHandle;

    public Attrib(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.4 ATTRIB (2) page 107

        int dataFlags = dataStream.getUnsignedRC();
        if ((dataFlags & 0x01) == 0) {
            elevation = dataStream.getRD();
        }
        insertionPoint = dataStream.get2RD();
        if ((dataFlags & 0x02) == 0) {
            alignPoint = dataStream.get2DD(insertionPoint);
        }
        extrusion = dataStream.getBE();
        thickness = dataStream.getBT();
        if ((dataFlags & 0x04) == 0) {
            obliqueAngle = dataStream.getRD();
        }
        if ((dataFlags & 0x08) == 0) {
            rotationAngle = dataStream.getRD();
        }
        height = dataStream.getRD();
        if ((dataFlags & 0x10) == 0) {
        widthFactor = dataStream.getRD();
        }
        value = stringStream.getTU();
        if ((dataFlags & 0x20) == 0) {
        generation = dataStream.getBS();
        }
        if ((dataFlags & 0x40) == 0) {
            horizontalAlignment = dataStream.getBS();
        }
        if ((dataFlags & 0x80) == 0) {
            verticalAlignment = dataStream.getBS();
        }
        version = dataStream.getRC();
        tag = stringStream.getTU();
        fieldLength = dataStream.getBS();
        flags = dataStream.getRC();
        lockPosition = dataStream.getB();

        styleHandle = handleStream.getHandle(handleOfThisObject);
        
        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

	public String toString() {
		return "ATTRIB";
	}

    public CadObject getStyle() {
        CadObject result = objectMap.parseObject(styleHandle);
        return (CadObject) result;
    }

} public class BlockControlObj extends NonEntityObject {

    private Handle[] blockObjectHandles;
    private Handle modelSpaceHandle;
    private Handle paperSpaceHandle;

	public BlockControlObj(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.49 BLOCK CONTROL (48) page 154

        int numentries = dataStream.getBL();
        
        // The handles
        
        blockObjectHandles = new Handle[numentries];
        for (int i = 0; i < numentries; i++) {
            blockObjectHandles[i] = handleStream.getHandle(handleOfThisObject);
        }
        modelSpaceHandle = handleStream.getHandle();
        paperSpaceHandle = handleStream.getHandle();
        
        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
		return "BLOCK CONTROL OBJ";
	}

    public List<BlockHeader> getBlockHeaders()
    {
        return new AbstractList<BlockHeader>() {

            @Override
            public BlockHeader get(int index)
            {
                CadObject result = objectMap.parseObjectPossiblyNull(blockObjectHandles[index]);
                return (BlockHeader) result;
            }

            @Override
            public int size()
            {
                return blockObjectHandles.length;
            }
        };
    }

    public CadObject getModelSpace() {
        CadObject result = objectMap.parseObjectPossiblyNull(modelSpaceHandle);
        return (CadObject) result;
    }

    public CadObject getPaperSpace() {
        CadObject result = objectMap.parseObjectPossiblyNull(paperSpaceHandle);
        return (CadObject) result;
    }

} public class BlockHeader extends NonEntityObject {

    public String entryName;
    public boolean sixtyFourFlag;
    private boolean blockIsXref;
    private boolean xrefOverlaid;
    public boolean loadedBit;
    public Point3D basePoint;
    public String xrefPName;
    public String blockDescription;
    private int[] previewData;
    public int insertUnits;
    public boolean explodable;
    public int blockScaling;
    private Handle firstEntityHandle;
    private Handle lastEntityHandle;
    private Handle[] ownedObjectHandles;
    private Handle endBlockHandle;
    private List<List<Handle>> insertHandles;
    private Handle layoutHandle;

    public BlockHeader(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream,
            FileVersion fileVersion) {
        // 19.4.50 BLOCK HEADER (49) page 155

        entryName = stringStream.getTU();
        sixtyFourFlag = dataStream.getB();
        /*
         * At this point we always seem to be three bits prior to a word that
         * best matches the expected flags. We get there reliably if we don't
         * read the xrefindex value. TODO investigate this as this code may not
         * be correct.
         */
        // int xrefordinal = dataStream.getBS();
        boolean xdep = dataStream.getB();
        boolean anonymous = dataStream.getB();
        boolean hasAttributes = dataStream.getB();
        blockIsXref = dataStream.getB();
        xrefOverlaid = dataStream.getB();
        loadedBit = dataStream.getB();
        int ownedObjectCount = dataStream.getBL();
        basePoint = dataStream.get3BD();
        xrefPName = stringStream.getTU();

        List<Integer> insertCounts = new ArrayList<>();
        int thisInsertCount = dataStream.getRC();
        while (thisInsertCount != 0) {
            insertCounts.add(thisInsertCount);
            thisInsertCount = dataStream.getRC();
        }

        blockDescription = stringStream.getTU();
        int sizeOfPreviewData = dataStream.getBL();
        previewData = dataStream.getBytes(sizeOfPreviewData);
        insertUnits = dataStream.getBS();
        explodable = dataStream.getB();
        blockScaling = dataStream.getRC();

        // The handles

        if (!blockIsXref && !xrefOverlaid) {
            firstEntityHandle = handleStream.getHandle(handleOfThisObject);
            lastEntityHandle = handleStream.getHandle(handleOfThisObject);
        }

        ownedObjectHandles = new Handle[ownedObjectCount];
        for (int i = 0; i < ownedObjectCount; i++) {
            ownedObjectHandles[i] = handleStream.getHandle();
        }

        endBlockHandle = handleStream.getHandle();

        insertHandles = new ArrayList<>();
        for (Integer insertCount : insertCounts) {
            List<Handle> thisList = new ArrayList<>();
            for (int i = 0; i < insertCount; i++) {
                Handle insertHandle = handleStream.getHandle();
                thisList.add(insertHandle);
            }
            insertHandles.add(thisList);
        }

        layoutHandle = handleStream.getHandle();

        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    @Override
    public String toString() {
        return "BLOCK HEADER";
    }

    public CadObject getFirstEntity() {
        if (firstEntityHandle == null) {
            return null;
        } else {
            CadObject result = objectMap.parseObjectPossiblyNull(firstEntityHandle);
            return result;
        }
    }

    public CadObject getLastEntity() {
        if (lastEntityHandle == null) {
            return null;
        } else {
            CadObject result = objectMap.parseObjectPossiblyNull(lastEntityHandle);
            return result;
        }
    }

    public List<CadObject> getOwnedObjects() {
        return new AbstractList<CadObject>() {

            @Override
            public CadObject get(int index) {
                CadObject result = objectMap.parseObject(ownedObjectHandles[index]);
                return result;
            }

            @Override
            public int size() {
                return ownedObjectHandles.length;
            }
        };
    }

    public CadObject getEndBlock() {
        CadObject result = objectMap.parseObject(endBlockHandle);
        return result;
    }

    /**
     * This method returns all inserts in a flat list. This no doubt needs to be
     * changed to return a two dimensional list in case consumers need the extra
     * information.
     */
    public List<Insert> getInserts() {
        List<Insert> result = new ArrayList<>();

        for (List<Handle> x : insertHandles) {
            for (Handle y : x) {
                CadObject insert = objectMap.parseObjectPossiblyOrphaned(y);
                result.add((Insert) insert);
            }
        }
        return result;
    }

    public CadObject getLayout() {
        CadObject result = objectMap.parseObjectPossiblyNull(layoutHandle);
        return result;
    }

} public abstract class CadObject {

    protected final ObjectMap objectMap;

    public Handle handleOfThisObject;

    // Defined in this class but always set in derived classes
    protected Handle[] reactorHandles;

    private List<Handle> genericHandles = new ArrayList<>();

    private Map<Handle, Object[]> extendedEntityData = new HashMap<>();

    // Defined in this class but always set in derived classes
    protected Handle xdicobjhandle;

    public CadObject(ObjectMap objectMap) {
        this.objectMap = objectMap;
    }

    public void readFromStreams(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream,
            FileVersion fileVersion) {
        handleOfThisObject = dataStream.getHandle();

        // Page 254 Chapter 27 Extended Entity Data

        int sizeOfExtendedObjectData = dataStream.getBS();
        while (sizeOfExtendedObjectData != 0) {
            Handle appHandle = dataStream.getHandle();

            List<Object> components = new ArrayList<>();
            Stack<List<Object>> componentStack = new Stack<>();

            int expected = dataStream.position() + sizeOfExtendedObjectData * 8;

            while (dataStream.position() < expected) {
                int dxfGroupCode = dataStream.getRC();

                switch (dxfGroupCode) {
                case 0: {
                    int length = dataStream.getRC();
                    byte[] x = new byte[length * 2];
                    for (int i = 0; i < length * 2; i++) {
                        x[i] = (byte) dataStream.getRC();
                    }
                    try {
                        String text = new String(x, "UTF-16");
                        components.add(text);
                    } catch (UnsupportedEncodingException e) {
                        throw new RuntimeException("Should not happen");
                    }

                    int unknown = dataStream.getRC();

                    break;
                }

                case 2: {
                    int x = dataStream.getRC();
                    switch (x) {
                    case 0:
                        componentStack.push(components);
                        components = new ArrayList<>();
                        break;
                    case 1:
                        List<Object> justCompletedList = components;
                        components = componentStack.pop();
                        components.add(justCompletedList.toArray());
                        break;
                    }
                }
                break;

                case 3:
                case 5: {
                    int[] handleBytes = dataStream.getBytes(8);
                    Handle handle = new Handle(5, handleBytes);
                    // TODO distinguish between 3 and 5, otherwise
                    // the information is lost.
                    components.add(handle);
                }
                break;

                case 4: {
                    int length = dataStream.getRC();
                    byte[] x = new byte[length];
                    for (int i = 0; i < length; i++) {
                        x[i] = (byte) dataStream.getRC();
                    }
                    components.add(x);
                }
                break;

                case 10:
                case 11:
                case 12:
                case 13: {
                    double x = dataStream.getRD();
                    double y = dataStream.getRD();
                    double z = dataStream.getRD();
                    Point3D point = new Point3D(x, y, z);
                    components.add(point);
                }
                break;

                case 40:
                case 41:
                case 42: {
                    double x = dataStream.getRD();
                    components.add(x);
                }
                break;

                case 70: {
                    int x = dataStream.getRS();
                    components.add(Short.valueOf((short) x));
                }
                break;

                case 71: {
                    int x = dataStream.getRL();
                    components.add(Long.valueOf(x));
                }
                break;

                default:
                    throw new RuntimeException("Unexpected case");
                }
            }

            assert dataStream.position() == expected;

            extendedEntityData.put(appHandle, components.toArray());

            sizeOfExtendedObjectData = dataStream.getBS();
        }

        readPostCommonFields(dataStream, stringStream, handleStream, fileVersion);
    }

    abstract protected void readPostCommonFields(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream,
            FileVersion fileVersion);

    protected void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream,
            FileVersion fileVersion) {
        // For the time being, provide this default implementation that
        // just reads all the handles.
        // Ultimately this should be an abstract method.

        try {
            do {
                Handle referencedHandle = handleStream.getHandle(handleOfThisObject);
                genericHandles.add(referencedHandle);
            } while (true);
        } catch (RuntimeException e) {

        }
        handleStream.advanceToByteBoundary();
        handleStream.assertEndOfStream();
    }

    public List<CadObject> getReactors() {
        return new AbstractList<CadObject>() {

            @Override
            public CadObject get(int index) {
                if (reactorHandles[index] == null) {
                    return null;
                } else {
                    CadObject result = objectMap.parseObject(reactorHandles[index]);
                    return result;
                }
            }

            @Override
            public int size() {
                return reactorHandles.length;
            }
        };
    }

    public Dictionary getXDictionary() {
        if (xdicobjhandle == null) {
            return null;
        } else {
            CadObject result = objectMap.parseObject(xdicobjhandle);
            return (Dictionary) result;
        }
    }

    public Map<Appid, Object[]> getExtendedEntityData() {
        return new AbstractMap<Appid, Object[]>() {

            @Override
            public Set<java.util.Map.Entry<Appid, Object[]>> entrySet() {
                return new AbstractSet<java.util.Map.Entry<Appid, Object[]>>() {

                    @Override
                    public Iterator<java.util.Map.Entry<Appid, Object[]>> iterator() {
                        final Iterator<java.util.Map.Entry<Handle, Object[]>> iter = extendedEntityData.entrySet()
                                .iterator();
                        return new Iterator<java.util.Map.Entry<Appid, Object[]>>() {

                            @Override
                            public boolean hasNext() {
                                return iter.hasNext();
                            }

                            @Override
                            public java.util.Map.Entry<Appid, Object[]> next() {
                                final java.util.Map.Entry<Handle, Object[]> e = iter.next();
                                return new java.util.Map.Entry<Appid, Object[]>() {

                                    @Override
                                    public Appid getKey() {
                                        return (Appid) objectMap.parseObject(e.getKey());
                                    }

                                    @Override
                                    public Object[] getValue() {
                                        return e.getValue();
                                    }

                                    @Override
                                    public Object[] setValue(Object[] arg0) {
                                        throw new UnsupportedOperationException();
                                    }
                                };
                            }

                            @Override
                            public void remove() {
                                throw new UnsupportedOperationException();
                            }
                        };
                    }

                    @Override
                    public int size() {
                        return extendedEntityData.size();
                    }
                };
            }
        };
    }

    public Dictionary getXdicobj() {
        if (xdicobjhandle == null) {
            return null;
        } else {
            CadObject result = objectMap.parseObject(xdicobjhandle);
            return (Dictionary) result;
        }
    }

    public List<CadObject> getGenericObjects() {
        return new AbstractList<CadObject>() {

            @Override
            public CadObject get(int index) {
                CadObject result = objectMap.parseObjectPossiblyNullOrOrphaned(genericHandles.get(index));
                return result;
            }

            @Override
            public int size() {
                return genericHandles.size();
            }
        };
    }
} public class Circle extends EntityObject {

    public Point3D center;
    public double radius;
    public double thickness;
    public Point3D extrusion;

    public Circle(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.19 CIRCLE (18) page 118    

        center = dataStream.get3BD();
        radius = dataStream.getBD();
        thickness = dataStream.getBT();
        extrusion = dataStream.getBE();

        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
        return "CIRCLE";
    }
} public class Dictionary extends NonEntityObject {

    public Map<String, Handle> dictionaryMap;

    public Dictionary(ObjectMap objectMap) {
        super(objectMap);
    }
    
    @Override
	public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.42 DICTIONARY (42)

        readCommonDictionaryData(dataStream, stringStream, handleStream);
        
        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
	}

    protected void readCommonDictionaryData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream) {
        int numItems = dataStream.getBL();

        int cloningFlag = dataStream.getBS();
        int hardOwnerFlag = dataStream.getRC();
    
        // The handles
        
        dictionaryMap = new HashMap<>();
        for (int i = 0; i < numItems; i++) {
            String key = stringStream.getTU();
            Handle handle = handleStream.getHandle(handleOfThisObject);
            dictionaryMap.put(key, handle);
        }
    }

	public String toString() {
		return "DICTIONARY";
	}

    public CadObject lookupObject(String key)
    {
        Handle handle = dictionaryMap.get(key);
        return objectMap.parseObjectPossiblyNull(handle);
    }

    public Set<String> getKeys() {
        return dictionaryMap.keySet();
    }

} public abstract class EntityObject extends CadObject {

    public Handle parentHandle;
    private Handle layerHandle;
    private Handle linetypeHandle;
    private Handle materialHandle;
    private Handle plotstyleHandle;

    public EntityObject(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readPostCommonFields(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {

        // 19.4.1 Common entity data, page 104

        boolean graphicImageFlag = dataStream.getB();
        if (graphicImageFlag) {
            // 28 Proxy Entity Graphics, page 255

            long sizeOfGraphicImageInBytes = dataStream.getRL();
            long type = dataStream.getRL();
            for (int i = 0; i < sizeOfGraphicImageInBytes*8; i++) {
                dataStream.getB();
            }
        }

        // entMode is documented only for R13-R14 on page 101.  However its meaning
        // appears to be unchanged in 2010+.
        int entMode = dataStream.getBB();

        // Generally, entMode indicates whether or not the owner relative handle reference is present.
        boolean hasOwnerHandleReference;
        switch (entMode) {
        case 0:
            // The owner relative handle reference is present.
            // Applies to the following:
            // VERTEX, ATTRIB, and SEQEND.
            // BLOCK, ENDBLK, and the defining entities in all
            // block defs except *MODEL_SPACE and *PAPER_SPACE.
            hasOwnerHandleReference = true;
            break;
        case 1:
            // PSPACE entity without a owner relative handle ref.
            hasOwnerHandleReference = false;
            break;
        case 2:
            // MSPACE entity without a owner relative handle ref.
            hasOwnerHandleReference = false;
            break;
        default:
            throw new RuntimeException("unexpected value for 'entMode': 3");
        }

        int numReactors = dataStream.getBS();

        // It appears that the xDicMissingFlag is not included.  As it has been known before for fields in the spec to actually not be there,
        // a process of skipping the reading of each field in turn was tried.  Perhaps the xdic is never there for entities.
        // TODO This needs more investigation.

        //        xDicMissingFlag = dataStream.getB();

        boolean hasBinaryData = false;
        if (fileVersion.is2013OrLater()) {
            hasBinaryData = dataStream.getB();
        }
        boolean areLinkersPresent = dataStream.getB();
        CmColor entityColor = dataStream.getENC();
        double linetypeScale = dataStream.getBD();
        int linetypeFlag = dataStream.getBB();
        int plotstyleFlag = dataStream.getBB();
        int materialFlag = dataStream.getBB();
        int shadowFlags = dataStream.getRC();
        boolean hasFullVisualStyle = dataStream.getB();
        boolean hasFaceVisualStyle = dataStream.getB();
        boolean hasEdgeVisualStyle = dataStream.getB();
        int isInvisible = dataStream.getBS();
        int entityLineweightFlag = dataStream.getRC();

        // 19.4.2 Common Entity Handle Data page 105

        if (hasOwnerHandleReference) {
            parentHandle = handleStream.getHandle(handleOfThisObject);
        }

        reactorHandles = new Handle[numReactors];
        for (int i = 0; i< numReactors; i++) {
            Handle reactorHandle = handleStream.getHandle(handleOfThisObject);
            reactorHandles[i] = reactorHandle;
        }

        // Correct for 2013, may not be correct for 2010...
        boolean xDicMissingFlag = hasBinaryData || !fileVersion.is2013OrLater();
        if (!xDicMissingFlag) {
            Handle xdicobjhandle = handleStream.getHandle();
        }

        // This seems to not be present???
        //        Handle colorBookColorHandle = handleStream.getHandle();

        layerHandle = handleStream.getHandle();
        CadObject myLayer = objectMap.parseObject(layerHandle);
        if (myLayer instanceof Dictionary) {
            // try next handle
            layerHandle = handleStream.getHandle();
            CadObject myLayer2 = objectMap.parseObject(layerHandle);
        }

        if (linetypeFlag == 3) {
            linetypeHandle = handleStream.getHandle();
        }
        if (materialFlag == 3) {
            materialHandle = handleStream.getHandle();
        }
        if (plotstyleFlag == 3) {
            plotstyleHandle = handleStream.getHandle();
        }

        readObjectTypeSpecificData(dataStream, stringStream, handleStream, fileVersion);
    }

    public Layer getLayer()
    {
        CadObject result = objectMap.parseObject(layerHandle);
        if (this instanceof Insert && result instanceof Dictionary) {
            return null;
        }
        return (Layer) result;
    }

    public LType getLinetype() {
        if (linetypeHandle == null) {
            return null;
        } else {
            CadObject result = objectMap.parseObject(linetypeHandle);
            return (LType) result;
        }
    }

    public NonEntityObject getMaterial() {
        if (materialHandle == null) {
            return null;
        } else {
            CadObject result = objectMap.parseObject(materialHandle);
            return (NonEntityObject) result;
        }
    }

    public NonEntityObject getPlotstyle() {
        if (plotstyleHandle == null) {
            return null;
        } else {
            CadObject result = objectMap.parseObject(plotstyleHandle);
            return (NonEntityObject) result;
        }
    }
} public class Insert extends EntityObject {

    public Point3D insertPoint;
    public double xScaleFactor;
    public double yScaleFactor;
    public double zScaleFactor;
    public double rotation;
    public Point3D extrusion;
    private Handle blockHeaderHandle;
    private Handle[] ownedObjectHandles;
    private Handle seqEndHandle;

    public Insert(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.9 INSERT (7) page 111

        insertPoint = dataStream.get3BD();
        int dataFlags = dataStream.getBB();
        switch (dataFlags) {
        case 0:
            xScaleFactor = dataStream.getRD();
            yScaleFactor = dataStream.getDD(xScaleFactor);
            zScaleFactor = dataStream.getDD(xScaleFactor);
            break;
        case 1:
            xScaleFactor = 1.0;
            yScaleFactor = dataStream.getDD(xScaleFactor);
            zScaleFactor = dataStream.getDD(xScaleFactor);
            break;
        case 2:
            xScaleFactor = dataStream.getRD();
            yScaleFactor = xScaleFactor;
            zScaleFactor = xScaleFactor;
            break;
        case 3:
            xScaleFactor = 1.0;
            yScaleFactor = 1.0;
            zScaleFactor = 1.0;
            break;
        }

        rotation = dataStream.getBD();
        extrusion = dataStream.get3BD();
        boolean hasAttributes = dataStream.getB();
        int ownedObjectCount = 0;
        if (hasAttributes) {
            ownedObjectCount = dataStream.getBL();
        }

        // The handles

        blockHeaderHandle = handleStream.getHandle();

        ownedObjectHandles = new Handle[ownedObjectCount];
        for (int i = 0; i< ownedObjectCount; i++) {
            ownedObjectHandles[i] = handleStream.getHandle();
        }

        if (hasAttributes) {
            seqEndHandle = handleStream.getHandle();
        }

        handleStream.advanceToByteBoundary();
        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    @Override
    public String toString() {
        return "INSERT";
    }

    public BlockHeader getBlockHeader() {
        CadObject result = objectMap.parseObject(blockHeaderHandle);
        return (BlockHeader) result;
    }

    public List<CadObject> getOwnedObjects()
    {
        return new AbstractList<CadObject>() {

            @Override
            public CadObject get(int index)
            {
                CadObject result = objectMap.parseObject(ownedObjectHandles[index]);
                return result;
            }

            @Override
            public int size()
            {
                return ownedObjectHandles.length;
            }
        };
    }

    public CadObject getSeqEnd() {
        if (seqEndHandle == null) {
            return null;
        } else {
            CadObject result = objectMap.parseObject(seqEndHandle);
            return result;
        }
    }

} public class LType extends NonEntityObject {

    public class Dash
    {
        public double dashLength;
        public int complexShapecode;
        public double xOffset;
        public double yOffset;
        public double scale;
        public double rotation;
        public int shapeFlag;
        private Handle shapefileForDashHandle;
        public String textToBeDrawn;

        public void readFromDataStream(BitBuffer dataStream) {
            dashLength = dataStream.getBD();
            complexShapecode = dataStream.getBS();
            xOffset = dataStream.getRD();
            yOffset = dataStream.getRD();
            scale = dataStream.getBD();
            rotation = dataStream.getBD();
            shapeFlag = dataStream.getBS();
        }
        
        public void readFromHandleStream(BitBuffer handleStream) {
            shapefileForDashHandle = handleStream.getHandle();
        }

        public CadObject getShapefileForDash() {
            CadObject result = objectMap.parseObjectPossiblyNull(shapefileForDashHandle);
            return (CadObject) result;
        }

        public void extractTextFromTextArea(int[] textArea) {
            int textLength = 0;
            for (int i = complexShapecode; i < textArea.length && textArea[i] != 0; i++) {
                textLength++;
            }
            byte[] extractedBytes = new byte[textLength];
            for (int j = 0; j < textLength; j++) {
                extractedBytes[j] = (byte)textArea[complexShapecode+j];
            }
            try {
                textToBeDrawn = new String(extractedBytes, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                objectMap.getIssues().addWarning("Invalid text in LType text area.  The text is not valid UTF-8.");
            }
        }
    }

    public String entryName;
    public boolean sixtyFourFlag;
    public boolean xDep;
    public String description;
    public double patternLen;
    public Dash[] dashes;
    private Handle externalReferenceBlockHandle;

    public LType(ObjectMap objectMap) {
        super(objectMap);
    }
    
    @Override
	public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
		// 19.4.56 LTYPE 57 page 162  

		entryName = stringStream.getTU();
		
		sixtyFourFlag = dataStream.getB();
		xDep = dataStream.getB();
		description = stringStream.getTU();
		patternLen = dataStream.getBD();
		dataStream.expectRC(65);
		int numDashes = dataStream.getRC();

		boolean isTextAreaPresent = false;
		
		dashes = new Dash[numDashes];
        for (int i = 0; i < numDashes; i++) {
            dashes[i] = new Dash();
            dashes[i].readFromDataStream(dataStream);
            
            isTextAreaPresent = isTextAreaPresent || ((dashes[i].shapeFlag & 0x02) != 0);
        }
		
		// The 512 byte area
        if (isTextAreaPresent) {
            int[] textArea = dataStream.getBytes(512);
            
            for (int i = 0; i < numDashes; i++) {
                if (((dashes[i].shapeFlag & 0x02) != 0)) {
                    dashes[i].extractTextFromTextArea(textArea);
                }
            }
        }

		// The handles.
		
		externalReferenceBlockHandle = handleStream.getHandle();
		for (int i = 0; i < numDashes; i++) {
            dashes[i].readFromHandleStream(handleStream);
        }
		
		handleStream.advanceToByteBoundary();
        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
		handleStream.assertEndOfStream();
	}

	public String toString() {
		return "LTYPE";
	}

    public CadObject getExternalReferenceBlock() {
        CadObject result = objectMap.parseObjectPossiblyNull(externalReferenceBlockHandle);
        return (CadObject) result;
    }

} public class LTypeControlObj extends NonEntityObject {

	private Handle[] lineTypeHandles;
	private Handle bylayerLinetypeHandle;
	private Handle byblockLinetypeHandle;

	public LTypeControlObj(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
	public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
		// 19.4.55 LINETYPE CONTROL (56)
		
		int numEntries = dataStream.getBL();

		dataStream.assertEndOfStream();
		
		// Here starts the handle area
		
		lineTypeHandles = new Handle[numEntries];
		for (int i = 0; i < numEntries; i++){
		    lineTypeHandles[i] = handleStream.getHandle(handleOfThisObject);
		}

		bylayerLinetypeHandle = handleStream.getHandle();
		byblockLinetypeHandle = handleStream.getHandle();
		
		handleStream.advanceToByteBoundary();
		handleStream.assertEndOfStream();
	}

	public String toString() {
		return "LTYPE CONTROL OBJ";
	}

    public List<LType> getLineTypes() {
        return new AbstractList<LType>() {

            @Override
            public LType get(int index)
            {
                return (LType)objectMap.parseObjectPossiblyNull(lineTypeHandles[index]);
            }

            @Override
            public int size()
            {
                return lineTypeHandles.length;
            }
        };
    }

    public LType getBylayerLinetype() {
        CadObject result = objectMap.parseObject(bylayerLinetypeHandle);
        return (LType) result;
    }

    public LType getByblockLinetype() {
        CadObject result = objectMap.parseObject(byblockLinetypeHandle);
        return (LType) result;
    }

} public class Layer extends NonEntityObject {

    public String entryName;
    public CmColor color;
    private Handle externalReferenceBlockHandle;
    private Handle plotStyleHandle;
    private Handle lineTypeHandle;
    private Handle materialHandle;

    public Layer(ObjectMap objectMap) {
        super(objectMap);
    }
    
    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.52 LAYER (51) page 158

        entryName = stringStream.getTU();
        boolean sixtyFourFlag = dataStream.getB();
        /*
         * At this point we always seem to be three bits prior to a word
         * that best matches the expected flags.  We get there reliably if
         * we don't read the xrefindex value.  TODO investigate this as this
         * code may not be correct.
         */
//        int xrefordinal = dataStream.getBS();
        boolean xdep = dataStream.getB();
        int flags = dataStream.getBS();
        color = dataStream.getCMC();
        
        // The handles
        
        externalReferenceBlockHandle = handleStream.getHandle();
        plotStyleHandle = handleStream.getHandle();
        materialHandle = handleStream.getHandle(handleOfThisObject);
        lineTypeHandle = handleStream.getHandle(handleOfThisObject);

        // It appears that, contrary to the specification, this handle is not present in 2010 (R24).
        if (fileVersion.is2013OrLater()) {
            Handle nullHandle = handleStream.getHandle();
        }

        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
        return "LAYER";
    }

    public CadObject getExternalReferenceBlock() {
        CadObject result = objectMap.parseObjectPossiblyNull(externalReferenceBlockHandle);
        return (CadObject) result;
    }

    public AcdbPlaceHolder getPlotStyle() {
        CadObject result = objectMap.parseObject(plotStyleHandle);
        return (AcdbPlaceHolder) result;
    }

    public CadObject getLineType() {
        CadObject result = objectMap.parseObject(lineTypeHandle);
        return (CadObject) result;
    }

    public CadObject getMaterial() {
        CadObject result = objectMap.parseObject(materialHandle);
        return (CadObject) result;
    }

} public class LayerControlObj extends NonEntityObject {

    public Handle[] layerObjectHandles;

    public LayerControlObj(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream,
            FileVersion fileVersion) {
        // 19.4.51 LAYER CONTROL (50) page 157

        int numentries = dataStream.getBL();

        // The handles

        layerObjectHandles = new Handle[numentries];
        for (int i = 0; i < numentries; i++) {
            layerObjectHandles[i] = handleStream.getHandle(handleOfThisObject);
        }

        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    @Override
    public String toString() {
        return "LAYER CONTROL OBJ";
    }

    public List<Layer> getLayers() {
        return new AbstractList<Layer>() {

            @Override
            public Layer get(int index) {
                CadObject result = objectMap.parseObjectPossiblyNull(layerObjectHandles[index]);
                return (Layer) result;
            }

            @Override
            public int size() {
                return layerObjectHandles.length;
            }
        };
    }

} public class Layout extends NonEntityObject {

    public String pageSetupName;
    public String printerOrConfig;
    public int plotLayoutFlags;
    public double leftMargin;
    public double bottomMargin;
    public double rightMargin;
    public double topMargin;
    public double paperWidth;
    public double paperHeight;
    public String paperSize;
    public Point2D plotOrigin;
    public int paperUnits;
    public int plotRotation;
    public int plotType;
    public Point2D windowMinimum;
    public Point2D windowMaximum;
    public double realWorldUnits;
    public double drawingUnits;
    public String currentStyleSheet;
    public int scaleType;
    public double scaleFactor;
    public Point2D paperImageOrigin;
    public int shadePlotMode;
    public int shadePlotResLevel;
    public int shadePlotCustomDpi;
    public String layoutName;
    public int tabOrder;
    public int flag;
    public Point3D ucsOrigin;
    public Point2D limitMinimum;
    public Point2D limitMaximum;
    public Point3D insertionPoint;
    public Point3D ucsXAxis;
    public Point3D ucsYAxis;
    public double elevation;
    public int orthographicViewType;
    public Point3D extentMinimum;
    public Point3D extentMaximum;
    private Handle plotViewHandle;
    private Handle visualStyleHandle;
    private Handle paperspaceBlockRecordHandle;
    private Handle lastActiveViewportHandle;
    private Handle baseUcsHandle;
    private Handle namedUcsHandle;
    private Handle[] viewportHandles;

    public Layout(ObjectMap objectMap) {
        super(objectMap);
    }
    
    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.81 LAYOUT (82)  page 196

        pageSetupName = stringStream.getTU();
        printerOrConfig = stringStream.getTU();
        plotLayoutFlags = dataStream.getBS();
        leftMargin = dataStream.getBD();
        bottomMargin = dataStream.getBD();
        rightMargin = dataStream.getBD();
        topMargin = dataStream.getBD();
        paperWidth = dataStream.getBD();
        paperHeight = dataStream.getBD();
        paperSize = stringStream.getTU();
        plotOrigin = dataStream.get2BD();
        paperUnits = dataStream.getBS();
        plotRotation = dataStream.getBS();
        plotType = dataStream.getBS();
        windowMinimum = dataStream.get2BD();
        windowMaximum = dataStream.get2BD();
        realWorldUnits = dataStream.getBD();
        drawingUnits = dataStream.getBD();
        currentStyleSheet = stringStream.getTU();
        scaleType = dataStream.getBS();
        scaleFactor = dataStream.getBD();
        paperImageOrigin = dataStream.get2BD();
        shadePlotMode = dataStream.getBS();
        shadePlotResLevel = dataStream.getBS();
        shadePlotCustomDpi = dataStream.getBS();
        layoutName = stringStream.getTU();
        tabOrder = dataStream.getBS();
        flag = dataStream.getBS();
        ucsOrigin = dataStream.get3BD();
        limitMinimum = dataStream.get2RD();
        limitMaximum = dataStream.get2RD();
        insertionPoint = dataStream.get3BD();
        ucsXAxis = dataStream.get3BD();
        ucsYAxis = dataStream.get3BD();
        elevation = dataStream.getBD();
        orthographicViewType = dataStream.getBS();
        extentMinimum = dataStream.get3BD();
        extentMaximum = dataStream.get3BD();
        
        // Only 2 bits left, so certainly not a raw long!
        // Bit long more likely.
        int viewportCount = dataStream.getBL();
    
        // The handles

        plotViewHandle = handleStream.getHandle();
        visualStyleHandle = handleStream.getHandle(handleOfThisObject);
        paperspaceBlockRecordHandle = handleStream.getHandle(handleOfThisObject);
        lastActiveViewportHandle = handleStream.getHandle(handleOfThisObject);
        baseUcsHandle = handleStream.getHandle();
        namedUcsHandle = handleStream.getHandle();
        viewportHandles = new Handle[viewportCount];
        for (int i = 0; i < viewportCount; i++) {
            viewportHandles[i] = handleStream.getHandle(handleOfThisObject);
        }
        
        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public CadObject getPlotView() {
        CadObject result = objectMap.parseObjectPossiblyNull(plotViewHandle);
        return result;
    }

    public CadObject getVisualStyle() {
        CadObject result = objectMap.parseObjectPossiblyNull(visualStyleHandle);
        return result;
    }

    public BlockHeader getPaperspaceBlockRecord() {
        CadObject result = objectMap.parseObject(paperspaceBlockRecordHandle);
        return (BlockHeader)result;
    }

    public CadObject getLastActiveViewport() {
        CadObject result = objectMap.parseObjectPossiblyNull(lastActiveViewportHandle);
        if (!(result == null || result instanceof VPort || result instanceof ViewPort)) {
            throw new RuntimeException("unexpected object class");
        }
        return result;
    }

    public CadObject getBaseUcs() {
        CadObject result = objectMap.parseObjectPossiblyNull(baseUcsHandle);
        return result;
    }

    public CadObject getNamedUcs() {
        CadObject result = objectMap.parseObjectPossiblyNull(namedUcsHandle);
        return result;
    }

    // TODO common base for ViewPort and VPort ????
    public List<CadObject> getViewPorts()
    {
        return new AbstractList<CadObject>() {

            @Override
            public CadObject get(int index)
            {
                CadObject result = objectMap.parseObject(viewportHandles[index]);
                if (!(result instanceof VPort || result instanceof ViewPort)) {
                    throw new RuntimeException("unexpected object class");
                }
                return (CadObject) result;
            }

            @Override
            public int size()
            {
                return viewportHandles.length;
            }
        };
    }

	public String toString() {
		return "LAYOUT";
	}

} public class Line extends EntityObject {

    public Point3D start;
    public Point3D end;
    public double thickness;
    public Point3D extrusion;

    public Line(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {

        // 19.4.20 LINE (19) page 118

        boolean zAreZero = dataStream.getB();

        double startX = dataStream.getRD();
        double endX = dataStream.getDD(startX);
        double startY = dataStream.getRD();
        double endY = dataStream.getDD(startY);
        double startZ;
        double endZ;
        if (zAreZero) {
            startZ = 0;
            endZ = 0;
        } else {
            startZ = dataStream.getRD();
            endZ = dataStream.getDD(startZ);
        }
        start = new Point3D(startX, startY, startZ);
        end = new Point3D(endX, endY, endZ);
        
        thickness = dataStream.getBT();
        extrusion = dataStream.getBE();

        // Read all handles (until we figure out what they are)

//        try {
//            do {
//                Handle referencedHandle = handleStream.getHandle(handleOfThisObject);
//                genericHandles.add(referencedHandle);
//            } while (true);
//        } catch (RuntimeException e) {
//
//        }


        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
        return "LINE";
    }
} public class LwPolyline extends EntityObject {

    public Handle referencedHandle;

    public List<VertexOfLwPolyline> points;

    public Double constantWidth;

    public boolean isClosed;

    public LwPolyline(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {

        boolean b1 = dataStream.getB();
        boolean b2 = dataStream.getB();

        int numberOfPoints;
        int numberOfVariableWidths = 0;
        int numberOfBulges = 0;

        if (b1) {
            // It seems that the polyline is always closed when b1 is set.
            isClosed = true;

            numberOfPoints = dataStream.getBS();
        } else {
            boolean bit1 = dataStream.getB();
            boolean bit2 = dataStream.getB();
            boolean areVariableWidthsPresent = dataStream.getB();
            boolean areBulgesPresent = dataStream.getB();
            dataStream.expectB(false);
            boolean bitC = dataStream.getB();
            dataStream.expectB(false);
            dataStream.expectB(false);

            assert !bit1;
            assert !bit2;

            if (!areVariableWidthsPresent) {

                if (areBulgesPresent != bitC) {
                    System.out.println("different");
                }

                boolean hasWidth = areBulgesPresent;

                if (!b2) {
                    int flags17 = dataStream.getBitsUnsigned(8);
                    if (flags17 != 2) {
                        throw new RuntimeException("new case");
                    }

                    isClosed = true;

                    if (hasWidth) {
                        constantWidth = dataStream.getBD();
                        numberOfPoints = dataStream.getBS();
                        if (numberOfPoints == 0) {
                            System.out.println("it's gone wrong");
                        }
                        if (areVariableWidthsPresent == areBulgesPresent) {
                            System.out.println("here");
                        }
                        if (areVariableWidthsPresent) {
                            numberOfVariableWidths = dataStream.getBS();
                            if (numberOfPoints != numberOfVariableWidths) {
                                throw new RuntimeException();
                            }
                        }
                        if (areBulgesPresent) {
                            numberOfBulges = dataStream.getBS();
                            if (numberOfPoints != numberOfBulges) {
                                throw new RuntimeException();
                            }
                        }
                    } else {
                        if (bitC) {
                            // When this bit is set there appears to be an extra
                            // bit-double.  TODO compare to DXF to determine what
                            // this is.
                            double x = dataStream.getBD();
                        }
                        numberOfPoints = dataStream.getBS();
                        if (numberOfPoints == 0) {
                            System.out.println("it's gone wrong");
                        }
                        numberOfVariableWidths = 0;
                        numberOfBulges = 0;
                    }
                } else {
                    isClosed = false;

                    constantWidth = dataStream.getBD();
                    numberOfPoints = dataStream.getBS();
                    if (numberOfPoints == 0) {
                        System.out.println("it's gone wrong");
                    }

                    numberOfVariableWidths = 0;
                    numberOfBulges = 0;
                    if (hasWidth) {
                        if (areVariableWidthsPresent == areBulgesPresent) {
                            System.out.println("");
                        }
                        if (areVariableWidthsPresent) {
                            numberOfVariableWidths = dataStream.getBS();
                            if (numberOfVariableWidths != numberOfPoints) {
                                throw new RuntimeException();
                            }
                        }
                        if (areBulgesPresent) {
                            numberOfBulges = dataStream.getBS();
                            if (numberOfBulges != numberOfPoints) {
                                throw new RuntimeException();
                            }
                        }
                    }
                }
            } else {
                isClosed = false;

                if (!b2) {
                    // Actually this path is never hit in the test files.
                    System.out.println("this case fails");
                }

                if (areBulgesPresent || bitC) {
                    System.out.println("here");
                }

                numberOfPoints = dataStream.getBS();
                if (areVariableWidthsPresent) {
                    numberOfVariableWidths = dataStream.getBS();
                    if (numberOfVariableWidths != numberOfPoints) {
                        throw new RuntimeException();
                    }
                } else {
                    numberOfBulges = dataStream.getBS();
                    if (numberOfBulges != numberOfPoints) {
                        throw new RuntimeException();
                    }
                }
                if (numberOfPoints != 2 || numberOfVariableWidths != 2) {
                    System.out.println("here");
                }
            }
        }

        points = new ArrayList<>(numberOfPoints);

        double x = dataStream.getRD();
        double y = dataStream.getRD();
        points.add(new VertexOfLwPolyline(x, y));
        for (int index = 1; index < numberOfPoints; index++) {
            x = dataStream.getDD(x);
            y = dataStream.getDD(y);
            points.add(new VertexOfLwPolyline(x, y));
        }

        for (int index = 0; index < numberOfVariableWidths; index++) {
            double startingWidth = dataStream.getBD();
            double endingWidth = dataStream.getBD();
            points.get(index).setVariableWidth(startingWidth, endingWidth);
        }

        for (int index = 0; index < numberOfBulges; index++) {
            double bulge = dataStream.getBD();
            points.get(index).setBulge(bulge);
        }

        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    @Override
    public String toString() {
        return "LWPOLYLINE";
    }

    public static class VertexOfLwPolyline {

        public final Point2D vertex;
        public double startingWidth;
        public double endingWidth;
        public double bulge;

        public VertexOfLwPolyline(double x, double y) {
            vertex = new Point2D(x, y);
        }

        @Override
        public String toString() {
            return vertex.toString();
        }

        public void setVariableWidth(double startingWidth, double endingWidth) {
            this.startingWidth = startingWidth;
            this.endingWidth = endingWidth;
        }

        public void setBulge(double bulge) {
            this.bulge = bulge;
        }
    }
} public static class VertexOfLwPolyline {

    public final Point2D vertex;
    public double startingWidth;
    public double endingWidth;
    public double bulge;

    public VertexOfLwPolyline(double x, double y) {
        vertex = new Point2D(x, y);
    }

    @Override
    public String toString() {
        return vertex.toString();
    }

    public void setVariableWidth(double startingWidth, double endingWidth) {
        this.startingWidth = startingWidth;
        this.endingWidth = endingWidth;
    }

    public void setBulge(double bulge) {
        this.bulge = bulge;
    }
} public class MText extends EntityObject {

    public Point3D insertionPoint;
    public Point3D extrusion;
    public Point3D xAxisDir;
    public double rectWidth;
    public double rectHeight;
    public double textHeight;
    public int attachment;
    public int drawingDir;
    public double extents;
    public double extentsWid;
    public String text;
    public int linespacingStyle;
    public double linespacingFactor;
    public boolean unknownBit;
    public double backgroundScaleFactor;
    public CmColor backgroundColor;
    public int backgroundTransparency;
    private Handle styleHandle;

    public MText(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.44 MTEXT (44) page 146    

        insertionPoint = dataStream.get3BD();
        extrusion = dataStream.get3BD();
        xAxisDir = dataStream.get3BD();
        rectWidth = dataStream.getBD();
        rectHeight = dataStream.getBD();
        textHeight = dataStream.getBD();
        attachment = dataStream.getBS();
        drawingDir = dataStream.getBS();
        extents = dataStream.getBD();
        extentsWid = dataStream.getBD();
        text = stringStream.getTU();
        linespacingStyle = dataStream.getBS();
        linespacingFactor = dataStream.getBD();
        unknownBit = dataStream.getB();
        int backgroundFlags = dataStream.getBL();
        
        if (backgroundFlags == 1) {
            backgroundScaleFactor = dataStream.getBD();
            backgroundColor = dataStream.getCMC();
            backgroundTransparency = dataStream.getBL();
        } else {
            backgroundScaleFactor = 1.5;
        }

        // The Handles
        
        styleHandle = handleStream.getHandle();
        
        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
        return "MTEXT";
    }

    public CadObject getStyle() {
        CadObject result = objectMap.parseObject(styleHandle);
        return (CadObject) result;
    }

} public class Point extends EntityObject {

    public Point3D point;
    
    public double thickness;
    
    public Point3D extrusion;
    
    public double xAxisAngle;

    public Point(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {

        // 19.4.29 POINT (27) page 125

        point = dataStream.get3BD();
        thickness = dataStream.getBT();
        extrusion = dataStream.getBE();
        xAxisAngle = dataStream.getBD();

        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
        return "POINT";
    }
} public class PolylineMesh extends EntityObject {

    public int flags;

    public int curveType;

    public int mVertexCount;

    public int nVertexCount;
    
    public int mDensity;

    public int nDensity;

    private Handle[] vertexHandles;

    private Handle seqEndHandle;

	public PolylineMesh(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.32 POLYLINE (MESH) (30) page 128

        flags = dataStream.getBS();
        curveType = dataStream.getBS();
        mVertexCount = dataStream.getBS();
        nVertexCount = dataStream.getBS();
        mDensity = dataStream.getBS();
        nDensity = dataStream.getBS();
        int ownedObjectCount = dataStream.getBL();
        
        // The handles
        
        vertexHandles = new Handle[ownedObjectCount];
        for (int i = 0; i < ownedObjectCount; i++) {
            vertexHandles[i] = handleStream.getHandle(handleOfThisObject);
        }
        seqEndHandle = handleStream.getHandle();
        
        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
		return "POLYLINE (MESH)";
	}

    public List<EntityObject> getOwnedObjects()
    {
        return new AbstractList<EntityObject>() {

            @Override
            public EntityObject get(int index)
            {
                CadObject result = objectMap.parseObject(vertexHandles[index]);
                return (EntityObject) result;
            }

            @Override
            public int size()
            {
                return vertexHandles.length;
            }
        };
    }

    public CadObject getSeqEnd() {
        CadObject result = objectMap.parseObjectPossiblyNull(seqEndHandle);
        return (CadObject) result;
    }

} public class PolylinePFace extends EntityObject {

    public int numVertexes;

    public int numFaces;
    
    private Handle[] vertexHandles;

    private Handle seqEndHandle;

	public PolylinePFace(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.31 POLYLINE (PFACE) (29) page 127

        numVertexes = dataStream.getBS();
        numFaces = dataStream.getBS();
        int ownedObjectCount = dataStream.getBL();
        
        // The handles
        
        vertexHandles = new Handle[ownedObjectCount];
        for (int i = 0; i < ownedObjectCount; i++) {
            vertexHandles[i] = handleStream.getHandle(handleOfThisObject);
        }
        seqEndHandle = handleStream.getHandle();
        
        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
		return "POLYLINE (PFACE)";
	}

    public List<EntityObject> getOwnedObjects()
    {
        return new AbstractList<EntityObject>() {

            @Override
            public EntityObject get(int index)
            {
                CadObject result = objectMap.parseObject(vertexHandles[index]);
                return (EntityObject) result;
            }

            @Override
            public int size()
            {
                return vertexHandles.length;
            }
        };
    }

    public CadObject getSeqEnd() {
        CadObject result = objectMap.parseObjectPossiblyNull(seqEndHandle);
        return (CadObject) result;
    }

} public class SortEntsTable extends NonEntityObject {

    private Handle[] sortHandles;
    private Handle ownerHandle;
    private Handle[] entityHandles;

    public SortEntsTable(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.90 SORTENTSTABLE page 207

        int numEntries = dataStream.getBL();

        sortHandles = new Handle[numEntries];
        for (int i = 0; i < numEntries; i++) {
            sortHandles[i] = dataStream.getHandle();
        }

        // The handles

        ownerHandle = handleStream.getHandle(handleOfThisObject);

        entityHandles = new Handle[numEntries];
        for (int i = 0; i < numEntries; i++) {
            entityHandles[i] = handleStream.getHandle(handleOfThisObject);
        }

        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public List<CadObject> getSortObjects()
    {
        return new AbstractList<CadObject>() {

            @Override
            public CadObject get(int index)
            {
                /*
                 * The sort values may contain handles that actually do not
                 * exist any more in the handle table.
                 */
                CadObject result = objectMap.parseObjectPossiblyOrphaned(sortHandles[index]);
                return result;
            }

            @Override
            public int size()
            {
                return sortHandles.length;
            }
        };
    }

    public CadObject getOwner() {
        CadObject result = objectMap.parseObject(ownerHandle);
        return result;
    }

    public List<CadObject> getEntities()
    {
        return new AbstractList<CadObject>() {

            @Override
            public CadObject get(int index)
            {
                /*
                 * The sort entities may contain handles that actually do not
                 * exist any more in the handle table.
                 */
                CadObject result = objectMap.parseObjectPossiblyOrphaned(entityHandles[index]);
                return result;
            }

            @Override
            public int size()
            {
                return entityHandles.length;
            }
        };
    }

    @Override
    public String toString() {
        return "SORTENTSTABLE";
    }

} public class Text extends EntityObject {

    public double elevation;
    public Point2D insertionPoint;
    public Point2D alignmentPoint;
    public Point3D extrusion;
    public double thickness;
    public double obliqueAngle;
    public double rotationAngle;
    public double height;
    public double widthFactor;
    public String textValue;
    public int generation;
    public int horizontalAlignment;
    public int verticalAlignment;
    public Handle styleHandle;

    public Text(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.3 TEXT (1) page 106    

        int dataFlags = dataStream.getUnsignedRC();
        if ((dataFlags & 0x01) == 0) {
            elevation = dataStream.getRD();
        }
        insertionPoint = dataStream.get2RD();
        if ((dataFlags & 0x02) == 0) {
            alignmentPoint = dataStream.get2DD(insertionPoint);
        }
        extrusion = dataStream.getBE();
        thickness = dataStream.getBT();
        if ((dataFlags & 0x04) == 0) {
            obliqueAngle = dataStream.getRD();
        }
        if ((dataFlags & 0x08) == 0) {
            rotationAngle = dataStream.getRD();
        }
        height = dataStream.getRD();
        if ((dataFlags & 0x10) == 0) {
            widthFactor = dataStream.getRD();
        }
        textValue = stringStream.getTU();
        if ((dataFlags & 0x20) == 0) {
            generation = dataStream.getBS();
        }
        if ((dataFlags & 0x40) == 0) {
            horizontalAlignment = dataStream.getBS();
        }
        if ((dataFlags & 0x80) == 0) {
            verticalAlignment = dataStream.getBS();
        }

        // The Handles
        
        styleHandle = handleStream.getHandle();
        
        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
        return "TEXT";
    }
} public class TwoDPolyline extends EntityObject {

    public boolean isClosed;
    public boolean curveFitVerticesAdded;
    public boolean splineFitVerticesAdded;
    public boolean is3DPolyline;
    public boolean is3DPolygonMesh;
    public boolean isMeshClosedInNDirection;
    public boolean isPolyfaceMesh;
    public boolean continuousLinetypePattern;
    public int curveType;
    public double startWidth;
    public double endWidth;
    public double thickness;
    public double elevation;
    public Point3D extrusion;
    private Handle[] ownedObjectHandles;
    private Handle seqEndHandle;

    public TwoDPolyline(ObjectMap objectMap) {
        super(objectMap);
    }

    @Override
    public void readObjectTypeSpecificData(BitBuffer dataStream, BitBuffer stringStream, BitBuffer handleStream, FileVersion fileVersion) {
        // 19.4.16 2D POLYLINE (15) page 115

        int flags = dataStream.getBS();

        isClosed = (flags & 0x01) != 0;
        curveFitVerticesAdded = (flags & 0x02) != 0;
        splineFitVerticesAdded = (flags & 0x04) != 0;
        is3DPolyline = (flags & 0x08) != 0;
        is3DPolygonMesh = (flags & 0x10) != 0;
        isMeshClosedInNDirection = (flags & 0x20) != 0;
        isPolyfaceMesh = (flags & 0x40) != 0;
        continuousLinetypePattern = (flags & 0x80) != 0;

        curveType = dataStream.getBS();
        startWidth = dataStream.getBD();
        endWidth = dataStream.getBD();
        thickness = dataStream.getBT();
        elevation = dataStream.getBD();
        extrusion = dataStream.getBE();
        int ownedObjectCount = dataStream.getBL();

        // The Handles

        ownedObjectHandles = new Handle[ownedObjectCount];
        for (int i = 0; i< ownedObjectCount; i++) {
            ownedObjectHandles[i] = handleStream.getHandle();
        }

        seqEndHandle = handleStream.getHandle();

        handleStream.advanceToByteBoundary();

        dataStream.assertEndOfStream();
        stringStream.assertEndOfStream();
        handleStream.assertEndOfStream();
    }

    public String toString() {
        return "2D POLYLINE";
    }

    public List<EntityObject> getOwnedObjects()
    {
        return new AbstractList<EntityObject>() {

            @Override
            public EntityObject get(int index)
            {
                CadObject result = objectMap.parseObject(ownedObjectHandles[index]);
                return (EntityObject) result;
            }

            @Override
            public int size()
            {
                return ownedObjectHandles.length;
            }
        };
    }

    public SeqEnd getSeqEnd() {
        CadObject result = objectMap.parseObjectPossiblyNull(seqEndHandle);
        return (SeqEnd) result;
    }

}
import java.io.File;
import java.io.FileOutputStream;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.junit.BeforeClass;
import org.junit.Test;
import com.onespatial.dwglib.bitstreams.Handle;
import com.onespatial.dwglib.bitstreams.Point3D;
import com.onespatial.dwglib.objects.AcdbDictionaryWithDefault;
import com.onespatial.dwglib.objects.Appid;
import com.onespatial.dwglib.objects.AppidControlObj;
import com.onespatial.dwglib.objects.Arc;
import com.onespatial.dwglib.objects.Attdef;
import com.onespatial.dwglib.objects.Attrib;
import com.onespatial.dwglib.objects.BlockControlObj;
import com.onespatial.dwglib.objects.BlockHeader;
import com.onespatial.dwglib.objects.CadObject;
import com.onespatial.dwglib.objects.Circle;
import com.onespatial.dwglib.objects.Dictionary;
import com.onespatial.dwglib.objects.EntityObject;
import com.onespatial.dwglib.objects.Insert;
import com.onespatial.dwglib.objects.LType;
import com.onespatial.dwglib.objects.LTypeControlObj;
import com.onespatial.dwglib.objects.Layer;
import com.onespatial.dwglib.objects.LayerControlObj;
import com.onespatial.dwglib.objects.Layout;
import com.onespatial.dwglib.objects.Line;
import com.onespatial.dwglib.objects.LwPolyline;
import com.onespatial.dwglib.objects.LwPolyline.VertexOfLwPolyline;
import com.onespatial.dwglib.objects.MText;
import com.onespatial.dwglib.objects.Point;
import com.onespatial.dwglib.objects.PolylineMesh;
import com.onespatial.dwglib.objects.PolylinePFace;
import com.onespatial.dwglib.objects.SortEntsTable;
import com.onespatial.dwglib.objects.Text;
import com.onespatial.dwglib.objects.TwoDPolyline;
package com.onespatial.dwglib;



public class TraversalTest {

    /**
     * a sample file, available from Autodesk, in 2010 format
     */
    static File testFile = new File("visualization_-_aerial.dwg");


    /**
     * @throws java.lang.Exception
     */
    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        if (!testFile.exists()) {

            URL urlToTestFile = new URL("http://download.autodesk.com/us/samplefiles/acad/visualization_-_aerial.dwg");
            try (
                    ReadableByteChannel inputChannel = Channels.newChannel(urlToTestFile.openStream());
                    FileOutputStream outputStream = new FileOutputStream(testFile);
                    ) {
                outputStream.getChannel().transferFrom(inputChannel, 0, Long.MAX_VALUE);
            }
        }
    }

    @Test
    public void traversalTest() throws Exception {
        try (Reader reader = new Reader(testFile)) {
            BlockControlObj blockControl = reader.getBlockControlObject();
            BlockHeader ms = (BlockHeader)blockControl.getModelSpace();
            if (ms != null) {
                for (CadObject entity : ms.getOwnedObjects()) {
                    traceEntity("", (EntityObject)entity, null);
                }
            }
        }
    }

    static Layer previousLayer = null;

    private void traceEntity(String indent, EntityObject entity, Layer parentLayer)
    {
        Layer layer = entity.getLayer();
        if (layer.entryName.equals("0")) {
            // Block layer, so inherit from parent
            if (parentLayer == null) {
                throw new RuntimeException("Can't have '0' layer entity at top-level.");
            }
            layer = parentLayer;
        } else {
            /*
             * Generally blocks have entities defined in layer '0'.
             * Occasionally the entities are in another layer. If a block
             * has its entities defined in the '0' layer then for most uses
             * one probably wants to expand the entities into the layer into
             * which the insert occurs.
             */
            if (layer != previousLayer) {
                System.out.println("Layer: " + (layer == null ? "null" : layer.entryName));
                previousLayer = layer;
            }
        }

        if (entity instanceof Point) {
            Point point = (Point)entity;
            System.out.println(indent + "    Point: " + point.point);
        } else if (entity instanceof Line) {
            Line line = (Line)entity;
            System.out.println(indent + "    Line: " + line.start + " to " + line.end);
        } else if (entity instanceof LwPolyline) {
            LwPolyline polyline = (LwPolyline)entity;

            System.out.print(indent + "    LwPolyline: ");
            for (VertexOfLwPolyline point : polyline.points) {
                System.out.print(" " + point.vertex);
            }
            System.out.println("");
        } else if (entity instanceof Arc) {