/* ====================================================================
   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.hslf.usermodel;

import java.awt.Color;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.logging.log4j.Logger;
import org.apache.poi.logging.PoiLogManager;
import org.apache.poi.ddf.AbstractEscherOptRecord;
import org.apache.poi.ddf.EscherArrayProperty;
import org.apache.poi.ddf.EscherBSERecord;
import org.apache.poi.ddf.EscherColorRef;
import org.apache.poi.ddf.EscherContainerRecord;
import org.apache.poi.ddf.EscherPropertyTypes;
import org.apache.poi.ddf.EscherRecordTypes;
import org.apache.poi.ddf.EscherSimpleProperty;
import org.apache.poi.hslf.record.Document;
import org.apache.poi.sl.draw.DrawPaint;
import org.apache.poi.sl.usermodel.ColorStyle;
import org.apache.poi.sl.usermodel.FillStyle;
import org.apache.poi.sl.usermodel.PaintStyle;
import org.apache.poi.sl.usermodel.PaintStyle.GradientPaint;
import org.apache.poi.sl.usermodel.PaintStyle.GradientPaint.GradientType;
import org.apache.poi.sl.usermodel.PaintStyle.TexturePaint;
import org.apache.poi.sl.usermodel.PictureData;
import org.apache.poi.sl.usermodel.Shape;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.Units;

import static org.apache.logging.log4j.util.Unbox.box;

/**
 * Represents functionality provided by the 'Fill Effects' dialog in PowerPoint.
 */
@SuppressWarnings("WeakerAccess")
public final class HSLFFill {
    private static final Logger LOG = PoiLogManager.getLogger(HSLFFill.class);

    /**
     *  Fill with a solid color
     */
    static final int FILL_SOLID = 0;

    /**
     *  Fill with a pattern (bitmap)
     */
    static final int FILL_PATTERN = 1;

    /**
     *  A texture (pattern with its own color map)
     */
    static final int FILL_TEXTURE = 2;

    /**
     *  Center a picture in the shape
     */
    static final int FILL_PICTURE = 3;

    /**
     *  Shade from start to end points
     */
    static final int FILL_SHADE = 4;

    /**
     *  Shade from bounding rectangle to end point
     */
    static final int FILL_SHADE_CENTER = 5;

    /**
     *  Shade from shape outline to end point
     */
    static final int FILL_SHADE_SHAPE = 6;

    /**
     *  Similar to FILL_SHADE, but the fill angle
     *  is additionally scaled by the aspect ratio of
     *  the shape. If shape is square, it is the same as FILL_SHADE
     */
    static final int FILL_SHADE_SCALE = 7;

    /**
     *  shade to title
     */
    static final int FILL_SHADE_TITLE = 8;

    /**
     *  Use the background fill color/pattern
     */
    static final int FILL_BACKGROUND = 9;

    /**
     * A bit that specifies whether the RecolorFillAsPicture bit is set.
     * A value of 0x0 specifies that the fRecolorFillAsPicture MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_USE_RECOLOR_FILL_AS_PICTURE = BitFieldFactory.getInstance(0x00400000);

    /**
     * A bit that specifies whether the UseShapeAnchor bit is set.
     * A value of 0x0 specifies that the fUseShapeAnchor MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_USE_USE_SHAPE_ANCHOR = BitFieldFactory.getInstance(0x00200000);

    /**
     * A bit that specifies whether the Filled bit is set.
     * A value of 0x0 specifies that the Filled MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_USE_FILLED = BitFieldFactory.getInstance(0x00100000);

    /**
     * A bit that specifies whether the HitTestFill bit is set.
     * A value of 0x0 specifies that the HitTestFill MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_USE_HIT_TEST_FILL = BitFieldFactory.getInstance(0x00080000);

    /**
     * A bit that specifies whether the fillShape bit is set.
     * A value of 0x0 specifies that the fillShape MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_USE_FILL_SHAPE = BitFieldFactory.getInstance(0x00040000);

    /**
     * A bit that specifies whether the fillUseRect bit is set.
     * A value of 0x0 specifies that the fillUseRect MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_USE_FILL_USE_RECT = BitFieldFactory.getInstance(0x00020000);

    /**
     * A bit that specifies whether the fNoFillHitTest bit is set.
     * A value of 0x0 specifies that the fNoFillHitTest MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_USE_NO_FILL_HIT_TEST = BitFieldFactory.getInstance(0x00010000);

    /**
     * A bit that specifies how to recolor a picture fill. If this bit is set to 0x1, the pictureFillCrMod
     * property of the picture fill is used for recoloring. If this bit is set to 0x0, the fillCrMod property,
     * as defined in section 2.3.7.6, is used for recoloring.
     * If UsefRecolorFillAsPicture equals 0x0, this value MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_RECOLOR_FILL_AS_PICTURE = BitFieldFactory.getInstance(0x00000040);

    /**
     * A bit that specifies whether the fill is rotated with the shape.
     * If UseUseShapeAnchor equals 0x0, this value MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_USE_SHAPE_ANCHOR = BitFieldFactory.getInstance(0x00000020);

    /**
     * A bit that specifies whether the fill is rendered if the shape is a 2-D shape.
     * If this bit is set to 0x1, the fill of this shape is rendered based on the properties of the Fill Style
     * property set. If this bit is set to 0x0, the fill of this shape is not rendered.
     * If UseFilled is 0x0, this value MUST be ignored. The default value for this property is 0x1.
     */
    private static final BitField FILL_FILLED = BitFieldFactory.getInstance(0x00000010);

    /**
     * A bit that specifies whether this fill will be hit tested.
     * If UsefHitTestFill equals 0x0, this value MUST be ignored.
     * The default value for this property is 0x1.
     */
    private static final BitField FILL_HIT_TEST_FILL = BitFieldFactory.getInstance(0x00000008);

    /**
     * A bit that specifies how the fill is aligned. If this bit is set to 0x1, the fill is
     * aligned relative to the shape so that it moves with the shape. If this bit is set to 0x0,
     * the fill is aligned with the origin of the view. If fUsefillShape equals 0x0, this value MUST be ignored.
     * The default value for this property is 0x1.
     */
    private static final BitField FILL_FILL_SHAPE = BitFieldFactory.getInstance(0x00000004);

    /**
     * A bit that specifies whether to use the rectangle specified by the fillRectLeft, fillRectRight,
     * fillRectTop, and fillRectBottom properties, rather than the bounding rectangle of the shape,
     * as the filled area. If fUsefillUseRect equals 0x0, this value MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_FILL_USE_RECT = BitFieldFactory.getInstance(0x00000002);

    /**
     * A bit that specifies whether this shape will be hit tested as though it were filled.
     * If UsefNoFillHitTest equals 0x0, this value MUST be ignored.
     * The default value for this property is 0x0.
     */
    private static final BitField FILL_NO_FILL_HIT_TEST = BitFieldFactory.getInstance(0x00000001);


    /**
     * The shape this background applies to
     */
    private HSLFShape shape;

    /**
     * Construct a {@code Fill} object for a shape.
     * Fill information will be read from shape's escher properties.
     *
     * @param shape the shape this background applies to
     */
    public HSLFFill(HSLFShape shape){
        this.shape = shape;
    }


    public FillStyle getFillStyle() {
        return this::getPaintStyle;
    }

    private PaintStyle getPaintStyle() {
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();

        EscherSimpleProperty hitProp = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__NOFILLHITTEST);
        int propVal = (hitProp == null) ? 0 : hitProp.getPropertyValue();

        EscherSimpleProperty masterProp = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.SHAPE__MASTER);

        if (!FILL_USE_FILLED.isSet(propVal) && masterProp != null) {
            int masterId = masterProp.getPropertyValue();
            HSLFShape o = shape.getSheet().getMasterSheet().getShapes().stream().filter(s -> s.getShapeId() == masterId).findFirst().orElse(null);
            return o != null ? o.getFillStyle().getPaint() : null;
        }

        final int fillType = getFillType();
        // TODO: fix gradient types, this mismatches with the MS-ODRAW definition ...
        // need to handle (not only) the type (radial,rectangular,linear),
        // the direction, e.g. top right, and bounds (e.g. for rectangular boxes)
        switch (fillType) {
            case FILL_BACKGROUND:
                return DrawPaint.createSolidPaint(getBackgroundColor());
            case FILL_SOLID:
                return DrawPaint.createSolidPaint(getForegroundColor());
            case FILL_SHADE_SHAPE:
                return getGradientPaint(GradientType.shape);
            case FILL_SHADE_CENTER:
            case FILL_SHADE_TITLE:
                return getGradientPaint(GradientType.circular);
            case FILL_SHADE:
            case FILL_SHADE_SCALE:
                return getGradientPaint(GradientType.linear);
            case FILL_TEXTURE:
            case FILL_PICTURE:
                return getTexturePaint();
            default:
                LOG.atWarn().log("unsupported fill type: {}", box(fillType));
                return null;
        }
    }

    private boolean isRotatedWithShape() {
        // NOFILLHITTEST can be in the normal escher opt record but also in the tertiary record
        // the extended bit fields seem to be in the second
        AbstractEscherOptRecord opt = shape.getEscherChild(EscherRecordTypes.USER_DEFINED);
        EscherSimpleProperty p = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__NOFILLHITTEST);
        int propVal = (p == null) ? 0 : p.getPropertyValue();
        return FILL_USE_USE_SHAPE_ANCHOR.isSet(propVal) && FILL_USE_SHAPE_ANCHOR.isSet(propVal);
    }

    private GradientPaint getGradientPaint(final GradientType gradientType) {
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();

        EscherSimpleProperty p = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__NOFILLHITTEST);
        int propVal = (p == null) ? 0 : p.getPropertyValue();

        if (FILL_USE_FILLED.isSet(propVal) && !FILL_FILLED.isSet(propVal)) {
            return null;
        }


        final EscherArrayProperty ep = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__SHADECOLORS);
        final int colorCnt = (ep == null) ? 0 : ep.getNumberOfElementsInArray();

        final List<Color> colors = new ArrayList<>();
        final List<Float> fractions = new ArrayList<>();

        // TODO: handle palette colors and alpha(?) value
        if (colorCnt == 0) {
            colors.add(getBackgroundColor());
            colors.add(getForegroundColor());
            fractions.add(0f);
            fractions.add(1f);
        } else {
            ep.forEach(data -> {
                EscherColorRef ecr = new EscherColorRef(data, 0, 4);
                colors.add(shape.getColor(ecr));
                double pos = Units.fixedPointToDouble(LittleEndian.getInt(data, 4));
                fractions.add((float)pos);
            });
        }

        int focus = getFillFocus();
        if (focus == 100 || focus == -100) {
            Collections.reverse(colors);
        } else if (focus != 0) {
            if (focus < 0) {
                focus = 100+focus;
            }
            // TODO: depending on fill focus, rotation with shape and other escher properties
            // there are still a lot of cases where we get the gradients wrong
            List<Color> reflectedColors = new ArrayList<>(colors.subList(1,colors.size()));
            Collections.reverse(reflectedColors);
            colors.addAll(0, reflectedColors);

            final List<Float> fractRev = new ArrayList<>();
            for (int i=fractions.size()-2; i >= 0; i--) {
                float val = (float)(1 - fractions.get(i) * focus / 100.);
                fractRev.add(val);
            }
            for (int i=0; i<fractions.size(); i++) {
                float val = (float)(fractions.get(i) * focus / 100.);
                fractions.set(i, val);
            }
            fractions.addAll(fractRev);
        }

        return new GradientPaint() {
            @Override
            public double getGradientAngle() {
                // A value of type FixedPoint, as specified in [MS-OSHARED] section 2.2.1.6,
                // that specifies the angle of the gradient fill. Zero degrees represents a vertical vector from
                // bottom to top. The default value for this property is 0x00000000.
                int rot = shape.getEscherProperty(EscherPropertyTypes.FILL__ANGLE);
                return 90-Units.fixedPointToDouble(rot);
            }

            @Override
            public ColorStyle[] getGradientColors() {
                return colors.stream().map(this::wrapColor).toArray(ColorStyle[]::new);
            }

            private ColorStyle wrapColor(Color col) {
                return (col == null) ? null : DrawPaint.createSolidPaint(col).getSolidColor();
            }

            @Override
            public float[] getGradientFractions() {
                float[] frc = new float[fractions.size()];
                for (int i = 0; i<fractions.size(); i++) {
                    frc[i] = fractions.get(i);
                }
                return frc;
            }

            @Override
            public boolean isRotatedWithShape() {
                return HSLFFill.this.isRotatedWithShape();
            }

            @Override
            public GradientType getGradientType() {
                return gradientType;
            }
        };
    }

    private TexturePaint getTexturePaint() {
        final HSLFPictureData pd = getPictureData();
        if (pd == null) {
            return null;
        }

        return new TexturePaint() {
            @Override
            public InputStream getImageData() {
                return new ByteArrayInputStream(pd.getData());
            }

            @Override
            public String getContentType() {
                return pd.getContentType();
            }

            @Override
            public int getAlpha() {
                return (int)(shape.getAlpha(EscherPropertyTypes.FILL__FILLOPACITY)*100000.0);
            }

            @Override
            public boolean isRotatedWithShape() {
                return HSLFFill.this.isRotatedWithShape();
            }

            @Override
            public Shape getShape() {
                return shape;
            }
        };
    }

    /**
     * Returns fill type.
     * Must be one of the {@code FILL_*} constants defined in this class.
     *
     * @return type of fill
     */
    public int getFillType(){
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();
        EscherSimpleProperty prop = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__FILLTYPE);
        return prop == null ? FILL_SOLID : prop.getPropertyValue();
    }

    /**
     * The fillFocus property specifies the relative position of the last color in the shaded fill.
     * Its used to specify the center of an reflected fill. 0 = no reflection, 50 = reflected in the middle.
     * If fillFocus is less than 0, the relative position of the last color is outside the shape,
     * and the relative position of the first color is within the shape.
     *
     * @return a percentage in the range of -100 .. 100; defaults to 0
     */
    public int getFillFocus() {
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();
        EscherSimpleProperty prop = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__FOCUS);
        return prop == null ? 0 : prop.getPropertyValue();
    }

    void afterInsert(HSLFSheet sh){
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();
        EscherSimpleProperty p = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__PATTERNTEXTURE);
        if(p != null) {
            int idx = p.getPropertyValue();
            EscherBSERecord bse = getEscherBSERecord(idx);
            if (bse != null) {
                bse.setRef(bse.getRef() + 1);
            }
        }
    }

    @SuppressWarnings("resource")
    EscherBSERecord getEscherBSERecord(int idx){
        HSLFSheet sheet = shape.getSheet();
        if(sheet == null) {
            LOG.atDebug().log("Fill has not yet been assigned to a sheet");
            return null;
        }
        HSLFSlideShow ppt = sheet.getSlideShow();
        Document doc = ppt.getDocumentRecord();
        EscherContainerRecord dggContainer = doc.getPPDrawingGroup().getDggContainer();
        EscherContainerRecord bstore = HSLFShape.getEscherChild(dggContainer, EscherContainerRecord.BSTORE_CONTAINER);
        if(bstore == null) {
            LOG.atDebug().log("EscherContainerRecord.BSTORE_CONTAINER was not found ");
            return null;
        }
        return (EscherBSERecord) bstore.getChild(idx-1);
    }

    /**
     * Sets fill type.
     * Must be one of the {@code FILL_*} constants defined in this class.
     *
     * @param type type of the fill
     */
    public void setFillType(int type){
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();
        HSLFShape.setEscherProperty(opt, EscherPropertyTypes.FILL__FILLTYPE, type);
    }

    /**
     * Foreground color
     */
    public Color getForegroundColor(){
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();
        EscherSimpleProperty p = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__NOFILLHITTEST);
        int propVal = (p == null) ? 0 : p.getPropertyValue();

        return (!FILL_USE_FILLED.isSet(propVal) || (FILL_USE_FILLED.isSet(propVal) && FILL_FILLED.isSet(propVal)))
            ? shape.getColor(EscherPropertyTypes.FILL__FILLCOLOR, EscherPropertyTypes.FILL__FILLOPACITY)
            : null;
    }

    /**
     * Foreground color
     */
    public void setForegroundColor(Color color){
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();
        opt.removeEscherProperty(EscherPropertyTypes.FILL__FILLOPACITY);
        opt.removeEscherProperty(EscherPropertyTypes.FILL__FILLCOLOR);

        if (color != null) {
            int rgb = new Color(color.getBlue(), color.getGreen(), color.getRed(), 0).getRGB();
            HSLFShape.setEscherProperty(opt, EscherPropertyTypes.FILL__FILLCOLOR, rgb);
            int alpha = color.getAlpha();
            if (alpha < 255) {
                int alphaFP = Units.doubleToFixedPoint(alpha/255d);
                HSLFShape.setEscherProperty(opt, EscherPropertyTypes.FILL__FILLOPACITY, alphaFP);
            }
        }

        EscherSimpleProperty p = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__NOFILLHITTEST);
        int propVal = (p == null) ? 0 : p.getPropertyValue();
        propVal = FILL_FILLED.setBoolean(propVal, color != null);
        propVal = FILL_NO_FILL_HIT_TEST.setBoolean(propVal, color != null);
        propVal = FILL_USE_FILLED.set(propVal);
        propVal = FILL_USE_FILL_SHAPE.set(propVal);
        propVal = FILL_USE_NO_FILL_HIT_TEST.set(propVal);
        // TODO: check why we always clear this ...
        propVal = FILL_FILL_SHAPE.clear(propVal);

        HSLFShape.setEscherProperty(opt, EscherPropertyTypes.FILL__NOFILLHITTEST, propVal);
    }

    /**
     * Background color
     */
    public Color getBackgroundColor(){
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();
        EscherSimpleProperty p = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__NOFILLHITTEST);
        int propVal = (p == null) ? 0 : p.getPropertyValue();

        return (!FILL_USE_FILLED.isSet(propVal) || (FILL_USE_FILLED.isSet(propVal) && FILL_FILLED.isSet(propVal)))
            ? shape.getColor(EscherPropertyTypes.FILL__FILLBACKCOLOR, EscherPropertyTypes.FILL__FILLOPACITY)
            : null;
    }

    /**
     * Background color
     */
    public void setBackgroundColor(Color color){
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();
        if (color == null) {
            HSLFShape.setEscherProperty(opt, EscherPropertyTypes.FILL__FILLBACKCOLOR, -1);
        }
        else {
            int rgb = new Color(color.getBlue(), color.getGreen(), color.getRed(), 0).getRGB();
            HSLFShape.setEscherProperty(opt, EscherPropertyTypes.FILL__FILLBACKCOLOR, rgb);
        }
    }

    /**
     * {@code PictureData} object used in a texture, pattern of picture fill.
     */
    @SuppressWarnings("resource")
    public HSLFPictureData getPictureData(){
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();
        EscherSimpleProperty p = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__PATTERNTEXTURE);
        if (p == null) {
            return null;
        }

        HSLFSlideShow ppt = shape.getSheet().getSlideShow();
        List<HSLFPictureData> pict = ppt.getPictureData();
        Document doc = ppt.getDocumentRecord();

        EscherContainerRecord dggContainer = doc.getPPDrawingGroup().getDggContainer();
        EscherContainerRecord bstore = HSLFShape.getEscherChild(dggContainer, EscherContainerRecord.BSTORE_CONTAINER);

        int idx = p.getPropertyValue();
        if (idx == 0){
            LOG.atWarn().log("no reference to picture data found ");
        } else {
            EscherBSERecord bse = (EscherBSERecord) bstore.getChild(idx - 1);
            for (HSLFPictureData pd : pict) {

                // Reference equals is safe because these BSE belong to the same slideshow
                if (pd.bse == bse) {
                    return pd;
                }
            }
        }

        return null;
    }

    /**
     * Assign picture used to fill the underlying shape.
     *
     * @param data the picture data added to this ppt by {@link HSLFSlideShow#addPicture(byte[], PictureData.PictureType)} method.
     */
    public void setPictureData(HSLFPictureData data){
        AbstractEscherOptRecord opt = shape.getEscherOptRecord();
        HSLFShape.setEscherProperty(opt, EscherPropertyTypes.FILL__PATTERNTEXTURE, true, (data == null ? 0 : data.getIndex()));
        if(data != null && shape.getSheet() != null) {
            EscherBSERecord bse = getEscherBSERecord(data.getIndex());
            if (bse != null) {
                bse.setRef(bse.getRef() + 1);
            }
        }
    }

}
