/* ====================================================================
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
==================================================================== */

package org.apache.poi.hemf.record.emfplus;

import static org.apache.poi.util.GenericRecordUtil.getBitsAsString;

import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.PrimitiveIterator.OfInt;
import java.util.function.BiFunction;
import java.util.function.Supplier;

import org.apache.commons.math3.linear.LUDecomposition;
import org.apache.commons.math3.linear.MatrixUtils;
import org.apache.commons.math3.linear.RealMatrix;
import org.apache.poi.hemf.draw.HemfDrawProperties;
import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.record.emf.HemfFill;
import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId;
import org.apache.poi.hwmf.record.HwmfBrushStyle;
import org.apache.poi.hwmf.record.HwmfColorRef;
import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode;
import org.apache.poi.hwmf.record.HwmfPenStyle;
import org.apache.poi.hwmf.record.HwmfTernaryRasterOp;
import org.apache.poi.hwmf.record.HwmfText;
import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.GenericRecordJsonWriter;
import org.apache.poi.util.GenericRecordUtil;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;
import org.apache.poi.util.StringUtil;

@SuppressWarnings("WeakerAccess")
public final class HemfPlusDraw {
    private static final int MAX_OBJECT_SIZE = 1_000_000;

    private HemfPlusDraw() {}

    public enum EmfPlusUnitType {
        /** Specifies a unit of logical distance within the world space. */
        World(0x00),
        /** Specifies a unit of distance based on the characteristics of the physical display. */
        Display(0x01),
        /** Specifies a unit of 1 pixel. */
        Pixel(0x02),
        /** Specifies a unit of 1 printer's point, or 1/72 inch. */
        Point(0x03),
        /** Specifies a unit of 1 inch. */
        Inch(0x04),
        /** Specifies a unit of 1/300 inch. */
        Document(0x05),
        /** Specifies a unit of 1 millimeter. */
        Millimeter(0x06)
        ;

        public final int id;

        EmfPlusUnitType(int id) {
            this.id = id;
        }

        public static EmfPlusUnitType valueOf(int id) {
            for (EmfPlusUnitType wrt : values()) {
                if (wrt.id == id) return wrt;
            }
            return null;
        }

    }

    public interface EmfPlusCompressed {
        /**
         * This bit indicates whether the data in the RectData field is compressed.
         * If set, RectData contains an EmfPlusRect object.
         * If clear, RectData contains an EmfPlusRectF object object.
         */
        BitField COMPRESSED = BitFieldFactory.getInstance(0x4000);

        int getFlags();

        /**
         * The index in the EMF+ Object Table to associate with the object
         * created by this record. The value MUST be zero to 63, inclusive.
         */
        default boolean isCompressed() {
            return COMPRESSED.isSet(getFlags());
        }

        default BiFunction<LittleEndianInputStream, Rectangle2D, Integer> getReadRect() {
            return isCompressed() ? HemfPlusDraw::readRectS : HemfPlusDraw::readRectF;
        }
    }

    public interface EmfPlusRelativePosition {
        /**
         * This bit indicates whether the PointData field specifies relative or absolute locations.
         * If set, each element in PointData specifies a location in the coordinate space that is relative to the
         * location specified by the previous element in the array. In the case of the first element in PointData,
         * a previous location at coordinates (0,0) is assumed.
         * If clear, PointData specifies absolute locations according to the {@link EmfPlusCompressed#isCompressed()} flag.
         *
         * Note If this flag is set, the {@link EmfPlusCompressed#isCompressed()} flag (above) is undefined and MUST be ignored.
         */
        BitField POSITION = BitFieldFactory.getInstance(0x0800);

        int getFlags();

        default boolean isRelativePosition() {
            return POSITION.isSet(getFlags());
        }
    }

    public interface EmfPlusSolidColor {
        /**
         * If set, brushId specifies a color as an EmfPlusARGB object.
         * If clear, brushId contains the index of an EmfPlusBrush object in the EMF+ Object Table.
         */
        BitField SOLID_COLOR = BitFieldFactory.getInstance(0x8000);

        int getFlags();

        int getBrushIdValue();

        default boolean isSolidColor() {
            return SOLID_COLOR.isSet(getFlags());
        }

        default int getBrushId() {
            return (isSolidColor()) ? -1 : getBrushIdValue();
        }

        default Color getSolidColor() {
            return (isSolidColor()) ? readARGB(getBrushIdValue()) : null;
        }

        default void applyColor(HemfGraphics ctx) {
            HemfDrawProperties prop = ctx.getProperties();
            if (isSolidColor()) {
                prop.setBrushStyle(HwmfBrushStyle.BS_SOLID);
                prop.setBrushColor(new HwmfColorRef(getSolidColor()));
            } else {
                ctx.applyPlusObjectTableEntry(getBrushId());
            }
        }
    }


    /**
     * The EmfPlusDrawPath record specifies drawing a graphics path
     */
    public static class EmfPlusDrawPath implements HemfPlusRecord, EmfPlusObjectId {
        private int flags;
        private int penId;

        @Override
        public HemfPlusRecordType getEmfPlusRecordType() {
            return HemfPlusRecordType.drawPath;
        }

        @Override
        public int getFlags() {
            return flags;
        }

        public int getPenId() {
            return penId;
        }

        @Override
        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
            this.flags = flags;

            // A 32-bit unsigned integer that specifies an index in the EMF+ Object Table for an EmfPlusPen object
            // to use for drawing the EmfPlusPath. The value MUST be zero to 63, inclusive.
            penId = leis.readInt();
            assert (0 <= penId && penId <= 63);

            assert (dataSize == LittleEndianConsts.INT_SIZE);

            return LittleEndianConsts.INT_SIZE;
        }

        @Override
        public void draw(HemfGraphics ctx) {
            ctx.applyPlusObjectTableEntry(penId);
            ctx.applyPlusObjectTableEntry(getObjectId());

            HemfDrawProperties prop = ctx.getProperties();
            final Path2D path = prop.getPath();
            if (path != null) {
                ctx.draw(path);
            }
        }

        @Override
        public String toString() {
            return GenericRecordJsonWriter.marshal(this);
        }

        @Override
        public HemfPlusRecordType getGenericRecordType() {
            return getEmfPlusRecordType();
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return GenericRecordUtil.getGenericProperties(
                "flags", this::getFlags,
                "penId", this::getPenId
            );
        }
    }

    /**
     * The EmfPlusFillRects record specifies filling the interiors of a series of rectangles.
     */
    public static class EmfPlusFillRects implements HemfPlusRecord, EmfPlusCompressed, EmfPlusSolidColor {
        private int flags;
        private int brushId;
        private final ArrayList<Rectangle2D> rectData = new ArrayList<>();

        @Override
        public HemfPlusRecordType getEmfPlusRecordType() {
            return HemfPlusRecordType.fillRects;
        }

        @Override
        public int getFlags() {
            return flags;
        }

        @Override
        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
            this.flags = flags;

            // A 32-bit unsigned integer that defines the brush, the content of which is
            // determined by the S bit in the Flags field.
            brushId = leis.readInt();

            // A 32-bit unsigned integer that specifies the number of rectangles in the RectData field.
            int count = leis.readInt();

            BiFunction<LittleEndianInputStream, Rectangle2D, Integer> readRect = getReadRect();

            rectData.ensureCapacity(count);

            int size = 2 * LittleEndianConsts.INT_SIZE;
            for (int i = 0; i<count; i++) {
                Rectangle2D rect = new Rectangle2D.Double();
                size += readRect.apply(leis, rect);
                rectData.add(rect);
            }

            return size;
        }

        @Override
        public void draw(HemfGraphics ctx) {
            HemfDrawProperties prop = ctx.getProperties();
            applyColor(ctx);

            Area area = new Area();
            rectData.stream().map(Area::new).forEach(area::add);
            HwmfPenStyle ps = prop.getPenStyle();
            try {
                prop.setPenStyle(null);
                ctx.fill(area);
            } finally {
                prop.setPenStyle(ps);
            }
        }

        @Override
        public int getBrushIdValue() {
            return brushId;
        }

        @Override
        public String toString() {
            return GenericRecordJsonWriter.marshal(this);
        }

        @Override
        public HemfPlusRecordType getGenericRecordType() {
            return getEmfPlusRecordType();
        }

        public List<Rectangle2D> getRectData() {
            return rectData;
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return GenericRecordUtil.getGenericProperties(
                "flags", this::getFlags,
                "brushId", this::getBrushId,
                "brushColor", this::getSolidColor,
                "rectData", this::getRectData
            );
        }
    }

    /** The EmfPlusDrawImagePoints record specifies drawing a scaled image inside a parallelogram. */
    @SuppressWarnings("unused")
    public static class EmfPlusDrawImagePoints implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed, EmfPlusRelativePosition {
        /**
         * This bit indicates that the rendering of the image includes applying an effect.
         * If set, an object of the Effect class MUST have been specified in an earlier EmfPlusSerializableObject record.
         */
        private static final BitField EFFECT = BitFieldFactory.getInstance(0x2000);

        private int flags;
        private int imageAttributesID;
        private EmfPlusUnitType srcUnit;
        private final Rectangle2D srcRect = new Rectangle2D.Double();
        private final Point2D upperLeft = new Point2D.Double();
        private final Point2D lowerRight = new Point2D.Double();
        private final Point2D lowerLeft = new Point2D.Double();
        private final AffineTransform trans = new AffineTransform();

        @Override
        public HemfPlusRecordType getEmfPlusRecordType() {
            return HemfPlusRecordType.drawImagePoints;
        }

        @Override
        public int getFlags() {
            return flags;
        }

        @Override
        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
            this.flags = flags;

            // A 32-bit unsigned integer that contains the index of the
            // optional EmfPlusImageAttributes object in the EMF+ Object Table.
            imageAttributesID = leis.readInt();

            // A 32-bit signed integer that defines the units of the SrcRect field.
            // It MUST be the UnitPixel value of the UnitType enumeration
            srcUnit = EmfPlusUnitType.valueOf(leis.readInt());
            assert(srcUnit == EmfPlusUnitType.Pixel);

            int size = 2 * LittleEndianConsts.INT_SIZE;

            // An EmfPlusRectF object that defines a portion of the image to be rendered.
            size += readRectF(leis, srcRect);

            // A 32-bit unsigned integer that specifies the number of points in the PointData array.
            // Exactly 3 points MUST be specified.
            int count = leis.readInt();
            assert(count == 3);
            size += LittleEndianConsts.INT_SIZE;

            BiFunction<LittleEndianInputStream, Point2D, Integer> readPoint;

            if (isRelativePosition()) {
                // If the POSITION flag is set in the Flags, the points specify relative locations.
                readPoint = HemfPlusDraw::readPointR;
            } else if (isCompressed()) {
                // If the POSITION bit is clear and the COMPRESSED bit is set in the Flags field, the points
                // specify absolute locations with integer values.
                readPoint = HemfPlusDraw::readPointS;
            } else {
                // If the POSITION bit is clear and the COMPRESSED bit is clear in the Flags field, the points
                // specify absolute locations with floating-point values.
                readPoint = HemfPlusDraw::readPointF;
            }

            // TODO: handle relative coordinates

            // An array of Count points that specify three points of a parallelogram.
            // The three points represent the upper-left, upper-right, and lower-left corners of the parallelogram.
            // The fourth point of the parallelogram is extrapolated from the first three.
            // The portion of the image specified by the SrcRect field SHOULD have scaling and shearing transforms
            // applied if necessary to fit inside the parallelogram.


            // size += readPoint.apply(leis, upperLeft);
            // size += readPoint.apply(leis, upperRight);
            // size += readPoint.apply(leis, lowerLeft);

            size += readPoint.apply(leis, lowerLeft);
            size += readPoint.apply(leis, lowerRight);
            size += readPoint.apply(leis, upperLeft);

            // https://math.stackexchange.com/questions/2772737/how-to-transform-arbitrary-rectangle-into-specific-parallelogram

            RealMatrix para2normal = MatrixUtils.createRealMatrix(new double[][] {
                { lowerLeft.getX(), lowerRight.getX(), upperLeft.getX() },
                { lowerLeft.getY(), lowerRight.getY(), upperLeft.getY() },
                { 1, 1, 1 }
            });

            RealMatrix rect2normal = MatrixUtils.createRealMatrix(new double[][]{
                { srcRect.getMinX(), srcRect.getMaxX(), srcRect.getMinX() },
                { srcRect.getMinY(), srcRect.getMinY(), srcRect.getMaxY() },
                { 1, 1, 1 }
            });

            RealMatrix normal2rect = new LUDecomposition(rect2normal).getSolver().getInverse();
            double[][] m = para2normal.multiply(normal2rect).getData();
            trans.setTransform(round10(m[0][0]), round10(m[1][0]), round10(m[0][1]), round10(m[1][1]), round10(m[0][2]), round10(m[1][2]));

            return size;
        }

        @Override
        public void draw(HemfGraphics ctx) {
            HemfDrawProperties prop = ctx.getProperties();

            ctx.applyPlusObjectTableEntry(imageAttributesID);
            ctx.applyPlusObjectTableEntry(getObjectId());

            final ImageRenderer ir = prop.getEmfPlusImage();
            if (ir == null) {
                return;
            }

            AffineTransform txSaved = ctx.getTransform();
            AffineTransform tx = (AffineTransform)txSaved.clone();
            HwmfTernaryRasterOp oldOp = prop.getRasterOp3();
            HwmfBkMode oldBk = prop.getBkMode();
            try {
                tx.concatenate(trans);
                ctx.setTransform(tx);

                prop.setRasterOp3(HwmfTernaryRasterOp.SRCCOPY);
                prop.setBkMode(HwmfBkMode.TRANSPARENT);

                // transformation from srcRect to destRect was already applied,
                // therefore use srcRect as third parameter
                ctx.drawImage(ir, srcRect, srcRect);
            } finally {
                prop.setBkMode(oldBk);
                prop.setRasterOp3(oldOp);
                ctx.setTransform(txSaved);
            }
        }

        @Override
        public String toString() {
            return GenericRecordJsonWriter.marshal(this);
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            final Map<String,Supplier<?>> m = new LinkedHashMap<>();
            m.put("flags", this::getFlags);
            m.put("imageAttributesID", () -> imageAttributesID);
            m.put("srcUnit", () -> srcUnit);
            m.put("srcRect", () -> srcRect);
            m.put("upperLeft", () -> upperLeft);
            m.put("lowerLeft", () -> lowerLeft);
            m.put("lowerRight", () -> lowerRight);
            m.put("transform", () -> trans);
            return Collections.unmodifiableMap(m);
        }
    }

    /** The EmfPlusDrawImage record specifies drawing a scaled image. */
    public static class EmfPlusDrawImage implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed {
        private int flags;
        private int imageAttributesID;
        private EmfPlusUnitType srcUnit;
        private final Rectangle2D srcRect = new Rectangle2D.Double();
        private final Rectangle2D rectData = new Rectangle2D.Double();

        @Override
        public HemfPlusRecordType getEmfPlusRecordType() {
            return HemfPlusRecordType.drawImage;
        }

        @Override
        public int getFlags() {
            return flags;
        }

        @Override
        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
            this.flags = flags;

            // A 32-bit unsigned integer that contains the index of the
            // optional EmfPlusImageAttributes object in the EMF+ Object Table.
            imageAttributesID = leis.readInt();

            // A 32-bit signed integer that defines the units of the SrcRect field.
            // It MUST be the UnitPixel value of the UnitType enumeration
            srcUnit = EmfPlusUnitType.valueOf(leis.readInt());
            assert(srcUnit == EmfPlusUnitType.Pixel);

            int size = 2 * LittleEndianConsts.INT_SIZE;

            // An EmfPlusRectF object that specifies a portion of the image to be rendered. The portion of the image
            // specified by this rectangle is scaled to fit the destination rectangle specified by the RectData field.
            size += readRectF(leis, srcRect);

            // Either an EmfPlusRect or EmfPlusRectF object that defines the bounding box of the image. The portion of
            // the image specified by the SrcRect field is scaled to fit this rectangle.
            size += getReadRect().apply(leis, rectData);

            return size;
        }

        @Override
        public void draw(HemfGraphics ctx) {
            ctx.applyPlusObjectTableEntry(imageAttributesID);
            ctx.applyPlusObjectTableEntry(getObjectId());

            HemfDrawProperties prop = ctx.getProperties();
            prop.setRasterOp3(HwmfTernaryRasterOp.SRCCOPY);
            prop.setBkMode(HwmfBkMode.TRANSPARENT);

            ctx.drawImage(prop.getEmfPlusImage(), srcRect, rectData);
        }

        @Override
        public String toString() {
            return GenericRecordJsonWriter.marshal(this);
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return GenericRecordUtil.getGenericProperties(
                "flags", this::getFlags,
                "imageAttributesID", () -> imageAttributesID,
                "srcUnit", () -> srcUnit,
                "srcRect", () -> srcRect,
                "rectData", () -> rectData
            );
        }
    }

    /** The EmfPlusFillRegion record specifies filling the interior of a graphics region. */
    public static class EmfPlusFillRegion implements HemfPlusRecord, EmfPlusSolidColor, EmfPlusObjectId {
        private int flags;
        private int brushId;

        @Override
        public HemfPlusRecordType getEmfPlusRecordType() {
            return HemfPlusRecordType.fillRegion;
        }

        @Override
        public int getFlags() {
            return flags;
        }

        @Override
        public int getBrushIdValue() {
            return brushId;
        }

        @Override
        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
            this.flags = flags;

            // A 32-bit unsigned integer that defines the brush, the content of which is determined by
            // the SOLID_COLOR bit in the Flags field.
            // If SOLID_COLOR is set, BrushId specifies a color as an EmfPlusARGB object.
            // If clear, BrushId contains the index of an EmfPlusBrush object in the EMF+ Object Table.
            brushId = leis.readInt();

            return LittleEndianConsts.INT_SIZE;
        }

        @Override
        public void draw(HemfGraphics ctx) {
            applyColor(ctx);
            ctx.applyPlusObjectTableEntry(getObjectId());
            HemfDrawProperties prop = ctx.getProperties();
            ctx.fill(prop.getPath());
        }

        @Override
        public String toString() {
            return GenericRecordJsonWriter.marshal(this);
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return GenericRecordUtil.getGenericProperties(
                "flags", this::getFlags,
                "brushId", () -> brushId
            );
        }
    }

    /** The EmfPlusFillPath record specifies filling the interior of a graphics path. */
    public static class EmfPlusFillPath extends EmfPlusFillRegion {

        @Override
        public HemfPlusRecordType getEmfPlusRecordType() {
            return HemfPlusRecordType.fillPath;
        }

    }

    /** The EmfPlusDrawDriverString record specifies text output with character positions. */
    @SuppressWarnings("unused")
    public static class EmfPlusDrawDriverString implements HemfPlusRecord, EmfPlusObjectId, EmfPlusSolidColor {
        /**
         * If set, the positions of character glyphs SHOULD be specified in a character map lookup table.
         * If clear, the glyph positions SHOULD be obtained from an array of coordinates.
         */
        private static final BitField CMAP_LOOKUP = BitFieldFactory.getInstance(0x0001);

        /**
         * If set, the string SHOULD be rendered vertically.
         * If clear, the string SHOULD be rendered horizontally.
         */
        private static final BitField VERTICAL = BitFieldFactory.getInstance(0x0002);

        /**
         * If set, character glyph positions SHOULD be calculated relative to the position of the first glyph.
         * If clear, the glyph positions SHOULD be obtained from an array of coordinates.
         */
        private static final BitField REALIZED_ADVANCE = BitFieldFactory.getInstance(0x0004);

        /**
         * If set, less memory SHOULD be used to cache anti-aliased glyphs, which produces lower quality text rendering.
         * If clear, more memory SHOULD be used, which produces higher quality text rendering.
         */
        private static final BitField LIMIT_SUBPIXEL = BitFieldFactory.getInstance(0x0008);

        private static final int[] OPTIONS_MASK = { 0x0001, 0x0002, 0x0004, 0x0008 };

        private static final String[] OPTIONS_NAMES = {
            "CMAP_LOOKUP", "VERTICAL", "REALIZED_ADVANCE", "LIMIT_SUBPIXEL"
        };

        private int flags;
        private int brushId;
        private int optionsFlags;
        private String glyphs;
        private final List<Point2D> glyphPos = new ArrayList<>();
        private final AffineTransform transformMatrix = new AffineTransform();

        @Override
        public HemfPlusRecordType getEmfPlusRecordType() {
            return HemfPlusRecordType.drawDriverString;
        }

        @Override
        public int getFlags() {
            return flags;
        }

        @Override
        public int getBrushIdValue() {
            return brushId;
        }

        @Override
        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
            this.flags = flags;

            // A 32-bit unsigned integer that specifies either the foreground color of the text or a graphics brush,
            // depending on the value of the SOLID_COLOR flag in the Flags.
            brushId = leis.readInt();

            // A 32-bit unsigned integer that specifies the spacing, orientation, and quality of rendering for the
            // string. This value MUST be composed of DriverStringOptions flags
            optionsFlags = leis.readInt();

            // A 32-bit unsigned integer that specifies whether a transform matrix is present in the
            // TransformMatrix field.
            int matrixPresent = leis.readInt();

            // A 32-bit unsigned integer that specifies number of glyphs in the string.
            int glyphCount = leis.readInt();

            int size = 4 * LittleEndianConsts.INT_SIZE;

            // TODO: implement Non-Cmap-Lookup correctly

            // If the CMAP_LOOKUP flag in the optionsFlags field is set, each value in this array specifies a
            // Unicode character. Otherwise, each value specifies an index to a character glyph in the EmfPlusFont
            // object specified by the ObjectId value in Flags field.
            byte[] glyphBuf = IOUtils.toByteArray(leis, glyphCount*2, MAX_OBJECT_SIZE);
            glyphs = StringUtil.getFromUnicodeLE(glyphBuf);

            size += glyphBuf.length;

            // An array of EmfPlusPointF objects that specify the output position of each character glyph.
            // There MUST be GlyphCount elements, which have a one-to-one correspondence with the elements
            // in the Glyphs array.
            //
            // Glyph positions are calculated from the position of the first glyph if the REALIZED_ADVANCE flag in
            // Options flags is set. In this case, GlyphPos specifies the position of the first glyph only.
            int glyphPosCnt = REALIZED_ADVANCE.isSet(optionsFlags) ? 1 : glyphCount;
            for (int i=0; i<glyphCount; i++) {
                Point2D p = new Point2D.Double();
                size += readPointF(leis, p);
                glyphPos.add(p);
            }

            if (matrixPresent != 0) {
                size += HemfFill.readXForm(leis, transformMatrix);
            }


            return size;
        }

        @Override
        public void draw(HemfGraphics ctx) {
            HemfDrawProperties prop = ctx.getProperties();
            prop.setTextAlignLatin(HwmfText.HwmfTextAlignment.LEFT);
            prop.setTextVAlignLatin(HwmfText.HwmfTextVerticalAlignment.BASELINE);

            ctx.applyPlusObjectTableEntry(getObjectId());
            if (isSolidColor()) {
                prop.setTextColor(new HwmfColorRef(getSolidColor()));
            } else {
                ctx.applyPlusObjectTableEntry(getBrushId());
            }

            if (REALIZED_ADVANCE.isSet(optionsFlags)) {
                byte[] buf = glyphs.getBytes(StandardCharsets.UTF_16LE);
                ctx.drawString(buf, buf.length, glyphPos.get(0), null, null, null, null, true);
            } else {
                final OfInt glyphIter = glyphs.codePoints().iterator();
                glyphPos.forEach(p -> {
                    byte[] buf = new String(new int[]{glyphIter.next()}, 0, 1).getBytes(StandardCharsets.UTF_16LE);
                    ctx.drawString(buf, buf.length, p, null, null, null, null, true);
                });
            }
        }

        @Override
        public String toString() {
            return GenericRecordJsonWriter.marshal(this);
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return GenericRecordUtil.getGenericProperties(
                "flags", this::getFlags,
                "brushId", this::getBrushId,
                "optionsFlags", getBitsAsString(() -> optionsFlags, OPTIONS_MASK, OPTIONS_NAMES),
                "glyphs", () -> glyphs,
                "glyphPos", () -> glyphPos,
                "transform", () -> transformMatrix
            );
        }
    }

    /** The EmfPlusDrawRects record specifies drawing a series of rectangles. */
    public static class EmfPlusDrawRects implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed {
        private int flags;
        private final List<Rectangle2D> rectData = new ArrayList<>();

        @Override
        public HemfPlusRecordType getEmfPlusRecordType() {
            return HemfPlusRecordType.drawRects;
        }

        @Override
        public int getFlags() {
            return flags;
        }

        @Override
        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
            this.flags = flags;

            // A 32-bit unsigned integer that specifies the number of rectangles in the RectData member.
            int count = leis.readInt();
            int size = LittleEndianConsts.INT_SIZE;

            BiFunction<LittleEndianInputStream, Rectangle2D, Integer> readRect = getReadRect();

            for (int i=0; i<count; i++) {
                Rectangle2D rect = new Rectangle2D.Double();
                size += readRect.apply(leis, rect);
            }

            return size;
        }

        @Override
        public String toString() {
            return GenericRecordJsonWriter.marshal(this);
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return GenericRecordUtil.getGenericProperties(
                "flags", this::getFlags,
                "rectData", () -> rectData
            );
        }
    }

    @SuppressWarnings("squid:S2111")
    static double round10(double d) {
        return BigDecimal.valueOf(d).setScale(10, RoundingMode.HALF_UP).doubleValue();
    }

    static int readRectS(LittleEndianInputStream leis, Rectangle2D bounds) {
        // A 16-bit signed integer that defines the ... coordinate
        final int x = leis.readShort();
        final int y = leis.readShort();
        final int width = leis.readShort();
        final int height = leis.readShort();
        bounds.setRect(x, y, width, height);

        return 4 * LittleEndianConsts.SHORT_SIZE;
    }

    static int readRectF(LittleEndianInputStream leis, Rectangle2D bounds) {
        // A 32-bit floating-point that defines the ... coordinate
        final double x = leis.readFloat();
        final double y = leis.readFloat();
        final double width = leis.readFloat();
        final double height = leis.readFloat();
        bounds.setRect(x, y, width, height);

        return 4 * LittleEndianConsts.INT_SIZE;
    }

    /**
     * The EmfPlusPoint object specifies an ordered pair of integer (X,Y) values that define an absolute
     * location in a coordinate space.
     */
    static int readPointS(LittleEndianInputStream leis, Point2D point) {
        double x = leis.readShort();
        double y = leis.readShort();
        point.setLocation(x,y);
        return 2*LittleEndianConsts.SHORT_SIZE;
    }

    /**
     * The EmfPlusPointF object specifies an ordered pair of floating-point (X,Y) values that define an
     * absolute location in a coordinate space.
     */
    static int readPointF(LittleEndianInputStream leis, Point2D point) {
        double x = leis.readFloat();
        double y = leis.readFloat();
        point.setLocation(x,y);
        return 2*LittleEndianConsts.INT_SIZE;
    }

    /**
     * The EmfPlusPointR object specifies an ordered pair of integer (X,Y) values that define a relative
     * location in a coordinate space.
     */
    static int readPointR(LittleEndianInputStream leis, Point2D point) {
        int[] p = { 0 };
        int size = readEmfPlusInteger(leis, p);
        double x = p[0];
        size += readEmfPlusInteger(leis, p);
        double y = p[0];
        point.setLocation(x,y);
        return size;
    }

    private static int readEmfPlusInteger(LittleEndianInputStream leis, int[] value) {
        value[0] = leis.readByte();
        // check for EmfPlusInteger7 value
        if ((value[0] & 0x80) == 0) {
            return LittleEndianConsts.BYTE_SIZE;
        }
        // ok we've read a EmfPlusInteger15
        value[0] = ((value[0] << 8) | (leis.readByte() & 0xFF)) & 0x7FFF;
        return LittleEndianConsts.SHORT_SIZE;
    }

    static Color readARGB(int argb) {
        return new Color((argb >>> 16) & 0xFF, (argb >>> 8) & 0xFF, argb & 0xFF, (argb >>> 24) & 0xFF);
    }

}
