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

import java.awt.Color;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

import org.apache.xmlgraphics.java2d.color.ColorConverter;
import org.apache.xmlgraphics.java2d.color.ColorUtil;

import org.apache.fop.afp.AFPDataObjectInfo;
import org.apache.fop.afp.AFPObjectAreaInfo;
import org.apache.fop.afp.Factory;
import org.apache.fop.afp.StructuredData;
import org.apache.fop.afp.fonts.CharacterSet;
import org.apache.fop.afp.goca.GraphicsAreaBegin;
import org.apache.fop.afp.goca.GraphicsAreaEnd;
import org.apache.fop.afp.goca.GraphicsBox;
import org.apache.fop.afp.goca.GraphicsChainedSegment;
import org.apache.fop.afp.goca.GraphicsCharacterString;
import org.apache.fop.afp.goca.GraphicsData;
import org.apache.fop.afp.goca.GraphicsEndProlog;
import org.apache.fop.afp.goca.GraphicsFillet;
import org.apache.fop.afp.goca.GraphicsFullArc;
import org.apache.fop.afp.goca.GraphicsImage;
import org.apache.fop.afp.goca.GraphicsLine;
import org.apache.fop.afp.goca.GraphicsSetArcParameters;
import org.apache.fop.afp.goca.GraphicsSetCharacterSet;
import org.apache.fop.afp.goca.GraphicsSetCurrentPosition;
import org.apache.fop.afp.goca.GraphicsSetFractionalLineWidth;
import org.apache.fop.afp.goca.GraphicsSetLineType;
import org.apache.fop.afp.goca.GraphicsSetLineWidth;
import org.apache.fop.afp.goca.GraphicsSetPatternSymbol;
import org.apache.fop.afp.goca.GraphicsSetProcessColor;

/**
 * Top-level GOCA graphics object.
 *
 * Acts as container and factory of all other graphic objects
 */
public class GraphicsObject extends AbstractDataObject {

    /** the graphics data */
    private GraphicsData currentData;

    /** list of objects contained within this container */
    protected List<GraphicsData> objects
        = new java.util.ArrayList<GraphicsData>();

    /** the graphics state */
    private final GraphicsState graphicsState = new GraphicsState();


    /** color  converter  */
    private ColorConverter colorConverter;

    /**
     * Default constructor
     *
     * @param factory the object factory
     * @param name the name of graphics object
     */
    public GraphicsObject(Factory factory, String name) {
        super(factory, name);
    }

    /** {@inheritDoc} */
    @Override
    public void setViewport(AFPDataObjectInfo dataObjectInfo) {
        super.setViewport(dataObjectInfo);

        AFPObjectAreaInfo objectAreaInfo = dataObjectInfo.getObjectAreaInfo();
        int width = objectAreaInfo.getWidth();
        int height = objectAreaInfo.getHeight();
        int widthRes = objectAreaInfo.getWidthRes();
        int heightRes = objectAreaInfo.getHeightRes();
        final int leftEdge = 0;
        final int topEdge = 0;
        GraphicsDataDescriptor graphicsDataDescriptor = factory.createGraphicsDataDescriptor(
                leftEdge, width, topEdge, height, widthRes, heightRes);

        getObjectEnvironmentGroup().setDataDescriptor(graphicsDataDescriptor);
    }

    /** @param object the structured data */
    public void addObject(StructuredData object) {
        if (currentData == null) {
            newData();
        } else if (currentData.getDataLength() + object.getDataLength()
                >= GraphicsData.MAX_DATA_LEN) {
            // graphics data full so transfer current incomplete segment to new data
            GraphicsChainedSegment currentSegment
                = (GraphicsChainedSegment)currentData.removeCurrentSegment();
            currentSegment.setName(newData().createSegmentName());
            currentData.addSegment(currentSegment);
        }
        currentData.addObject(object);
    }

    /**
     * Gets the current graphics data, creating a new one if necessary
     *
     * @return the current graphics data
     */
    private GraphicsData getData() {
        if (this.currentData == null) {
            return newData();
        }
        return this.currentData;
    }

    /**
     * Creates a new graphics data
     *
     * @return a newly created graphics data
     */
    private GraphicsData newData() {
        if (currentData != null) {
            currentData.setComplete(true);
        }
        this.currentData = factory.createGraphicsData();
        objects.add(currentData);
        return currentData;
    }

    /**
     * Sets the current color
     *
     * @param color the active color to use
     */
    public void setColor(Color color) {
        if (!ColorUtil.isSameColor(color, graphicsState.color)) {
            addObject(new GraphicsSetProcessColor(colorConverter.convert(color)));
            graphicsState.color = color;
        }
    }


    /**
     * Sets the color converter
     *
     * @param colorConverter ColorConverter to filter the color
     *           when creating a GraphicsSetProcessColor.
     */
    public void setColorConverter(ColorConverter colorConverter) {
       this.colorConverter = colorConverter;
    }


    /**
     * Sets the current position
     *
     * @param coords the x and y coordinates of the current position
     */
    public void setCurrentPosition(int[] coords) {
        addObject(new GraphicsSetCurrentPosition(coords));
    }

    /**
     * Sets the line width
     *
     * @param lineWidth the line width multiplier
     */
    public void setLineWidth(int lineWidth) {
        if ((float) lineWidth != graphicsState.lineWidth) {
            addObject(new GraphicsSetLineWidth(lineWidth));
            graphicsState.lineWidth = (float) lineWidth;
        }
    }

    /**
     * Sets the line width
     *
     * @param lineWidth the line width multiplier
     */
    public void setLineWidth(float lineWidth) {
        float epsilon = Float.intBitsToFloat(0x00800000); // Float.MIN_NORMAL (JDK1.6)
        if (Math.abs(graphicsState.lineWidth - lineWidth) > epsilon) {
            addObject(new GraphicsSetFractionalLineWidth(lineWidth));
            graphicsState.lineWidth = lineWidth;
        }
    }

    /**
     * Sets the line type
     *
     * @param lineType the line type
     */
    public void setLineType(byte lineType) {
        if (lineType != graphicsState.lineType) {
            addObject(new GraphicsSetLineType(lineType));
            graphicsState.lineType = lineType;
        }
    }

    /**
     * Sets whether the following shape is to be filled.
     *
     * @param fill true if the following shape is to be filled
     */
    public void setFill(boolean fill) {
        setPatternSymbol(fill
                ? GraphicsSetPatternSymbol.SOLID_FILL
                : GraphicsSetPatternSymbol.NO_FILL);
    }

    /**
     * Sets the fill pattern of the next shape.
     *
     * @param patternSymbol the fill pattern of the next shape
     */
    public void setPatternSymbol(byte patternSymbol) {
        if (patternSymbol != graphicsState.patternSymbol) {
            addObject(new GraphicsSetPatternSymbol(patternSymbol));
            graphicsState.patternSymbol = patternSymbol;
        }
    }

    /**
     * Sets the character set to use
     *
     * @param characterSet the character set (font) reference
     */
    public void setCharacterSet(int characterSet) {
        if (characterSet != graphicsState.characterSet) {
            graphicsState.characterSet = characterSet;
        }
        addObject(new GraphicsSetCharacterSet(characterSet));
    }

    /**
     * Adds a line at the given x/y coordinates
     *
     * @param coords the x/y coordinates (can be a series)
     */
    public void addLine(int[] coords) {
        addLine(coords, false);
    }

    /**
     * Adds a line at the given x/y coordinates
     *
     * @param coords the x/y coordinates (can be a series)
     * @param relative relative true for a line at current position (relative to)
     */
    public void addLine(int[] coords, boolean relative) {
        addObject(new GraphicsLine(coords, relative));
    }

    /**
     * Adds a box at the given coordinates
     *
     * @param coords the x/y coordinates
     */
    public void addBox(int[] coords) {
        addObject(new GraphicsBox(coords));
    }

    /**
     * Adds a fillet (curve) at the given coordinates
     *
     * @param coords the x/y coordinates
     */
    public void addFillet(int[] coords) {
        addFillet(coords, false);
    }

    /**
     * Adds a fillet (curve) at the given coordinates
     *
     * @param coords the x/y coordinates
     * @param relative relative true for a fillet (curve) at current position (relative to)
     */
    public void addFillet(int[] coords, boolean relative) {
        addObject(new GraphicsFillet(coords, relative));
    }

    /**
     * Sets the arc parameters
     *
     * @param xmaj the maximum value of the x coordinate
     * @param ymin the minimum value of the y coordinate
     * @param xmin the minimum value of the x coordinate
     * @param ymaj the maximum value of the y coordinate
     */
    public void setArcParams(int xmaj, int ymin, int xmin, int ymaj) {
        addObject(new GraphicsSetArcParameters(xmaj, ymin, xmin, ymaj));
    }

    /**
     * Adds a full arc
     *
     * @param x the x coordinate
     * @param y the y coordinate
     * @param mh the integer portion of the multiplier
     * @param mhr the fractional portion of the multiplier
     */
    public void addFullArc(int x, int y, int mh, int mhr) {
        addObject(new GraphicsFullArc(x, y, mh, mhr));
    }

    /**
     * Adds an image
     *
     * @param x the x coordinate
     * @param y the y coordinate
     * @param width the image width
     * @param height the image height
     * @param imgData the image data
     */
    public void addImage(int x, int y, int width, int height, byte[] imgData) {
        addObject(new GraphicsImage(x, y, width, height, imgData));
    }

    /**
     * Adds a string
     *
     * @param str the string
     * @param x the x coordinate
     * @param y the y coordinate
     * @param charSet the character set associated with the string
     */
    public void addString(String str, int x, int y, CharacterSet charSet) {
        addObject(new GraphicsCharacterString(str, x, y, charSet));
    }

    /**
     * Begins a graphics area (start of fill)
     */
    public void beginArea() {
        addObject(new GraphicsAreaBegin());
    }

    /**
     * Ends a graphics area (end of fill)
     */
    public void endArea() {
        addObject(new GraphicsAreaEnd());
    }

    /**
     * Ends the prolog.
     */
    public void endProlog() {
        addObject(new GraphicsEndProlog());
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return "GraphicsObject: " + getName();
    }

    /**
     * Creates a new graphics segment
     */
    public void newSegment() {
        getData().newSegment();
        graphicsState.lineWidth = 0; //Looks like a new segment invalidates the graphics state
        graphicsState.color = Color.BLACK;
    }

    /** {@inheritDoc} */
    @Override
    public void setComplete(boolean complete) {
        for (GraphicsData completedObject : objects) {
            completedObject.setComplete(true);
        }
        super.setComplete(complete);
    }

    /** {@inheritDoc} */
    @Override
    protected void writeStart(OutputStream os) throws IOException {
        super.writeStart(os);
        byte[] data = new byte[17];
        copySF(data, Type.BEGIN, Category.GRAPHICS);
        os.write(data);
    }

    /** {@inheritDoc} */
    @Override
    protected void writeContent(OutputStream os) throws IOException {
        super.writeContent(os);
        writeObjects(objects, os);
    }

    /** {@inheritDoc} */
    @Override
    protected void writeEnd(OutputStream os) throws IOException {
        byte[] data = new byte[17];
        copySF(data, Type.END, Category.GRAPHICS);
        os.write(data);
    }

    /** the internal graphics state */
    private static final class GraphicsState {

        private GraphicsState() {
        }

        /** the current color */
        private Color color;

        /** the current line type */
        private byte lineType;

        /** the current line width */
        private float lineWidth;

        /** the current fill pattern */
        private byte patternSymbol;

        /** the current character set */
        private int characterSet;
    }
}
