/*
 * Copyright 2014 Jacob Klinker
 *
 * Licensed 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 com.android.mms.dom.smil;

import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.smil.Time;

public class TimeImpl implements Time {
    static final int ALLOW_INDEFINITE_VALUE = (1 << 0);
    static final int ALLOW_OFFSET_VALUE     = (1 << 1);
    static final int ALLOW_SYNCBASE_VALUE   = (1 << 2);
    static final int ALLOW_SYNCTOPREV_VALUE = (1 << 3);
    static final int ALLOW_EVENT_VALUE      = (1 << 4);
    static final int ALLOW_MARKER_VALUE     = (1 << 5);
    static final int ALLOW_WALLCLOCK_VALUE  = (1 << 6);
    static final int ALLOW_NEGATIVE_VALUE   = (1 << 7);
    static final int ALLOW_ALL              = 0xFF;

    short mTimeType;
    boolean mResolved;
    double mResolvedOffset;

    /**
     * Creates a TimeImpl representation of a time-value represented as a String.
     * Time-values have the following syntax:
     * <p>
     * <pre>
     * Time-val ::= ( smil-1.0-syncbase-value
     *                          | "indefinite"
     *                          | offset-value
     *                          | syncbase-value
     *                          | syncToPrev-value
     *                          | event-value
     *                          | media-marker-value
     *                          | wallclock-sync-value )
     * Smil-1.0-syncbase-value ::=
     *          "id(" id-ref ")" ( "(" ( "begin" | "end" | clock-value ) ")" )?
     * Offset-value         ::= ( "+" | "-" )? clock-value
     * Syncbase-value       ::= ( id-ref "." ( "begin" | "end" ) ) ( ( "+" | "-" ) clock-value )?
     * SyncToPrev-value     ::= ( "prev.begin" | "prev.end" ) ( ( "+" | "-" ) clock-value )?
     * Event-value          ::= ( id-ref "." )? ( event-ref  ) ( ( "+" | "-" ) clock-value )?
     * Media-marker-value   ::= id-ref ".marker(" marker-name ")"
     * Wallclock-sync-value ::= "wallclock(" wallclock-value ")"
     * </pre>
     *
     * @param timeValue A String in the representation specified above
     * @param constraints Any combination of the #ALLOW_* flags
     * @return  A TimeImpl instance representing
     * @exception IllegalArgumentException if the timeValue input
     *          parameter does not comply with the defined syntax
     * @exception NullPointerException if the timekValue string is
     *          <code>null</code>
     */
    TimeImpl(String timeValue, int constraints) {
        /*
         * We do not support yet:
         *      - smil-1.0-syncbase-value
         *      - syncbase-value
         *      - syncToPrev-value
         *      - event-value
         *      - Media-marker-value
         *      - Wallclock-sync-value
         */
        // Will throw NullPointerException if timeValue is null
        if (timeValue.equals("indefinite")
                && ((constraints & ALLOW_INDEFINITE_VALUE) != 0) ) {
            mTimeType = SMIL_TIME_INDEFINITE;
        } else if ((constraints & ALLOW_OFFSET_VALUE) != 0) {
            int sign = 1;
            if (timeValue.startsWith("+")) {
                timeValue = timeValue.substring(1);
            } else if (timeValue.startsWith("-")) {
                timeValue = timeValue.substring(1);
                sign = -1;
            }
            mResolvedOffset = sign*parseClockValue(timeValue)/1000.0;
            mResolved = true;
            mTimeType = SMIL_TIME_OFFSET;
        } else {
            throw new IllegalArgumentException("Unsupported time value");
        }
    }

    /**
     * Converts a String representation of a clock value into the float
     * representation used in this API.
     * <p>
     * Clock values have the following syntax:
     * </p>
     * <p>
     * <pre>
     * Clock-val         ::= ( Full-clock-val | Partial-clock-val | Timecount-val )
     * Full-clock-val    ::= Hours ":" Minutes ":" Seconds ("." Fraction)?
     * Partial-clock-val ::= Minutes ":" Seconds ("." Fraction)?
     * Timecount-val     ::= Timecount ("." Fraction)? (Metric)?
     * Metric            ::= "h" | "min" | "s" | "ms"
     * Hours             ::= DIGIT+; any positive number
     * Minutes           ::= 2DIGIT; range from 00 to 59
     * Seconds           ::= 2DIGIT; range from 00 to 59
     * Fraction          ::= DIGIT+
     * Timecount         ::= DIGIT+
     * 2DIGIT            ::= DIGIT DIGIT
     * DIGIT             ::= [0-9]
     * </pre>
     *
     * @param clockValue A String in the representation specified above
     * @return  A float value in milliseconds that matches the string
     *          representation given as the parameter
     * @exception IllegalArgumentException if the clockValue input
     *          parameter does not comply with the defined syntax
     * @exception NullPointerException if the clockValue string is
     *          <code>null</code>
     */
    public static float parseClockValue(String clockValue) {
        try {
            float result = 0;

            // Will throw NullPointerException if clockValue is null
            clockValue = clockValue.trim();

            // Handle first 'Timecount-val' cases with metric
            if (clockValue.endsWith("ms")) {
                result = parseFloat(clockValue, 2, true);
            } else if (clockValue.endsWith("s")) {
                result = 1000*parseFloat(clockValue, 1, true);
            } else if (clockValue.endsWith("min")) {
                result = 60000*parseFloat(clockValue, 3, true);
            } else if (clockValue.endsWith("h")) {
                result = 3600000*parseFloat(clockValue, 1, true);
            } else {
                // Handle Timecount-val without metric
                try {
                    return parseFloat(clockValue, 0, true) * 1000;
                } catch (NumberFormatException e) {
                    // Ignore
                }

                // Split in {[Hours], Minutes, Seconds}
                String[] timeValues = clockValue.split(":");

                // Read Hours if present and remember location of Minutes
                int indexOfMinutes;
                if (timeValues.length == 2) {
                    indexOfMinutes = 0;
                } else if (timeValues.length == 3) {
                    result = 3600000*(int)parseFloat(timeValues[0], 0, false);
                    indexOfMinutes = 1;
                } else {
                    throw new IllegalArgumentException();
                }

                // Read Minutes
                int minutes = (int)parseFloat(timeValues[indexOfMinutes], 0, false);
                if ((minutes >= 00) && (minutes <= 59)) {
                    result += 60000*minutes;
                } else {
                    throw new IllegalArgumentException();
                }

                // Read Seconds
                float seconds = parseFloat(timeValues[indexOfMinutes + 1], 0, true);
                if ((seconds >= 00) && (seconds < 60)) {
                    result += 60000*seconds;
                } else {
                    throw new IllegalArgumentException();
                }

            }
            return result;
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Parse a value formatted as follows:
     * <p>
     * <pre>
     * Value    ::= Number ("." Decimal)? (Text)?
     * Number   ::= DIGIT+; any positive number
     * Decimal  ::= DIGIT+; any positive number
     * Text     ::= CHAR*;   any sequence of chars
     * DIGIT    ::= [0-9]
     * </pre>
     * @param value The Value to parse
     * @param ignoreLast The size of Text to ignore
     * @param parseDecimal Whether Decimal is expected
     * @return The float value without Text, rounded to 3 digits after '.'
     * @throws IllegalArgumentException if Decimal was not expected but encountered
     */
    private static float parseFloat(String value, int ignoreLast, boolean parseDecimal) {
        // Ignore last characters
        value = value.substring(0, value.length() - ignoreLast);

        float result;
        int indexOfComma = value.indexOf('.');
        if (indexOfComma != -1) {
            if (!parseDecimal) {
                throw new IllegalArgumentException("int value contains decimal");
            }
            // Ensure that there are at least 3 decimals
            value = value + "000";
            // Read value up to 3 decimals and cut the rest
            result = Float.parseFloat(value.substring(0, indexOfComma));
            result += Float.parseFloat(
                    value.substring(indexOfComma + 1, indexOfComma + 4))/1000;
        } else {
            result = Integer.parseInt(value);
        }

        return result;
    }

    /*
     * Time Interface
     */

    public boolean getBaseBegin() {
        // TODO Auto-generated method stub
        return false;
    }

    public Element getBaseElement() {
        // TODO Auto-generated method stub
        return null;
    }

    public String getEvent() {
        // TODO Auto-generated method stub
        return null;
    }

    public String getMarker() {
        // TODO Auto-generated method stub
        return null;
    }

    public double getOffset() {
        // TODO Auto-generated method stub
        return 0;
    }

    public boolean getResolved() {
        return mResolved;
    }

    public double getResolvedOffset() {
        return mResolvedOffset;
    }

    public short getTimeType() {
        return mTimeType;
    }

    public void setBaseBegin(boolean baseBegin) throws DOMException {
        // TODO Auto-generated method stub

    }

    public void setBaseElement(Element baseElement) throws DOMException {
        // TODO Auto-generated method stub

    }

    public void setEvent(String event) throws DOMException {
        // TODO Auto-generated method stub

    }

    public void setMarker(String marker) throws DOMException {
        // TODO Auto-generated method stub

    }

    public void setOffset(double offset) throws DOMException {
        // TODO Auto-generated method stub

    }
}
