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

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

import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Supplier;

import org.apache.poi.util.GenericRecordJsonWriter;
import org.apache.poi.util.GenericRecordUtil;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian;

/**
 * Atom storing information for an OLE object.
 *
 * offset   type    name         description
 *
 * 0        uint4   drawAspect   Stores whether the object can be completely seen
 *                               (value of 1), or if only the icon is visible (value of 4).
 *
 * 4        sint4    type        Specifies whether the object is embedded or linked.
 *                               0 - embedded
 *                               1 - linked
 *
 * 8        sint4    objID       Unique identifier for the OLE object
 *
 * 2        sint4    subType     This specifies the type of ole object.
 *                               0 - Default object
 *                               1 - Microsoft Clipart Gallery
 *                               2 - Microsoft Word table
 *                               3 - Microsoft Excel
 *                               4 - Microsoft Graph
 *                               5 - Microsoft Organization Chart
 *                               6 - Microsoft Equation Editor
 *                               7 - Microsoft Wordart object
 *                               8 - Sound
 *                               9 - Image
 *                               10 - PowerPoint presentation
 *                               11 - PowerPoint slide
 *                               12 - Microsoft Project
 *                               13 - Microsoft Note-It Ole
 *                               14 - Microsoft Excel chart
 *                               15 - Media Player object
 *
 * 16       sint4    objStgDataRef    Reference to persist object
 *
 * 20       bool1    isBlank          Set if the object's image is blank
 *           (note: KOffice has this as an int.)
 */
public class ExOleObjAtom extends RecordAtom {

    public enum OleType {
        /** An embedded OLE object; the object is serialized and saved within the file. */
        EMBEDDED,
        /** A linked OLE object; the object is saved outside of the file. */
        LINKED,
        /** The OLE object is an ActiveX control. */
        CONTROL
    }

    public enum Subtype {
        DEFAULT,
        CLIPART_GALLERY,
        WORD_TABLE,
        EXCEL,
        GRAPH,
        ORGANIZATION_CHART,
        EQUATION,
        WORDART,
        SOUND,
        IMAGE,
        POWERPOINT_PRESENTATION,
        POWERPOINT_SLIDE,
        PROJECT,
        NOTEIT,
        EXCEL_CHART,
        MEDIA_PLAYER
    }

    private static final int[] DRAWASPECT_MASKS = {
        0x0001, 0x0002, 0x0004, 0x0008
    };

    private static final String[] DRAWASPECT_NAMES = {
        "CONTENT", "THUMBNAIL", "ICON", "DOCPRINT"
    };

    //arbitrarily selected; may need to increase
    private static final int MAX_RECORD_LENGTH = 10_485_760;

    /**
     * The object) is displayed as an embedded object inside of a container,
     */
    public static final int DRAW_ASPECT_VISIBLE = 1;
    /**
     *   The object is displayed as a thumbnail image.
     */
    public static final int DRAW_ASPECT_THUMBNAIL = 2;
    /**
     *   The object is displayed as an icon.
     */
    public static final int DRAW_ASPECT_ICON = 4;
    /**
     *   The object is displayed on the screen as though it were printed to a printer.
     */
    public static final int DRAW_ASPECT_DOCPRINT = 8;

    /**
     * An embedded OLE object; the object is serialized and saved within the file.
     */
    public static final int TYPE_EMBEDDED = 0;
    /**
     * A linked OLE object; the object is saved outside of the file.
     */
    public static final int TYPE_LINKED = 1;
    /**
     * The OLE object is an ActiveX control.
     */
    public static final int TYPE_CONTROL = 2;

    public static final int SUBTYPE_DEFAULT = 0;
    public static final int SUBTYPE_CLIPART_GALLERY = 1;
    public static final int SUBTYPE_WORD_TABLE = 2;
    public static final int SUBTYPE_EXCEL = 3;
    public static final int SUBTYPE_GRAPH = 4;
    public static final int SUBTYPE_ORGANIZATION_CHART = 5;
    public static final int SUBTYPE_EQUATION = 6;
    public static final int SUBTYPE_WORDART = 7;
    public static final int SUBTYPE_SOUND = 8;
    public static final int SUBTYPE_IMAGE = 9;
    public static final int SUBTYPE_POWERPOINT_PRESENTATION = 10;
    public static final int SUBTYPE_POWERPOINT_SLIDE = 11;
    public static final int SUBTYPE_PROJECT = 12;
    public static final int SUBTYPE_NOTEIT = 13;
    public static final int SUBTYPE_EXCEL_CHART = 14;
    public static final int SUBTYPE_MEDIA_PLAYER = 15;

    /**
     * Record header.
     */
    private byte[] _header;

    /**
     * Record data.
     */
    private byte[] _data;

    /**
     * Constructs a brand new link related atom record.
     */
    public ExOleObjAtom() {
        _header = new byte[8];
        _data = new byte[24];

        LittleEndian.putShort(_header, 0, (short)1); //MUST be 0x1
        LittleEndian.putShort(_header, 2, (short)getRecordType());
        LittleEndian.putInt(_header, 4, _data.length);
    }

    /**
     * Constructs the link related atom record from its
     *  source data.
     *
     * @param source the source data as a byte array.
     * @param start the start offset into the byte array.
     * @param len the length of the slice in the byte array.
     */
    protected ExOleObjAtom(byte[] source, int start, int len) {
        // Get the header.
        _header = Arrays.copyOfRange(source, start, start+8);

        // Get the record data.
        _data = IOUtils.safelyClone(source, start+8, len-8, MAX_RECORD_LENGTH);

        // Must be at least 24 bytes long
        if(_data.length < 24) {
            throw new IllegalArgumentException("The length of the data for a ExOleObjAtom must be at least 24 bytes, but was only " + _data.length);
        }
    }

    /**
     * Gets whether the object can be completely seen, or if only the
     * icon is visible.
     *
     * @return the draw aspect, one of the {@code DRAW_ASPECT_*} constants.
     */
    public int getDrawAspect() {
        return LittleEndian.getInt(_data, 0);
    }

    /**
     * Sets whether the object can be completely seen, or if only the
     * icon is visible.
     *
     * @param aspect the draw aspect, one of the {@code DRAW_ASPECT_*} constants.
     */
     public void setDrawAspect(int aspect) {
        LittleEndian.putInt(_data, 0, aspect);
    }

    /**
     * Gets whether the object is embedded or linked.
     *
     * @return the type, one of the {@code TYPE_EMBEDDED_*} constants.
     */
    public int getType() {
        return LittleEndian.getInt(_data, 4);
    }

    /**
     * Sets whether the object is embedded or linked.
     *
     * @param type the type, one of the {@code TYPE_EMBEDDED_*} constants.
     */
    public void setType(int type) {
        LittleEndian.putInt(_data, 4, type);
    }

    /**
     * Gets the unique identifier for the OLE object.
     *
     * @return the object ID.
     */
    public int getObjID() {
        return LittleEndian.getInt(_data, 8);
    }

    /**
     * Sets the unique identifier for the OLE object.
     *
     * @param id the object ID.
     */
    public void setObjID(int id) {
        LittleEndian.putInt(_data, 8, id);
    }

    /**
     * Gets the type of OLE object.
     *
     * @return the sub-type, one of the {@code SUBTYPE_*} constants.
     */
    public int getSubType() {
        return LittleEndian.getInt(_data, 12);
    }

    /**
     * Sets the type of OLE object.
     *
     * @param type the sub-type, one of the {@code SUBTYPE_*} constants.
     */
    public void setSubType(int type) {
        LittleEndian.putInt(_data, 12, type);
    }

    /**
     * Gets the reference to the persistent object
     *
     * @return the reference to the persistent object, corresponds with an
     *         {@code ExOleObjStg} storage container.
     */
    public int getObjStgDataRef() {
        return LittleEndian.getInt(_data, 16);
    }

    /**
     * Sets the reference to the persistent object
     *
     * @param ref the reference to the persistent object, corresponds with an
     *         {@code ExOleObjStg} storage container.
     */
    public void setObjStgDataRef(int ref) {
        LittleEndian.putInt(_data, 16, ref);
    }

    /**
     * Gets whether the object's image is blank.
     *
     * @return {@code true} if the object's image is blank.
     */
    public boolean getIsBlank() {
        // Even though this is a mere boolean, KOffice's code says it's an int.
        return LittleEndian.getInt(_data, 20) != 0;
    }

    /**
     * Gets misc options (the last four bytes in the atom).
     *
     * @return {@code true} if the object's image is blank.
     */
    public int getOptions() {
        // Even though this is a mere boolean, KOffice's code says it's an int.
        return LittleEndian.getInt(_data, 20);
    }

    /**
     * Sets misc options (the last four bytes in the atom).
     */
    public void setOptions(int opts) {
        // Even though this is a mere boolean, KOffice's code says it's an int.
        LittleEndian.putInt(_data, 20, opts);
    }

    /**
     * Returns the type (held as a little endian in bytes 3 and 4)
     * that this class handles.
     */
    public long getRecordType() {
        return RecordTypes.ExOleObjAtom.typeID;
    }

    /**
     * Have the contents printer out into an OutputStream, used when
     * writing a file back out to disk
     * (Normally, atom classes will keep their bytes around, but
     * non atom classes will just request the bytes from their
     * children, then chuck on their header and return)
     */
    public void writeOut(OutputStream out) throws IOException {
        out.write(_header);
        out.write(_data);
    }

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

    @Override
    public Map<String, Supplier<?>> getGenericProperties() {
        return GenericRecordUtil.getGenericProperties(
            "drawAspect", getBitsAsString(this::getDrawAspect, DRAWASPECT_MASKS, DRAWASPECT_NAMES),
            "type", safeEnum(OleType.values(), this::getType),
            "objID", this::getObjID,
            "subType", safeEnum(Subtype.values(), this::getSubType),
            "objStgDataRef", this::getObjStgDataRef,
            "options", this::getOptions
        );
    }
}
