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

import java.io.IOException;

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.cos.COSStream;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.pdmodel.common.PDRange;
import org.apache.pdfbox.pdmodel.common.PDStream;

/**
 * This class represents a function in a PDF document.
 *
 * @author Ben Litchfield
 * 
 */
public abstract class PDFunction implements COSObjectable
{

    private PDStream functionStream = null;
    private COSDictionary functionDictionary = null;
    private COSArray domain = null;
    private COSArray range = null;
    private int numberOfInputValues = -1;
    private int numberOfOutputValues = -1;

    /**
     * Constructor.
     *
     * @param function The function stream.
     * 
     */
    public PDFunction( COSBase function )
    {
        if (function instanceof COSStream)
        {
            functionStream = new PDStream( (COSStream)function );
            functionStream.getCOSObject().setItem( COSName.TYPE, COSName.FUNCTION );
        }
        else if (function instanceof COSDictionary)
        {
            functionDictionary = (COSDictionary)function;
        }
    }

    /**
     * Returns the function type.
     * 
     * Possible values are:
     * 
     * 0 - Sampled function
     * 2 - Exponential interpolation function
     * 3 - Stitching function
     * 4 - PostScript calculator function
     * 
     * @return the function type.
     */
    public abstract int getFunctionType();
    
    /**
     * Returns the stream.
     * @return The stream for this object.
     */
    @Override
    public COSDictionary getCOSObject()
    {
        if (functionStream != null)
        {
            return functionStream.getCOSObject();
        }
        else 
        {
            return functionDictionary;
        }
    }

    /**
     * Returns the underlying PDStream.
     * @return The stream.
     */
    protected PDStream getPDStream()
    {
        return functionStream;
    }
    /**
     * Create the correct PD Model function based on the COS base function.
     *
     * @param function The COS function dictionary.
     *
     * @return The PDModel Function object.
     *
     * @throws IOException If we are unable to create the PDFunction object.
     */
    public static PDFunction create( COSBase function ) throws IOException
    {
        if (function == COSName.IDENTITY)
        {
            return new PDFunctionTypeIdentity(null);
        }
        
        COSBase base = function;
        if (function instanceof COSObject)
        {
            base = ((COSObject) function).getObject();
        }
        if (!(base instanceof COSDictionary))
        {
            throw new IOException("Error: Function must be a Dictionary, but is " +
                    (base == null ? "(null)" : base.getClass().getSimpleName()));
        }
        COSDictionary functionDictionary = (COSDictionary) base;
        int functionType = functionDictionary.getInt(COSName.FUNCTION_TYPE);
        switch (functionType)
        {
            case 0:
                return new PDFunctionType0(functionDictionary);
            case 2:
                return new PDFunctionType2(functionDictionary);
            case 3:
                return new PDFunctionType3(functionDictionary);
            case 4:
                return new PDFunctionType4(functionDictionary);
            default:
                throw new IOException("Error: Unknown function type " + functionType);
        }
    }

    /**
     * This will get the number of output parameters that
     * have a range specified.  A range for output parameters
     * is optional so this may return zero for a function
     * that does have output parameters, this will simply return the
     * number that have the range specified.
     *
     * @return The number of output parameters that have a range
     * specified.
     */
    public int getNumberOfOutputParameters()
    {
        if (numberOfOutputValues == -1)
        {
            COSArray rangeValues = getRangeValues();
            numberOfOutputValues = rangeValues.size() / 2;
        }
        return numberOfOutputValues;
    }

    /**
     * This will get the range for a certain output parameters.  This is will never
     * return null.  If it is not present then the range 0 to 0 will
     * be returned.
     *
     * @param n The output parameter number to get the range for.
     *
     * @return The range for this component.
     */
    public PDRange getRangeForOutput(int n)
    {
        COSArray rangeValues = getRangeValues();
        return new PDRange( rangeValues, n );
    }

    /**
     * This will set the range values.
     *
     * @param rangeValues The new range values.
     */
    public void setRangeValues(COSArray rangeValues)
    {
        range = rangeValues;
        getCOSObject().setItem(COSName.RANGE, rangeValues);
    }

    /**
     * This will get the number of input parameters that
     * have a domain specified.
     *
     * @return The number of input parameters that have a domain
     * specified.
     */
    public int getNumberOfInputParameters()
    {
        if (numberOfInputValues == -1)
        {
            COSArray array = getDomainValues();
            numberOfInputValues = array.size() / 2;
        }
        return numberOfInputValues;
    }

    /**
     * This will get the range for a certain input parameter.  This is will never
     * return null.  If it is not present then the range 0 to 0 will
     * be returned.
     *
     * @param n The parameter number to get the domain for.
     *
     * @return The domain range for this component.
     */
    public PDRange getDomainForInput(int n) 
    {
        COSArray domainValues = getDomainValues();
        return new PDRange( domainValues, n );
    }

    /**
     * This will set the domain values.
     *
     * @param domainValues The new domain values.
     */
    public void setDomainValues(COSArray domainValues)
    {
        domain = domainValues;
        getCOSObject().setItem(COSName.DOMAIN, domainValues);
    }

    /**
     * Evaluates the function at the given input.
     * ReturnValue = f(input)
     *
     * @param input The array of input values for the function. 
     * In many cases will be an array of a single value, but not always.
     * 
     * @return The of outputs the function returns based on those inputs. 
     * In many cases will be an array of a single value, but not always.
     * 
     * @throws IOException if something went wrong processing the function.  
     */
    public abstract float[] eval(float[] input) throws IOException;
    
    /**
     * Returns all ranges for the output values as COSArray .
     * Required for type 0 and type 4 functions
     * @return the ranges array. 
     */
    protected COSArray getRangeValues() 
    {
        if (range == null) 
        {
            range = getCOSObject().getCOSArray(COSName.RANGE);
        }
        return range;
    }

    /**
     * Returns all domains for the input values as COSArray.
     * Required for all function types.
     * @return the domains array. 
     */
    private COSArray getDomainValues()
    {
        if (domain == null)
        {
            domain = getCOSObject().getCOSArray(COSName.DOMAIN);
        }
        return domain;
    }

    /**
     * Clip the given input values to the ranges.
     * 
     * @param inputValues the input values
     * @return the clipped values
     */
    protected float[] clipToRange(float[] inputValues) 
    {
        COSArray rangesArray = getRangeValues();
        float[] result;
        if (rangesArray != null && rangesArray.size() > 0)
        {
            float[] rangeValues = rangesArray.toFloatArray();
            int numberOfRanges = rangeValues.length/2;
            result = new float[numberOfRanges];
            for (int i=0; i<numberOfRanges; i++)
            {
                int index = i << 1;
                result[i] = clipToRange(inputValues[i], rangeValues[index], rangeValues[index + 1]);
            }
        }
        else
        {
            result = inputValues;
        }
        return result;
    }

    /**
     * Clip the given input value to the given range.
     * 
     * @param x the input value
     * @param rangeMin the min value of the range
     * @param rangeMax the max value of the range

     * @return the clipped value
     */
    protected float clipToRange(float x, float rangeMin, float rangeMax) 
    {
        if (x < rangeMin)
        {
            return rangeMin;
        }
        else if (x > rangeMax)
        {
            return rangeMax;
        }
        return x;
    }

    /**
     * For a given value of x, interpolate calculates the y value 
     * on the line defined by the two points (xRangeMin , xRangeMax ) 
     * and (yRangeMin , yRangeMax ).
     * 
     * @param x the to be interpolated value.
     * @param xRangeMin the min value of the x range
     * @param xRangeMax the max value of the x range
     * @param yRangeMin the min value of the y range
     * @param yRangeMax the max value of the y range
     * @return the interpolated y value
     */
    protected float interpolate(float x, float xRangeMin, float xRangeMax, float yRangeMin, float yRangeMax) 
    {
        return yRangeMin + ((x - xRangeMin) * (yRangeMax - yRangeMin)/(xRangeMax - xRangeMin));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString()
    {
        return "FunctionType" + getFunctionType();
    }
}
