/*
 * 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 org.apache.fop.datatypes.Length;
import org.apache.fop.datatypes.LengthBase;
import org.apache.fop.datatypes.SimplePercentBaseContext;
import org.apache.fop.fo.Constants;
import org.apache.fop.fonts.Font;
import org.apache.fop.traits.WritingMode;

/**
 * The alignment context is carried within a LayoutContext and as
 * part of the Knuth Inline elements to facilitate proper line building.
 * All measurements are in mpt.
 */
public class AlignmentContext implements Constants {

    /** The height or BPD of this context. */
    private int areaHeight;

    /** The computed line-height property value applicable. */
    private int lineHeight;

    /** The distance in BPD from the top of the box to the alignmentPoint. */
    private int alignmentPoint;

    /** The baseline shift value in effect. */
    private int baselineShiftValue;

    /** The computed alignment baseline identifier. */
    private int alignmentBaselineIdentifier;

    /** The x height. */
    private int xHeight;

    private ScaledBaselineTable scaledBaselineTable;
    private ScaledBaselineTable actualBaselineTable;
    private AlignmentContext parentAlignmentContext;

    /**
     * Creates a new instance of AlignmentContext
     * for graphics areas.
     * @param height the total height of the area
     * @param alignmentAdjust the alignment-adjust property
     * @param alignmentBaseline the alignment-baseline property
     * @param baselineShift the baseline-shift property
     * @param dominantBaseline the dominant-baseline property
     * @param parentAlignmentContext the parent alignment context
     */
    AlignmentContext(int height,
            Length alignmentAdjust,
            int alignmentBaseline,
            Length baselineShift,
            int dominantBaseline,
            AlignmentContext parentAlignmentContext) {

        this(height, 0, height, height, alignmentAdjust, alignmentBaseline, baselineShift,
                dominantBaseline, parentAlignmentContext);
    }

    /**
     * Creates a new instance.
     *
     * @param font the font
     * @param lineHeight the computed value of the lineHeight property
     * @param alignmentAdjust the alignment-adjust property
     * @param alignmentBaseline the alignment-baseline property
     * @param baselineShift the baseline-shift property
     * @param dominantBaseline the dominant-baseline property
     * @param parentAlignmentContext the parent alignment context
     */
    AlignmentContext(Font font,
            int lineHeight,
            Length alignmentAdjust,
            int alignmentBaseline,
            Length baselineShift,
            int dominantBaseline,
            AlignmentContext parentAlignmentContext) {
        this(font.getAscender(), font.getDescender(), lineHeight, font.getXHeight(),
                alignmentAdjust, alignmentBaseline, baselineShift, dominantBaseline,
                parentAlignmentContext);
    }

    /**
     * Creates a new instance of AlignmentContext.
     * @param altitude the altitude of the area
     * @param depth the depth of the area
     * @param lineHeight the line height
     * @param xHeight the xHeight
     * @param alignmentAdjust the alignment-adjust property
     * @param alignmentBaseline the alignment-baseline property
     * @param baselineShift the baseline-shift property
     * @param dominantBaseline the dominant-baseline property
     * @param parentAlignmentContext the parent alignment context
     */
    private AlignmentContext(int altitude,
            int depth,
            int lineHeight,
            int xHeight,
            Length alignmentAdjust,
            int alignmentBaseline,
            Length baselineShift,
            int dominantBaseline,
            AlignmentContext parentAlignmentContext) {

        this.areaHeight = altitude - depth;
        this.lineHeight = lineHeight;
        this.xHeight = xHeight;
        this.parentAlignmentContext = parentAlignmentContext;
        this.scaledBaselineTable = parentAlignmentContext.getScaledBaselineTable();
        setAlignmentBaselineIdentifier(alignmentBaseline
                                       , parentAlignmentContext.getDominantBaselineIdentifier());
        setBaselineShift(baselineShift);
        int dominantBaselineIdentifier = parentAlignmentContext.getDominantBaselineIdentifier();
        boolean newScaledBaselineTableRequired = false;
        if (baselineShiftValue != 0) {
            newScaledBaselineTableRequired = true;
        }
        switch (dominantBaseline) {
            case EN_AUTO:
                newScaledBaselineTableRequired = baselineShiftValue != 0;
                break;
            case EN_USE_SCRIPT: // TODO
                break;
            case EN_NO_CHANGE:
                break;
            case EN_RESET_SIZE:
                newScaledBaselineTableRequired = true;
                break;
            default:
                newScaledBaselineTableRequired = true;
                dominantBaselineIdentifier = dominantBaseline;
                break;
        }
        actualBaselineTable = new ScaledBaselineTable(
                altitude,
                depth,
                xHeight,
                dominantBaselineIdentifier,
                scaledBaselineTable.getWritingMode());
        if (newScaledBaselineTableRequired) {
            scaledBaselineTable = new ScaledBaselineTable(
                    altitude,
                    depth,
                    xHeight,
                    dominantBaselineIdentifier,
                    scaledBaselineTable.getWritingMode());
        }
        setAlignmentAdjust(alignmentAdjust);
    }

    /**
     * Creates a new instance of AlignmentContext based simply
     * on the font and the writing mode.
     * @param font the font
     * @param lineHeight the computed value of the lineHeight property
     * @param writingMode the current writing mode
     */
    AlignmentContext(Font font, int lineHeight, WritingMode writingMode) {
        this.areaHeight = font.getAscender() - font.getDescender();
        this.lineHeight = lineHeight;
        this.xHeight = font.getXHeight();
        this.scaledBaselineTable = new ScaledBaselineTable(font.getAscender(), font.getDescender(),
                font.getXHeight(), Constants.EN_ALPHABETIC, writingMode);
        this.actualBaselineTable = scaledBaselineTable;
        this.alignmentBaselineIdentifier = getDominantBaselineIdentifier();
        this.alignmentPoint = font.getAscender();
        this.baselineShiftValue = 0;
    }

    /**
     * Returns the alignment point for this context.
     * This is the point on the start edge of the area this context
     * applies to measured from the before edge of the area.
     * @return the default alignment point
     */
    public int getAlignmentPoint() {
        return alignmentPoint;
    }

    /**
     * Returns the current value of baseline shift in effect.
     * @return the baseline shift
     */
    public int getBaselineShiftValue() {
        return baselineShiftValue;
    }

    /**
     * Returns the current alignment baseline identifier.
     *
     * @return the alignment baseline identifier
     */
    public int getAlignmentBaselineIdentifier() {
        return alignmentBaselineIdentifier;
    }

    /**
     * Sets the current alignment baseline identifier. For
     * alignment-baseline values of "auto" and "baseline" this
     * method does the conversion into the appropriate computed
     * value assuming script is "auto" and the fo is not fo:character.
     * @param alignmentBaseline the alignment-baseline property
     * @param parentDominantBaselineIdentifier the dominant baseline of the parent fo
     */
    private void setAlignmentBaselineIdentifier(int alignmentBaseline
                                               , int parentDominantBaselineIdentifier) {
        switch (alignmentBaseline) {
            case EN_AUTO: // fall through
            case EN_BASELINE:
                this.alignmentBaselineIdentifier = parentDominantBaselineIdentifier;
                break;
            case EN_BEFORE_EDGE:
            case EN_TEXT_BEFORE_EDGE:
            case EN_CENTRAL:
            case EN_MIDDLE:
            case EN_AFTER_EDGE:
            case EN_TEXT_AFTER_EDGE:
            case EN_IDEOGRAPHIC:
            case EN_ALPHABETIC:
            case EN_HANGING:
            case EN_MATHEMATICAL:
                this.alignmentBaselineIdentifier = alignmentBaseline;
                break;
            default: throw new IllegalArgumentException(String.valueOf(alignmentBaseline));
        }
    }

    /**
     * Sets the current alignment baseline identifer. For
     * alignment-baseline values of "auto" and "baseline" this
     * method does the conversion into the appropriate computed
     * value assuming script is "auto" and the fo is not fo:character.
     * @param alignmentAdjust the alignment-adjust property
     */
    private void setAlignmentAdjust(Length alignmentAdjust) {
        int beforeEdge = actualBaselineTable.getBaseline(EN_BEFORE_EDGE);
        switch (alignmentAdjust.getEnum()) {
            case EN_AUTO:
                alignmentPoint = beforeEdge
                                    - actualBaselineTable.getBaseline(alignmentBaselineIdentifier);
                break;
            case EN_BASELINE:
                alignmentPoint = beforeEdge;
                break;
            case EN_BEFORE_EDGE:
            case EN_TEXT_BEFORE_EDGE:
            case EN_CENTRAL:
            case EN_MIDDLE:
            case EN_AFTER_EDGE:
            case EN_TEXT_AFTER_EDGE:
            case EN_IDEOGRAPHIC:
            case EN_ALPHABETIC:
            case EN_HANGING:
            case EN_MATHEMATICAL:
                alignmentPoint = beforeEdge
                                    - actualBaselineTable.getBaseline(alignmentAdjust.getEnum());
                break;
            default:
                alignmentPoint = beforeEdge
                    + alignmentAdjust.getValue(new SimplePercentBaseContext(null
                                                        , LengthBase.ALIGNMENT_ADJUST
                                                        , lineHeight));
                break;
        }
    }

    /**
     * Return the scaled baseline table for this context.
     * @return the scaled baseline table
     */
    private ScaledBaselineTable getScaledBaselineTable() {
        return this.scaledBaselineTable;
    }

    /**
     * Return the dominant baseline identifier.
     * @return the dominant baseline identifier
     */
    public int getDominantBaselineIdentifier() {
        return actualBaselineTable.getDominantBaselineIdentifier();
    }

    /**
     * Return the writing mode.
     * @return the writing mode
     */
/*    public WritingMode getWritingMode() {
        return scaledBaselineTable.getWritingMode();
    }*/

    /**
     * Calculates the baseline shift value based on the baseline-shift
     * property value.
     * @param baselineShift the baseline shift property value
     */
    private void setBaselineShift(Length baselineShift) {
        baselineShiftValue = 0;
        switch (baselineShift.getEnum()) {
            case EN_BASELINE: //Nothing to do
                break;
            case EN_SUB:
                baselineShiftValue = Math.round(-((float)xHeight / 2)
                                + (float)parentAlignmentContext.getActualBaselineOffset(EN_ALPHABETIC)
                                );
                break;
            case EN_SUPER:
                baselineShiftValue = Math.round((float)parentAlignmentContext.getXHeight()
                                + (float)parentAlignmentContext.getActualBaselineOffset(EN_ALPHABETIC)
                                );
                break;
            case 0: // A <length> or <percentage> value
                baselineShiftValue = baselineShift.getValue(
                    new SimplePercentBaseContext(null
                                                , LengthBase.CUSTOM_BASE
                                                , parentAlignmentContext.getLineHeight()));
                break;
            default: throw new IllegalArgumentException(String.valueOf(baselineShift.getEnum()));
        }
    }

    /**
     * Return the parent alignment context.
     * @return the parent alignment context
     */
    public AlignmentContext getParentAlignmentContext() {
        return parentAlignmentContext;
    }

    /**
     * Return the offset between the current dominant baseline and
     * the parent dominant baseline.
     * @return the offset in shift direction
     */
    private int getBaselineOffset() {
        if (parentAlignmentContext == null) {
            return 0;
        }
        return parentAlignmentContext.getScaledBaselineTable()
                                    .getBaseline(alignmentBaselineIdentifier)
                - scaledBaselineTable
                    .deriveScaledBaselineTable(parentAlignmentContext
                                               .getDominantBaselineIdentifier())
                    .getBaseline(alignmentBaselineIdentifier)
                - scaledBaselineTable
                    .getBaseline(parentAlignmentContext
                                 .getDominantBaselineIdentifier())
                + baselineShiftValue;
    }

    /**
     * Return the offset between the current dominant baseline and
     * the outermost parent dominant baseline.
     * @return the offset in shift direction
     */
    private int getTotalBaselineOffset() {
        int offset = 0;
        if (parentAlignmentContext != null) {
            offset = getBaselineOffset() + parentAlignmentContext.getTotalBaselineOffset();
        }
        return offset;
    }

    /**
     * Return the offset between the alignment baseline and
     * the outermost parent dominant baseline.
     * @return the offset in shift direction
     */
    public int getTotalAlignmentBaselineOffset() {
        return getTotalAlignmentBaselineOffset(alignmentBaselineIdentifier);
    }

    /**
     * Return the offset between the given alignment baseline and
     * the outermost parent dominant baseline.
     * @param alignmentBaselineId the alignment baseline
     * @return the offset
     */
    private int getTotalAlignmentBaselineOffset(int alignmentBaselineId) {
        int offset = baselineShiftValue;
        if (parentAlignmentContext != null) {
            offset = parentAlignmentContext.getTotalBaselineOffset()
                    + parentAlignmentContext.getScaledBaselineTable()
                        .getBaseline(alignmentBaselineId)
                    + baselineShiftValue;
        }
        return offset;
    }

    /**
     * Return the offset between the dominant baseline and
     * the given actual baseline.
     *
     * @param baselineIdentifier the baseline
     * @return the offset
     */
    private int getActualBaselineOffset(int baselineIdentifier) {
        // This is the offset from the dominant baseline to the alignment baseline
        int offset = getTotalAlignmentBaselineOffset() - getTotalBaselineOffset();
        // Add the offset to the actual baseline we want
        offset += actualBaselineTable.deriveScaledBaselineTable(alignmentBaselineIdentifier)
                    .getBaseline(baselineIdentifier);
        return offset;
    }

    /**
     * Return the offset the outermost parent dominant baseline
     * and the top of this box.
     * @return the offset
     */
    private int getTotalTopOffset() {
        int offset = getTotalAlignmentBaselineOffset() + getAltitude();
        return offset;
    }

    /**
     * Return the total height of the context.
     * @return the height
     */
    public int getHeight() {
        return areaHeight;
    }

    /**
     * Return the line height of the context.
     * @return the height
     */
    private int getLineHeight() {
        return lineHeight;
    }

    /**
     * The altitude of the context that is the height above the
     * alignment point.
     * @return the altitude
     */
    public int getAltitude() {
        return alignmentPoint;
    }

    /**
     * The depth of the context that is the height below
     * alignment point.
     * @return the altitude
     */
    public int getDepth() {
        return getHeight() - alignmentPoint;
    }

    /**
     * The x height of the context.
     * @return the x height
     */
    private int getXHeight() {
        return this.xHeight;
    }

    /**
     * Resizes the line as specified. Assumes that the new alignment point
     * is on the dominant baseline, that is this function should be called for
     * line areas only.
     * @param newLineHeight the new height of the line
     * @param newAlignmentPoint the new alignment point
     */
    public void resizeLine(int newLineHeight, int newAlignmentPoint) {
        areaHeight = newLineHeight;
        alignmentPoint = newAlignmentPoint;
        scaledBaselineTable.setBeforeAndAfterBaselines(alignmentPoint
                                                        , alignmentPoint - areaHeight);
    }

    /**
     * Returns the offset from the before-edge of the parent to
     * this context.
     * @return the offset for rendering
     */
    public int getOffset() {
        int offset = 0;
        if (parentAlignmentContext != null) {
            offset = parentAlignmentContext.getTotalTopOffset() - getTotalTopOffset();
        } else {
            offset = getAltitude() - scaledBaselineTable.getBaseline(EN_TEXT_BEFORE_EDGE);
        }
        return offset;
    }

    /**
     * Returns an indication if we still use the initial baseline table.
     * The initial baseline table is the table generated by the Line LM.
     * @return true if this is still the initial baseline table
     */
    public boolean usesInitialBaselineTable() {
        return parentAlignmentContext == null
               || (scaledBaselineTable == parentAlignmentContext.getScaledBaselineTable()
                    && parentAlignmentContext.usesInitialBaselineTable());
    }

    /* private boolean isHorizontalWritingMode() {
        return (getWritingMode() == WritingMode.LR_TB || getWritingMode() == WritingMode.RL_TB);
    }*/

    /** {@inheritDoc} */
    public String toString() {
        StringBuffer sb = new StringBuffer(64);
        sb.append("areaHeight=").append(areaHeight);
        sb.append(" lineHeight=").append(lineHeight);
        sb.append(" alignmentPoint=").append(alignmentPoint);
        sb.append(" alignmentBaselineID=").append(alignmentBaselineIdentifier);
        sb.append(" baselineShift=").append(baselineShiftValue);
        return sb.toString();
    }

}
