parks/park.js

import promiseRetry from 'promise-retry';
import Entity from './entity.js';
import moment from 'moment-timezone';
import Cache from '../cache/scopedCache.js';
import * as tags from './tags.js';
import {reusePromise, reusePromiseForever} from '../reusePromises.js';
import {queueType, returnTimeState} from './parkTypes.js';

// quick helper function to wait x milliseconds as a Promise
const delay = (milliseconds) => {
  return new Promise((resolve) => {
    setTimeout(resolve, milliseconds);
  });
};

/**
 * Base Park Object
 * @class
 */
export class Park extends Entity {
  /**
   * Create a new park object
   * @param {object} options
   */
  constructor(options = {}) {
    // how often to wait between updates to run another update
    options.updateInterval = 1000 * 60 * 5; // 5 minutes
    // disable auto-update for this object
    //  set this if the update is being handled by an external system
    options.disableParkUpdate = false;

    super(options);

    // create a new cache object for this park
    this.cache = new Cache(this.constructor.name, this.config.cacheVersion || 0);

    this.initialised = false;
    this.hasRunPostInit = false;
    this.hasRunUpdate = false;
    this._pendingTags = {};

    this._attractions = [];

    // track the park's current date
    //  we'll fire an event whenever this changes
    this._currentDate = null;

    // make attractions and calendar functions work offline
    this.registerOfflineFunction('getAttractions');
    this.registerOfflineFunction('getCalendar');
  }

  /**
   * Call this to shutdown the park object.
   * This is an async call, so wait until it has resolved to continue.
   */
  async shutdown() {
    // disable any park updates
    this.config.disableParkUpdate = true;
  }

  /**
   * Get a globally unique ID for this park
   * @return {string}
   */
  getParkUniqueID() {
    return this.getUniqueID();
  }

  /**
   * Get Park Attractions
   */
  async getAttractions() {
    await this.ensureReady();

    return this._attractions;
  }

  /**
   * Setup the park for use
   * Call to ensure the object has been initialised before accessing data
   */
  async init() {
    // skip init if we're offline
    if (this.offline) {
      this.initialised = true;
      this.hasRunUpdate = true;
      return;
    }

    await reusePromiseForever(this, this._runInit);
    if (!this.hasRunPostInit) {
      this.hasRunPostInit = true;
      await this._postInit();
    }
  }

  /**
   * @inheritdocs
   */
  async _postOfflineLoad() {
    await this.postUpdate();
  }

  /**
   * Run all the internal stages of the init process
   * @private
   */
  async _runInit() {
    try {
      await this._init();
      this.initialised = true;

      if (!this.config.disableParkUpdate && !this.offline) {
        // start an update loop

        // use a separate function so we can quickly loop back around
        const scheduleUpdate = async () => {
          // pause for our updateInterval time
          await delay(this.config.updateInterval);

          // if our udpates get disabled during our timer, then skip and exit our
          if (this.config.disableParkUpdate) return;

          // wait for Promise to resolve, grab any catches, then continue anyway
          this.update().then().catch().then(() => {
            if (this.config.disableParkUpdate) return;

            // schedule another update
            setImmediate(scheduleUpdate.bind(this));
          });
        };

        // start the first loop timer
        scheduleUpdate();
      }
    } catch (e) {
      console.error('Error initialising park', e);
    }
  }

  /**
   * Awaits until park is initalised and has run at least one update.
   * @return {Promise}
   */
  async ensureReady() {
    await this.init();

    if (!this.hasRunUpdate) {
      await this.update();
    }

    await this.ensureHasOfflineData();
  }

  /**
   * Cache an attraction
   * @param {string} attractionID ID of the attraction to be cached
   */
  async cacheAttractionObject(attractionID) {
    // find our attraction
    //  don't call the standard "find" function, as this will also create the object
    //  we don't want to actually create ths attraction if it doesn't exist, just ignore it
    const uniqueAttractionID = `${this.getParkUniqueID()}_${attractionID}`;
    const attraction = this._attractions.find((attr) => attr.id == uniqueAttractionID);
    if (attraction !== undefined) {
      await this.cache.set(uniqueAttractionID, attraction, Number.MAX_SAFE_INTEGER);
    }
  }

  /**
   * Build an object representing an attraction from sourced data
   * This object should not contain any "state" data, just static information about the attraction
   * @param {string} attractionID Unique Attraction ID
   * @return {object} Object containing at least 'name'.
   * Also accepts 'type', which is an {@link attractionType}
   */
  async _buildAttractionObject(attractionID) {
    throw new Error('Missing _buildAttractionObject Implementation', this.constructor.name);
  }

  /**
   * Get the attraction object for a given ID (skips all generation/safety checks etc,)
   * @param {string} attractionID
   * @return {object}
   * @private
   */
  _getAttractionByIDInternal(attractionID) {
    // search our existing store for this attraction
    const attraction = this._attractions.find((attr) => attr.rideId == attractionID);
    if (attraction) {
      return attraction;
    }
    return undefined;
  }

  /**
   * Get data about a attraction from its ID
   * @param {string} attractionID Unique Attraction ID
   * @return {object} The attraction object for the given ID, or undefined
   */
  async findAttractionByID(attractionID) {
    // wrap our actual function so multiple calls will return the same object
    return await reusePromise(this, this._findAttractionByID, `${attractionID}`);
  }

  /**
   * Get data about a attraction from its ID
   * @param {string} attractionID Unique Attraction ID
   * @return {object} The attraction object for the given ID, or undefined
   * @private
   */
  async _findAttractionByID(attractionID) {
    // search our existing store for this attraction
    const attraction = this._getAttractionByIDInternal(attractionID);
    if (attraction) {
      return attraction;
    }

    // build a unique attraction ID by prefixing the park's unique ID
    const uniqueAttractionID = `${this.getParkUniqueID()}_${attractionID}`;

    // attraction wasn't found, try and add one to our store
    const newAttraction = {
      id: uniqueAttractionID,
      // TODO - rename to attractionID
      rideId: attractionID, // unique attraction ID without the park prefix
      name: undefined,
      type: null,
      status: {
        status: null,
        lastUpdated: null,
        lastChanged: null,
      },
      queue: {},
      tags: [],
    };

    // list of fields we want to accept from the park class
    //  we don't want the park to add random fields to our attraction object
    //  park-specific data should be added using "tags" instead, so our object structure is the same for all parks
    const fieldsToCopyFromParkClass = [
      'name',
      'type', // TODO - rename this to attractionType, or some other morphing type based on entityType
      'entityType', // TODO - validate incoming types
    ];

    // restore stored live attraction data from a cache
    //  restore from cache *before* we build the actual attraction data
    //  this will allow the park API to fill in any out-of-date fields with live data
    const cachedAttraction = await this.cache.get(uniqueAttractionID);
    if (cachedAttraction !== undefined) {
      Object.keys(cachedAttraction).forEach((key) => {
        // TODO - do we need to do anyhting special here for sub-fields?
        if (key === 'tags') {
          // TODO - re-validate tags
        }
        newAttraction[key] = cachedAttraction[key];
      });
    }

    // ask the park implementation to supply us with some basic attraction information (name, type, etc.)
    //  we'll then inject this into our attraction object, assuming it returns successfully
    try {
      const builtAttractionObject = {
        // clone the object, to ensure we don't mess with the original
        ...(await this._buildAttractionObject(attractionID)),
      };
      if (builtAttractionObject !== undefined && !!builtAttractionObject.name) {
        // clear out any _src data (if present)
        delete builtAttractionObject._src;

        // add to our attractions array once we've got a valid attraction (not undefined) from child class
        this._attractions.push(newAttraction);

        // copy fields we're interested in into our new attraction object
        fieldsToCopyFromParkClass.forEach((key) => {
          if (builtAttractionObject[key] !== undefined) {
            newAttraction[key] = builtAttractionObject[key];
          }
        });

        const tags = (this._pendingTags[attractionID] || []).concat(builtAttractionObject.tags || []);
        delete this._pendingTags[attractionID];

        // we also manually accept the "tags" field
        //  add each tag to the attraction after it's added to our object above
        await Promise.allSettled(tags.map((tag) => {
          return this.setAttractionTag(attractionID, tag.key, tag.type, tag.value);
        }));

        // cache attraction object so it can be restored quickly on future app intialisations
        await this.cacheAttractionObject(attractionID);

        return newAttraction;
      }
    } catch (e) {
      this.emit('error', e);
      console.error('Error building attraction object:', e);
    }

    return undefined;
  }

  /**
   * Remove a tag from a given attraction ID
   * @param {string} attractionID
   * @param {string} key
   * @param {tagType} type
   */
  async removeAttractionTag(attractionID, key, type) {
    const attraction = await this._getAttractionByIDInternal(attractionID);
    if (!attraction) return;

    const existingTag = attraction.tags.findIndex((t) => t.key === key && t.type === type);
    if (existingTag >= 0) {
      attraction.tags.splice(existingTag, 1);
      await this.cacheAttractionObject(attractionID);
    }
  }

  /**
   * Set a toggle tag for an attraction.
   * This is different from more complex tags that expect a data structure.
   * Use this for tags that don't have any actual value, but are just present. Eg. FastPass as a feature.
   * @param {string} attractionID
   * @param {tagType} type
   * @param {boolean} value
   */
  async toggleAttractionTag(attractionID, type, value) {
    return await this.setAttractionTag(attractionID, null, type, value);
  }

  /**
   * Set an attraction tag
   * Used for metadata on rides, such as location, thrill level, fastpass availability etc.
   * @param {string|object} attractionID Attraction ID to update (or the actual object)
   * @param {string} key Tag key to set
   * @param {tagType} type Tag type to use
   * @param {*} value Tag value to set
   * @return {boolean} True if tag was stored successfully
   */
  async setAttractionTag(attractionID, key, type, value) {
    // validate tag value
    const newTag = tags.getValidTagObject(key, type, value);
    if (newTag === undefined) {
      return false;
    }

    // different path for simple tags that are being removed
    if (tags.isSimpleTagType(type) && !value) {
      // if value is false, remove the key
      return await this.removeAttractionTag(attractionID, newTag.key, type);
    }

    // find attraction and apply tag to it
    const attraction = await this._getAttractionByIDInternal(attractionID);
    if (attraction) {
      const existingTag = attraction.tags.findIndex((t) => t.key === newTag.key && t.type === newTag.type);
      if (existingTag < 0) {
        // push our new tag onto our attraction
        attraction.tags.push(newTag);
      } else {
        // update existing tag entry
        attraction.tags[existingTag] = newTag;
      }
      await this.cacheAttractionObject(attractionID);
      return true;
    } else {
      // attraction isn't valid. Push to our pending array to process when/if it does become valid
      this._pendingTags[attractionID] = [{key, type, value}].concat(
          this._pendingTags[attractionID] || [],
      );
    }

    return false;
  }

  /**
   * Update an attraction state
   * @param {string} attractionID Unique Attraction ID
   * @param {statusType} status New Attraction state
   */
  async updateAttractionState(attractionID, status) {
    if (attractionID === undefined) return;

    const existingRide = await this.findAttractionByID(attractionID);
    if (existingRide) {
      // if we found a matching attraction, update its "state" property with our new data
      const now = this.getTimeNow();

      // last updated is always kept up-to-date, regardless of whether the data changed
      existingRide.status.lastUpdated = now;

      // only update "lastChanged" if the status has changed
      const previousStatus = existingRide.status.status;
      if (previousStatus !== status || existingRide.status.lastChanged === null) {
        existingRide.status.status = status;
        existingRide.status.lastChanged = now;

        // broadcast updated ride event
        //  try to make sure we have updated everything before we fire this event
        this.emit('attractionStatus', existingRide, previousStatus);
      }

      // write updated attraction data to cache
      await this.cacheAttractionObject(attractionID);
    }
  }

  /**
   * Update the queue status for an attraction
   * @param {string} attractionID Attraction ID to update
   * @param {number|object} queueValue Updated Wait Time in minutes, or null if wait time doesn't exist or isn't valid.
   * Set waitTime to undefined to remove this queue type from the attraction.
   * BoardingGroup and ReturnTime types expect an object containing queue details instead of a number
   * @param {queueType} type Type of queue to update (standup, virtual, fastpass etc.)
   */
  async updateAttractionQueue(attractionID, queueValue = undefined, type = type.standBy) {
    if (attractionID === undefined) return;

    const existingRide = await this.findAttractionByID(attractionID);
    if (existingRide) {
      if (!existingRide.queue) {
        existingRide.queue = {};
      }

      // edge-case, if we supply undefined, the queue has been removed
      //  (or never existed and we're just double-checking it's not present)
      if (queueValue === undefined) {
        if (existingRide.queue[type] !== undefined) {
          const previousWaitTime = existingRide.queue[type].waitTime;
          delete existingRide.queue[type];

          // fire event anyway, the queue has technically been updated (it's just not present at all now)
          this.emit('attractionQueue', existingRide, type, previousWaitTime);

          await this.cacheAttractionObject(attractionID);
        }

        // don't continue operations, early exit here
        return;
      }

      if (!existingRide.queue[type]) {
        existingRide.queue[type] = {
          lastUpdated: null,
          lastChanged: null,
        };

        if (type === queueType.standBy) {
          // default standby state
          existingRide.queue[type].waitTime = null;
        } else if (type === queueType.returnTime) {
          // default return time state
          existingRide.queue[type].returnStart = null;
          existingRide.queue[type].returnEnd = null;
          existingRide.queue[type].state = null;
        }
      }

      const queueData = existingRide.queue[type];

      const now = this.getTimeNow();

      if (type == queueType.standBy || type == queueType.singleRider) {
        // wait times must be a positive number (in minutes)
        //  if wait time is unknown (because it is not tracker or there is some issue), waitTime should be null
        const newWaitTime = (isNaN(queueValue) || queueValue < 0) ? null : queueValue;
        const previousWaitTime = queueData.waitTime;

        // store last updated time
        queueData.lastUpdated = now;

        if (newWaitTime !== previousWaitTime || queueData.lastChanged === null) {
          queueData.waitTime = newWaitTime;
          queueData.lastChanged = now;

          // broadcast updated ride event
          //  try to make sure we have updated everything before we fire this event
          this.emit('attractionQueue', existingRide, type, previousWaitTime);
        }
      } else if (type == queueType.returnTime) {
        // handle "return time" style queue updates
        // validate incoming data
        if (
          queueValue?.returnStart === undefined ||
          queueValue?.returnEnd === undefined ||
          // state can be null for "unknown" states
          (queueValue?.state !== null && Object.values(returnTimeState).indexOf(queueValue?.state) < 0)
        ) {
          this.emit(
              'error',
              new Error(`Invalid return time object used ${JSON.stringify(queueValue)} (${attractionID})`),
          );
          return;
        }

        // make sure we're moment formatted strings
        queueValue.returnStart = queueValue.returnStart === null ? null :
          moment.tz(queueValue.returnStart, this.config.timezone).format();
        queueValue.returnEnd = queueValue.returnEnd === null ? null :
          moment.tz(queueValue.returnEnd, this.config.timezone).format();

        const originalReturnTime = {
          returnStart: queueData?.returnStart,
          returnEnd: queueData?.returnEnd,
          state: queueData?.state,
        };

        // mark data as "updated", even if it hasn't changed
        queueData.lastUpdated = now;

        if (
          queueValue.returnStart != originalReturnTime.returnStart ||
          queueValue.returnEnd != originalReturnTime.returnEnd ||
          queueValue.state != originalReturnTime.state
        ) {
          // return time has changed!
          queueData.returnStart = queueValue.returnStart;
          queueData.returnEnd = queueValue.returnEnd;
          queueData.state = queueValue.state;
          queueData.lastChanged = now;

          // broadcast updated ride event
          this.emit('attractionQueue', existingRide, type, originalReturnTime);
        }
      } else if (type == queueType.boardingGroup) {
        // TODO - handle baording group style queues
        return;
      }

      // write updated attraction data to cache
      await this.cacheAttractionObject(attractionID);
    }
  }

  /**
   * Called after each successful update, handle any clean-up or extra work here
   * @private
   */
  async postUpdate() {
    // check if our date has changed
    await this._checkDate();
  }

  /**
   * Update this park
   * This is automatically called for you unless disableParkUpdate is set to false
   */
  async update() {
    return reusePromise(this, this._runUpdate);
  }

  /**
   * Internal method to actually run our update
   * @private
   */
  async _runUpdate() {
    // wait and catch the update Promise
    try {
    // start the _update call in a retry loop
      await promiseRetry({
        retries: 5,
      }, (retryFn) => {
        return this._update().catch(retryFn);
      });
    } catch (e) {
      // emit error and print to screen
      console.error('Error running _update()', e);
      this.emit('error', e);
      return;
    }

    this.hasRunUpdate = true;

    try {
      await this.postUpdate();
    } catch (e) {
      console.error('Error running postUpdate()', e);
      this.emit('error', e);
    }
  }

  /**
   * Called when the park's date changes
   * Eg. when passing midnight in the park's local timezone
   *  or if late opening hours finish the morning after (eg. open until 2am, will be called just after 2am)
   * @param {string} newDate Current Park Date
   * @param {string} oldDate The previous date for this park before the update (can be null if park just initialised)
   * @abstract
   */
  async _dateRefresh(newDate, oldDate) {}

  /**
   * Check if the park's "active date" has changed
   */
  async _checkDate() {
    const todaysDate = await this.getActiveParkDate();
    if (this._currentDate !== todaysDate) {
      // store the previous date and update the current date immediately
      //  this makes sure the park object is in the correct state before firing the newDate events
      const originalDate = this._currentDate;
      this._currentDate = todaysDate;

      // broadcast event when the park's day changes
      //  we can use this to update ride schedules etc.
      await this._dateRefresh(todaysDate, originalDate);
      this.emit('newDate', todaysDate, originalDate);
    }
  }

  /**
   * Internal function
   * Called by init() to initialise the object
   * @private
   * @abstract
   */
  async _init() {
    // implementation should be setup in child classes
    throw new Error('_init() needs an implementation', this.constructor.name);
  }

  /**
   * Internal function
   * Called by init() to initialise the object
   * @private
   * @abstract
   */
  async _postInit() {
    // implementation should be setup in child classes
  }

  /**
   * Update function the park object calls on interval to update internal state
   * @private
   * @abstract
   */
  async _update() {
    // implementation should be setup in child classes
    throw new Error('_update() needs an implementation', this.constructor.name);
  }

  /**
   * Given a moment date, return an array of opening hours for this park, or undefined
   * Each entry should contain openingTime, closingTime, and type (of scheduleType)
   * @param {moment} date
   * @private
   * @abstract
   */
  async _getOperatingHoursForDate(date) {
    // implementation should be setup in child classes
    throw new Error('_getOperatingHoursForDate() needs an implementation', this.constructor.name);
  }

  /**
   * Get the operating hours for the supplied date
   * Will return undefined if the park API cannot return data for this date
   *  (park is closed, too far in future, too far in past, etc.)
   * @param {moment} date A momentjs object
   * @return {object}
   */
  async getOperatingHoursForDate(date) {
    // cache each calendar date to avoid recalculating it all the time
    return this.cache.wrap(`calendar_${date.format('YYYY-MM-DD')}`, async () => {
      return await this._getOperatingHoursForDate(date);
    }, 1000 * 60 * 60 * 6); // cache for 6 hours
  }

  /**
   * Given a moment date, return an array of opening hours for the restaurants in this park, or undefined
   * Ech entry should contain openingTime closingTime
   * @param {moment} date
   */
  async _getRestaurantOperatingHoursForDate(date) {
    // implementation should be setup in child classes
    throw new Error('_getRestaurantOperatingHoursForDate() needs an implementation', this.constructor.name);
  }

  /**
   * Get the restaurant operating hours for the supplied date
   * @param {moment} date
   */
  async getRestaurantOperatingHoursForDate(date) {
    return this.cache.wrap(`restaurant_${date.format('YYYY-MM-DD')}`, async () => {
      return await this._getRestaurantOperatingHoursForDate(date);
    }, 1000 * 60 * 60 * 6); // cache for 6 hours
  }

  /**
   * Get Operating Calendar for this park
   * @return{object} Object keyed to dates in YYYY-MM-DD format.
   * Each date entry will contain an array of operating hours.
   */
  async getCalendar() {
    try {
      // make sure the park is initialised before continuing
      await this.init();

      // populate from yesterday onwards (if the API even gives us yesterday)
      //  try to catch weird edge-cases where we're just past midnight but the park is still open
      const yesterday = this.getTimeNowMoment().subtract(1, 'days');
      // populate forward up to 60 days
      const endFillDate = yesterday.clone().add(60 + 1, 'days');

      const now = this.getTimeNowMoment();

      const dates = {};
      // get calendar by looping over each date
      for (let date = yesterday; date.isSameOrBefore(endFillDate); date.add(1, 'day')) {
        const hours = await this.getOperatingHoursForDate(date);
        if (hours !== undefined) {
          if (!Array.isArray(hours)) {
            this.emit(
                'error',
                new Error(
                    // eslint-disable-next-line max-len
                    `Hours for ${this.name} date ${date.format('YYYY-MM-DD')} returned invalid non-Array ${JSON.stringify(hours)}`,
                ),
            );
            continue;
          }
          // ignore if we're not within the operating hours AND the date is in the past
          //  this will strip out yesterday once we've left that day's opening hours
          const isInsideAnyDateHours = hours.find((h) => {
            return now.isBetween(h.openingTime, h.closingTime);
          });
          if (now.isAfter(date, 'day') && isInsideAnyDateHours === undefined) {
            continue;
          }
          dates[date.format('YYYY-MM-DD')] = hours;
        }
      }

      return dates;
    } catch (err) {
      console.error('Error getting calendar', err);
      this.emit('error', err);
    }

    return undefined;
  }

  /**
   * Return the number of milliseconds until the next time the park is open.
   * Will return 0 if park is already open.
   * @return{number} Milliseconds until the park is open
   */
  async getNextOpeningTime() {
    const calendar = await this.getCalendar();
    const now = this.getTimeNowMoment();

    const dates = Object.keys(calendar);
    const nextOpeningTime = dates.reduce((p, date) => {
      return Math.min(p, calendar[date].reduce((p2, time) => {
        const msUntilOpening = moment(time.openingTime).diff(now);
        // if the opening time is in the past, is the closing time in the future?
        if (msUntilOpening <= 0) {
          if (moment(time.closingTime).diff(now) > 0) {
            return 0;
          } else {
            // otherwise this entire time block is in the past, ignore it
            return p2;
          }
        }
        return Math.min(p2, msUntilOpening);
      }, Number.MAX_SAFE_INTEGER));
    }, Number.MAX_SAFE_INTEGER);

    return nextOpeningTime === Number.MAX_SAFE_INTEGER ? null : nextOpeningTime;
  }

  /**
   * Return the number of milliseconds until closing time. Or 0 if already closed.
   * @return {number}
   */
  async getNextClosingTime() {
    const today = await this.getCalendarForToday();
    if (today !== undefined) {
      const now = this.getTimeNowMoment();
      const closingTime = today.reduce((p, hours) => {
        if (!now.isBetween(hours.openingTime, hours.closingTime)) {
          return 0;
        }

        return Math.max(moment(hours.closingTime).diff(now), p);
      }, 0);

      return closingTime;
    }
    return 0;
  }

  /**
   * Return the time until the park is open.
   * @return{momentDuration} Time until park opens as a Moment Duration.
   * Zero if already open, or null if unable to find time
   */
  async getNextOpeningTimeMomentDuration() {
    const ms = await this.getNextOpeningTime();

    if (ms === null) return null;
    return moment.duration(ms, 'milliseconds');
  }

  /**
   * Get the current park date, taking into consideration park hours past midnight etc.
   * Eg. if the park is open past midnight, return yesterday's date.
   * @return{moment} Park's "active date" as a Moment object
   */
  async getActiveParkDateMoment() {
    const calendar = await this.getCalendar();

    const nowInPark = moment(this.getTimeNow()).tz(this.config.timezone);
    // check yesterday, today, and tomorrow to find any park hours that we're currently in
    //  (including any extra hours etc.)
    //  we will fall-back to the current date if none of these match
    const isInParkHours = [
      nowInPark.clone().add(-1, 'day'),
      nowInPark,
      nowInPark.clone().add(1, 'day'),
    ].map((date) => {
      // build array of our park calendar entries
      return {
        date,
        data: calendar[date.format('YYYY-MM-DD')],
      };
    }).filter((parkHours) => {
      // filter out any park hours that doesn't include the current time
      if (!parkHours.data) return false;
      const isInAnyParkHours = parkHours.data.find((hours) => {
        return (nowInPark.isBetween(moment(hours.openingTime), moment(hours.closingTime)));
      });
      return !!isInAnyParkHours;
    });

    if (isInParkHours.length === 0) {
      // just return today's calendar
      return nowInPark;
    }
    // otherwise return the hours that we currently match
    return isInParkHours[0].date;
  }

  /**
   * Get the current park date, taking into consideration park hours past midnight etc.
   * Eg. if the park is open past midnight, return yesterday's date.
   * @return{string} Date in YYYY-MM-DD format
   */
  async getActiveParkDate() {
    return (await this.getActiveParkDateMoment()).format('YYYY-MM-DD');
  }

  /**
   * Get the park opening hours for today
   */
  async getCalendarForToday() {
    const todaysDate = await this.getActiveParkDate();
    const calendar = await this.getCalendar();
    return calendar[todaysDate];
  }

  /**
   * Get the park opening hours for tomorrow
   */
  async getCalendarForTomorrow() {
    const todaysDate = await this.getActiveParkDate();
    const tomorrow = moment(todaysDate, 'YYYY-MM-DD').add(1, 'day');
    const calendar = await this.getCalendar();
    return calendar[tomorrow.format('YYYY-MM-DD')];
  }
}

export default Park;