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

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

import org.apache.logging.log4j.Logger;
import org.apache.poi.logging.PoiLogManager;
import org.apache.poi.hssf.model.HSSFFormulaParser;
import org.apache.poi.hssf.record.cf.BorderFormatting;
import org.apache.poi.hssf.record.cf.FontFormatting;
import org.apache.poi.hssf.record.cf.PatternFormatting;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.ss.formula.Formula;
import org.apache.poi.ss.formula.FormulaType;
import org.apache.poi.ss.formula.ptg.Ptg;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.GenericRecordUtil;
import org.apache.poi.util.LittleEndianOutput;

/**
 * Conditional Formatting Rules. This can hold old-style rules
 *
 *
 * <p>This is for the older-style Excel conditional formattings,
 *  new-style (Excel 2007+) also make use of {@link CFRule12Record}
 *  for their rules.</p>
 */
public abstract class CFRuleBase extends StandardRecord {
    // FIXME: Merge with org.apache.poi.ss.usermodel.ComparisonOperator and rewrite as an enum
    public interface ComparisonOperator {
        byte NO_COMPARISON = 0;
        byte BETWEEN       = 1;
        byte NOT_BETWEEN   = 2;
        byte EQUAL         = 3;
        byte NOT_EQUAL     = 4;
        byte GT            = 5;
        byte LT            = 6;
        byte GE            = 7;
        byte LE            = 8;
        byte max_operator  = 8;
    }

    // The only kinds that CFRuleRecord handles
    public static final byte CONDITION_TYPE_CELL_VALUE_IS = 1;
    public static final byte CONDITION_TYPE_FORMULA = 2;
    // These are CFRule12Rule only
    public static final byte CONDITION_TYPE_COLOR_SCALE = 3;
    public static final byte CONDITION_TYPE_DATA_BAR = 4;
    public static final byte CONDITION_TYPE_FILTER = 5;
    public static final byte CONDITION_TYPE_ICON_SET = 6;

    public static final int TEMPLATE_CELL_VALUE = 0x0000;
    public static final int TEMPLATE_FORMULA = 0x0001;
    public static final int TEMPLATE_COLOR_SCALE_FORMATTING = 0x0002;
    public static final int TEMPLATE_DATA_BAR_FORMATTING = 0x0003;
    public static final int TEMPLATE_ICON_SET_FORMATTING = 0x0004;
    public static final int TEMPLATE_FILTER = 0x0005;
    public static final int TEMPLATE_UNIQUE_VALUES = 0x0007;
    public static final int TEMPLATE_CONTAINS_TEXT = 0x0008;
    public static final int TEMPLATE_CONTAINS_BLANKS = 0x0009;
    public static final int TEMPLATE_CONTAINS_NO_BLANKS = 0x000A;
    public static final int TEMPLATE_CONTAINS_ERRORS = 0x000B;
    public static final int TEMPLATE_CONTAINS_NO_ERRORS = 0x000C;
    public static final int TEMPLATE_TODAY = 0x000F;
    public static final int TEMPLATE_TOMORROW = 0x0010;
    public static final int TEMPLATE_YESTERDAY = 0x0011;
    public static final int TEMPLATE_LAST_7_DAYS = 0x0012;
    public static final int TEMPLATE_LAST_MONTH = 0x0013;
    public static final int TEMPLATE_NEXT_MONTH = 0x0014;
    public static final int TEMPLATE_THIS_WEEK = 0x0015;
    public static final int TEMPLATE_NEXT_WEEK = 0x0016;
    public static final int TEMPLATE_LAST_WEEK = 0x0017;
    public static final int TEMPLATE_THIS_MONTH = 0x0018;
    public static final int TEMPLATE_ABOVE_AVERAGE = 0x0019;
    public static final int TEMPLATE_BELOW_AVERAGE = 0x001A;
    public static final int TEMPLATE_DUPLICATE_VALUES = 0x001B;
    public static final int TEMPLATE_ABOVE_OR_EQUAL_TO_AVERAGE = 0x001D;
    public static final int TEMPLATE_BELOW_OR_EQUAL_TO_AVERAGE = 0x001E;

    protected static final Logger LOG = PoiLogManager.getLogger(CFRuleBase.class);

    static final BitField modificationBits = bf(0x003FFFFF); // Bits: font,align,bord,patt,prot
    static final BitField alignHor         = bf(0x00000001); // 0 = Horizontal alignment modified
    static final BitField alignVer         = bf(0x00000002); // 0 = Vertical alignment modified
    static final BitField alignWrap        = bf(0x00000004); // 0 = Text wrapped flag modified
    static final BitField alignRot         = bf(0x00000008); // 0 = Text rotation modified
    static final BitField alignJustLast    = bf(0x00000010); // 0 = Justify last line flag modified
    static final BitField alignIndent      = bf(0x00000020); // 0 = Indentation modified
    static final BitField alignShrin       = bf(0x00000040); // 0 = Shrink to fit flag modified
    static final BitField mergeCell        = bf(0x00000080); // Normally 1, 0 = Merge Cell flag modified
    static final BitField protLocked       = bf(0x00000100); // 0 = Cell locked flag modified
    static final BitField protHidden       = bf(0x00000200); // 0 = Cell hidden flag modified
    static final BitField bordLeft         = bf(0x00000400); // 0 = Left border style and colour modified
    static final BitField bordRight        = bf(0x00000800); // 0 = Right border style and colour modified
    static final BitField bordTop          = bf(0x00001000); // 0 = Top border style and colour modified
    static final BitField bordBot          = bf(0x00002000); // 0 = Bottom border style and colour modified
    static final BitField bordTlBr         = bf(0x00004000); // 0 = Top-left to bottom-right border flag modified
    static final BitField bordBlTr         = bf(0x00008000); // 0 = Bottom-left to top-right border flag modified
    static final BitField pattStyle        = bf(0x00010000); // 0 = Pattern style modified
    static final BitField pattCol          = bf(0x00020000); // 0 = Pattern colour modified
    static final BitField pattBgCol        = bf(0x00040000); // 0 = Pattern background colour modified
    static final BitField notUsed2         = bf(0x00380000); // Always 111 (ifmt / ifnt / 1)
    static final BitField undocumented     = bf(0x03C00000); // Undocumented bits
    static final BitField fmtBlockBits     = bf(0x7C000000); // Bits: font,align,bord,patt,prot
    static final BitField font             = bf(0x04000000); // 1 = Record contains font formatting block
    static final BitField align            = bf(0x08000000); // 1 = Record contains alignment formatting block
    static final BitField bord             = bf(0x10000000); // 1 = Record contains border formatting block
    static final BitField patt             = bf(0x20000000); // 1 = Record contains pattern formatting block
    static final BitField prot             = bf(0x40000000); // 1 = Record contains protection formatting block
    static final BitField alignTextDir     = bf(0x80000000); // 0 = Text direction modified

    private static BitField bf(int i) {
        return BitFieldFactory.getInstance(i);
    }


    private byte condition_type;
    private byte comparison_operator;

    protected int formatting_options;
    // TODO Decode this properly
    protected short formatting_not_used;

    protected FontFormatting _fontFormatting;
    protected BorderFormatting _borderFormatting;
    protected PatternFormatting _patternFormatting;

    private Formula formula1;
    private Formula formula2;

    /**
     * Creates new CFRuleRecord
     *
     * @param conditionType the condition type
     * @param comparisonOperation the comparison operation
     */
    protected CFRuleBase(byte conditionType, byte comparisonOperation) {
        setConditionType(conditionType);
        setComparisonOperation(comparisonOperation);
        formula1 = Formula.create(Ptg.EMPTY_PTG_ARRAY);
        formula2 = Formula.create(Ptg.EMPTY_PTG_ARRAY);
    }

    protected CFRuleBase(byte conditionType, byte comparisonOperation, Ptg[] formula1, Ptg[] formula2) {
        this(conditionType, comparisonOperation);
        this.formula1 = Formula.create(formula1);
        this.formula2 = Formula.create(formula2);
    }

    protected CFRuleBase() {}

    protected CFRuleBase(CFRuleBase other) {
        super(other);
        setConditionType(other.getConditionType());
        setComparisonOperation(other.getComparisonOperation());
        formatting_options = other.formatting_options;
        formatting_not_used = other.formatting_not_used;
        _fontFormatting = (!other.containsFontFormattingBlock()) ? null : other.getFontFormatting().copy();
        _borderFormatting = (!other.containsBorderFormattingBlock()) ? null : other.getBorderFormatting().copy();
        _patternFormatting = (!other.containsPatternFormattingBlock()) ? null : other.getPatternFormatting().copy();
        formula1 = other.getFormula1().copy();
        formula2 = other.getFormula2().copy();
    }

    protected int readFormatOptions(RecordInputStream in) {
        formatting_options = in.readInt();
        formatting_not_used = in.readShort();

        int len = 6;

        if (containsFontFormattingBlock()) {
            _fontFormatting = new FontFormatting(in);
            len += _fontFormatting.getDataLength();
        }

        if (containsBorderFormattingBlock()) {
            _borderFormatting = new BorderFormatting(in);
            len += _borderFormatting.getDataLength();
        }

        if (containsPatternFormattingBlock()) {
            _patternFormatting = new PatternFormatting(in);
            len += _patternFormatting.getDataLength();
        }

        return len;
    }

    public byte getConditionType() {
        return condition_type;
    }
    protected void setConditionType(byte condition_type) {
        if ((this instanceof CFRuleRecord)) {
            if (!(condition_type == CONDITION_TYPE_CELL_VALUE_IS ||
                condition_type == CONDITION_TYPE_FORMULA)) {
                throw new IllegalArgumentException("CFRuleRecord only accepts Value-Is and Formula types");
            }
        }
        this.condition_type = condition_type;
    }

    public void setComparisonOperation(byte operation) {
        if (operation < 0 || operation > ComparisonOperator.max_operator)
            throw new IllegalArgumentException(
                    "Valid operators are only in the range 0 to " +ComparisonOperator.max_operator);

        this.comparison_operator = operation;
    }
    public byte getComparisonOperation() {
        return comparison_operator;
    }

    public boolean containsFontFormattingBlock() {
        return getOptionFlag(font);
    }
    public void setFontFormatting(FontFormatting fontFormatting) {
        _fontFormatting = fontFormatting;
        setOptionFlag(fontFormatting != null, font);
    }
    public FontFormatting getFontFormatting() {
        if( containsFontFormattingBlock()) {
            return _fontFormatting;
        }
        return null;
    }

    public boolean containsAlignFormattingBlock() {
        return getOptionFlag(align);
    }
    public void setAlignFormattingUnchanged() {
        setOptionFlag(false,align);
    }

    public boolean containsBorderFormattingBlock() {
        return getOptionFlag(bord);
    }
    public void setBorderFormatting(BorderFormatting borderFormatting) {
        _borderFormatting = borderFormatting;
        setOptionFlag(borderFormatting != null, bord);
    }
    public BorderFormatting getBorderFormatting() {
        if( containsBorderFormattingBlock()) {
            return _borderFormatting;
        }
        return null;
    }

    public boolean containsPatternFormattingBlock() {
        return getOptionFlag(patt);
    }
    public void setPatternFormatting(PatternFormatting patternFormatting) {
        _patternFormatting = patternFormatting;
        setOptionFlag(patternFormatting!=null, patt);
    }
    public PatternFormatting getPatternFormatting() {
        if( containsPatternFormattingBlock())
        {
            return _patternFormatting;
        }
        return null;
    }

    public boolean containsProtectionFormattingBlock() {
        return getOptionFlag(prot);
    }
    public void setProtectionFormattingUnchanged() {
        setOptionFlag(false,prot);
    }

    /**
     * get the option flags
     *
     * @return bit mask
     */
    public int getOptions() {
        return formatting_options;
    }

    private boolean isModified(BitField field) {
        return !field.isSet(formatting_options);
    }
    private void setModified(boolean modified, BitField field) {
        formatting_options = field.setBoolean(formatting_options, !modified);
    }

    public boolean isLeftBorderModified() {
        return isModified(bordLeft);
    }
    public void setLeftBorderModified(boolean modified) {
        setModified(modified,bordLeft);
    }

    public boolean isRightBorderModified() {
        return isModified(bordRight);
    }
    public void setRightBorderModified(boolean modified)
    {
        setModified(modified,bordRight);
    }

    public boolean isTopBorderModified() {
        return isModified(bordTop);
    }
    public void setTopBorderModified(boolean modified) {
        setModified(modified,bordTop);
    }

    public boolean isBottomBorderModified() {
        return isModified(bordBot);
    }
    public void setBottomBorderModified(boolean modified) {
        setModified(modified,bordBot);
    }

    public boolean isTopLeftBottomRightBorderModified() {
        return isModified(bordTlBr);
    }
    public void setTopLeftBottomRightBorderModified(boolean modified) {
        setModified(modified,bordTlBr);
    }

    public boolean isBottomLeftTopRightBorderModified() {
        return isModified(bordBlTr);
    }
    public void setBottomLeftTopRightBorderModified(boolean modified) {
        setModified(modified,bordBlTr);
    }

    public boolean isPatternStyleModified() {
        return isModified(pattStyle);
    }
    public void setPatternStyleModified(boolean modified) {
        setModified(modified,pattStyle);
    }

    public boolean isPatternColorModified() {
        return isModified(pattCol);
    }
    public void setPatternColorModified(boolean modified) {
        setModified(modified,pattCol);
    }

    public boolean isPatternBackgroundColorModified() {
        return isModified(pattBgCol);
    }
    public void setPatternBackgroundColorModified(boolean modified) {
        setModified(modified,pattBgCol);
    }

    private boolean getOptionFlag(BitField field) {
        return field.isSet(formatting_options);
    }
    private void setOptionFlag(boolean flag, BitField field) {
        formatting_options = field.setBoolean(formatting_options, flag);
    }

    protected int getFormattingBlockSize() {
        return 6 +
          (containsFontFormattingBlock()?_fontFormatting.getRawRecord().length:0)+
          (containsBorderFormattingBlock()?8:0)+
          (containsPatternFormattingBlock()?4:0);
    }
    protected void serializeFormattingBlock(LittleEndianOutput out) {
        out.writeInt(formatting_options);
        out.writeShort(formatting_not_used);

        if (containsFontFormattingBlock()) {
            byte[] fontFormattingRawRecord  = _fontFormatting.getRawRecord();
            out.write(fontFormattingRawRecord);
        }

        if (containsBorderFormattingBlock()) {
            _borderFormatting.serialize(out);
        }

        if (containsPatternFormattingBlock()) {
            _patternFormatting.serialize(out);
        }
    }

    /**
     * get the stack of the 1st expression as a list
     *
     * @return list of tokens (casts stack to a list and returns it!)
     * this method can return null is we are unable to create Ptgs from
     *   existing excel file
     * callers should check for null!
     */
    public Ptg[] getParsedExpression1() {
        return formula1.getTokens();
    }
    public void setParsedExpression1(Ptg[] ptgs) {
        formula1 = Formula.create(ptgs);
    }
    protected Formula getFormula1() {
        return formula1;
    }
    protected void setFormula1(Formula formula1) {
        this.formula1 = formula1;
    }

    /**
     * get the stack of the 2nd expression as a list
     *
     * @return array of {@link Ptg}s, possibly {@code null}
     */
    public Ptg[] getParsedExpression2() {
        return Formula.getTokens(formula2);
    }
    public void setParsedExpression2(Ptg[] ptgs) {
        formula2 = Formula.create(ptgs);
    }
    protected Formula getFormula2() {
        return formula2;
    }
    protected void setFormula2(Formula formula2) {
        this.formula2 = formula2;
    }

    /**
     * @param formula must not be {@code null}
     * @return encoded size of the formula tokens (does not include 2 bytes for ushort length)
     */
    protected static int getFormulaSize(Formula formula) {
        return formula.getEncodedTokenSize();
    }

    /**
     * TODO - parse conditional format formulas properly i.e. produce tRefN and tAreaN instead of tRef and tArea
     * this call will produce the wrong results if the formula contains any cell references
     * One approach might be to apply the inverse of SharedFormulaRecord.convertSharedFormulas(Stack, int, int)
     * Note - two extra parameters (rowIx &amp; colIx) will be required. They probably come from one of the Region objects.
     *
     * @param formula  The formula to parse, excluding the leading equals sign.
     * @param sheet  The sheet that the formula is on.
     * @return {@code null} if {@code formula} was null.
     */
    public static Ptg[] parseFormula(String formula, HSSFSheet sheet) {
        if(formula == null) {
            return null;
        }
        int sheetIndex = sheet.getWorkbook().getSheetIndex(sheet);
        return HSSFFormulaParser.parse(formula, sheet.getWorkbook(), FormulaType.CELL, sheetIndex);
    }

    @Override
    public abstract CFRuleBase copy();

    @Override
    public Map<String, Supplier<?>> getGenericProperties() {
        return GenericRecordUtil.getGenericProperties(
            "conditionType", this::getConditionType,
            "comparisonOperation", this::getComparisonOperation,
            "formattingOptions", this::getOptions,
            "formattingNotUsed", () -> formatting_not_used,
            "fontFormatting", this::getFontFormatting,
            "borderFormatting", this::getBorderFormatting,
            "patternFormatting", this::getPatternFormatting,
            "formula1", this::getFormula1,
            "formula2", this::getFormula2
        );
    }
}
