package org.mafagafogigante.dungeon.date;

import org.mafagafogigante.dungeon.logging.DungeonLogger;

import org.jetbrains.annotations.NotNull;

import java.io.Serializable;

/**
 * Dungeon Date class. Stores an immutable time stamp.
 *
 * <p>Dungeon's date system has years of 10 months, months of 10 days and days of 24 hours of 60 minutes of 60 seconds.
 * All the public constants made available by this class refer to this date system. Therefore they should not be used to
 * represent or take place into calculations of real world time.
 */
public class Date implements Comparable<Date>, Serializable {

  /**
   * The time, in milliseconds.
   */
  private long time; // Supports 1067519911 full years.

  private Date(long millis) {
    time = millis;
  }

  /**
   * Constructs a date from the provided parameters.
   *
   * @param year the year, positive
   * @param month the month, positive, smaller than or equal to the number of months in a year
   * @param day the day, positive, smaller than or equal to the number of days in a month
   * @param hour the hour, nonnegative, smaller than the number of hours in a day
   * @param minute the minute, nonnegative, smaller than the number of minutes in an hour
   * @param second the second, nonnegative, smaller than the number of seconds in a minute
   */
  public Date(long year, long month, long day, long hour, long minute, long second) {
    this(year, month, day);
    if (hour < 0) {
      DungeonLogger.warning("Tried to construct Date with negative hour.");
      hour = 0;
    } else if (hour >= DungeonTimeUnit.DAY.as(DungeonTimeUnit.HOUR)) {
      DungeonLogger.warning("Tried to construct Date with nonexistent hour.");
      // First hour of the next day. Even if the code supplied this, log a warning as this is likely a bug.
      hour = DungeonTimeUnit.DAY.as(DungeonTimeUnit.HOUR);
    }
    if (minute < 0) {
      DungeonLogger.warning("Tried to construct Date with negative minute.");
      minute = 0;
    } else if (minute >= DungeonTimeUnit.HOUR.as(DungeonTimeUnit.MINUTE)) {
      DungeonLogger.warning("Tried to construct Date with nonexistent minute.");
      // First minute of the next hour. Even if the code supplied this, log a warning as this is likely a bug.
      minute = DungeonTimeUnit.HOUR.as(DungeonTimeUnit.MINUTE);
    }
    if (second < 0) {
      DungeonLogger.warning("Tried to construct Date with negative second.");
      second = 0;
    } else if (second >= DungeonTimeUnit.MINUTE.as(DungeonTimeUnit.SECOND)) {
      DungeonLogger.warning("Tried to construct Date with nonexistent second.");
      // First second of the next minute. Even if the code supplied this, log a warning as this is likely a bug.
      second = DungeonTimeUnit.MINUTE.as(DungeonTimeUnit.SECOND);
    }
    time += hour * DungeonTimeUnit.HOUR.milliseconds + minute * DungeonTimeUnit.MINUTE.milliseconds +
        second * DungeonTimeUnit.SECOND.milliseconds;
  }

  /**
   * Constructs a date from the provided parameters.
   *
   * @param year the year, positive
   * @param month the month, positive, smaller than or equal to the number of months in a year
   * @param day the day, positive, smaller than or equal to the number of days in a month
   */
  public Date(long year, long month, long day) {
    if (year <= 0) {
      DungeonLogger.warning("Tried to construct Date with nonpositive year.");
      year = 1;
    }
    if (month <= 0) {
      DungeonLogger.warning("Tried to construct Date with nonpositive month.");
      month = 1;
    } else if (month > DungeonTimeUnit.YEAR.as(DungeonTimeUnit.MONTH)) {
      DungeonLogger.warning("Tried to construct Date with nonexistent month.");
      month = DungeonTimeUnit.YEAR.as(DungeonTimeUnit.MONTH);
    }
    if (day <= 0) {
      DungeonLogger.warning("Tried to construct Date with nonpositive day.");
      day = 1;
    } else if (day > DungeonTimeUnit.MONTH.as(DungeonTimeUnit.DAY)) {
      DungeonLogger.warning("Tried to construct Date with nonexistent day.");
      day = DungeonTimeUnit.MONTH.as(DungeonTimeUnit.DAY);
    }
    time = DungeonTimeUnit.YEAR.milliseconds * (year - 1) + DungeonTimeUnit.MONTH.milliseconds * (month - 1) +
        DungeonTimeUnit.DAY.milliseconds * (day - 1);
  }

  /**
   * Returns the time since epoch in milliseconds.
   */
  public long getTime() {
    return time;
  }

  private long getSecond() {
    return (time % DungeonTimeUnit.MINUTE.milliseconds) / DungeonTimeUnit.SECOND.milliseconds;
  }

  private long getMinute() {
    return (time % DungeonTimeUnit.HOUR.milliseconds) / DungeonTimeUnit.MINUTE.milliseconds;
  }

  public long getHour() {
    return (time % DungeonTimeUnit.DAY.milliseconds) / DungeonTimeUnit.HOUR.milliseconds;
  }

  public long getDay() {
    return (time % DungeonTimeUnit.MONTH.milliseconds) / DungeonTimeUnit.DAY.milliseconds + 1;
  }

  public long getMonth() {
    return (time % DungeonTimeUnit.YEAR.milliseconds) / DungeonTimeUnit.MONTH.milliseconds + 1;
  }

  public long getYear() {
    return time / DungeonTimeUnit.YEAR.milliseconds + 1;
  }

  /**
   * Returns a new Date object corresponding to this Date plus the specified amount of time.
   *
   * @param amount a positive integer
   * @param unit a DungeonTimeUnit value, not null
   * @return a new Date object
   * @throws IllegalArgumentException if amount is not positive or if unit is null
   */
  public Date plus(int amount, DungeonTimeUnit unit) {
    if (amount <= 0) {
      throw new IllegalArgumentException("amount must be positive.");
    } else if (unit == null) {
      throw new IllegalArgumentException("unit should not be null.");
    } else {
      return new Date(time + amount * unit.milliseconds);
    }
  }

  /**
   * Returns a new Date object corresponding to this Date minus the specified amount of time.
   *
   * @param amount a positive integer
   * @param unit a DungeonTimeUnit value, not null
   * @return a new Date object
   * @throws IllegalArgumentException if amount is not positive or if unit is null
   */
  public Date minus(int amount, DungeonTimeUnit unit) {
    if (amount <= 0) {
      throw new IllegalArgumentException("amount must be positive.");
    } else if (unit == null) {
      throw new IllegalArgumentException("unit should not be null.");
    } else {
      return new Date(time - amount * unit.milliseconds);
    }
  }

  public String toDateString() {
    return "day " + getDay() + " of month " + getMonth() + " of the year " + getYear();
  }

  public String toTimeString() {
    return String.format("%02d:%02d:%02d", getHour(), getMinute(), getSecond());
  }

  @Override
  public int compareTo(@NotNull Date date) {
    if (time > date.time) {
      return 1;
    } else if (time == date.time) {
      return 0;
    } else {
      return -1;
    }
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    Date date = (Date) o;
    return time == date.time;
  }

  @Override
  public int hashCode() {
    return (int) (time ^ (time >>> 32));
  }

  /**
   * Returns a String representation of this date, from year to second.
   *
   * @return a String
   */
  @Override
  public String toString() {
    String format = "%d-%02d-%02d %02d:%02d:%02d";
    return String.format(format, getYear(), getMonth(), getDay(), getHour(), getMinute(), getSecond());
  }

}
