/*
 * 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.sejda.sambox.pdmodel.common.function;

import java.io.IOException;

import org.sejda.sambox.cos.COSArray;
import org.sejda.sambox.cos.COSBase;
import org.sejda.sambox.cos.COSName;
import org.sejda.sambox.pdmodel.common.PDRange;

/**
 * This class represents a Type 3 (stitching) function in a PDF document.
 *
 * @author Ben Litchfield
 */
public class PDFunctionType3 extends PDFunction
{
    private COSArray functions = null;
    private COSArray encode = null;
    private COSArray bounds = null;
    private PDFunction[] functionsArray = null;
    private float[] boundsValues = null;

    /**
     * Constructor.
     *
     * @param functionStream The function .
     */
    public PDFunctionType3(COSBase functionStream)
    {
        super(functionStream);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getFunctionType()
    {
        return 3;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public float[] eval(float[] input) throws IOException
    {
        // This function is known as a "stitching" function. Based on the input, it decides which child function to
        // call.
        // All functions in the array are 1-value-input functions
        // See PDF Reference section 3.9.3.
        PDFunction function = null;
        float x = input[0];
        PDRange domain = getDomainForInput(0);
        // clip input value to domain
        x = clipToRange(x, domain.getMin(), domain.getMax());

        if (functionsArray == null)
        {
            COSArray ar = getFunctions();
            functionsArray = new PDFunction[ar.size()];
            for (int i = 0; i < ar.size(); ++i)
            {
                COSBase base = ar.getObject(i);
                if (base != null) 
                {
                    functionsArray[i] = PDFunction.create(base);
                }
            }
        }

        if (functionsArray.length == 1)
        {
            // This doesn't make sense but it may happen ...
            function = functionsArray[0];
            PDRange encRange = getEncodeForParameter(0);
            x = interpolate(x, domain.getMin(), domain.getMax(), encRange.getMin(),
                    encRange.getMax());
        }
        else
        {
            if (boundsValues == null)
            {
                boundsValues = getBounds().toFloatArray();
            }
            int boundsSize = boundsValues.length;
            // create a combined array containing the domain and the bounds values
            // domain.min, bounds[0], bounds[1], ...., bounds[boundsSize-1], domain.max
            float[] partitionValues = new float[boundsSize + 2];
            int partitionValuesSize = partitionValues.length;
            partitionValues[0] = domain.getMin();
            partitionValues[partitionValuesSize - 1] = domain.getMax();
            System.arraycopy(boundsValues, 0, partitionValues, 1, boundsSize);
            // find the partition
            for (int i = 0; i < partitionValuesSize - 1; i++)
            {
                if (x >= partitionValues[i] && (x < partitionValues[i + 1]
                        || (i == partitionValuesSize - 2 && x == partitionValues[i + 1])))
                {
                    function = functionsArray[i];
                    PDRange encRange = getEncodeForParameter(i);
                    x = interpolate(x, partitionValues[i], partitionValues[i + 1],
                            encRange.getMin(), encRange.getMax());
                    break;
                }
            }
            if (function == null)
            {
                throw new IOException("partition not found in type 3 function");
            }
        }
        float[] functionValues = new float[] { x };
        // calculate the output values using the chosen function
        float[] functionResult = function.eval(functionValues);
        // clip to range if available
        return clipToRange(functionResult);
    }

    /**
     * Returns all functions values as COSArray.
     * 
     * @return the functions array.
     */
    public COSArray getFunctions()
    {
        if (functions == null)
        {
            functions = (COSArray) (getCOSObject().getDictionaryObject(COSName.FUNCTIONS));
        }
        return functions;
    }

    /**
     * Returns all bounds values as COSArray.
     * 
     * @return the bounds array.
     */
    public COSArray getBounds()
    {
        if (bounds == null)
        {
            bounds = (COSArray) (getCOSObject().getDictionaryObject(COSName.BOUNDS));
        }
        return bounds;
    }

    /**
     * Returns all encode values as COSArray.
     * 
     * @return the encode array.
     */
    public COSArray getEncode()
    {
        if (encode == null)
        {
            encode = (COSArray) (getCOSObject().getDictionaryObject(COSName.ENCODE));
        }
        return encode;
    }

    /**
     * Get the encode for the input parameter.
     *
     * @param n The function parameter number.
     *
     * @return The encode parameter range or null if none is set.
     */
    private PDRange getEncodeForParameter(int n)
    {
        COSArray encodeValues = getEncode();
        return new PDRange(encodeValues, n);
    }
}
