/* ====================================================================
   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.hwmf.record;

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

import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.hwmf.draw.HwmfDrawProperties;
import org.apache.poi.hwmf.draw.HwmfGraphics;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.GenericRecordUtil;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;

public class HwmfPalette {

    public static class PaletteEntry implements GenericRecord {
        private static final BitField PC_RESERVED   = BitFieldFactory.getInstance(0x01);
        private static final BitField PC_EXPLICIT   = BitFieldFactory.getInstance(0x02);
        private static final BitField PC_NOCOLLAPSE = BitFieldFactory.getInstance(0x04);

        private static final int[] FLAGS_MASKS = { 1,2,4 };

        private static final String[] FLAGS_NAMES = { "RESERVED", "EXPLICIT", "NOCOLLAPSE" };

        private int values;
        private Color colorRef;

        public PaletteEntry() {
            this.values = PC_RESERVED.set(0);
            this.colorRef = Color.BLACK;
        }

        public PaletteEntry(PaletteEntry other) {
            this.values = other.values;
            this.colorRef = other.colorRef;
        }

        public int init(LittleEndianInputStream leis) throws IOException {
            // Values (1 byte):  An 8-bit unsigned integer that defines how the palette entry is to be used.
            // The Values field MUST be 0x00 or one of the values in the PaletteEntryFlag Enumeration table.
            values = leis.readUByte();
            // Blue (1 byte): An 8-bit unsigned integer that defines the blue intensity value for the palette entry.
            int blue = leis.readUByte();
            // Green (1 byte): An 8-bit unsigned integer that defines the green intensity value for the palette entry.
            int green = leis.readUByte();
            // Red (1 byte): An 8-bit unsigned integer that defines the red intensity value for the palette entry.
            int red = leis.readUByte();
            colorRef = new Color(red, green, blue);

            return 4*LittleEndianConsts.BYTE_SIZE;
        }

        /**
         * Specifies that the logical palette entry be used for palette animation. This value
         * prevents other windows from matching colors to the palette entry because the color frequently
         * changes. If an unused system-palette entry is available, the color is placed in that entry.
         * Otherwise, the color is not available for animation.
         */
        public boolean isReserved() {
            return PC_RESERVED.isSet(values);
        }

        /**
         * Specifies that the low-order word of the logical palette entry designates a hardware
         * palette index. This value allows the application to show the contents of the display device palette.
         */
        public boolean isExplicit() {
            return PC_EXPLICIT.isSet(values);
        }

        /**
         * Specifies that the color be placed in an unused entry in the system palette
         * instead of being matched to an existing color in the system palette. If there are no unused entries
         * in the system palette, the color is matched normally. Once this color is in the system palette,
         * colors in other logical palettes can be matched to this color.
         */
        public boolean isNoCollapse() {
            return PC_NOCOLLAPSE.isSet(values);
        }

        public Color getColorRef() {
            return colorRef;
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return GenericRecordUtil.getGenericProperties(
                "flags", getBitsAsString(() -> values, FLAGS_MASKS, FLAGS_NAMES),
                "color", this::getColorRef
            );
        }
    }

    public static abstract class WmfPaletteParent implements HwmfRecord, HwmfObjectTableEntry  {

        /**
         * Start (2 bytes):  A 16-bit unsigned integer that defines the offset into the Palette Object when
         * used with the META_SETPALENTRIES and META_ANIMATEPALETTE record types.
         * When used with META_CREATEPALETTE, it MUST be 0x0300
         */
        protected int start;

        protected final List<PaletteEntry> palette = new ArrayList<>();

        @Override
        public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException {
            start = leis.readUShort();
            int size = readPaletteEntries(leis, -1);
            return size + LittleEndianConsts.SHORT_SIZE;
        }

        protected int readPaletteEntries(LittleEndianInputStream leis, int nbrOfEntries) throws IOException {
            // NumberOfEntries (2 bytes):  A 16-bit unsigned integer that defines the number of objects in
            // aPaletteEntries.
            final int numberOfEntries = (nbrOfEntries > -1) ? nbrOfEntries : leis.readUShort();
            int size = (nbrOfEntries > -1) ? 0 : LittleEndianConsts.SHORT_SIZE;
            for (int i=0; i<numberOfEntries; i++) {
                PaletteEntry pe = new PaletteEntry();
                size += pe.init(leis);
                palette.add(pe);
            }
            return size;
        }

        @Override
        public final void draw(HwmfGraphics ctx) {
            ctx.addObjectTableEntry(this);
        }
        
        List<PaletteEntry> getPaletteCopy() {
            List<PaletteEntry> newPalette = new ArrayList<>();
            for (PaletteEntry et : palette) {
                newPalette.add(new PaletteEntry(et));
            }
            return newPalette;
        }

        int getPaletteStart() {
            return start;
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return GenericRecordUtil.getGenericProperties(
                "paletteStart", this::getPaletteStart,
                "pallete", this::getPaletteCopy
            );
        }
    }

    /**
     * The META_CREATEPALETTE record creates a Palette Object
     */
    public static class WmfCreatePalette extends WmfPaletteParent implements HwmfObjectTableEntry {
        @Override
        public HwmfRecordType getWmfRecordType() {
            return HwmfRecordType.createPalette;
        }

        @Override
        public void applyObject(HwmfGraphics ctx) {
            ctx.getProperties().setPalette(getPaletteCopy());
        }
    }

    /**
     * The META_SETPALENTRIES record defines RGB color values in a range of entries in the logical
     * palette that is defined in the playback device context.
     */
    public static class WmfSetPaletteEntries extends WmfPaletteParent {
        @Override
        public HwmfRecordType getWmfRecordType() {
            return HwmfRecordType.setPalEntries;
        }

        @Override
        public void applyObject(HwmfGraphics ctx) {
            HwmfDrawProperties props = ctx.getProperties();
            List<PaletteEntry> palette = props.getPalette();
            if (palette == null) {
                palette = new ArrayList<>();
            }
            int start = getPaletteStart();
            for (int i=palette.size(); i<start; i++) {
                palette.add(new PaletteEntry());
            }
            int index = start;
            for (PaletteEntry palCopy : getPaletteCopy()) {
                if (palette.size() <= index) {
                    palette.add(palCopy);
                } else {
                    palette.set(index, palCopy);
                }
                index++;
            }
            props.setPalette(palette);
        }
    }

    /**
     * The META_RESIZEPALETTE record redefines the size of the logical palette that is defined in the
     * playback device context.
     */
    public static class WmfResizePalette implements HwmfRecord, HwmfObjectTableEntry {
        /**
         * A 16-bit unsigned integer that defines the number of entries in
         * the logical palette.
         */
        protected int numberOfEntries;

        @Override
        public HwmfRecordType getWmfRecordType() {
            return HwmfRecordType.resizePalette;
        }

        @Override
        public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException {
            numberOfEntries = leis.readUShort();
            return LittleEndianConsts.SHORT_SIZE;
        }

        @Override
        public void draw(HwmfGraphics ctx) {
            ctx.addObjectTableEntry(this);
        }
        
        @Override
        public void applyObject(HwmfGraphics ctx) {
            HwmfDrawProperties props = ctx.getProperties();
            List<PaletteEntry> palette = props.getPalette();
            if (palette == null) {
                palette = new ArrayList<>();
            }
            for (int i=palette.size(); i<numberOfEntries; i++) {
                palette.add(new PaletteEntry());
            }
            palette = palette.subList(0, numberOfEntries);
            props.setPalette(palette);
        }

        public int getNumberOfEntries() {
            return numberOfEntries;
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return GenericRecordUtil.getGenericProperties("numberOfEntries", this::getNumberOfEntries);
        }
    }

    /**
     * The META_SELECTPALETTE record defines the current logical palette with a specified Palette Object.
     */
    public static class WmfSelectPalette implements HwmfRecord {
        /**
         * A 16-bit unsigned integer used to index into the WMF Object Table to get
         * the Palette Object to be selected.
         */
        protected int paletteIndex;

        @Override
        public HwmfRecordType getWmfRecordType() {
            return HwmfRecordType.selectPalette;
        }

        @Override
        public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException {
            paletteIndex = leis.readUShort();
            return LittleEndianConsts.SHORT_SIZE;
        }

        @Override
        public void draw(HwmfGraphics ctx) {
            ctx.applyObjectTableEntry(paletteIndex);
        }

        public int getPaletteIndex() {
            return paletteIndex;
        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return GenericRecordUtil.getGenericProperties("paletteIndex", this::getPaletteIndex);
        }
    }

    /**
     * The META_REALIZEPALETTE record maps entries from the logical palette that
     * is defined in the playback device context to the system palette.
     */
    public static class WmfRealizePalette implements HwmfRecord {
        @Override
        public HwmfRecordType getWmfRecordType() {
            return HwmfRecordType.realizePalette;
        }

        @Override
        public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException {
            return 0;
        }

        @Override
        public void draw(HwmfGraphics ctx) {

        }

        @Override
        public Map<String, Supplier<?>> getGenericProperties() {
            return null;
        }
    }

    /**
     * The META_ANIMATEPALETTE record redefines entries in the logical palette that
     * is defined in the playback device context with the specified Palette object
     *
     * The logical palette that is specified by the Palette object in this record is the
     * source of the palette changes, and the logical palette that is currently selected
     * into the playback device context is the destination. Entries in the destination
     * palette with the PC_RESERVED PaletteEntryFlag set SHOULD be modified by this record,
     * and entries with that flag clear SHOULD NOT be modified.
     * If none of the entries in the destination palette have the PC_RESERVED flag set, then
     * this record SHOULD have no effect.
     */
    public static class WmfAnimatePalette extends WmfPaletteParent {
        @Override
        public HwmfRecordType getWmfRecordType() {
            return HwmfRecordType.animatePalette;
        }

        @Override
        public void applyObject(HwmfGraphics ctx) {
            HwmfDrawProperties props = ctx.getProperties();
            List<PaletteEntry> dest = props.getPalette();
            List<PaletteEntry> src = getPaletteCopy();
            final int start = getPaletteStart();
            if (dest == null) {
                dest = new ArrayList<>();
            }
            for (int i=dest.size(); i<start; i++) {
                dest.add(new PaletteEntry());
            }
            for (int i=0; i<src.size(); i++) {
                PaletteEntry pe = src.get(i);
                if (dest.size() <= start+i) {
                    dest.add(pe);
                } else {
                    PaletteEntry peDst = dest.get(start+i);
                    if (peDst.isReserved()) {
                        dest.set(start+i, pe);
                    }
                }
            }
            props.setPalette(dest);
        }
    }
}
