/*

   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.

 */
package org.apache.batik.parser;

import java.io.IOException;
import java.util.Calendar;
import java.util.SimpleTimeZone;

import org.apache.batik.xml.XMLUtilities;

/**
 * An abstract base class for SMIL timing value parsers.
 *
 * @author <a href="mailto:cam%40mcc%2eid%2eau">Cameron McCormack</a>
 * @version $Id: TimingParser.java 1802297 2017-07-18 13:58:12Z ssteiner $
 */
public abstract class TimingParser extends AbstractParser {

    // Constants used in the return values of parseTimingSpecifier.
    protected static final int TIME_OFFSET          = 0;
    protected static final int TIME_SYNCBASE        = 1;
    protected static final int TIME_EVENTBASE       = 2;
    protected static final int TIME_REPEAT          = 3;
    protected static final int TIME_ACCESSKEY       = 4;
    protected static final int TIME_ACCESSKEY_SVG12 = 5;
    protected static final int TIME_MEDIA_MARKER    = 6;
    protected static final int TIME_WALLCLOCK       = 7;
    protected static final int TIME_INDEFINITE      = 8;

    /**
     * Allows the use of accessKey() timing specifiers with a single
     * character, as specified in SVG 1.1.
     */
    protected boolean useSVG11AccessKeys;

    /**
     * Allows the use of accessKey() timing specifiers with a DOM 3
     * key name, as specified in SVG 1.2.
     */
    protected boolean useSVG12AccessKeys;

    /**
     * Creates a new TimingParser.
     * @param useSVG11AccessKeys allows the use of accessKey() timing
     *                           specifiers with a single character
     * @param useSVG12AccessKeys allows the use of accessKey() with a
     *                           DOM 3 key name
     */
    public TimingParser(boolean useSVG11AccessKeys,
                        boolean useSVG12AccessKeys) {
        this.useSVG11AccessKeys = useSVG11AccessKeys;
        this.useSVG12AccessKeys = useSVG12AccessKeys;
    }

    /**
     * Parses a timing specifier.  Returns an array of Objects of the
     * form:
     * <ul>
     *   <li>{ TIME_OFFSET,          offset }</li>
     *   <li>{ TIME_SYNCBASE,        offset, id, time-symbol }</li>
     *   <li>{ TIME_EVENTBASE,       offset, id, event-ref }</li>
     *   <li>{ TIME_REPEAT,          offset, id, repeat-count }</li>
     *   <li>{ TIME_ACCESSKEY,       offset, character }</li>
     *   <li>{ TIME_ACCESSKEY_SVG12, offset, key-name }</li>
     *   <li>{ TIME_MEDIA_MARKER,    id, marker-name }</li>
     *   <li>{ TIME_WALLCLOCK,       wallclock-value }</li>
     *   <li>{ TIME_INDEFINITE }</li>
     * </ul>
     */
    protected Object[] parseTimingSpecifier() throws ParseException, IOException {
        skipSpaces();
        boolean escaped = false;
        if (current == '\\') {
            escaped = true;
            current = reader.read();
        }
        Object[] ret = null;
        if (current == '+' || (current == '-' && !escaped)
                || (current >= '0' && current <= '9')) {
            float offset = parseOffset();
            ret = new Object[] {TIME_OFFSET, offset};
        } else if (XMLUtilities.isXMLNameFirstCharacter((char) current)) {
            ret = parseIDValue(escaped);
        } else {
            reportUnexpectedCharacterError( current );
        }
        return ret;
    }

    /**
     * Parses an XML name with optional escaping in the middle.
     */
    protected String parseName() throws ParseException, IOException {
        StringBuffer sb = new StringBuffer();
        boolean midEscaped = false;
        do {
            sb.append((char) current);
            current = reader.read();
            midEscaped = false;
            if (current == '\\') {
                midEscaped = true;
                current = reader.read();
            }
        } while (XMLUtilities.isXMLNameCharacter((char) current)
                && (midEscaped || (current != '-' && current != '.')));
        return sb.toString();
    }

    /**
     * Parses a timing specifier that starts with a word.
     * @param escaped whether a backslash appeared before this timing specifier
     */
    protected Object[] parseIDValue(boolean escaped)
            throws ParseException, IOException {
        String id = parseName();
        if ((id.equals("accessKey") && useSVG11AccessKeys
                || id.equals("accesskey"))
                && !escaped) {
            if (current != '(') {
                reportUnexpectedCharacterError( current );
            }
            current = reader.read();
            if (current == -1) {
                reportError("end.of.stream", new Object[0]);
            }
            char key = (char) current;
            current = reader.read();
            if (current != ')') {
                reportUnexpectedCharacterError( current );
            }
            current = reader.read();
            skipSpaces();
            float offset = 0;
            if (current == '+' || current == '-') {
                offset = parseOffset();
            }
            return new Object[] {TIME_ACCESSKEY,
                    offset,
                    key};
        } else if (id.equals("accessKey") && useSVG12AccessKeys && !escaped) {
            if (current != '(') {
                reportUnexpectedCharacterError( current );
            }
            current = reader.read();
            StringBuffer keyName = new StringBuffer();
            while (current >= 'A' && current <= 'Z'
                    || current >= 'a' && current <= 'z'
                    || current >= '0' && current <= '9'
                    || current == '+') {
                keyName.append((char) current);
                current = reader.read();
            }
            if (current != ')') {
                reportUnexpectedCharacterError( current );
            }
            current = reader.read();
            skipSpaces();
            float offset = 0;
            if (current == '+' || current == '-') {
                offset = parseOffset();
            }
            return new Object[] {TIME_ACCESSKEY_SVG12,
                    offset,
                                  keyName.toString() };
        } else if (id.equals("wallclock") && !escaped) {
            if (current != '(') {
                reportUnexpectedCharacterError( current );
            }
            current = reader.read();
            skipSpaces();
            Calendar wallclockValue = parseWallclockValue();
            skipSpaces();
            if (current != ')') {
                reportError("character.unexpected",
                            new Object[] {current});
            }
            current = reader.read();
            return new Object[] {TIME_WALLCLOCK, wallclockValue };
        } else if (id.equals("indefinite") && !escaped) {
            return new Object[] {TIME_INDEFINITE};
        } else {
            if (current == '.') {
                current = reader.read();
                if (current == '\\') {
                    escaped = true;
                    current = reader.read();
                }
                if (!XMLUtilities.isXMLNameFirstCharacter((char) current)) {
                    reportUnexpectedCharacterError( current );
                }
                String id2 = parseName();
                if ((id2.equals("begin") || id2.equals("end")) && !escaped) {
                    skipSpaces();
                    float offset = 0;
                    if (current == '+' || current == '-') {
                        offset = parseOffset();
                    }
                    return new Object[] {TIME_SYNCBASE,
                            offset,
                                          id,
                                          id2 };
                } else if (id2.equals("repeat") && !escaped) {
                    Integer repeatIteration = null;
                    if (current == '(') {
                        current = reader.read();
                        repeatIteration = parseDigits();
                        if (current != ')') {
                            reportUnexpectedCharacterError( current );
                        }
                        current = reader.read();
                    }
                    skipSpaces();
                    float offset = 0;
                    if (current == '+' || current == '-') {
                        offset = parseOffset();
                    }
                    return new Object[] {TIME_REPEAT,
                            offset,
                                          id,
                                          repeatIteration };
                } else if (id2.equals("marker") && !escaped) {
                    if (current != '(') {
                        reportUnexpectedCharacterError( current );
                    }
                    String markerName = parseName();
                    if (current != ')') {
                        reportUnexpectedCharacterError( current );
                    }
                    current = reader.read();
                    return new Object[] {TIME_MEDIA_MARKER,
                                          id,
                                          markerName };
                } else {
                    skipSpaces();
                    float offset = 0;
                    if (current == '+' || current == '-') {
                        offset = parseOffset();
                    }
                    return new Object[] {TIME_EVENTBASE,
                            offset,
                                          id,
                                          id2 };
                }
            } else {
                skipSpaces();
                float offset = 0;
                if (current == '+' || current == '-') {
                    offset = parseOffset();
                }
                return new Object[] {TIME_EVENTBASE,
                        offset,
                                      null,
                                      id };
            }
        }
    }

    /**
     * Parses a clock value.
     */
    protected float parseClockValue() throws ParseException, IOException {
        int d1 = parseDigits();
        float offset;
        if (current == ':') {
            current = reader.read();
            int d2 = parseDigits();
            if (current == ':') {
                current = reader.read();
                int d3 = parseDigits();
                offset = d1 * 3600 + d2 * 60 + d3;
            } else {
                offset = d1 * 60 + d2;
            }
            if (current == '.') {
                current = reader.read();
                offset += parseFraction();
            }
        } else if (current == '.') {
            current = reader.read();
            offset = (parseFraction() + d1) * parseUnit();
        } else {
            offset = d1 * parseUnit();
        }
        return offset;
    }

    /**
     * Parses an offset value.
     */
    protected float parseOffset() throws ParseException, IOException {
        boolean offsetNegative = false;
        if (current == '-') {
            offsetNegative = true;
            current = reader.read();
            skipSpaces();
        } else if (current == '+') {
            current = reader.read();
            skipSpaces();
        }
        if (offsetNegative) {
            return -parseClockValue();
        }
        return parseClockValue();
    }

    /**
     * Parses a sequence of digits and returns the integer.
     */
    protected int parseDigits() throws ParseException, IOException {
        int value = 0;
        if (current < '0' || current > '9') {
            reportUnexpectedCharacterError( current );
        }
        do {
            value = value * 10 + (current - '0');
            current = reader.read();
        } while (current >= '0' && current <= '9');
        return value;
    }

    /**
     * Parses a '.' and a sequence of digits and returns the float.
     */
    protected float parseFraction() throws ParseException, IOException {
        float value = 0;
        if (current < '0' || current > '9') {
            reportUnexpectedCharacterError( current );
        }
        float weight = 0.1f;
        do {
            value += weight * (current - '0');
            weight *= 0.1f;
            current = reader.read();
        } while (current >= '0' && current <= '9');
        return value;
    }

    /**
     * Parses a time unit and returns the float for the multiplier.
     */
    protected float parseUnit() throws ParseException, IOException {
        if (current == 'h') {
            current = reader.read();
            return 3600;
        } else if (current == 'm') {
            current = reader.read();
            if (current == 'i') {
                current = reader.read();
                if (current != 'n') {
                    reportUnexpectedCharacterError( current );
                }
                current = reader.read();
                return 60;
            } else if (current == 's') {
                current = reader.read();
                return 0.001f;
            } else {
                reportUnexpectedCharacterError( current );
            }
        } else if (current == 's') {
            current = reader.read();
        }
        return 1;
    }

    /**
     * Parses a wallclock value and returns it as a {@link Calendar}.
     */
    protected Calendar parseWallclockValue()
            throws ParseException, IOException {
        int y = 0, M = 0, d = 0, h = 0, m = 0, s = 0, tzh = 0, tzm = 0;
        float frac = 0;
        boolean dateSpecified = false;
        boolean timeSpecified = false;
        boolean tzSpecified = false;
        boolean tzNegative = false;
        String tzn = null;

        int digits1 = parseDigits();
        do {
            if (current == '-') {
                dateSpecified = true;
                y = digits1;
                current = reader.read();
                M = parseDigits();
                if (current != '-') {
                    reportUnexpectedCharacterError( current );
                }
                current = reader.read();
                d = parseDigits();
                if (current != 'T') {
                    break;
                }
                current = reader.read();
                digits1 = parseDigits();
                if (current != ':') {
                    reportUnexpectedCharacterError( current );
                }
            }
            if (current == ':') {
                timeSpecified = true;
                h = digits1;
                current = reader.read();
                m = parseDigits();
                if (current == ':') {
                    current = reader.read();
                    s = parseDigits();
                    if (current == '.') {
                        current = reader.read();
                        frac = parseFraction();
                    }
                }
                if (current == 'Z') {
                    tzSpecified = true;
                    tzn = "UTC";
                    current = reader.read();
                } else if (current == '+' || current == '-') {
                    StringBuffer tznb = new StringBuffer();
                    tzSpecified = true;
                    if (current == '-') {
                        tzNegative = true;
                        tznb.append('-');
                    } else {
                        tznb.append('+');
                    }
                    current = reader.read();
                    tzh = parseDigits();
                    if (tzh < 10) {
                        tznb.append('0');
                    }
                    tznb.append(tzh);
                    if (current != ':') {
                        reportUnexpectedCharacterError( current );
                    }
                    tznb.append(':');
                    current = reader.read();
                    tzm = parseDigits();
                    if (tzm < 10) {
                        tznb.append('0');
                    }
                    tznb.append(tzm);
                    tzn = tznb.toString();
                }
            }
        } while (false);
        if (!dateSpecified && !timeSpecified) {
            reportUnexpectedCharacterError( current );
        }
        Calendar wallclockTime;
        if (tzSpecified) {
            int offset = (tzNegative ? -1 : 1)
                * (tzh * 3600000 + tzm * 60000);
            wallclockTime = Calendar.getInstance(new SimpleTimeZone(offset, tzn));
        } else {
            wallclockTime = Calendar.getInstance();
        }
        if (dateSpecified && timeSpecified) {
            wallclockTime.set(y, M, d, h, m, s);
        } else if (dateSpecified) {
            wallclockTime.set(y, M, d, 0, 0, 0);
        } else {
            wallclockTime.set(Calendar.HOUR, h);
            wallclockTime.set(Calendar.MINUTE, m);
            wallclockTime.set(Calendar.SECOND, s);
        }
        if (frac == 0.0f) {
            wallclockTime.set(Calendar.MILLISECOND, (int) (frac * 1000));
        } else {
            wallclockTime.set(Calendar.MILLISECOND, 0);
        }
        return wallclockTime;
    }
}
