/*   Copyright 2004 The Apache Software Foundation
 *
 *   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 org.apache.xmlbeans;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;

import org.apache.xmlbeans.impl.util.MathUtil;

/**
 * Used to build {@link GDuration GDurations}.
 */
public class GDurationBuilder implements GDurationSpecification, java.io.Serializable {
    private static final long serialVersionUID = 1L;

    private int _sign;
    private int _CY;
    private int _M;
    private int _D;
    private int _h;
    private int _m;
    private int _s;
    private BigDecimal _fs;

    /**
     * Constructs an empty GDurationBuilder representing zero seconds.
     */
    public GDurationBuilder() {
        _sign = +1;
        _fs = GDate._zero;
    }

    /**
     * Constructs a GDuration from a lexical
     * representation.
     */
    public GDurationBuilder(String s) {
        this(new GDuration(s));
    }

    /**
     * Constructs a GDurationBuilder with the specified sign,
     * year, month, day, hours, minutes, seconds, and optional
     * fractional seconds.
     *
     * @param sign +1 for a positive duration, -1 for a negative duration
     * @throws java.lang.IllegalArgumentException if the sign is not 1 or -1
     */
    public GDurationBuilder(
        int sign,
        int year,
        int month,
        int day,
        int hour,
        int minute,
        int second,
        BigDecimal fraction) {
        if (sign != 1 && sign != -1) {
            throw new IllegalArgumentException();
        }
        _sign = sign;
        _CY = year;
        _M = month;
        _D = day;
        _h = hour;
        _m = minute;
        _s = second;
        _fs = fraction == null ? GDate._zero : fraction;
    }

    /**
     * Constructs a GDurationBuilder from another GDurationBuilderSpecification.
     */
    public GDurationBuilder(GDurationSpecification gDuration) {
        _sign = gDuration.getSign();
        _CY = gDuration.getYear();
        _M = gDuration.getMonth();
        _D = gDuration.getDay();
        _h = gDuration.getHour();
        _m = gDuration.getMinute();
        _s = gDuration.getSecond();
        _fs = gDuration.getFraction();
    }

    /**
     * Builds another GDurationBuilder with the same value
     * as this one.
     */
    public Object clone() {
        return new GDurationBuilder(this);
    }

    /**
     * Builds a GDuration from this GDurationBuilder.
     */
    public GDuration toGDuration() {
        return new GDuration(this);
    }

    /**
     * Adds to this duration.  Does a fieldwise add, with no
     * normalization.
     */
    public void addGDuration(GDurationSpecification duration) {
        int sign = _sign * duration.getSign();
        _add(duration, sign);
    }

    /**
     * Subtracts from this duration.  Does a fieldwise subtraction,
     * with no normalization.
     */
    public void subtractGDuration(GDurationSpecification duration) {
        int sign = -_sign * duration.getSign();
        _add(duration, sign);
    }

    private void _add(GDurationSpecification duration, int sign) {
        _CY += sign * duration.getYear();
        _M += sign * duration.getMonth();
        _D += sign * duration.getDay();
        _h += sign * duration.getHour();
        _m += sign * duration.getMinute();
        _s += sign * duration.getSecond();

        if (duration.getFraction().signum() == 0) {
            return;
        }

        if (_fs.signum() == 0 && sign == 1) {
            _fs = duration.getFraction();
        } else {
            _fs = sign > 0 ?
                _fs.add(duration.getFraction()) :
                _fs.subtract(duration.getFraction());
        }
    }

    /**
     * Sets the sign.
     */
    public final void setSign(int sign) {
        if (sign != 1 && sign != -1) {
            throw new IllegalArgumentException();
        }
        _sign = sign;
    }

    /**
     * Sets the year component.
     */
    public void setYear(int year) {
        _CY = year;
    }

    /**
     * Sets the month component.
     */
    public void setMonth(int month) {
        _M = month;
    }

    /**
     * Sets the day component.
     */
    public void setDay(int day) {
        _D = day;
    }

    /**
     * Sets the hour component.
     */
    public void setHour(int hour) {
        _h = hour;
    }

    /**
     * Sets the minute component.
     */
    public void setMinute(int minute) {
        _m = minute;
    }

    /**
     * Sets the second component.
     */
    public void setSecond(int second) {
        _s = second;
    }

    /**
     * Sets the fraction-of-second component.
     */
    public void setFraction(BigDecimal fraction) {
        _fs = fraction == null ? GDate._zero : fraction;
    }

    /**
     * All GDuration instances return true.
     */
    public final boolean isImmutable() {
        return true;
    }

    /**
     * Returns the sign of the duration: +1 is forwards
     * and -1 is backwards in time.
     * This value does not necessarily reflect the
     * true direction of the duration if the duration
     * is not normalized or not normalizable.
     */
    public final int getSign() {
        return _sign;
    }

    /**
     * Gets the year component.
     */
    public final int getYear() {
        return _CY;
    }

    /**
     * Gets the month-of-year component.
     */
    public final int getMonth() {
        return _M;
    }

    /**
     * Gets the day-of-month component.
     */
    public final int getDay() {
        return _D;
    }

    /**
     * Gets the hour-of-day component.
     */
    public final int getHour() {
        return _h;
    }

    /**
     * Gets the minute-of-hour component.
     */
    public final int getMinute() {
        return _m;
    }

    /**
     * Gets the second-of-minute component.
     */
    public final int getSecond() {
        return _s;
    }


    /**
     * Gets the fraction-of-second. Range from 0 (inclusive) to 1 (exclusive).
     */
    public BigDecimal getFraction() {
        return _fs;
    }

    /**
     * Returns true if all of the individual components
     * of the duration are nonnegative.
     */
    public boolean isValid() {
        return GDurationBuilder.isValidDuration(this);
    }


    /**
     * Normalize a duration value. This ensures that months,
     * hours, minutes, seconds, and fractions are positive and
     * within the ranges 0..11, 0..23, 0..59, etc. Negative
     * durations are indicated by a negative sign rather
     * than negative components.
     * <p>
     * Most duration specifications can be normalized to
     * valid durations with all positive components, but
     * not all of them can.
     * <p>
     * The only situations which cannot be normalized are
     * where the year/month and the day/hour/minute/second
     * offsets are of opposite sign. Days cannot be carried
     * into months since the length of a Gregorian month is
     * variable depending on when the duration is applied.
     * In these cases, this method normalizes the components
     * so that "day" is the only negative component.
     */
    public void normalize() {
        _normalizeImpl(true);
    }

    /**
     * fQuotient(a, b) = the greatest integer less than or equal to a/b
     */
    private static long _fQuotient(long a, int b) {
        if ((a < 0) == (b < 0)) {
            return a / b;
        }

        return -((b - a - 1) / b);
    }

    /**
     * modulo(a, b) = a - fQuotient(a,b)*b
     */
    private static int _mod(long a, int b, long quotient) {
        return Math.toIntExact(a - quotient * b);
    }


    /**
     * Private implemenation of normalize. The flag is
     * to facilitate this method calling itself without
     * danger of infinite recursion.
     */
    private void _normalizeImpl(boolean adjustSign) {
        long temp;

        // months to years
        if (_M < 0 || _M > 11) {
            temp = _M;
            long ycarry = _fQuotient(temp, 12);
            _M = _mod(temp, 12, ycarry);
            _CY = Math.addExact(_CY, Math.toIntExact(ycarry));
        }

        long carry = 0;

        // fractions to seconds
        if (_fs != null && (_fs.signum() < 0 || _fs.compareTo(GDate._one) >= 0)) {
            BigDecimal bdcarry = _fs.setScale(0, RoundingMode.FLOOR);
            _fs = _fs.subtract(bdcarry);
            carry = MathUtil.toInt(bdcarry);
        }

        if (carry != 0 || _s < 0 || _s > 59 || _m < 0 || _m > 50 || _h < 0 || _h > 23) {
            // seconds
            temp = _s + carry;
            carry = _fQuotient(temp, 60);
            _s = _mod(temp, 60, carry);

            // minutes
            temp = _m + carry;
            carry = _fQuotient(temp, 60);
            _m = _mod(temp, 60, carry);

            // hours
            temp = _h + carry;
            carry = _fQuotient(temp, 24);
            _h = _mod(temp, 24, carry);
            _D = Math.addExact(_D, Math.toIntExact(carry));
        }

        if (_CY == 0 && _M == 0 && _D == 0 && _h == 0 && _m == 0 && _s == 0 && (_fs == null || _fs.signum() == 0)) {
            _sign = 1;
        }

        if (adjustSign && (_D < 0 || _CY < 0)) {
            int sign = (_D <= 0 && (_CY < 0 || _CY == 0 && _M == 0)) ? -_sign : _getTotalSignSlowly();
            if (sign == 2) {
                sign = (_CY < 0) ? -_sign : _sign;
            }
            if (sign == 0) {
                sign = 1;
            }
            if (sign != _sign) {
                _sign = sign;
                _CY = -_CY;
                _M = -_M;
                _D = -_D;
                _h = -_h;
                _m = -_m;
                _s = -_s;
                if (_fs != null) {
                    _fs = _fs.negate();
                }
            }
            _normalizeImpl(false);
        }
    }


    /* package */
    static boolean isValidDuration(GDurationSpecification spec) {
        if (!(spec.getSign() == 1 || spec.getSign() == -1)) {
            return false;
        }

        return (spec.getYear() >= 0 && spec.getMonth() >= 0 && spec.getDay() >= 0 &&
                spec.getHour() >= 0 && spec.getMinute() >= 0 && spec.getSecond() >= 0 &&
                spec.getFraction().signum() >= 0);
    }

    /**
     * Comparison to another GDuration.
     * <ul>
     * <li>Returns -1 if this &lt; duration. (less-than)
     * <li>Returns 0 if this == duration. (equal)
     * <li>Returns 1 if this &gt; duration. (greater-than)
     * <li>Returns 2 if this &lt;&gt; duration. (incomparable)
     * </ul>
     * Two instances are incomparable if they have different amounts
     * of information.
     */
    public final int compareToGDuration(GDurationSpecification duration) {
        return GDurationBuilder.compareDurations(this, duration);
    }

    /**
     * The natural string representation of the duration.
     * <p>
     * Any components that are zero are omitted. Note that if the duration
     * is invalid, i.e., it has negative components, those negative
     * components are serialized out here. To check for validity, use
     * the isValid() method; and to normalize most durations to a valid
     * form use the normalize() method.
     */
    public String toString() {
        return GDurationBuilder.formatDuration(this);
    }

    /* package */
    static int compareDurations(GDurationSpecification d1, GDurationSpecification d2) {
        // first do an all-fields check
        if (d1.getFraction().signum() == 0 && d2.getFraction().signum() == 0) {
            int s1 = d1.getSign();
            int s2 = d2.getSign();
            long month1 = s1 * ((long) d1.getYear() * 12 + d1.getMonth());
            long month2 = s2 * ((long) d2.getYear() * 12 + d2.getMonth());
            long sec1 = s1 * ((((long) d1.getDay() * 24 + d1.getHour()) * 60 + d1.getMinute()) * 60 + d1.getSecond());
            long sec2 = s2 * ((((long) d2.getDay() * 24 + d2.getHour()) * 60 + d2.getMinute()) * 60 + d2.getSecond());
            if (month1 == month2) {
                if (sec1 == sec2) {
                    return 0;
                }
                return sec1 < sec2 ? -1 : 1;
            }
            if (month1 < month2 && sec1 - sec2 < 28 * 24 * 60 * 60) {
                return -1;
            }
            if (month1 > month2 && sec2 - sec1 < 28 * 24 * 60 * 60) {
                return 1;
            }
        }

        // the answer isn't obvious, so then do a total-sign check
        GDurationBuilder diff = new GDurationBuilder(d1);
        diff.subtractGDuration(d2);
        return diff._getTotalSignSlowly();
    }

    /**
     * Per schema spec, comparison of durations is simply done
     * by calculating adding the duration to these four dates and
     * comparing the results. If the results are ambiguous, the
     * answer is "incomparable".
     */
    private static final GDate[] _compDate = new GDate[]
        {
            new GDate(1696, 9, 1, 0, 0, 0, null, 0, 0, 0),
            new GDate(1697, 2, 1, 0, 0, 0, null, 0, 0, 0),
            new GDate(1903, 3, 1, 0, 0, 0, null, 0, 0, 0),
            new GDate(1903, 7, 1, 0, 0, 0, null, 0, 0, 0)
        };


    /**
     * This returns the total sign of the duration, +1
     * if the duration moves forward in time, -1 if the
     * duration moves backwards in time, 0 if the duration
     * is zero-length, and 2 if the duration may be positive
     * or negative depending on the date.
     * <p>
     * (For example, one month minus 30 days is indeterminate).
     */
    private int _getTotalSignSlowly() {
        int pos = 0;
        int neg = 0;
        int zer = 0;

        GDateBuilder enddate = new GDateBuilder();
        for (GDate gDate : _compDate) {
            enddate.setGDate(gDate);
            enddate.addGDuration(this);
            switch (enddate.compareToGDate(gDate)) {
                case -1:
                    neg++;
                    break;
                case 0:
                    zer++;
                    break;
                case 1:
                    pos++;
                    break;
            }
        }

        if (pos == _compDate.length) {
            return +1;
        }
        if (neg == _compDate.length) {
            return -1;
        }
        if (zer == _compDate.length) {
            return 0;
        }
        return 2;
    }

    /* package */
    static String formatDuration(GDurationSpecification duration) {
        // Sign+P:      (-)?P
        // Year:        (?:(\d+)Y)?
        // Month:       (?:(\d+)M)?
        // Day:         (?:(\d+)D)?
        // Time:        (?:(T)
        // Hours:          (?:(\d+)H)?
        // Minutes:        (?:(\d+)M)?
        // Seconds:        (?:(\d+(?:\.\d*)?|(?:.\d+)S)?

        StringBuilder message = new StringBuilder(30);

        if (duration.getSign() < 0) {
            message.append('-');
        }

        message.append('P');

        if (duration.getYear() != 0) {
            message.append(duration.getYear());
            message.append('Y');
        }

        if (duration.getMonth() != 0) {
            message.append(duration.getMonth());
            message.append('M');
        }

        if (duration.getDay() != 0) {
            message.append(duration.getDay());
            message.append('D');
        }

        if (duration.getHour() != 0 || duration.getMinute() != 0 || duration.getSecond() != 0 ||
            (duration.getFraction().signum() != 0)) {
            message.append('T');
        }

        if (duration.getHour() != 0) {
            message.append(duration.getHour());
            message.append('H');
        }

        if (duration.getMinute() != 0) {
            message.append(duration.getMinute());
            message.append('M');
        }

        if (duration.getFraction().signum() != 0) {
            BigDecimal s = duration.getFraction();
            if (duration.getSecond() != 0) {
                s = s.add(BigDecimal.valueOf(duration.getSecond()));
            }
            // todo when upgrade to 1.5  message.append(s.stripTrailingZeros().toPlainString());
            message.append(stripTrailingZeros(toPlainString(s)));
            message.append('S');
        } else if (duration.getSecond() != 0) {
            message.append(duration.getSecond());
            message.append('S');
        } else if (message.length() <= 2)
        // Specify zero seconds if everything was 0
        {
            message.append("T0S");
        }

        return message.toString();
    }

    public static String toPlainString(BigDecimal bd) {
        BigInteger intVal = bd.unscaledValue();
        int scale = bd.scale();
        String intValStr = intVal.toString();
        if (scale == 0) {
            return intValStr;
        }

        boolean isNegative = (intValStr.charAt(0) == '-');

        int point = intValStr.length() - scale - (isNegative ? 1 : 0);

        StringBuilder sb = new StringBuilder(intValStr.length() + 2 + (point <= 0 ? (-point + 1) : 0));
        if (point <= 0) {
            // prepend zeros and a decimal point.
            if (isNegative) {
                sb.append('-');
            }
            sb.append('0').append('.');
            while (point < 0) {
                sb.append('0');
                point++;
            }
            sb.append(intValStr.substring(isNegative ? 1 : 0));
        } else if (point < intValStr.length()) {
            // No zeros needed
            sb.append(intValStr);
            sb.insert(point + (isNegative ? 1 : 0), '.');
        } else {
            // append zeros if not 0
            sb.append(intValStr);
            if (!intVal.equals(BigInteger.ZERO)) {
                for (int i = intValStr.length(); i < point; i++) {
                    sb.append('0');
                }
            }
        }
        return sb.toString();
    }

    public static String stripTrailingZeros(String s) {
        boolean seenDot = false;
        int i = s.length() - 1;
        int zeroIndex = i;

        while (i >= 0) {
            if (s.charAt(i) != '0') {
                break;
            }
            i--;
            zeroIndex--;
        }
        while (i >= 0) {
            if (s.charAt(i) == 'E') {
                return s;
            }
            if (s.charAt(i) == '.') {
                seenDot = true;
                break;
            }
            i--;
        }

        return seenDot ? s.substring(0, zeroIndex + 1) : s;
    }
}
