/*
 *  Copyright 2017-2018 TWO SIGMA OPEN SOURCE, LLC
 *
 *  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.twosigma.flint.timeseries.time

import java.util.concurrent.TimeUnit

import org.joda.time.format.{ DateTimeFormat, DateTimeFormatter, ISODateTimeFormat }
import org.joda.time.{ DateTime, DateTimeZone }

import scala.concurrent.duration.TimeUnit
import scala.util.Try

object TimeFormat {

  /**
   * Parses a date time from the given text.
   * <p>
   * It will try all possible known formatters with the given time zone (default UTC) as the default time Zone and
   * ISO chronology to parse, respectively. It will return the first parsable date time.
   * <p>
   * If the text contains a time zone string then that will be taken into account. However, the underneath MILLISECONDS
   * from 1970-01-01T00:00:00Z won't be changed.
   *
   * @param text the text to parse, not null
   * @return a parsed [[org.joda.time.DateTime]].
   */
  @throws(classOf[IllegalArgumentException])
  protected[flint] def parseDateTime(text: String, timeZone: DateTimeZone = DateTimeZone.UTC): DateTime = {
    val parsedOption = formatters.view.flatMap { formatter =>
      Try(formatter.withZone(timeZone).parseDateTime(text.trim)).toOption
    }.headOption

    parsedOption.getOrElse(
      throw new IllegalArgumentException(s"Can't parse the given text $text as date time.")
    )
  }

  /**
   * Parses a date-time from the given text and returning it in terms of `timeUnit` since the epoch,
   * 1970-01-01T00:00:00Z.
   * <p>
   * It will try all possible known formatters with the given time zone (default: UTC) and
   * ISO chronology to parse, respectively. It will return the first parsable one.
   * <p>
   * If the text contains a time zone string then that will be taken into account. However, the underneath MILLISECONDS
   * from 1970-01-01T00:00:00Z won't be changed.
   *
   * @param text the text to parse, not null
   * @return an parsed NANOSECONDS since the epoch 1970-01-01T00:00:00Z.
   */
  protected[flint] def parse(
    text: String,
    timeZone: DateTimeZone = DateTimeZone.UTC,
    timeUnit: TimeUnit = TimeUnit.NANOSECONDS
  ): Long =
    timeUnit.convert(parseDateTime(text, timeZone).getMillis, TimeUnit.MILLISECONDS)

  /**
   * Parses a date-time from the given text and returning the number of NANOSECONDS since the epoch,
   * 1970-01-01T00:00:00Z.
   *
   * @see [[parse(String, DateTimeZone, TimeUnit)]] for parsing rules.
   *
   * @param text the text to parse, not null
   * @return an parsed NANOSECONDS since the epoch 1970-01-01T00:00:00Z.
   */
  protected[flint] def parseNano(text: String, timeZone: DateTimeZone = DateTimeZone.UTC): Long =
    parse(text, timeZone, timeUnit = TimeUnit.NANOSECONDS)

  private val formatters: List[DateTimeFormatter] = List(
    // Double `HH` formatter
    DateTimeFormat.forPattern("yyyyMMdd HH:mm:ss.SSS Z"),
    DateTimeFormat.forPattern("yyyyMMdd HH:mm:ss Z"),
    DateTimeFormat.forPattern("yyyyMMdd HH:mm Z"),
    DateTimeFormat.forPattern("yyyyMMdd HH:mm:ss.SSS"),
    DateTimeFormat.forPattern("yyyyMMdd HH:mm:ss"),
    DateTimeFormat.forPattern("yyyyMMdd HH:mm"),
    DateTimeFormat.forPattern("yyyyMMdd"),
    DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS Z"),
    DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss Z"),
    DateTimeFormat.forPattern("yyyy-MM-dd HH:mm Z"),
    DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS"),
    DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"),
    DateTimeFormat.forPattern("yyyy-MM-dd HH:mm"),
    DateTimeFormat.forPattern("yyyy-MM-dd"),
    // Single `H` formatter
    DateTimeFormat.forPattern("yyyyMMdd H:mm:ss.SSS"),
    DateTimeFormat.forPattern("yyyyMMdd H:mm:ss.SSS Z"),
    DateTimeFormat.forPattern("yyyy-MM-dd H:mm:ss.SSS"),
    DateTimeFormat.forPattern("yyyy-MM-dd H:mm:ss.SSS Z"),

    // ISO DateTime
    ISODateTimeFormat.dateTimeParser()
  )
}