/* ====================================================================
   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 static org.apache.poi.util.GenericRecordUtil.getBitsAsString;

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

import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.LittleEndianOutput;

/**
 * Stores the row information for the sheet.
 *
 * @since 2.0-pre
 */
public final class RowRecord extends StandardRecord {
    public static final short sid = 0x0208;

    public static final int ENCODED_SIZE = 20;

    private static final int OPTION_BITS_ALWAYS_SET = 0x0100;
    //private static final int DEFAULT_HEIGHT_BIT = 0x8000;

    private static final BitField outlineLevel  = BitFieldFactory.getInstance(0x07);
    // bit 3 reserved
    private static final BitField collapsed     = BitFieldFactory.getInstance(0x10);
    private static final BitField zeroHeight    = BitFieldFactory.getInstance(0x20);
    private static final BitField badFontHeight = BitFieldFactory.getInstance(0x40);
    private static final BitField formatted     = BitFieldFactory.getInstance(0x80);

    private static final BitField xfIndex       = BitFieldFactory.getInstance(0xFFF);
    private static final BitField topBorder     = BitFieldFactory.getInstance(0x1000);
    private static final BitField bottomBorder  = BitFieldFactory.getInstance(0x2000);
    private static final BitField phoneticGuide = BitFieldFactory.getInstance(0x4000);
    // bit 15 is unused

    private int field_1_row_number;
    private int field_2_first_col;
    private int field_3_last_col; // plus 1
    private short field_4_height;
    private short field_5_optimize; // hint field for gui, can/should be set to zero

    // for generated sheets.
    private short field_6_reserved;
    /** 16 bit options flags */
    private int field_7_option_flags;
    /** 16 bit options flags */
    private int field_8_option_flags;   // only if isFormatted

    public RowRecord(RowRecord other) {
        super(other);
        field_1_row_number = other.field_1_row_number;
        field_2_first_col = other.field_2_first_col;
        field_3_last_col = other.field_3_last_col;
        field_4_height = other.field_4_height;
        field_5_optimize = other.field_5_optimize;
        field_6_reserved = other.field_6_reserved;
        field_7_option_flags = other.field_7_option_flags;
        field_8_option_flags = other.field_8_option_flags;
    }


    public RowRecord(int rowNumber) {
        if(rowNumber < 0) {
            throw new IllegalArgumentException("Invalid row number (" + rowNumber + ")");
        }
        field_1_row_number = rowNumber;
        field_4_height = (short)0xFF;
        field_5_optimize = ( short ) 0;
        field_6_reserved = ( short ) 0;
        field_7_option_flags = OPTION_BITS_ALWAYS_SET; // seems necessary for outlining

        field_8_option_flags = ( short ) 0xf;
        setEmpty();
    }

    public RowRecord(RecordInputStream in) {
        field_1_row_number   = in.readUShort();
        if(field_1_row_number < 0) {
            throw new IllegalArgumentException("Invalid row number " + field_1_row_number + " found in InputStream");
        }
        field_2_first_col    = in.readShort();
        field_3_last_col     = in.readShort();
        field_4_height       = in.readShort();
        field_5_optimize     = in.readShort();
        field_6_reserved     = in.readShort();
        field_7_option_flags = in.readShort();
        field_8_option_flags = in.readShort();
    }

    /**
     * Updates the firstCol and lastCol fields to the reserved value (-1)
     * to signify that this row is empty
     */
    public void setEmpty() {
        field_2_first_col = 0;
        field_3_last_col = 0;
    }
    public boolean isEmpty() {
        return (field_2_first_col | field_3_last_col) == 0;
    }

    /**
     * set the logical row number for this row (0 based index)
     * @param row - the row number
     */
    public void setRowNumber(int row) {
        field_1_row_number = row;
    }

    /**
     * set the logical col number for the first cell this row (0 based index)
     * @param col - the col number
     */
    public void setFirstCol(int col) {
        field_2_first_col = col;
    }

    /**
     * @param col - one past the zero-based index to the last cell in this row
     */
    public void setLastCol(int col) {
        field_3_last_col = col;
    }

    /**
     * set the height of the row
     * @param height of the row
     */
    public void setHeight(short height) {
        field_4_height = height;
    }

    /**
     * set whether to optimize or not (set to 0)
     * @param optimize (set to 0)
     */
    public void setOptimize(short optimize) {
        field_5_optimize = optimize;
    }

    // option bitfields

    /**
     * set the outline level of this row
     * @param ol - the outline level
     */
    public void setOutlineLevel(short ol) {
        field_7_option_flags = outlineLevel.setValue(field_7_option_flags, ol);
    }

    /**
     * set whether or not to collapse this row
     * @param c - collapse or not
     */
    public void setColapsed(boolean c) {
        field_7_option_flags = collapsed.setBoolean(field_7_option_flags, c);
    }

    /**
     * set whether or not to display this row with 0 height
     * @param z  height is zero or not.
     */
    public void setZeroHeight(boolean z) {
        field_7_option_flags = zeroHeight.setBoolean(field_7_option_flags, z);
    }

    /**
     * set whether the font and row height are not compatible
     * @param  f  true if they aren't compatible (damn not logic)
     */
    public void setBadFontHeight(boolean f) {
        field_7_option_flags = badFontHeight.setBoolean(field_7_option_flags, f);
    }

    /**
     * set whether the row has been formatted (even if its got all blank cells)
     * @param f  formatted or not
     */
    public void setFormatted(boolean f) {
        field_7_option_flags = formatted.setBoolean(field_7_option_flags, f);
    }

    // end bitfields

    /**
     * if the row is formatted then this is the index to the extended format record
     * @see org.apache.poi.hssf.record.ExtendedFormatRecord
     * @param index to the XF record
     */
    public void setXFIndex(short index) {
        field_8_option_flags = xfIndex.setValue(field_8_option_flags, index);
    }

    /**
     * bit that specifies whether any cell in the row has a thick top border, or any
     * cell in the row directly above the current row has a thick bottom border.
     * @param f has thick top border
     */
    public void setTopBorder(boolean f) {
        field_8_option_flags = topBorder.setBoolean(field_8_option_flags, f);
    }

    /**
     * A bit that specifies whether any cell in the row has a medium or thick
     * bottom border, or any cell in the row directly below the current row has
     * a medium or thick top border.
     * @param f has thick bottom border
     */
    public void setBottomBorder(boolean f) {
        field_8_option_flags = bottomBorder.setBoolean(field_8_option_flags, f);
    }

    /**
     * A bit that specifies whether the phonetic guide feature is enabled for
     * any cell in this row.
     * @param f use phoenetic guide
     */
    public void setPhoeneticGuide(boolean f) {
        field_8_option_flags = phoneticGuide.setBoolean(field_8_option_flags, f);
    }

    /**
     * get the logical row number for this row (0 based index)
     * @return row - the row number
     */
    public int getRowNumber() {
        return field_1_row_number;
    }

    /**
     * get the logical col number for the first cell this row (0 based index)
     * @return col - the col number
     */
    public int getFirstCol() {
        return field_2_first_col;
    }

    /**
     * get the logical col number for the last cell this row (0 based index), plus one
     * @return col - the last col index + 1
     */
    public int getLastCol() {
        return field_3_last_col;
    }

    /**
     * get the height of the row
     * @return height of the row
     */
    public short getHeight() {
        return field_4_height;
    }

    /**
     * get whether to optimize or not (set to 0)
     * @return optimize (set to 0)
     */
    public short getOptimize() {
        return field_5_optimize;
    }

    /**
     * gets the option bitmask.  (use the individual bit setters that refer to this
     * method)
     * @return options - the bitmask
     */
    public short getOptionFlags() {
        return (short)field_7_option_flags;
    }

    // option bitfields

    /**
     * get the outline level of this row
     * @return ol - the outline level
     * @see #getOptionFlags()
     */
    public short getOutlineLevel() {
        return (short)outlineLevel.getValue(field_7_option_flags);
    }

    /**
     * get whether or not to colapse this row
     * @return c - colapse or not
     * @see #getOptionFlags()
     */
    public boolean getColapsed() {
        return (collapsed.isSet(field_7_option_flags));
    }

    /**
     * get whether or not to display this row with 0 height
     * @return - z height is zero or not.
     * @see #getOptionFlags()
     */
    public boolean getZeroHeight() {
        return zeroHeight.isSet(field_7_option_flags);
    }

    /**
     * get whether the font and row height are not compatible
     * @return - f -true if they aren't compatible (damn not logic)
     * @see #getOptionFlags()
     */
    public boolean getBadFontHeight() {
        return badFontHeight.isSet(field_7_option_flags);
    }

    /**
     * get whether the row has been formatted (even if its got all blank cells)
     * @return formatted or not
     * @see #getOptionFlags()
     */
    public boolean getFormatted() {
        return formatted.isSet(field_7_option_flags);
    }

    // end bitfields

    /**
     * gets the 2nd option bitmask.  (use the individual bit setters that refer to this
     * method)
     * @return options - the bitmask
     */
    public short getOptionFlags2() {
        return (short)field_8_option_flags;
    }

    /**
     * if the row is formatted then this is the index to the extended format record
     * @see org.apache.poi.hssf.record.ExtendedFormatRecord
     * @return index to the XF record or bogus value (undefined) if isn't formatted
     */
    public short getXFIndex() {
        return xfIndex.getShortValue((short)field_8_option_flags);
    }

    /**
     * A bit that specifies whether any cell in the row has a thick top border, or any
     * cell in the row directly above the current row has a thick bottom border.
     * @return has cells with a thick top border
     */
    public boolean getTopBorder() {
        return topBorder.isSet(field_8_option_flags);
    }

    /**
     * A bit that specifies whether any cell in the row has a medium or thick bottom border,
     * or any cell in the row directly below the current row has a medium or thick top border.
     * @return has cells with a thick bottom border
     */
    public boolean getBottomBorder() {
        return bottomBorder.isSet(field_8_option_flags);
    }

    /**
     * A bit that specifies whether the phonetic guide feature is enabled for
     * any cell in this row.
     * @return has phoentic guide
     */
    public boolean getPhoeneticGuide() {
        return phoneticGuide.isSet(field_8_option_flags);
    }

    public void serialize(LittleEndianOutput out) {
        out.writeShort(getRowNumber());
        out.writeShort(getFirstCol() == -1 ? (short)0 : getFirstCol());
        out.writeShort(getLastCol() == -1 ? (short)0 : getLastCol());
        out.writeShort(getHeight());
        out.writeShort(getOptimize());
        out.writeShort(field_6_reserved);
        out.writeShort(getOptionFlags());
        out.writeShort(getOptionFlags2());
    }

    protected int getDataSize() {
        return ENCODED_SIZE - 4;
    }

    public short getSid() {
        return sid;
    }

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

    @Override
    public HSSFRecordTypes getGenericRecordType() {
        return HSSFRecordTypes.ROW;
    }

    @Override
    public Map<String, Supplier<?>> getGenericProperties() {
        final Map<String,Supplier<?>> m = new LinkedHashMap<>();
        m.put("rowNumber", this::getRowNumber);
        m.put("firstCol", this::getFirstCol);
        m.put("lastCol", this::getLastCol);
        m.put("height", this::getHeight);
        m.put("optimized", this::getOptimize);
        m.put("reserved", () -> field_6_reserved);
        m.put("options", getBitsAsString(this::getOptionFlags,
            new BitField[]{collapsed,zeroHeight,badFontHeight,formatted},
            new String[]{"COLAPSED","ZERO_HEIGHT","BAD_FONT_HEIGHT","FORMATTED"}));
        m.put("outlineLevel", this::getOutlineLevel);
        m.put("optionFlags2", getBitsAsString(this::getOptionFlags2,
            new BitField[]{topBorder, bottomBorder, phoneticGuide},
            new String[]{"TOP_BORDER","BOTTOM_BORDER","PHOENETIC_GUIDE"}));
        m.put("xfIndex", this::getXFIndex);
        return Collections.unmodifiableMap(m);
    }
}
