/*
 * 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: BorderProps.java 1884907 2020-12-29 13:33:34Z ssteiner $ */

package org.apache.fop.traits;

import java.awt.Color;
import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.fo.expr.PropertyException;
import org.apache.fop.util.ColorUtil;

/**
 * Border properties.
 * Class to store border trait properties for the area tree.
 */
public class BorderProps implements Serializable {

    private static final long serialVersionUID = 8022237892391068187L;

    public enum Mode {
        SEPARATE("separate") {
            @Override
            int getClippedWidth(BorderProps bp) {
                return 0;
            }
        },
        COLLAPSE_INNER("collapse-inner"), // for borders inside a table
        COLLAPSE_OUTER("collapse-outer"); // for borders at the table's outer border

        private final String value;

        Mode(String value) {
            this.value = value;
        }

        int getClippedWidth(BorderProps bp) {
            return bp.width / 2;
        };
    }

    /** Border style (one of EN_*) */
    public final int style; // Enum for border style
    /** Border color */
    public final Color color;

    /** Border width */
    public final int width;

    private final int radiusStart;

    private final int radiusEnd;

    /** Border mode */
    private final Mode mode;

    /**
     * Constructs a new BorderProps instance.
     * @param style border style (one of EN_*)
     * @param width border width
     * @param radiusStart radius of start corner in the direction perpendicular to border segment
     * @param radiusEnd radius of end corner in the direction perpendicular to border segment
     * @param color border color
     * @param mode border mode ((one of SEPARATE, COLLAPSE_INNER and COLLAPSE_OUTER)
     */
    public BorderProps(int style, int width, int radiusStart, int radiusEnd, Color color, Mode mode) {
        this.style = style;
        this.width = width;
        this.radiusStart = radiusStart;
        this.radiusEnd = radiusEnd;
        this.color = color;
        this.mode = mode;
    }

    /**
     * Factory method for a new BorderProps instance with rectangular corners.
     * @param style border style (one of EN_*)
     * @param width border width
     * @param color border color
     * @param mode border mode ((one of SEPARATE, COLLAPSE_INNER and COLLAPSE_OUTER)
     */
    public static BorderProps makeRectangular(int style, int width, Color color, Mode mode) {
        return new BorderProps(style, width, 0, 0, color, mode);
    }

    private BorderProps(String style, int width, int radiusStart, int radiusEnd, Color color, Mode mode) {
        this(getConstantForStyle(style), width, radiusStart, radiusEnd, color, mode);
    }

    /**
     *
     * @return the radius of the corner adjacent to the before or start border
     */
    public int getRadiusStart() {
        return radiusStart;
    }

    /**
     * @return the radius of the corner adjacent to the after or end border
     */
    public int getRadiusEnd() {
        return radiusEnd;
    }

    /**
     * @return the border mode ((one of SEPARATE, COLLAPSE_INNER and COLLAPSE_OUTER)
     */
    public Mode getMode() {
        return mode;
    }

    /**
     * @param bp the border properties or null
     * @return the effective width of the clipped part of the border
     */
    public static int getClippedWidth(BorderProps bp) {
        return bp == null ? 0 : bp.mode.getClippedWidth(bp);
    }

    private String getStyleString() {
        return BorderStyle.valueOf(style).getName();
    }

    private static int getConstantForStyle(String style) {
        return BorderStyle.valueOf(style).getEnumValue();
    }

    public boolean isCollapseOuter() {
        return mode == Mode.COLLAPSE_OUTER;
    }

    /** {@inheritDoc} */
    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    /** {@inheritDoc} */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        } else if (obj == this) {
            return true;
        } else {
            if (obj instanceof BorderProps) {
                BorderProps other = (BorderProps) obj;
                return (style == other.style)
                        && org.apache.xmlgraphics.java2d.color.ColorUtil.isSameColor(
                                color, other.color)
                        && width == other.width
                        && mode == other.mode
                        && radiusStart == other.radiusStart
                        && radiusEnd == other.radiusEnd;
            }
        }
        return false;
    }

    /**
     * Returns a BorderProps represtation of a string of the format as written by
     * BorderProps.toString().
     * @param foUserAgent FOP user agent caching ICC profiles
     * @param s the string
     * @return a BorderProps instance
     */
    public static BorderProps valueOf(FOUserAgent foUserAgent, String s) {
        return BorderPropsDeserializer.INSTANCE.valueOf(foUserAgent, s);
    }
    /** {@inheritDoc} */
    @Override
    public String toString() {
        StringBuffer sbuf = new StringBuffer();
        sbuf.append('(')
                .append(getStyleString()).append(',')
                .append(ColorUtil.colorToString(color)).append(',')
                .append(width);
        if (!mode.equals(Mode.SEPARATE)) {
            sbuf.append(",").append(mode.value);
        }

        if (radiusStart != 0 || radiusEnd != 0) {
            if (mode.equals(Mode.SEPARATE)) {
                // Because of the corner radii properties the mode must be set
                // so that the parameter index is consistent
                sbuf.append(",").append(Mode.SEPARATE.value);
            }
            sbuf.append(',').append(radiusStart)
            .append(',').append(radiusEnd);
        }
        sbuf.append(')');
        return sbuf.toString();
    }

    private static final class BorderPropsDeserializer {

        private static final BorderPropsDeserializer INSTANCE = new BorderPropsDeserializer();

        private static final Pattern PATTERN = Pattern.compile("([^,\\(]+(?:\\(.*\\))?)");

        private BorderPropsDeserializer() {
        }

        public BorderProps valueOf(FOUserAgent foUserAgent, String s) {
            if (s.startsWith("(") && s.endsWith(")")) {
                s = s.substring(1, s.length() - 1);
                Matcher m = PATTERN.matcher(s);
                m.find();
                String style = m.group();
                m.find();
                String color = m.group();
                m.find();
                int width = Integer.parseInt(m.group());
                Mode mode = Mode.SEPARATE;
                if (m.find()) {
                    String ms = m.group();
                    if (Mode.COLLAPSE_INNER.value.equalsIgnoreCase(ms)) {
                        mode = Mode.COLLAPSE_INNER;
                    } else if (Mode.COLLAPSE_OUTER.value.equalsIgnoreCase(ms)) {
                        mode = Mode.COLLAPSE_OUTER;
                    }
                }
                Color c;
                try {
                    c = ColorUtil.parseColorString(foUserAgent, color);
                } catch (PropertyException e) {
                    throw new IllegalArgumentException(e.getMessage());
                }
                int startRadius = 0;
                int endRadius = 0;
                if (m.find()) {
                    startRadius = Integer.parseInt(m.group());
                    m.find();
                    endRadius = Integer.parseInt(m.group());
                }
                return new BorderProps(style, width, startRadius, endRadius, c, mode);
            } else {
                throw new IllegalArgumentException("BorderProps must be surrounded by parentheses");
            }
        }
    }

}
