/*
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package com.sun.pdfview.function;

import com.sun.pdfview.PDFObject;
import com.sun.pdfview.PDFParseException;

import java.io.IOException;

/**
 * <p>PDF Functions are defined in the reference as Section 3.9.</p>
 * 
 * <p>A PDF function maps some set of <i>m</i> inputs into some set
 * of <i>n</i> outputs.  There are 4 types of functions:
 * <ul><li>Type 0: Sampled functions. (PDF 1.2)<br> 
 *                  A sampled function (type 0) uses a table of sample values 
 *                  to define the function. Various techniques are used to 
 *                  interpolate values between the sample values 
 *                  (see Section 3.9.1, "Type 0 (Sampled) Functions").</li>
 *     <li>Type 2: Exponential Interpolation. (PDF 1.3)<br>
 *                  An exponential interpolation function (type 2) 
 *                  defines a set of coefficients for an exponential function 
 *                  (see Section 3.9.2, 
 *                  "Type 2 (Exponential Interpolation) Functions").</li>
 *     <li>Type 3: Stitching functions. (PDF 1.3)<br>
 *                  A stitching function (type 3) is a combination of 
 *                  other functions, partitioned across a domain 
 *                  (see Section 3.9.3, "Type 3 (Stitching) Functions").</li>
 *     <li>Type 4: Postscript calculations. (PDF 1.3)<br>
 *                  A PostScript calculator function (type 4) uses operators 
 *                  from the PostScript language to describe an arithmetic 
 *                  expression (see Section 3.9.4, 
 *                  "Type 4 (PostScript Calculator) Functions").</li>
 * </ul>
 * </p>
 * 
 * <p>
 * The function interface contains a single method, <i>calculate</i> which
 * takes an array of <i>m</i> floats an interprets them into an array of
 * </i>n</i> floats.
 * <p> 
 * PDFFunctions do not have accessible constructors.  Instead, use the
 * static <i>getFunction()</i> method to read a functions from a PDF Object.
 *
 */
public abstract class PDFFunction {

    /** Sampled function */
    public static final int TYPE_0 = 0;

    /** Exponential interpolation function */
    public static final int TYPE_2 = 2;

    /** Stitching function. */
    public static final int TYPE_3 = 3;

    /** PostScript calculator function. */
    public static final int TYPE_4 = 4;

    /** the type of this function from the list of known types */
    private int type;

    /** the input domain of this function, an array of 2 * <i>m</i> floats */
    private float[] domain;

    /** the output range of this functions, and array of 2 * <i>n</i> floats.
     *  required for type 0 and 4 functions
     */
    private float[] range;

    /** Creates a new instance of PDFFunction */
    protected PDFFunction (int type) {
        this.type = type;
    }

    /**
     * Get a PDFFunction from a PDFObject
     */
    public static PDFFunction getFunction (PDFObject obj)
            throws IOException {
        PDFFunction function;
        int type;
        float[] domain = null;
        float[] range = null;

        // read the function type (required)
        PDFObject typeObj = obj.getDictRef ("FunctionType");
        if (typeObj == null) {
            throw new PDFParseException (
                    "No FunctionType specified in function!");
        }
        type = typeObj.getIntValue ();

        // read the function's domain (required)
        PDFObject domainObj = obj.getDictRef ("Domain");
        if (domainObj == null) {
            throw new PDFParseException ("No Domain specified in function!");
        }

        PDFObject[] domainAry = domainObj.getArray ();
        domain = new float[domainAry.length];
        for (int i = 0; i < domainAry.length; i++) {
            domain[i] = domainAry[i].getFloatValue ();
        }

        // read the function's range (optional)
        PDFObject rangeObj = obj.getDictRef ("Range");
        if (rangeObj != null) {
            PDFObject[] rangeAry = rangeObj.getArray ();
            range = new float[rangeAry.length];
            for (int i = 0; i < rangeAry.length; i++) {
                range[i] = rangeAry[i].getFloatValue ();
            }
        }

        // now create the acual function object
        switch (type) {
            case TYPE_0:
                if (rangeObj == null) {
                    throw new PDFParseException (
                            "No Range specified in Type 0 Function!");
                }
                function = new FunctionType0 ();
                break;
            case TYPE_2:
                function = new FunctionType2 ();
                break;
            case TYPE_3:
                function = new FunctionType3 ();
                break;
            case TYPE_4:
                if (rangeObj == null) {
                    throw new PDFParseException (
                            "No Range specified in Type 4 Function!");
                }
                function = new FunctionType4 ();
                break;
            default:
                throw new PDFParseException (
                        "Unsupported function type: " + type);
        }

        // fill in the domain and optionally the range
        function.setDomain (domain);
        if (range != null) {
            function.setRange (range);
        }

        // now initialize the function
        function.parse (obj);

        return function;
    }

    /**
     * Get the type of this function
     *
     * @return one of the types of function (0-4)
     */
    public int getType () {
        return this.type;
    }

    /**
     * Get the number of inputs, <i>m</i>, required by this function
     *
     * @return the number of input values expected by this function
     */
    public int getNumInputs () {
        return (this.domain.length / 2);
    }

    /**
     * Get the number of outputs, <i>n</i>, returned by this function
     *
     * @return the number of output values this function will return
     */
    public int getNumOutputs () {
        if (this.range == null) {
            return 0;
        }
        return (this.range.length / 2);
    }

    /**
     * Get a component of the domain of this function
     *
     * @param i the index into the domain array, which has size 2 * <i>m</i>.
     *          the <i>i</i>th entry in the array has index 2<i>i</i>, 
     *           2<i>i</i> + 1
     * @return the <i>i</i>th entry in the domain array 
     */
    protected float getDomain (int i) {
        return this.domain[i];
    }

    /**
     *  Set the domain of this function
     */
    protected void setDomain (float[] domain) {
        this.domain = domain;
    }

    /**
     * Get a component of the range of this function
     *
     * @param i the index into the range array, which has size 2 * <i>n</i>.
     *          the <i>i</i>th entry in the array has index 2<i>i</i>, 
     *           2<i>i</i> + 1
     * @return the <i>i</i>th entry in the range array 
     */
    protected float getRange (int i) {
        if (this.range == null) {
            if ((i % 2) == 0) {
                return Float.MIN_VALUE;
            } else {
                return Float.MAX_VALUE;
            }
        }
        return this.range[i];
    }

    /**
     * Set the range of this function
     */
    protected void setRange (float[] range) {
        this.range = range;
    }

    /**
     * Map from <i>m</i> input values to <i>n</i> output values.
     * The number of inputs <i>m</i> must be exactly one half the size of the
     * domain.  The number of outputs should match one half the size of the
     * range.
     *
     * @param inputs an array of >= <i>m</i> input values
     * @return the array of <i>n</i> output values
     */
    public float[] calculate (float[] inputs) {
        float[] outputs = new float[getNumOutputs ()];
        calculate (inputs, 0, outputs, 0);
        return outputs;
    }

    /**
     * Map from <i>m</i> input values to <i>n</i> output values.
     * The number of inputs <i>m</i> must be exactly one half the size of the
     * domain.  The number of outputs should match one half the size of the
     * range.
     *
     * @param inputs an array of >= <i>m</i> input values
     * @param inputOffset the offset into the input array to read from
     * @param outputs an array of size >= <i>n</i> which will be filled
     *                with the output values
     * @param outputOffset the offset into the output array to write to
     * @return the array of <i>n</i> output values
     */
    public float[] calculate (float[] inputs, int inputOffset,
                              float[] outputs, int outputOffset) {
        // check the inputs
        if (inputs.length - inputOffset < getNumInputs ()) {
            throw new IllegalArgumentException (
                    "Wrong number of inputs to function!");
        }

        // check the outputs
        if (this.range != null && outputs.length - outputOffset < getNumOutputs ()) {
            throw new IllegalArgumentException (
                    "Wrong number of outputs for function!");
        }

        // clip the inputs to domain
        for (int i = 0; i < inputs.length; i++) {
            // clip to the domain -- min(max(x<i>, domain<2i>), domain<2i+1>)
            inputs[i] = Math.max (inputs[i], getDomain (2 * i));
            inputs[i] = Math.min (inputs[i], getDomain ((2 * i) + 1));
        }

        // do the actual calculation
        doFunction (inputs, inputOffset, outputs, outputOffset);

        // clip the outputs to range
        for (int i = 0; this.range != null && i < outputs.length; i++) {
            // clip to range -- min(max(r<i>, range<2i>), range<2i + 1>)
            outputs[i] = Math.max (outputs[i], getRange (2 * i));
            outputs[i] = Math.min (outputs[i], getRange ((2 * i) + 1));
        }

        return outputs;
    }

    /**
     * Subclasses must implement this method to perform the actual function
     * on the given set of data.  Note that the inputs are guaranteed to be
     * clipped to the domain, while the outputs will be automatically clipped
     * to the range after being returned from this function.
     *
     * @param inputs guaranteed to be at least as big as 
     *        <code>getNumInputs()</code> and all values within range
     * @param inputOffset the offset into the inputs array to read from
     * @param outputs guaranteed to be at least as big as
     *        <code>getNumOutputs()</code>, but not yet clipped to domain
     * @param outputOffset the offset into the output array to write to
     */
    protected abstract void doFunction (float[] inputs, int inputOffset,
                                        float[] outputs, int outputOffset);

    /** Read the function information from a PDF Object */
    protected abstract void parse (PDFObject obj) throws IOException;
}