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

import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Stack;

/**
 * A base class which holds information about the current painting state.
 */
public abstract class AbstractPaintingState implements Cloneable, Serializable {

    private static final long serialVersionUID = 5998356138437094188L;

    /** current state data */
    private AbstractData data;

    /** the state stack */
    private StateStack<AbstractData> stateStack = new StateStack<AbstractData>();

    /**
     * Instantiates a new state data object
     *
     * @return a new state data object
     */
    protected abstract AbstractData instantiateData();

    /**
     * Instantiates a new state object
     *
     * @return a new state object
     */
    protected abstract AbstractPaintingState instantiate();

    /**
     * Returns the currently valid state
     *
     * @return the currently valid state
     */
    public AbstractData getData() {
        if (data == null) {
            data = instantiateData();
        }
        return data;
    }

    /**
     * Set the current color.
     * Check if the new color is a change and then set the current color.
     *
     * @param col the color to set
     * @return true if the color has changed
     */
    public boolean setColor(Color col) {
        Color other = getData().color;
        if (!org.apache.xmlgraphics.java2d.color.ColorUtil.isSameColor(col, other)) {
            getData().color = col;
            return true;
        }
        return false;
    }

    /**
     * Get the color.
     *
     * @return the color
     */
    public Color getColor() {
        if (getData().color == null) {
            getData().color = Color.black;
        }
        return getData().color;
    }

    /**
     * Get the background color.
     *
     * @return the background color
     */
    public Color getBackColor() {
        if (getData().backColor == null) {
            getData().backColor = Color.white;
        }
        return getData().backColor;
    }

    /**
     * Set the current background color.
     * Check if the new background color is a change and then set the current background color.
     *
     * @param col the background color to set
     * @return true if the color has changed
     */
    public boolean setBackColor(Color col) {
        Color other = getData().backColor;
        if (!org.apache.xmlgraphics.java2d.color.ColorUtil.isSameColor(col, other)) {
            getData().backColor = col;
            return true;
        }
        return false;
    }

    /**
     * Set the current font name
     *
     * @param internalFontName the internal font name
     * @return true if the font name has changed
     */
    public boolean setFontName(String internalFontName) {
        if (!internalFontName.equals(getData().fontName)) {
            getData().fontName = internalFontName;
            return true;
        }
        return false;
    }

    /**
     * Gets the current font name
     *
     * @return the current font name
     */
    public String getFontName() {
        return getData().fontName;
    }

    /**
     * Gets the current font size
     *
     * @return the current font size
     */
    public int getFontSize() {
        return getData().fontSize;
    }

    /**
     * Set the current font size.
     * Check if the font size is a change and then set the current font size.
     *
     * @param size the font size to set
     * @return true if the font size has changed
     */
    public boolean setFontSize(int size) {
        if (size != getData().fontSize) {
            getData().fontSize = size;
            return true;
        }
        return false;
    }

    /**
     * Set the current line width.
     *
     * @param width the line width in points
     * @return true if the line width has changed
     */
    public boolean setLineWidth(float width) {
        if (getData().lineWidth != width) {
            getData().lineWidth = width;
            return true;
        }
        return false;
    }

    /**
     * Returns the current line width
     *
     * @return the current line width
     */
    public float getLineWidth() {
        return getData().lineWidth;
    }

    /**
     * Sets the dash array (line type) for the current basic stroke
     *
     * @param dash the line dash array
     * @return true if the dash array has changed
     */
    public boolean setDashArray(float[] dash) {
        if (!Arrays.equals(dash, getData().dashArray)) {
            getData().dashArray = dash;
            return true;
        }
        return false;
    }

    /**
     * Get the current transform.
     * This gets the combination of all transforms in the
     * current state.
     *
     * @return the calculate combined transform for the current state
     */
    public AffineTransform getTransform() {
       AffineTransform at = new AffineTransform();
       for (AbstractData data : stateStack) {
           AffineTransform stackTrans = data.getTransform();
           at.concatenate(stackTrans);
       }
       AffineTransform currentTrans = getData().getTransform();
       at.concatenate(currentTrans);
       return at;
    }

    /**
     * Check the current transform.
     * The transform for the current state is the combination of all
     * transforms in the current state. The parameter is compared
     * against this current transform.
     *
     * @param tf the transform the check against
     * @return true if the new transform is different then the current transform
     */
    public boolean checkTransform(AffineTransform tf) {
        return !tf.equals(getData().getTransform());
    }

    /**
     * Get a copy of the base transform for the page. Used to translate
     * IPP/BPP values into X,Y positions when positioning is "fixed".
     *
     * @return the base transform, or null if the state stack is empty
     */
    public AffineTransform getBaseTransform() {
       if (stateStack.isEmpty()) {
           return null;
       } else {
           AbstractData baseData = stateStack.get(0);
           return (AffineTransform) baseData.getTransform().clone();
       }
    }

    /**
     * Concatenates the given AffineTransform to the current one.
     *
     * @param at the transform to concatenate to the current level transform
     */
    public void concatenate(AffineTransform at) {
        getData().concatenate(at);
    }

    /**
     * Resets the current AffineTransform to the Base AffineTransform.
     */
    public void resetTransform() {
        getData().setTransform(getBaseTransform());
    }

    /**
     * Clears the current AffineTransform to the Identity AffineTransform
     */
    public void clearTransform() {
        getData().clearTransform();
    }


    /**
     * Save the current painting state.
     * This pushes the current painting state onto the stack.
     * This call should be used when the Q operator is used
     * so that the state is known when popped.
     */
    public void save() {
        AbstractData copy = (AbstractData)getData().clone();
        stateStack.push(copy);
    }

    /**
     * Restore the current painting state.
     * This pops the painting state from the stack and sets current values to popped state.
     *
     * @return the restored state, null if the stack is empty
     */
    public AbstractData restore() {
        if (!stateStack.isEmpty()) {
            setData(stateStack.pop());
            return this.data;
        } else {
            return null;
        }
    }

    /**
     * Save all painting state data.
     * This pushes all painting state data in the given list to the stack
     *
     * @param dataList a state data list
     */
    public void saveAll(List<AbstractData> dataList) {
        for (AbstractData data : dataList) {
            // save current data on stack
            save();
            setData(data);
        }
    }

    /**
     * Restore all painting state data.
     * This pops all painting state data from the stack
     *
     * @return a list of state data popped from the stack
     */
    public List<AbstractData> restoreAll() {
        List<AbstractData> dataList = new java.util.ArrayList<AbstractData>();
        AbstractData data;
        while (true) {
            data = getData();
            if (restore() == null) {
                break;
            }
            // insert because of stack-popping
            dataList.add(0, data);
        }
        return dataList;
    }

    /**
     * Sets the current state data
     *
     * @param data the state data
     */
    protected void setData(AbstractData data) {
        this.data = data;
    }

    /**
     * Clears the state stack
     */
    public void clear() {
        stateStack.clear();
        setData(null);
    }

    /**
     * Return the state stack
     *
     * @return the state stack
     */
    protected Stack<AbstractData> getStateStack() {
        return this.stateStack;
    }

    /** {@inheritDoc} */
    @Override
    public Object clone() {
        AbstractPaintingState state = instantiate();
        state.stateStack = new StateStack<AbstractData>(this.stateStack);
        if (this.data != null) {
            state.data = (AbstractData)this.data.clone();
        }
        return state;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return ", stateStack=" + stateStack
        + ", currentData=" + data;
    }


    /**
     * A stack implementation which holds state objects
     */
    // @SuppressFBWarnings("SE_INNER_CLASS")
    public class StateStack<E> extends java.util.Stack<E> {

        private static final long serialVersionUID = 4897178211223823041L;

        /**
         * Default constructor
         */
        public StateStack() {
        }

        /**
         * Copy constructor
         *
         * @param c initial contents of stack
         */
        public StateStack(Collection c) {
            elementCount = c.size();
            // 10% for growth
            elementData = new Object[
                          (int)Math.min((elementCount * 110L) / 100, Integer.MAX_VALUE)];
            c.toArray(elementData);
        }
    }


    /**
     * A base painting state data holding object
     */
    public abstract class AbstractData implements Cloneable, Serializable {

        private static final long serialVersionUID = 5208418041189828624L;

        /** The current color */
        protected Color color;

        /** The current background color */
        protected Color backColor;

        /** The current font name */
        protected String fontName;

        /** The current font size */
        protected int fontSize;

        /** The current line width */
        protected float lineWidth;

        /** The dash array for the current basic stroke (line type) */
        protected float[] dashArray;

        /** The current transform */
        protected AffineTransform transform;

        /** The current (optional content group) layer. */
        protected String layer;

        /**
         * Returns a newly create data object
         *
         * @return a new data object
         */
        protected abstract AbstractData instantiate();

        /**
         * Concatenate the given AffineTransform with the current thus creating
         * a new viewport. Note that all concatenation operations are logged
         * so they can be replayed if necessary (ex. for block-containers with
         * "fixed" positioning.
         *
         * @param at Transformation to perform
         */
        public void concatenate(AffineTransform at) {
            getTransform().concatenate(at);
        }

        /**
         * Get the current AffineTransform.
         *
         * @return the current transform
         */
        public AffineTransform getTransform() {
            if (transform == null) {
                transform = new AffineTransform();
            }
            return transform;
        }

        /**
         * Sets the current AffineTransform.
         * @param baseTransform the transform
         */
        public void setTransform(AffineTransform baseTransform) {
            this.transform = baseTransform;
        }

        /**
         * Resets the current AffineTransform.
         */
        public void clearTransform() {
            transform = new AffineTransform();
        }

        public void setLayer(String layer) {
            if (layer != null) {
                this.layer = layer;
            } else {
                throw new IllegalArgumentException();
            }
        }

        public String getLayer() {
            return this.layer;
        }

        /**
         * Returns the derived rotation from the current transform
         *
         * @return the derived rotation from the current transform
         */
        public int getDerivedRotation() {
            AffineTransform at = getTransform();
            double sx = at.getScaleX();
            double sy = at.getScaleY();
            double shx = at.getShearX();
            double shy = at.getShearY();
            int rotation = 0;
            if (sx == 0 && sy == 0 && shx > 0 && shy < 0) {
                rotation = 270;
            } else if (sx < 0 && sy < 0 && shx == 0 && shy == 0) {
                rotation = 180;
            } else if (sx == 0 && sy == 0 && shx < 0 && shy > 0) {
                rotation = 90;
            } else {
                rotation = 0;
            }
            return rotation;
        }

        /** {@inheritDoc} */
        @Override
        public Object clone() {
            AbstractData data = instantiate();
            data.color = this.color;
            data.backColor = this.backColor;
            data.fontName = this.fontName;
            data.fontSize = this.fontSize;
            data.lineWidth = this.lineWidth;
            data.dashArray = this.dashArray;
            if (this.transform == null) {
                this.transform = new AffineTransform();
            }
            data.transform = new AffineTransform(this.transform);
            data.layer = this.layer;
            return data;
        }

        /** {@inheritDoc} */
        @Override
        public String toString() {
            return "color=" + color
                + ", backColor=" + backColor
                + ", fontName=" + fontName
                + ", fontSize=" + fontSize
                + ", lineWidth=" + lineWidth
                + ", dashArray=" + Arrays.toString(dashArray)
                + ", transform=" + transform
                + ", layer=" + layer;
        }
    }
}
