/* ====================================================================
   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.ss.formula.ptg;

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

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.GenericRecordUtil;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInput;
import org.apache.poi.util.LittleEndianOutput;

/**
 * "Special Attributes"<p>
 * This seems to be a Misc Stuff and Junk record.  One function it serves is
 * in SUM functions (i.e. SUM(A1:A3) causes an area PTG then an ATTR with the SUM option set)
 */
public final class AttrPtg extends ControlPtg {
    public static final byte sid  = 0x19;
    private static final int  SIZE = 4;

    // flags 'volatile' and 'space', can be combined.
    // OOO spec says other combinations are theoretically possible but not likely to occur.
    private static final BitField semiVolatile = BitFieldFactory.getInstance(0x01);
    private static final BitField optiIf       = BitFieldFactory.getInstance(0x02);
    private static final BitField optiChoose   = BitFieldFactory.getInstance(0x04);
    private static final BitField optiSkip     = BitFieldFactory.getInstance(0x08);
    private static final BitField optiSum      = BitFieldFactory.getInstance(0x10);
    private static final BitField baxcel       = BitFieldFactory.getInstance(0x20); // 'assignment-style formula in a macro sheet'
    private static final BitField space        = BitFieldFactory.getInstance(0x40);

    public static final AttrPtg SUM = new AttrPtg(0x0010, 0, null, -1);

    public static final class SpaceType {
        private SpaceType() {
            // no instances of this class
        }

        /** 00H = Spaces before the next token (not allowed before tParen token) */
        public static final int SPACE_BEFORE = 0x00;
        /** 01H = Carriage returns before the next token (not allowed before tParen token) */
        public static final int CR_BEFORE = 0x01;
        /** 02H = Spaces before opening parenthesis (only allowed before tParen token) */
        public static final int SPACE_BEFORE_OPEN_PAREN = 0x02;
        /** 03H = Carriage returns before opening parenthesis (only allowed before tParen token) */
        public static final int CR_BEFORE_OPEN_PAREN = 0x03;
        /** 04H = Spaces before closing parenthesis (only allowed before tParen, tFunc, and tFuncVar tokens) */
        public static final int SPACE_BEFORE_CLOSE_PAREN = 0x04;
        /** 05H = Carriage returns before closing parenthesis (only allowed before tParen, tFunc, and tFuncVar tokens) */
        public static final int CR_BEFORE_CLOSE_PAREN = 0x05;
        /** 06H = Spaces following the equality sign (only in macro sheets) */
        public static final int SPACE_AFTER_EQUALITY = 0x06;
    }

    private final byte _options;
    private final short _data;

    /** only used for tAttrChoose: table of offsets to starts of args */
    private final int[] _jumpTable;
    /** only used for tAttrChoose: offset to the tFuncVar for CHOOSE() */
    private final int   _chooseFuncOffset;

    public AttrPtg(LittleEndianInput in) {
        _options = in.readByte();
        _data    = in.readShort();
        if (isOptimizedChoose()) {
            int[] jumpTable = new int[(int) _data];
            for (int i = 0; i < jumpTable.length; i++) {
                jumpTable[i] = in.readUShort();
            }
            _jumpTable = jumpTable;
            _chooseFuncOffset = in.readUShort();
        } else {
            _jumpTable = null;
            _chooseFuncOffset = -1;
        }

    }
    private AttrPtg(int options, int data, int[] jt, int chooseFuncOffset) {
        _options = (byte) options;
        _data = (short) data;
        _jumpTable = jt;
        _chooseFuncOffset = chooseFuncOffset;
    }

    /**
     * @param type a constant from {@code SpaceType}
     * @param count the number of space characters
     */
    public static AttrPtg createSpace(int type, int count) {
        int data = type & 0x00FF | (count << 8) & 0x00FFFF;
        return new AttrPtg(space.set(0), data, null, -1);
    }

    /**
     * @param dist distance (in bytes) to start of either <ul><li>false parameter</li>
     * <li>tFuncVar(IF) token (when false parameter is not present)</li></ul>
     */
    public static AttrPtg createIf(int dist) {
        return new AttrPtg(optiIf.set(0), dist, null, -1);
    }

    /**
     * @param dist distance (in bytes) to position behind tFuncVar(IF) token (minus 1)
     */
    public static AttrPtg createSkip(int dist) {
        return new AttrPtg(optiSkip.set(0), dist, null, -1);
    }

    public static AttrPtg getSumSingle() {
        return new AttrPtg(optiSum.set(0), 0, null, -1);
    }


    public boolean isSemiVolatile() {
        return semiVolatile.isSet(_options);
    }

    public boolean isOptimizedIf() {
        return optiIf.isSet(_options);
    }

    public boolean isOptimizedChoose() {
        return optiChoose.isSet(_options);
    }

    public boolean isSum() {
        return optiSum.isSet(_options);
    }
    public boolean isSkip() {
        return optiSkip.isSet(_options);
    }

    // lets hope no one uses this anymore
    private boolean isBaxcel() {
        return baxcel.isSet(_options);
    }

    public boolean isSpace() {
        return space.isSet(_options);
    }

    public short getData() {
        return _data;
    }
    public int[] getJumpTable() {
        return _jumpTable.clone();
    }
    public int getChooseFuncOffset() {
        if (_jumpTable == null) {
            throw new IllegalStateException("Not tAttrChoose");
        }
        return _chooseFuncOffset;
    }

    @Override
    public void write(LittleEndianOutput out) {
        out.writeByte(sid + getPtgClass());
        out.writeByte(_options);
        out.writeShort(_data);
        int[] jt = _jumpTable;
        if (jt != null) {
            for (int value : jt) {
                out.writeShort(value);
            }
            out.writeShort(_chooseFuncOffset);
        }
    }

    @Override
    public byte getSid() {
        return sid;
    }

    @Override
    public int getSize() {
        if (_jumpTable != null) {
            return SIZE + (_jumpTable.length + 1) * LittleEndianConsts.SHORT_SIZE;
        }
        return SIZE;
    }

    public String toFormulaString(String[] operands) {
        if(space.isSet(_options)) {
            return operands[ 0 ];
        } else if (optiIf.isSet(_options)) {
            return toFormulaString() + "(" + operands[0] + ")";
        } else if (optiSkip.isSet(_options)) {
            return toFormulaString() + operands[0];   //goto isn't a real formula element should not show up
        } else {
            return toFormulaString() + "(" + operands[0] + ")";
        }
    }


    public int getNumberOfOperands() {
        return 1;
    }

    public int getType() {
        return -1;
    }

    @Override
    public String toFormulaString() {
        if (semiVolatile.isSet(_options)) {
            return "ATTR(semiVolatile)";
        }
        if (optiIf.isSet(_options)) {
            return "IF";
        }
        if (optiChoose.isSet(_options)) {
            return "CHOOSE";
        }
        if (optiSkip.isSet(_options)) {
            return "";
        }
        if (optiSum.isSet(_options)) {
            return "SUM";
        }
        if (baxcel.isSet(_options)) {
            return "ATTR(baxcel)";
        }
        if (space.isSet(_options)) {
            return "";
        }
        return "UNKNOWN ATTRIBUTE";
    }

    @Override
    public AttrPtg copy() {
        // immutable
        return this;
    }

    @Override
    public Map<String, Supplier<?>> getGenericProperties() {
        return GenericRecordUtil.getGenericProperties(
            "volatile", this::isSemiVolatile,
            "options", getBitsAsString(() -> _options,
                new BitField[]{semiVolatile, optiIf, optiChoose, optiSkip, optiSum, baxcel, space},
                new String[]{"SEMI_VOLATILE", "OPTI_IF", "OPTI_CHOOSE", "OPTI_SKIP", "OPTI_SUM", "BAXCEL", "SPACE"}),
            "space_count", () -> (_data >> 8) & 0xFF,
            "space_type",  getEnumBitsAsString(() -> _data & 0xFF,
                new int[]{SpaceType.SPACE_BEFORE,SpaceType.CR_BEFORE,SpaceType.SPACE_BEFORE_OPEN_PAREN,SpaceType.CR_BEFORE_OPEN_PAREN,SpaceType.SPACE_BEFORE_CLOSE_PAREN,SpaceType.CR_BEFORE_CLOSE_PAREN,SpaceType.SPACE_AFTER_EQUALITY},
                new String[]{"SPACE_BEFORE","CR_BEFORE","SPACE_BEFORE_OPEN_PAREN","CR_BEFORE_OPEN_PAREN","SPACE_BEFORE_CLOSE_PAREN","CR_BEFORE_CLOSE_PAREN","SPACE_AFTER_EQUALITY"})
        );
    }
}
