// Copyright 2025 International Digital Economy Academy
//
// 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.

///|
/// A time without a time zone
struct PlainTime {
  hour : Int
  minute : Int
  second : Int
  nanosecond : Int
} derive(Eq, Compare)

///|
/// Creates a PlainTime from the hour, minute, second and nanosecond.
pub fn PlainTime::of(
  hour : Int,
  minute : Int,
  second : Int,
  nanosecond : Int,
) -> PlainTime raise Error {
  if validate_time(hour, minute, second, nanosecond) {
    { hour, minute, second, nanosecond }
  } else {
    fail(invalid_time_err)
  }
}

///|
/// Creates a PlainTime from a string, like '10:20:30.45678'.
pub fn PlainTime::from_string(str : String) -> PlainTime raise Error {
  let (hour, minute, second, nanosecond) = lexscan str with longest {
    (
      re"^" +
      (re"[0-9]{2}" as hour_str) +
      re":" +
      (re"[0-9]{2}$" as minute_str),
      after=_,
    ) => (@string.parse_int(hour_str), @string.parse_int(minute_str), 0, 0)
    (
      re"^" +
      (re"[0-9]{2}" as hour_str) +
      re":" +
      (re"[0-9]{2}" as minute_str) +
      re":" +
      (re"[0-9]{2}$" as second_str),
      after=_,
    ) =>
      (
        @string.parse_int(hour_str),
        @string.parse_int(minute_str),
        @string.parse_int(second_str),
        0,
      )
    (
      re"^" +
      (re"[0-9]{2}" as hour_str) +
      re":" +
      (re"[0-9]{2}" as minute_str) +
      re":" +
      (re"[0-9]{2}" as second_str) +
      re"[.]" +
      (re"[0-9]{1,9}$" as nanosecond_str),
      after=_,
    ) =>
      (
        @string.parse_int(hour_str),
        @string.parse_int(minute_str),
        @string.parse_int(second_str),
        @string.parse_int(add_suffix_zero(nanosecond_str.to_owned(), 9)),
      )
    _ => fail(invalid_time_err)
  }
  PlainTime::of(hour, minute, second, nanosecond)
}

///|
/// Returns a string representing the time.
pub fn PlainTime::to_string(self : PlainTime) -> String {
  let buf = StringBuilder(size_hint=8)
  buf.write_string(add_prefix_zero(self.hour.to_string(), 2))
  buf.write_char(':')
  buf.write_string(add_prefix_zero(self.minute.to_string(), 2))
  buf.write_char(':')
  buf.write_string(add_prefix_zero(self.second.to_string(), 2))
  if self.nanosecond > 0 {
    buf.write_string(format_nanos(self.nanosecond.to_int64()).to_string())
  }
  buf.to_string()
}

///|
pub impl Show for PlainTime with fn output(self : PlainTime, logger : &Logger) -> Unit {
  logger.write_string(self.to_string())
}

///|
pub impl ToJson for PlainTime with fn to_json(self) -> Json {
  Json::string(self.to_string())
}

///|
pub impl @json.FromJson for PlainTime with fn from_json(json, path) {
  guard json is String(s) else {
    json_decode_error(path, "PlainTime::from_json: expected string")
  }
  PlainTime::from_string(s) catch {
    error =>
      json_decode_error(path, "PlainTime::from_json: parsing failure \{error}")
  }
}

///|
/// Returns the hour of this time.
pub fn PlainTime::hour(self : PlainTime) -> Int {
  self.hour
}

///|
/// Returns the minute of this time.
pub fn PlainTime::minute(self : PlainTime) -> Int {
  self.minute
}

///|
/// Returns the second of this time.
pub fn PlainTime::second(self : PlainTime) -> Int {
  self.second
}

///|
/// Returns the nanosecond of this time.
pub fn PlainTime::nanosecond(self : PlainTime) -> Int {
  self.nanosecond
}

///|
/// Returns the total seconds of this time.
pub fn PlainTime::second_of_day(self : PlainTime) -> Int {
  let sec = self.hour * seconds_per_hour.to_int() +
    self.minute * seconds_per_minute.to_int() +
    self.second
  sec
}

///|
/// Returns the total nanoseconds of this time.
pub fn PlainTime::nanosecond_of_day(self : PlainTime) -> Int64 {
  let sec = self.second_of_day()
  sec.to_int64() * nanoseconds_per_second + self.nanosecond.to_int64()
}

///|
/// Creates a PlainTime from the total seconds of the day.
pub fn PlainTime::from_second_of_day(second : Int) -> PlainTime raise Error {
  if second < 0 || second > seconds_per_day.to_int() {
    fail(invalid_time_err)
  }
  let mut sec = second.to_int64()
  let hour = sec / seconds_per_hour
  sec -= hour * seconds_per_hour
  let minute = sec / seconds_per_minute
  sec -= minute * seconds_per_minute
  PlainTime::of(hour.to_int(), minute.to_int(), sec.to_int(), 0)
}

///|
/// Creates a PlainTime from the total nanoseconds of the day.
pub fn PlainTime::from_nanosecond_of_day(
  nanosecond : Int64,
) -> PlainTime raise Error {
  if nanosecond < 0L || nanosecond > nanoseconds_per_day {
    fail(invalid_time_err)
  }
  let mut nanos = nanosecond
  let hour = nanos / nanoseconds_per_hour
  nanos -= hour * nanoseconds_per_hour
  let minute = nanos / nanoseconds_per_minute
  nanos -= minute * nanoseconds_per_minute
  let second = nanos / nanoseconds_per_second
  nanos -= second * nanoseconds_per_second
  PlainTime::of(hour.to_int(), minute.to_int(), second.to_int(), nanos.to_int())
}

///|
/// Adds specified hours to this time, and returns a new time.
pub fn PlainTime::add_hours(
  self : PlainTime,
  hours : Int64,
) -> PlainTime raise Error {
  if hours == 0L {
    return self
  }
  let new_hour = (hours % hours_per_day + self.hour.to_int64()) % hours_per_day
  PlainTime::of(new_hour.to_int(), self.minute, self.second, self.nanosecond)
}

///|
/// Adds specified minutes to this time, and returns a new time.
pub fn PlainTime::add_minutes(
  self : PlainTime,
  minutes : Int64,
) -> PlainTime raise Error {
  if minutes == 0L {
    return self
  }
  let mins = self.hour.to_int64() * minutes_per_hour + self.minute.to_int64()
  let new_mins = (minutes % minutes_per_day + mins + minutes_per_day) %
    minutes_per_day
  if mins == new_mins {
    return self
  }
  let new_hour = new_mins / minutes_per_hour
  let new_minute = new_mins % minutes_per_hour
  PlainTime::of(
    new_hour.to_int(),
    new_minute.to_int(),
    self.second,
    self.nanosecond,
  )
}

///|
/// Adds specified seconds to this time, and returns a new time.
pub fn PlainTime::add_seconds(
  self : PlainTime,
  seconds : Int64,
) -> PlainTime raise Error {
  if seconds == 0L {
    return self
  }
  let secs_of_day = self.second_of_day().to_int64()
  let new_secs_of_day = (
      seconds % seconds_per_day + secs_of_day + seconds_per_day
    ) %
    seconds_per_day
  if secs_of_day == new_secs_of_day {
    return self
  }
  let new_hour = new_secs_of_day / seconds_per_hour
  let new_minute = new_secs_of_day / seconds_per_minute % minutes_per_hour
  let new_second = new_secs_of_day % seconds_per_minute
  PlainTime::of(
    new_hour.to_int(),
    new_minute.to_int(),
    new_second.to_int(),
    self.nanosecond,
  )
}

///|
/// Adds specified nanoseconds to this time, and returns a new time.
pub fn PlainTime::add_nanoseconds(
  self : PlainTime,
  nanoseconds : Int64,
) -> PlainTime raise Error {
  if nanoseconds == 0L {
    return self
  }
  let nanos = self.nanosecond_of_day()
  let new_nanos = (
      nanoseconds % nanoseconds_per_day + nanos + nanoseconds_per_day
    ) %
    nanoseconds_per_day
  if nanos == new_nanos {
    return self
  }
  PlainTime::from_nanosecond_of_day(new_nanos)
}

///|
/// Adds a duration to this time, and returns a new time.
pub fn PlainTime::add_duration(
  self : PlainTime,
  duration : Duration,
) -> PlainTime raise Error {
  if duration.is_zero() {
    return self
  }
  self.add_nanoseconds(duration.to_nanoseconds())
}

///|
/// Returns a new time with the specified hour.
pub fn PlainTime::with_hour(
  self : PlainTime,
  hour : Int,
) -> PlainTime raise Error {
  if hour == self.hour {
    return self
  }
  PlainTime::of(hour, self.minute, self.second, self.nanosecond)
}

///|
/// Returns a new time with the specified minute.
pub fn PlainTime::with_minute(
  self : PlainTime,
  minute : Int,
) -> PlainTime raise Error {
  if minute == self.minute {
    return self
  }
  PlainTime::of(self.hour, minute, self.second, self.nanosecond)
}

///|
/// Returns a new time with the specified second.
pub fn PlainTime::with_second(
  self : PlainTime,
  second : Int,
) -> PlainTime raise Error {
  if second == self.second {
    return self
  }
  PlainTime::of(self.hour, self.minute, second, self.nanosecond)
}

///|
/// Returns a new time with the specified nanosecond.
pub fn PlainTime::with_nanosecond(
  self : PlainTime,
  nanosecond : Int,
) -> PlainTime raise Error {
  if nanosecond == self.nanosecond {
    return self
  }
  PlainTime::of(self.hour, self.minute, self.second, nanosecond)
}

///|
/// Returns the duration between this time and another time.
pub fn PlainTime::until(
  self : PlainTime,
  end : PlainTime,
) -> Duration raise Error {
  let nanoseconds = end.nanosecond_of_day() - self.nanosecond_of_day()
  Duration::of(nanoseconds~)
}

///|
/// Combines this time with a date to creates a PlainDateTime
pub fn PlainTime::at_date(self : PlainTime, date : PlainDate) -> PlainDateTime {
  { date, time: self }
}

// *****************************
// * internal helper functions *
// *****************************

///|
let min_hour = 0

///|
let max_hour = 23

///|
let min_minute = 0

///|
let max_minute = 59

///|
let min_second = 0

///|
let max_second = 59

///|
let min_nanosecond = 0

///|
let max_nanosecond = 999_999_999

///|
let hours_per_day = 24L

///|
let minutes_per_hour = 60L

///|
let minutes_per_day : Int64 = 24L * minutes_per_hour

///|
let seconds_per_minute = 60L

///|
let seconds_per_hour : Int64 = seconds_per_minute * 60L

///|
let seconds_per_day : Int64 = seconds_per_hour * 24L

///|
let nanoseconds_per_second = 1_000_000_000L

///|
let nanoseconds_per_minute : Int64 = nanoseconds_per_second * seconds_per_minute

///|
let nanoseconds_per_hour : Int64 = nanoseconds_per_minute * minutes_per_hour

///|
let nanoseconds_per_day : Int64 = nanoseconds_per_second * seconds_per_day

///|
fn validate_time(h : Int, m : Int, s : Int, ns : Int) -> Bool {
  let valid_hour = validate_hour(h)
  let valid_minute = validate_minute(m)
  let valid_second = validate_second(s)
  let valid_nano = validate_nano(ns)
  let zero = h == 24 && m == 0 && s == 0 && ns == 0
  (valid_hour && valid_minute && valid_second && valid_nano) || zero
}

///|
fn validate_hour(h : Int) -> Bool {
  h >= min_hour && h <= max_hour
}

///|
fn validate_minute(m : Int) -> Bool {
  m >= min_minute && m <= max_minute
}

///|
fn validate_second(s : Int) -> Bool {
  s >= min_second && s <= max_second
}

///|
fn validate_nano(n : Int) -> Bool {
  n >= min_nanosecond && n <= max_nanosecond
}