/*
 * 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.
 */

/* $Id$ */

package org.apache.fop.layoutmgr.inline;

import java.awt.Dimension;
import java.awt.Rectangle;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.fop.datatypes.Length;
import org.apache.fop.datatypes.PercentBaseContext;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.GraphicsProperties;
import org.apache.fop.fo.properties.LengthRangeProperty;

/**
 * Helper class which calculates the size and position in the viewport of an image.
 */
public class ImageLayout implements Constants {

    /** logging instance */
    protected static final Log log = LogFactory.getLog(ImageLayout.class);

    //Input
    private GraphicsProperties props;
    private PercentBaseContext percentBaseContext;
    private Dimension intrinsicSize;

    //Output
    private Rectangle placement;
    private Dimension viewportSize = new Dimension(-1, -1);
    private boolean clip;

    /**
     * Main constructor
     * @param props the properties for the image
     * @param percentBaseContext the context object for percentage calculations
     * @param intrinsicSize the image's intrinsic size
     */
    public ImageLayout(GraphicsProperties props, PercentBaseContext percentBaseContext,
            Dimension intrinsicSize) {
        this.props = props;
        this.percentBaseContext = percentBaseContext;
        this.intrinsicSize = intrinsicSize;

        doLayout();
    }

    /**
     * Does the actual calculations for the image.
     */
    protected void doLayout() {
        Length len;

        int bpd = -1;
        int ipd = -1;

        len = props.getBlockProgressionDimension().getOptimum(percentBaseContext).getLength();
        if (len.getEnum() != EN_AUTO) {
            bpd = evaluateLength(len, intrinsicSize.height);
        }
        len = props.getBlockProgressionDimension().getMinimum(percentBaseContext).getLength();
        if (bpd == -1 && len.getEnum() != EN_AUTO) {
            bpd = evaluateLength(len, intrinsicSize.height);
        }

        len = props.getInlineProgressionDimension().getOptimum(percentBaseContext).getLength();
        if (len.getEnum() != EN_AUTO) {
            ipd = len.getValue(percentBaseContext);
        }
        len = props.getInlineProgressionDimension().getMinimum(percentBaseContext).getLength();
        if (ipd == -1 && len.getEnum() != EN_AUTO) {
            //Establish minimum viewport size
            ipd = len.getValue(percentBaseContext);
        }

        // if auto then use the intrinsic size of the content scaled
        // to the content-height and content-width
        boolean constrainIntrinsicSize = false;
        int cwidth = -1;
        int cheight = -1;
        len = props.getContentWidth();
        if (len.getEnum() != EN_AUTO) {
            switch (len.getEnum()) {
            case EN_SCALE_TO_FIT:
                if (ipd != -1) {
                    cwidth = ipd;
                }
                constrainIntrinsicSize = true;
                break;
            case EN_SCALE_DOWN_TO_FIT:
                if (ipd != -1 && intrinsicSize.width > ipd) {
                    cwidth = ipd;
                }
                constrainIntrinsicSize = true;
                break;
            case EN_SCALE_UP_TO_FIT:
                if (ipd != -1 && intrinsicSize.width < ipd) {
                    cwidth = ipd;
                }
                constrainIntrinsicSize = true;
                break;
            default:
                cwidth = len.getValue(percentBaseContext);
            }
        }
        len = props.getContentHeight();
        if (len.getEnum() != EN_AUTO) {
            switch (len.getEnum()) {
            case EN_SCALE_TO_FIT:
                if (bpd != -1) {
                    cheight = bpd;
                }
                constrainIntrinsicSize = true;
                break;
            case EN_SCALE_DOWN_TO_FIT:
                if (bpd != -1 && intrinsicSize.height > bpd) {
                    cheight = bpd;
                }
                constrainIntrinsicSize = true;
                break;
            case EN_SCALE_UP_TO_FIT:
                if (bpd != -1 && intrinsicSize.height < bpd) {
                    cheight = bpd;
                }
                constrainIntrinsicSize = true;
                break;
            default:
                cheight = len.getValue(percentBaseContext);
            }
        }

        Dimension constrainedIntrinsicSize;
        if (constrainIntrinsicSize) {
            constrainedIntrinsicSize = constrain(intrinsicSize);
        } else {
            constrainedIntrinsicSize = intrinsicSize;
        }

        //Derive content extents where not explicit
        Dimension adjustedDim = adjustContentSize(cwidth, cheight, constrainedIntrinsicSize);
        cwidth = adjustedDim.width;
        cheight = adjustedDim.height;

        //Adjust viewport if not explicit
        if (ipd == -1) {
            ipd = constrainExtent(cwidth,
                    props.getInlineProgressionDimension(), props.getContentWidth());
        }
        if (bpd == -1) {
            bpd = constrainExtent(cheight,
                    props.getBlockProgressionDimension(), props.getContentHeight());
        }

        this.clip = false;
        int overflow = props.getOverflow();
        if (overflow == EN_HIDDEN) {
            this.clip = true;
        } else if (overflow == EN_ERROR_IF_OVERFLOW) {
            if (cwidth > ipd || cheight > bpd) {
                //TODO Don't use logging to report error!
                log.error("Object overflows the viewport: clipping");
            }
            this.clip = true;
        }

        int xoffset = computeXOffset(ipd, cwidth);
        int yoffset = computeYOffset(bpd, cheight);

        //Build calculation results
        this.viewportSize.setSize(ipd, bpd);
        this.placement = new Rectangle(xoffset, yoffset, cwidth, cheight);
    }

    private int constrainExtent(int extent, LengthRangeProperty range, Length contextExtent) {
        boolean mayScaleUp = (contextExtent.getEnum() != EN_SCALE_DOWN_TO_FIT);
        boolean mayScaleDown = (contextExtent.getEnum() != EN_SCALE_UP_TO_FIT);
        Length len;
        len = range.getMaximum(percentBaseContext).getLength();
        if (len.getEnum() != EN_AUTO) {
            int max = evaluateLength(len);
            if (max != -1 && mayScaleDown) {
                extent = Math.min(extent, max);
            }
        }
        len = range.getMinimum(percentBaseContext).getLength();
        if (len.getEnum() != EN_AUTO) {
            int min = evaluateLength(len);
            if (min != -1 && mayScaleUp) {
                extent = Math.max(extent, min);
            }
        }
        return extent;
    }

    private Dimension constrain(Dimension size) {
        Dimension adjusted = new Dimension(size);
        int effWidth = constrainExtent(size.width,
                props.getInlineProgressionDimension(), props.getContentWidth());
        int effHeight = constrainExtent(size.height,
                props.getBlockProgressionDimension(), props.getContentHeight());
        int scaling = props.getScaling();
        if (scaling == EN_UNIFORM) {
            double rat1 = (double)effWidth / size.width;
            double rat2 = (double)effHeight / size.height;
            if (rat1 < rat2) {
                adjusted.width = effWidth;
                adjusted.height = (int)(rat1 * size.height);
            } else if (rat1 > rat2) {
                adjusted.width = (int)(rat2 * size.width);
                adjusted.height = effHeight;
            } else {
                adjusted.width = effWidth;
                adjusted.height = effHeight;
            }
        } else {
            adjusted.width = effWidth;
            adjusted.height = effHeight;
        }
        return adjusted;
    }

    private Dimension adjustContentSize(
            final int cwidth, final int cheight,
            Dimension defaultSize) {
        Dimension dim = new Dimension(cwidth, cheight);
        int scaling = props.getScaling();
        if ((scaling == EN_UNIFORM) || (cwidth == -1) || cheight == -1) {
            if (cwidth == -1 && cheight == -1) {
                dim.width = defaultSize.width;
                dim.height = defaultSize.height;
            } else if (cwidth == -1) {
                if (defaultSize.height == 0) {
                    dim.width = 0;
                } else {
                    dim.width = (int)(defaultSize.width * (double)cheight
                            / defaultSize.height);
                }
            } else if (cheight == -1) {
                if (defaultSize.width == 0) {
                    dim.height = 0;
                } else {
                    dim.height = (int)(defaultSize.height * (double)cwidth
                            / defaultSize.width);
                }
            } else {
                // adjust the larger
                if (defaultSize.width == 0 || defaultSize.height == 0) {
                    dim.width = 0;
                    dim.height = 0;
                } else {
                    double rat1 = (double)cwidth / defaultSize.width;
                    double rat2 = (double)cheight / defaultSize.height;
                    if (rat1 < rat2) {
                        // reduce height
                        dim.height = (int)(rat1 * defaultSize.height);
                    } else if (rat1 > rat2) {
                        dim.width = (int)(rat2 * defaultSize.width);
                    }
                }
            }
        }
        return dim;
    }

    /**
     * Given the ipd and the content width calculates the
     * required x offset based on the text-align property
     * @param ipd the inline-progression-dimension of the object
     * @param cwidth the calculated content width of the object
     * @return the X offset
     */
    public int computeXOffset(int ipd, int cwidth) {
        int xoffset = 0;
        switch (props.getTextAlign()) {
            case EN_CENTER:
                xoffset = (ipd - cwidth) / 2;
                break;
            case EN_END:
                xoffset = ipd - cwidth;
                break;
            case EN_START:
                break;
            case EN_JUSTIFY:
            default:
                break;
        }
        return xoffset;
    }

    /**
     * Given the bpd and the content height calculates the
     * required y offset based on the display-align property
     * @param bpd the block-progression-dimension of the object
     * @param cheight the calculated content height of the object
     * @return the Y offset
     */
    public int computeYOffset(int bpd, int cheight) {
        int yoffset = 0;
        switch (props.getDisplayAlign()) {
            case EN_BEFORE:
                break;
            case EN_AFTER:
                yoffset = bpd - cheight;
                break;
            case EN_CENTER:
                yoffset = (bpd - cheight) / 2;
                break;
            case EN_AUTO:
            default:
                break;
        }
        return yoffset;
    }

    /**
     * Returns the placement of the image inside the viewport.
     * @return the placement of the image inside the viewport (coordinates in millipoints)
     */
    public Rectangle getPlacement() {
        return this.placement;
    }

    /**
     * Returns the size of the image's viewport.
     * @return the viewport size (in millipoints)
     */
    public Dimension getViewportSize() {
        return this.viewportSize;
    }

    /**
     * Returns the size of the image's intrinsic (natural) size.
     * @return the intrinsic size (in millipoints)
     */
    public Dimension getIntrinsicSize() {
        return this.intrinsicSize;
    }

    /**
     * Indicates whether the image is clipped.
     * @return true if the image shall be clipped
     */
    public boolean isClipped() {
        return this.clip;
    }

    private int evaluateLength(Length length, int referenceValue) {
        double numericValue = length.getNumericValue(percentBaseContext);
        int bpd = numericValue < 0 ? referenceValue : (int) Math.round(numericValue);
        return bpd;
    }

    private int evaluateLength(Length length) {
        return evaluateLength(length, -1);
    }
}
