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

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

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Supplier;

import org.apache.poi.common.Duplicatable;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.hssf.record.RecordInputStream;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.GenericRecordJsonWriter;
import org.apache.poi.util.LittleEndian;

/**
 * Font Formatting Block of the Conditional Formatting Rule Record.
 */
public final class FontFormatting implements Duplicatable, GenericRecord {
    private static final int OFFSET_FONT_NAME = 0;
    private static final int OFFSET_FONT_HEIGHT = 64;
    private static final int OFFSET_FONT_OPTIONS = 68;
    private static final int OFFSET_FONT_WEIGHT = 72;
    private static final int OFFSET_ESCAPEMENT_TYPE = 74;
    private static final int OFFSET_UNDERLINE_TYPE = 76;
    private static final int OFFSET_FONT_COLOR_INDEX = 80;
    private static final int OFFSET_OPTION_FLAGS = 88;
    private static final int OFFSET_ESCAPEMENT_TYPE_MODIFIED = 92;
    private static final int OFFSET_UNDERLINE_TYPE_MODIFIED = 96;
    private static final int OFFSET_FONT_WEIGHT_MODIFIED = 100;
    private static final int OFFSET_NOT_USED1 = 104;
    private static final int OFFSET_NOT_USED2 = 108;
    private static final int OFFSET_NOT_USED3 = 112; // for some reason Excel always writes  0x7FFFFFFF at this offset
    private static final int OFFSET_FONT_FORMATING_END = 116;
    private static final int RAW_DATA_SIZE = 118;


    public static final int  FONT_CELL_HEIGHT_PRESERVED   = 0xFFFFFFFF;

    // option flags and font options masks
    // in the options flags, a true bit activates the overriding and in the font option the bit sets the state
    private static final BitField POSTURE = BitFieldFactory.getInstance(0x00000002);
    private static final BitField OUTLINE = BitFieldFactory.getInstance(0x00000008);
    private static final BitField SHADOW = BitFieldFactory.getInstance(0x00000010);
    private static final BitField CANCELLATION = BitFieldFactory.getInstance(0x00000080);

    /** Normal boldness (not bold) */
    private static final short FONT_WEIGHT_NORMAL = 0x190;

    /**
     * Bold boldness (bold)
     */
    private static final short FONT_WEIGHT_BOLD  = 0x2bc;

    private final byte[] _rawData = new byte[RAW_DATA_SIZE];

    public FontFormatting() {
        setFontHeight(-1);
        setItalic(false);
        setFontWieghtModified(false);
        setOutline(false);
        setShadow(false);
        setStrikeout(false);
        setEscapementType((short)0);
        setUnderlineType((byte)0);
        setFontColorIndex((short)-1);

        setFontStyleModified(false);
        setFontOutlineModified(false);
        setFontShadowModified(false);
        setFontCancellationModified(false);

        setEscapementTypeModified(false);
        setUnderlineTypeModified(false);

        setShort(OFFSET_FONT_NAME, 0);
        setInt(OFFSET_NOT_USED1, 0x00000001);
        setInt(OFFSET_NOT_USED2, 0x00000000);
        setInt(OFFSET_NOT_USED3, 0x7FFFFFFF);// for some reason Excel always writes  0x7FFFFFFF at this offset
        setShort(OFFSET_FONT_FORMATING_END, 0x0001);
    }

    public FontFormatting(FontFormatting other) {
        System.arraycopy(other._rawData, 0, _rawData, 0, RAW_DATA_SIZE);
    }

    public FontFormatting(RecordInputStream in) {
        in.readFully(_rawData);
    }

    private short getShort(int offset) {
        return LittleEndian.getShort( _rawData, offset);
    }
    private void setShort(int offset, int value) {
        LittleEndian.putShort( _rawData, offset, (short)value);
    }
    private int getInt(int offset) {
        return LittleEndian.getInt( _rawData, offset);
    }
    private void setInt(int offset, int value) {
        LittleEndian.putInt( _rawData, offset, value);
    }

    public byte[] getRawRecord()
    {
        return _rawData;
    }

    public int getDataLength() {
        return RAW_DATA_SIZE;
    }

    /**
     * sets the height of the font in 1/20th point units
     *
     *
     * @param height  fontheight (in points/20); or -1 to preserve the cell font height
     */

    public void setFontHeight(int height) {
        setInt(OFFSET_FONT_HEIGHT, height);
    }

    /**
     * gets the height of the font in 1/20th point units
     *
     * @return fontheight (in points/20); or -1 if not modified
     */
    public int getFontHeight() {
        return getInt(OFFSET_FONT_HEIGHT);
    }

    private void setFontOption(boolean option, BitField field) {
        int options = getInt(OFFSET_FONT_OPTIONS);
        options = field.setBoolean(options, option);
        setInt(OFFSET_FONT_OPTIONS, options);
    }

    private boolean getFontOption(BitField field) {
        int options = getInt(OFFSET_FONT_OPTIONS);
        return field.isSet(options);
    }

    /**
     * set the font to be italics or not
     *
     * @param italic - whether the font is italics or not
     * @see #setFontOption(boolean, org.apache.poi.util.BitField)
     */

    public void setItalic(boolean italic) {
        setFontOption(italic, POSTURE);
    }

    /**
     * get whether the font is to be italics or not
     *
     * @return italics - whether the font is italics or not
     * @see #getFontOption(org.apache.poi.util.BitField)
     */

    public boolean isItalic() {
        return getFontOption(POSTURE);
    }

    public void setOutline(boolean on) {
        setFontOption(on, OUTLINE);
    }

    public boolean isOutlineOn() {
        return getFontOption(OUTLINE);
    }

    public void setShadow(boolean on) {
        setFontOption(on, SHADOW);
    }

    public boolean isShadowOn() {
        return getFontOption(SHADOW);
    }

    /**
     * set the font to be stricken out or not
     *
     * @param strike - whether the font is stricken out or not
     */
    public void setStrikeout(boolean strike) {
        setFontOption(strike, CANCELLATION);
    }

    /**
     * get whether the font is to be stricken out or not
     *
     * @return strike - whether the font is stricken out or not
     * @see #getFontOption(org.apache.poi.util.BitField)
     */
    public boolean isStruckout() {
        return getFontOption(CANCELLATION);
    }

    /**
     * set the font weight (100-1000dec or 0x64-0x3e8).  Default is
     * 0x190 for normal and 0x2bc for bold
     *
     * @param bw - a number between 100-1000 for the fonts "boldness"
     */
    private void setFontWeight(short bw) {
        setShort(OFFSET_FONT_WEIGHT, Math.max(100, Math.min(1000, bw)));
    }

    /**
     * set the font weight to bold (weight=700) or to normal(weight=400) boldness.
     *
     * @param bold - set font weight to bold if true; to normal otherwise
     */
    public void setBold(boolean bold) {
        setFontWeight(bold?FONT_WEIGHT_BOLD:FONT_WEIGHT_NORMAL);
    }

    /**
     * get the font weight for this font (100-1000dec or 0x64-0x3e8).  Default is
     * 0x190 for normal and 0x2bc for bold
     *
     * @return bw - a number between 100-1000 for the fonts "boldness"
     */
    public short getFontWeight() {
        return getShort(OFFSET_FONT_WEIGHT);
    }

    /**
     * get whether the font weight is set to bold or not
     *
     * @return bold - whether the font is bold or not
     */
    public boolean isBold() {
        return getFontWeight()==FONT_WEIGHT_BOLD;
    }

    /**
     * get the type of super or subscript for the font
     *
     * @return super or subscript option
     * @see org.apache.poi.ss.usermodel.Font#SS_NONE
     * @see org.apache.poi.ss.usermodel.Font#SS_SUPER
     * @see org.apache.poi.ss.usermodel.Font#SS_SUB
     */
    public short getEscapementType() {
        return getShort(OFFSET_ESCAPEMENT_TYPE);
    }

    /**
     * set the escapement type for the font
     *
     * @param escapementType  super or subscript option
     * @see org.apache.poi.ss.usermodel.Font#SS_NONE
     * @see org.apache.poi.ss.usermodel.Font#SS_SUPER
     * @see org.apache.poi.ss.usermodel.Font#SS_SUB
     */
    public void setEscapementType( short escapementType) {
        setShort(OFFSET_ESCAPEMENT_TYPE, escapementType);
    }

    /**
     * get the type of underlining for the font
     *
     * @return font underlining type
     *
     * @see org.apache.poi.ss.usermodel.Font#U_NONE
     * @see org.apache.poi.ss.usermodel.Font#U_SINGLE
     * @see org.apache.poi.ss.usermodel.Font#U_DOUBLE
     * @see org.apache.poi.ss.usermodel.Font#U_SINGLE_ACCOUNTING
     * @see org.apache.poi.ss.usermodel.Font#U_DOUBLE_ACCOUNTING
     */
    public short getUnderlineType() {
        return getShort(OFFSET_UNDERLINE_TYPE);
    }

    /**
     * set the type of underlining type for the font
     *
     * @param underlineType underline option
     *
     * @see org.apache.poi.ss.usermodel.Font#U_NONE
     * @see org.apache.poi.ss.usermodel.Font#U_SINGLE
     * @see org.apache.poi.ss.usermodel.Font#U_DOUBLE
     * @see org.apache.poi.ss.usermodel.Font#U_SINGLE_ACCOUNTING
     * @see org.apache.poi.ss.usermodel.Font#U_DOUBLE_ACCOUNTING
     */
    public void setUnderlineType( short underlineType) {
        setShort(OFFSET_UNDERLINE_TYPE, underlineType);
    }


    public short getFontColorIndex() {
        return (short)getInt(OFFSET_FONT_COLOR_INDEX);
    }

    public void setFontColorIndex(short fci ) {
        setInt(OFFSET_FONT_COLOR_INDEX,fci);
    }

    private boolean getOptionFlag(BitField field) {
        int optionFlags = getInt(OFFSET_OPTION_FLAGS);
        int value = field.getValue(optionFlags);
        return value == 0;
    }

    private void setOptionFlag(boolean modified, BitField field) {
        int value = modified? 0 : 1;
        int optionFlags = getInt(OFFSET_OPTION_FLAGS);
        optionFlags = field.setValue(optionFlags, value);
        setInt(OFFSET_OPTION_FLAGS, optionFlags);
    }


    public boolean isFontStyleModified() {
        return getOptionFlag(POSTURE);
    }


    public void setFontStyleModified(boolean modified) {
        setOptionFlag(modified, POSTURE);
    }

    public boolean isFontOutlineModified() {
        return getOptionFlag(OUTLINE);
    }

    public void setFontOutlineModified(boolean modified) {
        setOptionFlag(modified, OUTLINE);
    }

    public boolean isFontShadowModified() {
        return getOptionFlag(SHADOW);
    }

    public void setFontShadowModified(boolean modified) {
        setOptionFlag(modified, SHADOW);
    }
    public void setFontCancellationModified(boolean modified) {
        setOptionFlag(modified, CANCELLATION);
    }

    public boolean isFontCancellationModified() {
        return getOptionFlag(CANCELLATION);
    }

    public void setEscapementTypeModified(boolean modified) {
        int value = modified? 0 : 1;
        setInt(OFFSET_ESCAPEMENT_TYPE_MODIFIED, value);
    }

    public boolean isEscapementTypeModified() {
        int escapementModified = getInt(OFFSET_ESCAPEMENT_TYPE_MODIFIED);
        return escapementModified == 0;
    }

    public void setUnderlineTypeModified(boolean modified) {
        int value = modified? 0 : 1;
        setInt(OFFSET_UNDERLINE_TYPE_MODIFIED, value);
    }

    public boolean isUnderlineTypeModified() {
        int underlineModified = getInt(OFFSET_UNDERLINE_TYPE_MODIFIED);
        return underlineModified == 0;
    }

    public void setFontWieghtModified(boolean modified) {
        int value = modified? 0 : 1;
        setInt(OFFSET_FONT_WEIGHT_MODIFIED, value);
    }

    public boolean isFontWeightModified() {
        int fontStyleModified = getInt(OFFSET_FONT_WEIGHT_MODIFIED);
        return fontStyleModified == 0;
    }

    @Override
    public Map<String, Supplier<?>> getGenericProperties() {
        final Map<String,Supplier<?>> m = new LinkedHashMap<>();
        m.put("fontHeight", this::getFontHeight);
        m.put("options", getBitsAsString(() -> getInt(OFFSET_OPTION_FLAGS),
            new BitField[]{POSTURE,OUTLINE,SHADOW,CANCELLATION},
            new String[]{"POSTURE_MODIFIED","OUTLINE_MODIFIED","SHADOW_MODIFIED","STRUCKOUT_MODIFIED"}));
        m.put("fontOptions", getBitsAsString(() -> getInt(OFFSET_FONT_OPTIONS),
            new BitField[]{POSTURE,OUTLINE,SHADOW,CANCELLATION},
            new String[]{"ITALIC","OUTLINE","SHADOW","STRUCKOUT"}));
        m.put("fontWEightModified", this::isFontWeightModified);
        m.put("fontWeight", getEnumBitsAsString(this::getFontWeight,
            new int[]{FONT_WEIGHT_NORMAL,FONT_WEIGHT_BOLD},
            new String[]{"NORMAL","BOLD"}));
        m.put("escapementTypeModified", this::isEscapementTypeModified);
        m.put("escapementType", this::getEscapementType);
        m.put("underlineTypeModified", this::isUnderlineTypeModified);
        m.put("underlineType", this::getUnderlineType);
        m.put("colorIndex", this::getFontColorIndex);
        return Collections.unmodifiableMap(m);
    }

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

    @Override
    public FontFormatting copy() {
        return new FontFormatting(this);
    }
}
