/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.telemetry.glean.utils

import java.lang.StringBuilder
import java.text.SimpleDateFormat
import mozilla.telemetry.glean.private.TimeUnit
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Suppress("TopLevelPropertyNaming")
internal val DATE_FORMAT_PATTERNS = mapOf(
    TimeUnit.Nanosecond to "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
    TimeUnit.Microsecond to "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
    TimeUnit.Millisecond to "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
    TimeUnit.Second to "yyyy-MM-dd'T'HH:mm:ssZ",
    TimeUnit.Minute to "yyyy-MM-dd'T'HH:mmZ",
    TimeUnit.Hour to "yyyy-MM-dd'T'HHZ",
    TimeUnit.Day to "yyyy-MM-ddZ"
)

// A mapping from the length of the date string to the format that would parse
// it.
// For example: "2018-12-19T12:36:00-0600" is 21 characters long and is parsed
// by the format "yyyy-MM-dd'T'HH:mmZ".
@Suppress("TopLevelPropertyNaming", "MagicNumber")
internal val DATE_FORMAT_PATTERN_BY_LENGTH = mapOf(
    28 to "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
    24 to "yyyy-MM-dd'T'HH:mm:ssZ",
    21 to "yyyy-MM-dd'T'HH:mmZ",
    18 to "yyyy-MM-dd'T'HHZ",
    15 to "yyyy-MM-ddZ"
)

internal val DATE_FORMAT_PATTERN_VALUES = DATE_FORMAT_PATTERNS.values.toSet()

/**
 * Generate an ISO8601 compliant time string for the given time.
 *
 * @param date the [Date] object to convert to string
 * @param truncateTo The TimeUnit to truncate the value to
 * @return a string containing the date, time and timezone offset
 */
internal fun getISOTimeString(
    date: Date = Date(),
    truncateTo: TimeUnit = TimeUnit.Minute
): String {
    val cal = Calendar.getInstance()
    cal.setTime(date)
    return getISOTimeString(cal, truncateTo)
}

/**
 * Generate an ISO8601 compliant time string for the given time.
 *
 * @param calendar the [Calendar] object to convert to string
 * @param truncateTo The TimeUnit to truncate the value to
 * @return a string containing the date, time and timezone offset
 */
internal fun getISOTimeString(
    calendar: Calendar,
    truncateTo: TimeUnit = TimeUnit.Minute
): String {
    val dateFormat = SimpleDateFormat(DATE_FORMAT_PATTERNS[truncateTo], Locale.US)
    dateFormat.setTimeZone(calendar.getTimeZone())
    val timeString = StringBuilder(dateFormat.format(calendar.getTime()))

    // Due to limitations of SDK version 21, there isn't a way to properly output the time
    // offset with a ':' character:
    // 2018-12-19T12:36:00-0600    -- This is what we get
    // 2018-12-19T12:36:00-06:00   -- This is what GCP will expect
    //
    // In order to satisfy time offset requirements of GCP, we manually insert the ":"
    timeString.insert(timeString.length - 2, ":")

    return timeString.toString()
}

/**
 * Parses the subset of ISO8601 datetime strings generated by [getISOTimeString].
 *
 * Always returns the result in the device's current timezone offset, regardless of the
 * timezone offset specified in the string.
 *
 * @param date a [String] representing an ISO date string generated by [getISOTimeString]
 * @return a [Date] object representation of the provided string
 */
@Suppress("MagicNumber", "ReturnCount")
internal fun parseISOTimeString(date: String): Date? {
    // Due to limitations of SDK version 21, there isn't a way to properly parse the time
    // offset with a ':' character:
    // 2018-12-19T12:36:00-06:00  -- This is what we store
    // 2018-12-19T12:36:00-0600   -- This is what SimpleDateFormat will expect

    val correctedDate = if (date.get(date.length - 3) == ':') {
        date.substring(0, date.length - 3) + date.substring(date.length - 2)
    } else {
        date
    }

    DATE_FORMAT_PATTERN_BY_LENGTH.get(correctedDate.length)?.let {
        val dateFormat = SimpleDateFormat(it, Locale.US)
        try {
            return dateFormat.parse(correctedDate)
        } catch (e: java.text.ParseException) {
            // fall through
        }
    }

    // Fall back to trying all formats if the obvious choice by length doesn't
    // work
    for (format in DATE_FORMAT_PATTERN_VALUES) {
        val dateFormat = SimpleDateFormat(format, Locale.US)
        try {
            return dateFormat.parse(correctedDate)
        } catch (e: java.text.ParseException) {
            continue
        }
    }

    return null
}
