/*
 * 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: XMLUtil.java 1805173 2017-08-16 10:50:04Z ssteiner $ */

package org.apache.fop.util;

import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

/**
 * A collection of utility method for XML handling.
 */
public final class XMLUtil implements XMLConstants {

    private XMLUtil() {
    }

    /**
     * Returns an attribute value as a boolean value.
     * @param attributes the Attributes object
     * @param name the name of the attribute
     * @param defaultValue the default value if the attribute is not specified
     * @return the attribute value as a boolean
     */
    public static boolean getAttributeAsBoolean(Attributes attributes, String name,
            boolean defaultValue) {
        String s = attributes.getValue(name);
        if (s == null) {
            return defaultValue;
        } else {
            return Boolean.valueOf(s);
        }
    }

    /**
     * Returns an attribute value as a int value.
     * @param attributes the Attributes object
     * @param name the name of the attribute
     * @param defaultValue the default value if the attribute is not specified
     * @return the attribute value as an int
     */
    public static int getAttributeAsInt(Attributes attributes, String name,
            int defaultValue) {
        String s = attributes.getValue(name);
        if (s == null) {
            return defaultValue;
        } else {
            return Integer.parseInt(s);
        }
    }

    /**
     * Returns an attribute value as a int value.
     * @param attributes the Attributes object
     * @param name the name of the attribute
     * @return the attribute value as an int
     * @throws SAXException if the attribute is missing
     */
    public static int getAttributeAsInt(Attributes attributes, String name) throws SAXException {
        String s = attributes.getValue(name);
        if (s == null) {
            throw new SAXException("Attribute '" + name + "' is missing");
        } else {
            return Integer.parseInt(s);
        }
    }

    /**
     * Returns an attribute value as a Integer value.
     * @param attributes the Attributes object
     * @param name the name of the attribute
     * @return the attribute value as an Integer or null if the attribute is missing
     */
    public static Integer getAttributeAsInteger(Attributes attributes, String name) {
        String s = attributes.getValue(name);
        if (s == null) {
            return null;
        } else {
            return Integer.valueOf(s);
        }
    }

    /**
     * Returns an attribute value as a Rectangle2D value. The string value is expected as 4
     * double-precision numbers separated by whitespace.
     * @param attributes the Attributes object
     * @param name the name of the attribute
     * @return the attribute value as an Rectangle2D
     */
    public static Rectangle2D getAttributeAsRectangle2D(Attributes attributes, String name) {
        String s = attributes.getValue(name).trim();
        double[] values = ConversionUtils.toDoubleArray(s, "\\s");
        if (values.length != 4) {
            throw new IllegalArgumentException("Rectangle must consist of 4 double values!");
        }
        return new Rectangle2D.Double(values[0], values[1], values[2], values[3]);
    }

    /**
     * Returns an attribute value as a Rectangle value. The string value is expected as 4
     * integer numbers separated by whitespace.
     * @param attributes the Attributes object
     * @param name the name of the attribute
     * @return the attribute value as an Rectangle
     */
    public static Rectangle getAttributeAsRectangle(Attributes attributes, String name) {
        String s = attributes.getValue(name);
        if (s == null) {
            return null;
        }
        int[] values = ConversionUtils.toIntArray(s.trim(), "\\s");
        if (values.length != 4) {
            throw new IllegalArgumentException("Rectangle must consist of 4 int values!");
        }
        return new Rectangle(values[0], values[1], values[2], values[3]);
    }

    /**
     * Returns an attribute value as a integer array. The string value is expected as 4
     * integer numbers separated by whitespace.
     * @param attributes the Attributes object
     * @param name the name of the attribute
     * @return the attribute value as an int array
     */
    public static int[] getAttributeAsIntArray(Attributes attributes, String name) {
        String s = attributes.getValue(name);
        if (s == null) {
            return null;
        } else {
            return ConversionUtils.toIntArray(s.trim(), "\\s");
        }
    }

    /**
     * Adds an attribute to a given {@link AttributesImpl} instance.
     * @param atts the attributes collection
     * @param attribute the attribute to add
     * @param value the attribute's CDATA value
     */
    public static void addAttribute(AttributesImpl atts,
            org.apache.xmlgraphics.util.QName attribute, String value) {
        atts.addAttribute(attribute.getNamespaceURI(),
                attribute.getLocalName(), attribute.getQName(), XMLUtil.CDATA, value);
    }

    /**
     * Adds an attribute to a given {@link AttributesImpl} instance. The attribute will be
     * added in the default namespace.
     * @param atts the attributes collection
     * @param localName the local name of the attribute
     * @param value the attribute's CDATA value
     */
    public static void addAttribute(AttributesImpl atts, String localName, String value) {
        atts.addAttribute("", localName, localName, XMLUtil.CDATA, value);
    }

    /**
     * Encode a glyph position adjustments array as a string, where the string value
     * adheres to the following syntax:
     *
     * count ( 'Z' repeat | number )
     *
     * where each token is separated by whitespace, except that 'Z' followed by repeat
     * are considered to be a single token with no intervening whitespace, and where
     * 'Z' repeat encodes repeated zeroes.
     * @param dp the adjustments array
     * @param paCount the number of entries to encode from adjustments array
     * @return the encoded value
     */
    public static String encodePositionAdjustments(int[][] dp, int paCount) {
        assert dp != null;
        StringBuffer sb = new StringBuffer();
        int na = paCount;
        int nz = 0;
        sb.append(na);
        for (int i = 0; i < na; i++) {
            int[] pa = dp [ i ];
            if (pa != null) {
                for (int k = 0; k < 4; k++) {
                    int a = pa [ k ];
                    if (a != 0) {
                        encodeNextAdjustment(sb, nz, a);
                        nz = 0;
                    } else {
                        nz++;
                    }
                }
            } else {
                nz += 4;
            }
        }
        encodeNextAdjustment(sb, nz, 0);
        return sb.toString();
    }

    /**
     * Encode a glyph position adjustments array as a string, where the string value
     * adheres to the following syntax:
     *
     * count ( 'Z' repeat | number )
     *
     * where each token is separated by whitespace, except that 'Z' followed by repeat
     * are considered to be a single token with no intervening whitespace.
     * @param dp the adjustments array
     * @return the encoded value
     */
    public static String encodePositionAdjustments(int[][] dp) {
        assert dp != null;
        return encodePositionAdjustments(dp, dp.length);
    }

    private static void encodeNextAdjustment(StringBuffer sb, int nz, int a) {
        encodeZeroes(sb, nz);
        encodeAdjustment(sb, a);
    }

    private static void encodeZeroes(StringBuffer sb, int nz) {
        if (nz > 0) {
            sb.append(' ');
            if (nz == 1) {
                sb.append('0');
            } else {
                sb.append('Z');
                sb.append(nz);
            }
        }
    }

    private static void encodeAdjustment(StringBuffer sb, int a) {
        if (a != 0) {
            sb.append(' ');
            sb.append(a);
        }
    }

    /**
     * Decode a string as a glyph position adjustments array, where the string
     * shall adhere to the syntax specified by {@link #encodePositionAdjustments}.
     * @param value the encoded value
     * @return the position adjustments array
     */
    public static int[][] decodePositionAdjustments(String value) {
        int[][] dp = null;
        if (value != null) {
            String[] sa = value.split("\\s");
            if (sa != null) {
                if (sa.length > 0) {
                    int na = Integer.parseInt(sa[0]);
                    dp = new int [ na ] [ 4 ];
                    for (int i = 1, n = sa.length, k = 0; i < n; i++) {
                        String s = sa [ i ];
                        if (s.charAt(0) == 'Z') {
                            int nz = Integer.parseInt(s.substring(1));
                            k += nz;
                        } else {
                            dp [ k / 4 ] [ k % 4 ] = Integer.parseInt(s);
                            k += 1;
                        }
                    }
                }
            }
        }
        return dp;
    }

    /**
     * Returns an attribute value as a glyph position adjustments array. The string value
     * is expected to be a non-empty sequence of either Z&lt;repeat&gt; or &lt;number&gt;, where the
     * former encodes a repeat count (of zeroes) and the latter encodes a integer number,
     * and where each item is separated by whitespace.
     * @param attributes the Attributes object
     * @param name the name of the attribute
     * @return the position adjustments array
     */
    public static int[][] getAttributeAsPositionAdjustments(Attributes attributes, String name) {
        String s = attributes.getValue(name);
        if (s == null) {
            return null;
        } else {
            return decodePositionAdjustments(s.trim());
        }
    }

    /**
     * Escape '&lt;', '&gt;' and '&amp;' using NCRs.
     * @param unescaped string
     * @return escaped string
     */
    public static String escape(String unescaped) {
        int needsEscape = 0;
        for (int i = 0, n = unescaped.length(); i < n; ++i) {
            char c = unescaped.charAt(i);
            if ((c == '<') || (c == '>') || (c == '&')) {
                ++needsEscape;
            }
        }
        if (needsEscape > 0) {
            StringBuffer sb = new StringBuffer(unescaped.length() + 6 * needsEscape);
            for (int i = 0, n = unescaped.length(); i < n; ++i) {
                char c = unescaped.charAt(i);
                if ((c == '<') || (c == '>') || (c == '&')) {
                    sb.append("&#x");
                    sb.append(Integer.toString(c, 16));
                    sb.append(';');
                } else {
                    sb.append(c);
                }
            }
            return sb.toString();
        } else {
            return unescaped;
        }
    }

}
