/*
 * 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.pdfbox.pdmodel.graphics.optionalcontent;

import java.util.ArrayList;
import java.util.Collection;

import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.pdmodel.common.COSObjectable;

/**
 * This class represents the optional content properties dictionary.
 *
 * @since PDF 1.5
 */
public class PDOptionalContentProperties implements COSObjectable
{

    /**
     * Enumeration for the BaseState dictionary entry on the "D" dictionary.
     */
    public enum BaseState
    {

        /** The "ON" value. */
        ON(COSName.ON),
        /** The "OFF" value. */
        OFF(COSName.OFF),
        /** The "Unchanged" value. */
        UNCHANGED(COSName.UNCHANGED);

        private final COSName name;

        private BaseState(COSName value)
        {
            this.name = value;
        }

        /**
         * Returns the PDF name for the state.
         * @return the name of the state
         */
        public COSName getName()
        {
            return this.name;
        }

        /**
         * Returns the base state represented by the given {@link COSName}.
         * @param state the state name
         * @return the state enum value
         */
        public static BaseState valueOf(COSName state)
        {
            if (state == null)
            {
                return BaseState.ON;
            }
            return BaseState.valueOf(state.getName().toUpperCase());
        }

    }

    private final COSDictionary dict;

    /**
     * Creates a new optional content properties dictionary.
     */
    public PDOptionalContentProperties()
    {
        this.dict = new COSDictionary();
        this.dict.setItem(COSName.OCGS, new COSArray());
        COSDictionary d = new COSDictionary();

        // Name optional but required for PDF/A-3
        d.setString(COSName.NAME, "Top");

        this.dict.setItem(COSName.D, d);
    }

    /**
     * Creates a new instance based on a given {@link COSDictionary}.
     * @param props the dictionary
     */
    public PDOptionalContentProperties(COSDictionary props)
    {
        this.dict = props;
    }

    /** {@inheritDoc} */
    @Override
    public COSDictionary getCOSObject()
    {
        return this.dict;
    }

    /**
     * Return the /OCGs array.
     * 
     * @return /OCGs array, never null.
     */
    private COSArray getOCGs()
    {
        COSArray ocgs = this.dict.getCOSArray(COSName.OCGS);
        if (ocgs == null)
        {
            ocgs = new COSArray();
            this.dict.setItem(COSName.OCGS, ocgs); //OCGs is required
        }
        return ocgs;
    }

    /**
     * Return the the /D dictionary.
     *
     * @return the /D dictionary, never null.
     */
    private COSDictionary getD()
    {
        COSDictionary d = dict.getCOSDictionary(COSName.D);
        if (d == null)
        {
            d = new COSDictionary();
            // Name optional but required for PDF/A-3
            d.setString(COSName.NAME, "Top");
            // D is required
            dict.setItem(COSName.D, d);
        }
        return d;
    }

    /**
     * Returns the first optional content group of the given name.
     *
     * @param name the group name
     * @return the optional content group or null, if there is no such group
     */
    public PDOptionalContentGroup getGroup(String name)
    {
        COSArray ocgs = getOCGs();
        for (COSBase o : ocgs)
        {
            COSDictionary ocg = toDictionary(o);
            if (ocg == null)
            {
                continue;
            }
            String groupName = ocg.getString(COSName.NAME);
            if (groupName.equals(name))
            {
                return new PDOptionalContentGroup(ocg);
            }
        }
        return null;
    }

    /**
     * Adds an optional content group (OCG).
     * @param ocg the optional content group
     */
    public void addGroup(PDOptionalContentGroup ocg)
    {
        COSArray ocgs = getOCGs();
        ocgs.add(ocg.getCOSObject());

        //By default, add new group to the "Order" entry so it appears in the user interface
        COSArray order = getD().getCOSArray(COSName.ORDER);
        if (order == null)
        {
            order = new COSArray();
            getD().setItem(COSName.ORDER, order);
        }
        order.add(ocg);
    }

    /**
     * Returns the collection of all optional content groups.
     * @return the optional content groups
     */
    public Collection<PDOptionalContentGroup> getOptionalContentGroups()
    {
        Collection<PDOptionalContentGroup> coll = new ArrayList<>();
        COSArray ocgs = getOCGs();
        for (COSBase base : ocgs)
        {
            COSDictionary dictionary = toDictionary(base);
            if (dictionary != null)
            {
                coll.add(new PDOptionalContentGroup(dictionary));
            }
        }
        return coll;
    }

    /**
     * Returns the base state for optional content groups.
     * @return the base state, never null.
     */
    public BaseState getBaseState()
    {
        COSDictionary d = getD();
        COSName name = d.getCOSName(COSName.BASE_STATE, COSName.ON);
        return BaseState.valueOf(name);
    }

    /**
     * Sets the base state for optional content groups.
     * @param state the base state
     */
    public void setBaseState(BaseState state)
    {
        COSDictionary d = getD();
        d.setItem(COSName.BASE_STATE, state.getName());
    }

    /**
     * Lists all optional content group names.
     * @return an array of all names
     */
    public String[] getGroupNames()
    {
        COSArray ocgs = dict.getCOSArray(COSName.OCGS);
        if (ocgs == null)
        {
            return new String[0];
        }
        int size = ocgs.size();
        String[] groups = new String[size];
        for (int i = 0; i < size; i++)
        {
            COSBase obj = ocgs.get(i);
            COSDictionary ocg = toDictionary(obj);
            if (ocg == null)
            {
                groups[i] = "";
            }
            else
            {
                groups[i] = ocg.getString(COSName.NAME);
            }
        }
        return groups;
    }

    /**
     * Indicates whether a particular optional content group is found in the PDF file.
     * @param groupName the group name
     * @return true if the group exists, false otherwise
     */
    public boolean hasGroup(String groupName)
    {
        String[] layers = getGroupNames();
        for (String layer : layers)
        {
            if (layer.equals(groupName))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * Indicates whether <em>at least one</em> optional content group with this name is enabled.
     * There may be disabled optional content groups with this name even if this function returns
     * true.
     *
     * @param groupName the group name
     * @return true if at least one group is enabled
     */
    public boolean isGroupEnabled(String groupName)
    {
        boolean result = false;
        COSArray ocgs = getOCGs();
        for (COSBase o : ocgs)
        {
            COSDictionary ocg = toDictionary(o);
            if (ocg == null)
            {
                continue;
            }
            String name = ocg.getString(COSName.NAME);
            if (groupName.equals(name) && isGroupEnabled(new PDOptionalContentGroup(ocg)))
            {
                result = true;
            }
        }
        return result;
    }

    /**
     * Indicates whether an optional content group is enabled.
     * @param group the group object
     * @return true if the group is enabled
     */
    public boolean isGroupEnabled(PDOptionalContentGroup group)
    {
        //TODO handle Optional Content Configuration Dictionaries,
        //i.e. OCProperties/Configs

        PDOptionalContentProperties.BaseState baseState = getBaseState();
        boolean enabled = baseState != BaseState.OFF;
        //TODO What to do with BaseState.Unchanged?

        if (group == null)
        {
            return enabled;
        }

        COSDictionary d = getD();
        COSArray on = d.getCOSArray(COSName.ON);
        if (on != null)
        {
            for (COSBase o : on)
            {
                COSDictionary dictionary = toDictionary(o);
                if (dictionary == group.getCOSObject())
                {
                    return true;
                }
            }
        }

        COSArray off = d.getCOSArray(COSName.OFF);
        if (off != null)
        {
            for (COSBase o : off)
            {
                COSDictionary dictionary = toDictionary(o);
                if (dictionary == group.getCOSObject())
                {
                    return false;
                }
            }
        }

        return enabled;
    }

    private COSDictionary toDictionary(COSBase o)
    {
        COSBase base;
        if (o instanceof COSObject)
        {
            base = ((COSObject) o).getObject();
        }
        else
        {
            base = o;
        }
        if (base instanceof COSDictionary)
        {
            return (COSDictionary) base;
        }
        return null;
    }

    /**
     * Enables or disables all optional content groups with the given name.
     *
     * @param groupName the group name
     * @param enable true to enable, false to disable
     * @return true if at least one group with this name already had an on or off setting, false
     * otherwise
     */
    public boolean setGroupEnabled(String groupName, boolean enable)
    {
        boolean result = false;
        COSArray ocgs = getOCGs();
        for (COSBase o : ocgs)
        {
            COSDictionary ocg = toDictionary(o);
            if (ocg == null)
            {
                continue;
            }
            String name = ocg.getString(COSName.NAME);
            if (groupName.equals(name) && setGroupEnabled(new PDOptionalContentGroup(ocg), enable))
            {
                result = true;
            }
        }
        return result;
    }

    /**
     * Enables or disables an optional content group.
     * @param group the group object
     * @param enable true to enable, false to disable
     * @return true if the group already had an on or off setting, false otherwise
     */
    public boolean setGroupEnabled(PDOptionalContentGroup group, boolean enable)
    {
        COSDictionary d = getD();
        COSArray on = d.getCOSArray(COSName.ON);
        if (on == null)
        {
            on = new COSArray();
            d.setItem(COSName.ON, on);
        }

        COSArray off = d.getCOSArray(COSName.OFF);
        if (off == null)
        {
            off = new COSArray();
            d.setItem(COSName.OFF, off);
        }

        boolean found = false;
        if (enable)
        {
            for (COSBase o : off)
            {
                COSDictionary groupDictionary = toDictionary(o);
                if (groupDictionary == group.getCOSObject())
                {
                    //enable group
                    off.remove(o);
                    on.add(o);
                    found = true;
                    break;
                }
            }
        }
        else
        {
            for (COSBase o : on)
            {
                COSDictionary groupDictionary = toDictionary(o);
                if (groupDictionary == group.getCOSObject())
                {
                    //disable group
                    on.remove(o);
                    off.add(o);
                    found = true;
                    break;
                }
            }
        }
        if (!found)
        {
            if (enable)
            {
                on.add(group.getCOSObject());
            }
            else
            {
                off.add(group.getCOSObject());
            }
        }
        return found;
    }
}
