/*
 * 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
 *
 *   https://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.
 *
 *   SPDX-License-Identifier: Apache-2.0
 */

package org.apache.jena.datatypes.xsd;

import java.util.*;

import org.apache.jena.datatypes.xsd.impl.XSDAbstractDateTimeType ;


/**
 * Represent an XSD date/time value. Rather than have a separate type for each
 * legal date/time value combination this is a combination type than does runtime
 * checks whether a given field is legal in the current circumstances.
 */
public class XSDDateTime extends AbstractDateTime {
    /** Mask to indicate whether year is present */
    public static final short YEAR_MASK = 0x1;

    /** Mask to indicate whether month is present */
    public static final short MONTH_MASK = 0x2;

    /** Mask to indicate whether day is present */
    public static final short DAY_MASK = 0x4;

    /** Mask to indicate whether time is present */
    public static final short TIME_MASK = 0x8;

    /** Mask to indicate all date/time are present */
    public static final short FULL_MASK = 0xf;

    // Unused /** table mapping xs type name to mask of legal values */
    //public static final HashMap maskMap = new HashMap();

    /** Set of legal fields for the particular date/time instance */
    protected short mask;

    /**
     * Constructor - should only be used by the internals but public scope because
     * the internals spread across multiple packages.
     *
     * @param value the date/time value returned by the parsing
     * @param mask bitmask defining which components are valid in this instance
     * (e.g. dates don't have valid time fields).
     */
    public XSDDateTime(Object value, int mask) {
        super(value);
        this.mask = (short)mask;
    }

    /**
     * Constructor - create a full DateTime object from a java calendar instance.
     *
     * @param date java calendar instance
     */
    public XSDDateTime(Calendar date) {
        super(convertCalendar(date));
        this.mask = FULL_MASK;
    }

    /**
     * Return the most specific xsd type which can represent
     * this date/time
     */
    public XSDDatatype getNarrowedDatatype() {
        switch (mask) {
        case TIME_MASK:
            return XSDDatatype.XSDtime;
        case MONTH_MASK:
            return XSDDatatype.XSDgMonth;
        case DAY_MASK:
            return XSDDatatype.XSDgDay;
        case YEAR_MASK:
            return XSDDatatype.XSDgYear;
        case MONTH_MASK | DAY_MASK:
            return XSDDatatype.XSDgMonthDay;
        case MONTH_MASK | YEAR_MASK:
            return XSDDatatype.XSDgYearMonth;
        case MONTH_MASK | YEAR_MASK | DAY_MASK:
            return XSDDatatype.XSDdate;
        default:
            return XSDDatatype.XSDdateTime;
        }
    }

    /**
     * Set the mask for this date/time to be that appropriate
     * for the given XSD subtype. If the type is a subtype of XSDdateTime the
     * mask will be narrowed appropriately, other types will be silently ignored.
     */
    public void narrowType(XSDDatatype dt) {
        if (dt.equals(XSDDatatype.XSDtime)) {
            mask = TIME_MASK;
        } else if (dt.equals(XSDDatatype.XSDgMonth)) {
            mask = MONTH_MASK;
        } else if (dt.equals(XSDDatatype.XSDgDay)) {
            mask = DAY_MASK;
        } else if (dt.equals(XSDDatatype.XSDgYear)) {
            mask = YEAR_MASK;
        } else if (dt.equals(XSDDatatype.XSDgMonthDay)) {
            mask = MONTH_MASK | DAY_MASK;
        } else if (dt.equals(XSDDatatype.XSDgYearMonth)) {
            mask = YEAR_MASK | MONTH_MASK;
        } else if (dt.equals(XSDDatatype.XSDdate)) {
            mask = MONTH_MASK | YEAR_MASK | DAY_MASK;
        }
    }

    /**
     * Convert a java calendar object to a new int[] in the format used by XSDAbstractDateTime
     */
    private static int[] convertCalendar(Calendar date) {
        int[] data = new int[TOTAL_SIZE];

        Calendar cal = (Calendar)date.clone();
        // By observation (Sun Java 6), this is necessary (to force internal calculations presumably) ...
        cal.get(Calendar.ZONE_OFFSET) ;
        // ... then we can rebase the calendar
        cal.setTimeZone(TimeZone.getTimeZone("GMT"));

        data[AbstractDateTime.CY] = cal.get(Calendar.YEAR);
        data[AbstractDateTime.M] = cal.get(Calendar.MONTH) + 1;
        data[AbstractDateTime.D] = cal.get(Calendar.DAY_OF_MONTH);
        data[AbstractDateTime.h] = cal.get(Calendar.HOUR_OF_DAY);
        data[AbstractDateTime.m] = cal.get(Calendar.MINUTE);
        data[AbstractDateTime.s] = cal.get(Calendar.SECOND);
        data[AbstractDateTime.utc] = 'Z';

        int ms = cal.get(Calendar.MILLISECOND);
        // Store value in a canonical form - no scale for trailing zeros.
        // This must align with the string parsing code.
        if ( ms == 0 ) {
            data[AbstractDateTime.ms] = 0 ;
            data[AbstractDateTime.msscale] = 0 ;
        } else if ( ms % 100 == 0 ) {
            data[AbstractDateTime.ms] = ms/100;
            data[AbstractDateTime.msscale] = 1 ;
        } else if ( ms % 10 == 0 ) {
            data[AbstractDateTime.ms] = ms/10;
            data[AbstractDateTime.msscale] = 2 ;
        } else {
            data[AbstractDateTime.ms] = ms;
            data[AbstractDateTime.msscale] = 3;
        }
        return data;
    }

    /**
     * Return the date time as a java Calendar object.
     * If the timezone has been specified then the object is normalized to GMT.
     * If the zone has not been specified then we use the default timezone.
     *
     * @throws IllegalDateTimeFieldException if this is not a full date + time
     */
    public Calendar asCalendar () throws IllegalDateTimeFieldException {
        TimeZone tz = data[utc] == 'Z' ? TimeZone.getTimeZone("GMT") : TimeZone.getDefault();
        Calendar calendar = new GregorianCalendar(tz);
        calendar.set(data[CY], data[M] - 1, data[D], data[h], data[m], data[s]);
        calendar.set(Calendar.MILLISECOND, (int)Math.round(1000.0 * fractionalSeconds));
        // was this to work around problems with some Linux JDKs
        // calendar.set(Calendar.MILLISECOND, 0);
        return calendar;
    }

    /**
     * Return the number of years in the dateTime.
     * @throws IllegalDateTimeFieldException if there is no legal year component
     */
    public int getYears() throws IllegalDateTimeFieldException {
        if ((mask & YEAR_MASK) == 0) throw new IllegalDateTimeFieldException("Year not available");
        return data[CY];
    }

    /**
     * Return the month in the dateTime, this is in ISO8601 format so january = 1
     * @throws IllegalDateTimeFieldException if there is no legal month component
     */
    public int getMonths() throws IllegalDateTimeFieldException {
        if ((mask & MONTH_MASK) == 0) throw new IllegalDateTimeFieldException("Month not available");
        return data[M];
    }

    /**
     * Return the number of years in the dateTime
     * @throws IllegalDateTimeFieldException if there is no legal day component
     */
    public int getDays() throws IllegalDateTimeFieldException {
        if ((mask & DAY_MASK) == 0) throw new IllegalDateTimeFieldException("Day not available");
        return data[D];
    }

    /**
     * Return the number of hours in the dateTime
     * @throws IllegalDateTimeFieldException if there is no legal time component
     */
    public int getHours() throws IllegalDateTimeFieldException {
        if ((mask & TIME_MASK) == 0) throw new IllegalDateTimeFieldException("Time not available");
        return data[h];
    }

    /**
     * Return the number of minutes in the dateTime
     * @throws IllegalDateTimeFieldException if there is no legal time component
     */
    public int getMinutes() throws IllegalDateTimeFieldException {
        if ((mask & TIME_MASK) == 0) throw new IllegalDateTimeFieldException("Time not available");
        return data[m];
    }

    /**
     * Return the number of full seconds in the dateTime
     * @throws IllegalDateTimeFieldException if there is no legal time component
     */
    public int getFullSeconds() throws IllegalDateTimeFieldException {
        if ((mask & TIME_MASK) == 0) throw new IllegalDateTimeFieldException("Time not available");
        return data[s];
    }

    /**
     * Return the number of seconds in the dateTime, including fractional part
     * @throws IllegalDateTimeFieldException if there is no legal time component
     */
    public double getSeconds() throws IllegalDateTimeFieldException {
        if ((mask & TIME_MASK) == 0) throw new IllegalDateTimeFieldException("Time not available");
        return data[s] + fractionalSeconds;
    }

    /**
     * Return the time component of the dateTime - i.e. just the hours/mins/seconds,
     * and returns the values in seconds.
     * @throws IllegalDateTimeFieldException if there is no legal time component
     */
    public double getTimePart() throws IllegalDateTimeFieldException {
        if ((mask & TIME_MASK) == 0) throw new IllegalDateTimeFieldException("Time not available");
        return ((data[h]) * 60l + data[m]) * 60l + getSeconds();
    }

    /**
     * Return legal serialized form.
     */
    @Override
    public String toString() {
        StringBuilder buff = new StringBuilder();
        if ((mask & YEAR_MASK) != 0) {
            int cy = data[CY] ;
            int absCY = Math.abs(cy) ;
            if ( cy    < 0   )  buff.append('-') ;
            // XSD dateTime requires at least 4 digits for the year field
            if ( absCY < 10  )  buff.append('0') ;
            if ( absCY < 100 )  buff.append('0') ;
            if ( absCY < 1000 ) buff.append('0') ;
            buff.append(absCY);
        } else {
            buff.append("-");
        }
        if ((mask & (MONTH_MASK | DAY_MASK)) != 0) {
            buff.append("-");
            if ((mask & MONTH_MASK) != 0) {
                if (data[M] <= 9) buff.append('0');
                buff.append(data[M]);
            } else {
                buff.append("-");
            }
            if ((mask & DAY_MASK) != 0) {
                if (mask != DAY_MASK) buff.append('-');
                if (data[D] <= 9) buff.append('0');
                buff.append(data[D]);
            }
        }
        if ((mask & TIME_MASK) != 0 ) {
            buff.append('T');
            buff.append(timeLexicalForm());
        }

        if ( data[utc] != 0 )
            buff.append('Z');

        return buff.toString();
    }

    /**
     * Return the lexical form of the time component.
     */
    public String timeLexicalForm() {
        StringBuilder buff = new StringBuilder();
        if(data[h]<10) buff.append("0");
        buff.append(data[h]);

        buff.append(":");
        if(data[m]<10) buff.append("0");
        buff.append(data[m]);

        buff.append(":");
        if(data[s]<10) buff.append("0");
        buff.append(data[s]);

        if (data[ms] != 0) {
            buff.append(".");
            XSDAbstractDateTimeType.appendFractionalTime(buff, data[ms], data[msscale]);
        }
        return buff.toString();
    }

}
