Home Reference Source

lib/parks/europapark/europaparkbase.js

import fetch from 'node-fetch';
import {scheduleType, entityType, queueType, fastPassStatus} from '../types.js';
import {Park} from '../park.js';
import moment from 'moment-timezone';
import Blowfish from 'egoroof-blowfish';
import crypto from 'crypto';

import dotenv from 'dotenv';
dotenv.config();

/**
* EuropaPark Park Object
* Make sure all environment variables are set in an .env file which should be in the main location.
* Not setting these variables will make the module exit early without returning data.
*
* This class is here to fetch the POI data and to attach queue times data to it.
* After the fetches this data is send to the end user and from there he could do whatever he wants to do.
*
* NOTE: Dutch language is supported, however strange things occur, such as deletion of VirtualLine entries making rides appear twice as 'normal'
*
* This class contains some login and refresh functions, but NEVER call them if you don't need them.
* Most park specific parameters are set already
* @class
*/
export class EuropaParkBase extends Park {
  /**
  * Create a new EuropaPark Park object
  * @param {object} options
  */
  constructor(options = {}) {
    // API options
    options.cachepoistime = process.env.CACHE_DURATION_POIS;
    options.apiBase = process.env.EUROPAPARK_APIBASE;
    options.loginurl = process.env.EUROPAPARK_LOGIN;
    options.fbAppId = process.env.EUROPAPARK_FBAPPID;
    options.fbApiKey = process.env.EUROPAPARK_FBAPIKEY;
    options.fbProjectId = process.env.EUROPAPARK_FBPROJECTID;
    options.encKey = process.env.EUROPAPARK_ENCKEY;
    options.encIV = process.env.EUROPAPARK_ENCIV;

    // Languages
    options.languages = process.env.LANGUAGES;

    options.langoptions = `{'en', 'nl', 'de', 'fr'}`; // Accidentally found that EP provides Dutch data, lmao EDIT: IT'S NEXT LEVEL HORRIBLE, DON'T EVER ENABLE IT!

    super(options);

    // Check for existance
    if (!this.config.apiBase) throw new Error('Missing Europa-Park apiBase!');
    if (!this.config.loginurl) throw new Error('Missing Europa-Park Login URL!');
    if (!this.config.fbAppId) throw new Error('Missing Europa-Park firebase app id!');
    if (!this.config.fbApiKey) throw new Error('Missing Europa-Park api key!');
    if (!this.config.fbProjectId) throw new Error('Missing Europa-Park firebase project id!');
    if (!this.config.encKey) throw new Error('Missing Europa-Park enc key!');
    if (!this.config.encIV) throw new Error('Missing Europa-Park enciv!');
    if (!this.config.languages) {
      this.config.languages = 'en';
    };
    if (!this.config.cachepoistime) {
      this.config.cachepoistime = '12';
    };

    /**
     * Setup BlowFish
     */
    this.bf = new Blowfish(this.config.encKey, Blowfish.MODE.CBC, Blowfish.PADDING.PKCS5 );
    this.bf.setIv(this.config.encIV);
  }

  /**
   * Get or generate a Firebase device ID
   */
  async getFirebaseID() {
    return await this.cache.wrap('fid', async () => {
      try {
        const fidByteArray = crypto.randomBytes(17).toJSON().data;
        fidByteArray[0] = 0b01110000 + (fidByteArray[0] % 0b00010000);
        const b64String = Buffer.from(String.fromCharCode(...fidByteArray))
            .toString('base64')
            .replace(/\+/g, '-')
            .replace(/\//g, '_');
        const fid = b64String.substr(0, 22);
        return /^[cdef][\w-]{21}$/.test(fid) ? fid : '';
      } catch (e) {
        this.emit('error', e);
        console.log(e);
        return '';
      }
    }, 1000 * 60 * 60 * 24 * 8); // 8 days DON'T CHANGE
  }

  /**
   * Get Europa Park config keys
   */
  async getConfig() {
    return await this.cache.wrap('auth', async () => {
      const fid = await this.getFirebaseID();

      return fetch(
          `https://firebaseremoteconfig.googleapis.com/v1/projects/${this.config.fbProjectId}/namespaces/firebase:fetch`,
          {
            method: 'POST',
            headers: {
              'X-Goog-Api-Key': this.config.fbApiKey,
            },
            body: JSON.stringify({
              'appInstanceId': fid,
              'appId': this.config.fbAppId,
              'packageName': 'com.EuropaParkMackKG.EPGuide',
              'languageCode': 'en_GB',
            }),
          },
      )
          .then((res) => res.json())
          .then((resp) => {
            const decrypt = (str) => {
              return this.bf.decode(Buffer.from(str, 'base64'), Blowfish.TYPE.STRING);
            };
            return {
              client_id: decrypt(resp.entries.v3_live_android_exozet_api_username),
              client_secret: decrypt(resp.entries.v3_live_android_exozet_api_password),
              grant_type: 'client_credentials',
            };
          });
    }, 1000 * 60 * 60 * 6); // 6 hours, DON'T CHANGE
  }

  /**
  * Login to EuropaPark API
  * Calling this method too fast can cause a perm block from the Macks
  * @return {string} EuropaPark JWT token
  */
  async loginEP() {
    const auth = await this.getConfig();
    return fetch(this.config.loginurl,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(auth),
        },
    )
        .then((res) => res.json())
        .then((login) => {
          const jwttoken = 'Bearer ' + login.access_token;
          return Promise.resolve(jwttoken);
        });
  }

  /**
  * Get All POIS of EuropaPark park
  * This data contains all the POIS in EuropaPark
  * @return {string} EP POIS without queues
  */
  async getPOIS() {
    return await this.cache.wrap('poi', async () => {
      const jwttoken = await this.loginEP();
      const checksum = (await this.cache.get('poi_checksum')) || 0;

      return fetch(this.config.apiBase +
        `latest/${this.config.languages}/live/${checksum}`,
      {
        headers: {
          'Content-Type': 'application/json',
          'JWTAuthorization': jwttoken,
        },
      },
      )
          .then((res) => res.json())
          .then((rideData) => {
            if (rideData.error?.code === 404 && checksum > 0) {
              // Return the old data back, nothing is added nor removed
              return this.cache.get('poidata');
            }
            if (!rideData.package) return undefined;
            const pois = [];
            Object.keys(rideData.package.data).forEach((key) => {
              rideData.package.data[key].forEach((poi) => {
                pois.push(poi);
              });
            });
            // Store the checksum and pois until things have changed.
            this.cache.set('poidata', pois, Number.MAX_SAFE_INTEGER);
            this.cache.set('poi_checksum', rideData.package.checksum);
            return Promise.resolve(pois); // Immediately return rideData, no need to fetch anything else here because their API is shit.
          });
    }, 1000 * 60 * 60 * this.config.cachepoistime );
  };

  /**
  * Build EuropaPark park ride object
  * This data contains general ride names, descriptions etc.
  * @return {string} All EuropaPark park ride POIS without queuetimes
  */
  async buildRidePOI() {
    let parent = null;
    return await this.cache.wrap(`${this.config.parkId}-ridedata`, async () => { // Rebuilding the ride object each time is SLOOOOOWWWW
      const poiData = await this.getPOIS(); // Get the POI Data

      if (!poiData) throw new Error('No PoiData for EuropaPark found!'); // API probably died, nothing new. Stay calm

      const poi = {};
      let singleRider = false; // EP doesn't send these values
      let fastPass = false; // Set fastpass to false as default
      let isVirtQueue = false; // Default poi isn't a virtqueue
      poiData.forEach((ride) => {
        if (ride.scopes) {
          if (ride.type === 'attraction' && ride.code !== null && ride.scopes.indexOf(`${this.config.parkId}`) === 0) { // Return rides and pois which haven't null
            if (ride.name.indexOf('Queue - ') === 0) return; // Ignore the Queue Pointers, really, why do they even exist?
            if (ride.name.indexOf('VirtualLine: ') === 0) { // So EP reports virtlane as seperate map pointer, they send it as a stand-alone POI, assign the VirtQueue tag here.
              fastPass = true;
              isVirtQueue = true;
              const paren = ride.name.slice(13);
              parent = paren;
              singleRider = true; // VirtLines are the SRL replacement
            } else { // Yay, it's not a Virtline entry!
              fastPass = false;
              isVirtQueue = false;
              parent = null;
            }
            let area = 'Germany'; // Really, this is the strangest empire thing ever
            if (ride.areaId == 10) {
              area = 'Adventureland';
            } else if (ride.areaId == 11) {
              area = 'Kingdom of the Minimoys';
            } else if (ride.areaId == 12) {
              area = 'Germany';
            } else if (ride.areaId == 13) {
              area = 'England';
            } else if (ride.areaId == 14) {
              area = 'France';
            } else if (ride.areaId == 15) {
              area = 'Greece';
            } else if (ride.areaId == 16) {
              area = `Grimm's Fairytale Forest`; // Thanks for the single quote. Really helpful indeed
            } else if (ride.areaId == 17) {
              area = 'Netherlands';
            } else if (ride.areaId == 19) {
              area = 'Ireland';
            } else if (ride.areaId == 20) {
              area = 'Iceland';
            } else if (ride.areaId == 21) {
              area = 'Italy';
            } else if (ride.areaId == 22) {
              area = 'Luxembourg';
            } else if (ride.areaId == 23) {
              area = 'Austria';
            } else if (ride.areaId == 24) {
              area = 'Portugal';
            } else if (ride.areaId == 25) {
              area = 'Russia';
            } else if (ride.areaId == 26) {
              area = 'Switzerland';
            } else if (ride.areaId == 27) {
              area = 'Scandinavia';
            } else if (ride.areaId == 28) {
              area = 'Spain';
            }
            // To be clear, there are even more areas, but yeah, useless.

            // EuropaPark actually provides some cool tags which I'll attach here.
            let Producer = undefined;
            let Opening = undefined;
            let Capacity = undefined;
            let Ridetime = undefined;
            let TheoreticalCapacity = undefined;
            let MaxGForce = undefined;
            let MaxSpeed = undefined;
            let Height = undefined;

            if (ride.attributes) {
              Object.keys(ride.attributes).forEach((poiat) => {
                if (ride.attributes[poiat].key === 'Producer') {
                  Producer = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Opening') {
                  Opening = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Capacity') {
                  Capacity = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Driving Time') {
                  Ridetime = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Theoretical Capacity') {
                  TheoreticalCapacity = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Max Acceleration') {
                  MaxGForce = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Max Speed') {
                  MaxSpeed = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Height') {
                  Height = ride.attributes[poiat].value;
                }
              });
            }

            const tags = {
              Producer,
              Opening,
              Capacity,
              Ridetime,
              TheoreticalCapacity,
              MaxGForce,
              MaxSpeed,
              Height,
            };

            // And some restrictions.
            let minHeight = undefined;
            let minHeightAccompanied = undefined;
            let minAge = undefined;
            let minAgeAccompanied = undefined;
            let maxHeight = undefined;
            if (ride.minHeight) {
              minHeight = ride.minHeight; // Minimum length
            };
            if (ride.minHeightAdult) {
              minHeightAccompanied = ride.minHeightAdult; // Minimum length accompanied, although, that's my guess
            };
            if (ride.minAge) {
              minAge = ride.minAge; // RECOMMENDED minimum age.
            };
            if (ride.minAgeAdult) {
              minAgeAccompanied = ride.minAgeAdult; // The most epic entry ever, a recommended minimum age whenever you're accompanied!!! Wow
            };
            if (ride.maxHeight) {
              maxHeight = ride.maxHeight; // Some people wants to grow like 3m high and that has to stop.
            }

            const restrictions = {
              minHeight,
              minHeightAccompanied,
              minAge,
              minAgeAccompanied,
              maxHeight,
            };

            // Build the ride object
            poi[ride.code] = {
              name: ride.name,
              id: `${this.config.name}_` + ride.code,
              waitTime: null,
              state: null,
              active: null,
              location: {
                area: area,
                latitude: ride.latitude,
                longitude: ride.longitude,
              },
              meta: {
                description: ride.description,
                short_description: ride.excerpt,
                single_rider: singleRider,
                fastPass: fastPass,
                parent: parent,
                type: entityType.ride,
                single_rider: singleRider,
                isVirtQueue: isVirtQueue,
                tags: tags,
                restrictions: restrictions,
              },
            };
          };
        }
      });
      return Promise.resolve(poi);
    }, 1000 * 60 * 60 * this.config.cachepoistime );
  };

  /**
  * Build EuropaPark park static object
  * This data contains general ride names, descriptions etc.
  * @return {string} All EuropaPark park static POIS without queuetimes
  */
  async buildStaticPOI() {
    return await this.cache.wrap(`${this.config.parkId}-static`, async () => { // Rebuilding the ride object each time is SLOOOOOWWWW
      const poiData = await this.getPOIS(); // Get the POI Data

      if (!poiData) throw new Error('No PoiData for EuropaPark found!');

      const poi = {};
      poiData.forEach((ride) => {
        if (ride.scopes) {
          if (ride.type === 'sight' && ride.code !== null && ride.scopes.indexOf(`${this.config.parkId}`) === 0) { // Return rides and pois which haven't null
            if (ride.name.indexOf('Queue - ') === 0) return; // Ignore the Queue Pointers
            let area = 'Germany'; // Really, this is the strangest empire thing ever
            if (ride.areaId == 10) {
              area = 'Adventureland';
            } else if (ride.areaId == 11) {
              area = 'Kingdom of the Minimoys';
            } else if (ride.areaId == 12) {
              area = 'Germany';
            } else if (ride.areaId == 13) {
              area = 'England';
            } else if (ride.areaId == 14) {
              area = 'France';
            } else if (ride.areaId == 15) {
              area = 'Greece';
            } else if (ride.areaId == 16) {
              area = `Grimm's Fairytale Forest`;
            } else if (ride.areaId == 17) {
              area = 'Netherlands';
            } else if (ride.areaId == 19) {
              area = 'Ireland';
            } else if (ride.areaId == 20) {
              area = 'Iceland';
            } else if (ride.areaId == 21) {
              area = 'Italy';
            } else if (ride.areaId == 22) {
              area = 'Luxembourg';
            } else if (ride.areaId == 23) {
              area = 'Austria';
            } else if (ride.areaId == 24) {
              area = 'Portugal';
            } else if (ride.areaId == 25) {
              area = 'Russia';
            } else if (ride.areaId == 26) {
              area = 'Switzerland';
            } else if (ride.areaId == 27) {
              area = 'Scandinavia';
            } else if (ride.areaId == 28) {
              area = 'Spain';
            }

            // EuropaPark actually provides some cool tags which I'll attach here.
            let producer = null;
            let opening = null;
            let capacity = null;
            let ridetime = null;
            let thcapacity = null;
            let gforce = null;
            let maxspeed = null;
            let height = null;

            if (ride.attributes) {
              Object.keys(ride.attributes).forEach((poiat) => {
                if (ride.attributes[poiat].key === 'Producer') {
                  producer = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Opening') {
                  opening = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Capacity') {
                  capacity = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Driving Time') {
                  ridetime = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Theoretical Capacity') {
                  thcapacity = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Max Acceleration') {
                  gforce = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Max Speed') {
                  maxspeed = ride.attributes[poiat].value;
                } else if (ride.attributes[poiat].key === 'Height') {
                  height = ride.attributes[poiat].value;
                }
              });
            }

            // And some restrictions.
            let minHeight = null;
            let minHeightAdult = null;
            let minAge = null;
            let minAgeAdult = null;
            let maxHeight = null;
            if (ride.minHeight) {
              minHeight = ride.minHeight;
            };
            if (ride.minHeightAdult) {
              minHeightAdult = ride.minHeightAdult;
            };
            if (ride.minAge) {
              minAge = ride.minAge;
            };
            if (ride.minAgeAdult) {
              minAgeAdult = ride.minAgeAdult;
            };
            if (ride.maxHeight) {
              maxHeight = ride.maxHeight;
            }

            // Build the static object - probably the only thing that'll ever appear here are fairytales and the macks house, not kidding, its listed
            poi[ride.code] = {
              name: ride.name,
              id: `${this.config.name}_` + ride.code,
              location: {
                area: area,
                latitude: ride.latitude,
                longitude: ride.longitude,
              },
              meta: {
                description: ride.description,
                short_description: ride.excerpt,
                type: entityType.static,
                tags: {
                  Producer: producer,
                  Capacity: capacity,
                  Opened: opening,
                  Duration: ridetime,
                  Theoretical_Capacity: thcapacity,
                  Max_GForce: gforce,
                  Max_Speed: maxspeed,
                  Height: height,
                },
                restrictions: {
                  minHeight: minHeight,
                  minHeightAccompanied: minHeightAdult,
                  maxHeight: maxHeight,
                  minAge: minAge,
                  minAgeAccompanied: minAgeAdult,
                },
              },
            };
          };
        }
      });
      return Promise.resolve(poi);
    }, 1000 * 60 * 60 * this.config.cachepoistime );
  };

  /**
  * Build EuropaPark park restaurant object
  * This data contains general restaurant names, descriptions etc.
  * @return {string} All EP park restaurant POIS
  */
  async buildRestaurantPOI() {
    return await this.cache.wrap(`${this.config.parkId}-restpoi`, async () => { // Rebuilding the ride object each time is SLOOOOOWWWW
      const poiData = await this.getPOIS(); // Get the POI Data

      if (!poiData) throw new Error('No PoiData for EuropaPark found!');

      const poi = {};
      poiData.forEach((ride) => {
        if (ride.scopes) {
          if (ride.type === 'gastronomy' && ride.code !== null && ride.scopes.indexOf(`${this.config.parkId}`) === 0) { // Return rides and pois which haven't null
            let area = undefined; // Default, Hotel is included as well which adds new areas
            if (ride.areaId == 10) {
              area = 'Adventureland';
            } else if (ride.areaId == 11) {
              area = 'Kingdom of the Minimoys';
            } else if (ride.areaId == 12) {
              area = 'Germany';
            } else if (ride.areaId == 13) {
              area = 'England';
            } else if (ride.areaId == 14) {
              area = 'France';
            } else if (ride.areaId == 15) {
              area = 'Greece';
            } else if (ride.areaId == 17) {
              area = 'Netherlands';
            } else if (ride.areaId == 19) {
              area = 'Ireland';
            } else if (ride.areaId == 20) {
              area = 'Iceland';
            } else if (ride.areaId == 21) {
              area = 'Italy';
            } else if (ride.areaId == 22) {
              area = 'Luxembourg';
            } else if (ride.areaId == 23) {
              area = 'Austria';
            } else if (ride.areaId == 24) {
              area = 'Portugal';
            } else if (ride.areaId == 25) {
              area = 'Russia';
            } else if (ride.areaId == 26) {
              area = 'Switzerland';
            } else if (ride.areaId == 27) {
              area = 'Scandinavia';
            } else if (ride.areaId == 28) {
              area = 'Spain';
            }

            // Build the restaurant object
            poi[ride.code] = {
              name: ride.name,
              id: `${this.config.name}_` + ride.code,
              location: {
                area: area,
                latitude: ride.latitude,
                longitude: ride.longitude,
              },
              meta: {
                description: ride.description,
                short_description: ride.excerpt,
                type: entityType.restaurant,
              },
            };
          };
        }
      });
      return Promise.resolve(poi);
    }, 1000 * 60 * 60 * this.config.cachepoistime );
  };

  /**
  * Build EuropaPark park Merchandise object
  * This data contains general merchandise names, descriptions etc.
  * @return {string} All EP park Merchandise POIS
  */
  async buildMerchandisePOI() {
    return await this.cache.wrap(`${this.config.parkId}-merchpoi`, async () => { // Rebuilding the ride object each time is SLOOOOOWWWW
      const poiData = await this.getPOIS(); // Get the POI Data

      if (!poiData) throw new Error('No PoiData for EuropaPark found!');

      const poi = {};
      poiData.forEach((ride) => {
        if (ride.scopes) {
          if (ride.type === 'shopping' && ride.code !== null && ride.scopes.indexOf(`${this.config.parkId}`) === 0) { // Return rides and pois which haven't null
            let area = undefined; // Default, Hotel is included as well which adds new areas
            if (ride.areaId == 10) {
              area = 'Adventureland';
            } else if (ride.areaId == 11) {
              area = 'Kingdom of the Minimoys';
            } else if (ride.areaId == 12) {
              area = 'Germany';
            } else if (ride.areaId == 13) {
              area = 'England';
            } else if (ride.areaId == 14) {
              area = 'France';
            } else if (ride.areaId == 15) {
              area = 'Greece';
            } else if (ride.areaId == 17) {
              area = 'Netherlands';
            } else if (ride.areaId == 19) {
              area = 'Ireland';
            } else if (ride.areaId == 20) {
              area = 'Iceland';
            } else if (ride.areaId == 21) {
              area = 'Italy';
            } else if (ride.areaId == 22) {
              area = 'Luxembourg';
            } else if (ride.areaId == 23) {
              area = 'Austria';
            } else if (ride.areaId == 24) {
              area = 'Portugal';
            } else if (ride.areaId == 25) {
              area = 'Russia';
            } else if (ride.areaId == 26) {
              area = 'Switzerland';
            } else if (ride.areaId == 27) {
              area = 'Scandinavia';
            } else if (ride.areaId == 28) {
              area = 'Spain';
            }

            // Build the merchandise object
            poi[ride.code] = {
              name: ride.name,
              id: `${this.config.name}_` + ride.code,
              location: {
                area: area,
                latitude: ride.latitude,
                longitude: ride.longitude,
              },
              meta: {
                description: ride.description,
                short_description: ride.excerpt,
                type: entityType.merchandise,
              },
            };
          };
        }
      });
      return Promise.resolve(poi);
    }, 1000 * 60 * 60 * this.config.cachepoistime );
  };

  /**
  * Build EuropaPark park Service object
  * This data contains general service names, descriptions etc.
  * @return {string} All EP park Services POIS
  */
  async buildServicePOI() {
    return await this.cache.wrap(`${this.config.parkId}-servicedata`, async () => { // Rebuilding the ride object each time is SLOOOOOWWWW
      const poiData = await this.getPOIS(); // Get the POI Data

      if (!poiData) throw new Error('No PoiData for EuropaPark found!');

      const poi = {};
      poiData.forEach((ride) => {
        if (ride.scopes) {
          if (ride.type === 'service' && ride.code !== null && ride.scopes.indexOf(`${this.config.parkId}`) === 0) { // Return rides and pois which haven't null
            let area = undefined; // Default, Hotel is included as well which adds new areas
            if (ride.areaId == 10) {
              area = 'Adventureland';
            } else if (ride.areaId == 11) {
              area = 'Kingdom of the Minimoys';
            } else if (ride.areaId == 12) {
              area = 'Germany';
            } else if (ride.areaId == 13) {
              area = 'England';
            } else if (ride.areaId == 14) {
              area = 'France';
            } else if (ride.areaId == 15) {
              area = 'Greece';
            } else if (ride.areaId == 17) {
              area = 'Netherlands';
            } else if (ride.areaId == 19) {
              area = 'Ireland';
            } else if (ride.areaId == 20) {
              area = 'Iceland';
            } else if (ride.areaId == 21) {
              area = 'Italy';
            } else if (ride.areaId == 22) {
              area = 'Luxembourg';
            } else if (ride.areaId == 23) {
              area = 'Austria';
            } else if (ride.areaId == 24) {
              area = 'Portugal';
            } else if (ride.areaId == 25) {
              area = 'Russia';
            } else if (ride.areaId == 26) {
              area = 'Switzerland';
            } else if (ride.areaId == 27) {
              area = 'Scandinavia';
            } else if (ride.areaId == 28) {
              area = 'Spain';
            }

            // Build the service object
            poi[ride.code] = {
              name: ride.name,
              id: `${this.config.name}_` + ride.code,
              location: {
                area: area,
                latitude: ride.latitude,
                longitude: ride.longitude,
              },
              meta: {
                description: ride.description,
                short_description: ride.excerpt,
                type: entityType.service,
              },
            };
          };
        }
      });
      return Promise.resolve(poi);
    }, 1000 * 60 * 60 * this.config.cachepoistime );
  };

  /**
  * Get All Queues of EuropaPark park
  * This data contains all the Queues in EuropaPark park, attached with pois above.
  * @return {string} EP POIS with queues
  */
  async getQueue() {
    const token = await this.loginEP();
    const rideData = await this.buildRidePOI();

    return fetch(this.config.apiBase +
      `waitingtimes`,
    {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'JWTAuthorization': token,
      },
    },
    )
        .then((res) => res.json())
        .then((poiData) => {
          const rides = [];
          poiData.waitingtimes.forEach((ridetime) => {
          // Declare default for rides that doesn't fetch right now
            let waitTime = null;
            let state = null;
            let fpstate = null;
            let active = null; // Accepting null as value, since some rides never will join the queue api because they simply never have a queue
            // However, setting '0' & state will set the ride all day closed, which isn't true obviously

            let returntime = undefined;
            if (ridetime.startAt) {
              returntime = {
                start: ridetime.startAt,
                end: ridetime.endAt,
              };
            } else {
              returntime = undefined;
            }

            // TO DO If ride isn't present in this api it just regrets it and returns nothing, however we at least want some tags.
            if (ridetime.time > 0 && ridetime.time < 91) { // Ride is open and there is a queuetime available
              waitTime = ridetime.time;
              state = queueType.operating;
              active = true;
            } else if (ridetime.time === 91) { // Ride is open and queue is over 90 min, basically stupidly long
              waitTime = 91;
              state = queueType.operating;
              active = true;
            } else if (ridetime.time === 333) { // Ride is closed
              waitTime = 0;
              state = queueType.closed;
              active = false;
            } else if (ridetime.time === 666) { // FastPass is temporarily full, will reopen at a later time
              waitTime = 0;
              fpstate = fastPassStatus.temporarilyFull;
              state = queueType.closed;
              active = false;
            } else if (ridetime.time === 777) { // FastPass is full, won't reopen at a later time
              waitTime = 0;
              fpstate = fastPassStatus.finished;
              state = queueType.closed;
              active = false;
            } else if (ridetime.status === 444 || ridetime.time === 555 || ridetime.time === 999) { // Ride is down (down / ice / weather)
              waitTime = 0;
              state = queueType.down;
              active = false;
            } else if (ridetime.time === 222) { // Ride is in maintenance (startup sequence is listed as maint as well lol)
              waitTime = 0;
              state = queueType.refurbishment;
              active = false;
            } else {
              waitTime = 0;
              state = queueType.closed;
              active = false;
            }

            if (rideData[ridetime.code]) { // Skip null variables
              rideData[ridetime.code].waitTime = waitTime;
              rideData[ridetime.code].state = state;
              rideData[ridetime.code].active = active;

              const rideobj = {
                name: rideData[ridetime.code].name,
                id: rideData[ridetime.code].id,
                waitTime: rideData[ridetime.code].waitTime,
                status: rideData[ridetime.code].state,
                active: rideData[ridetime.code].active,
                location: {
                  area: rideData[ridetime.code].location.area,
                  latitude: rideData[ridetime.code].location.latitude,
                  longitude: rideData[ridetime.code].location.longitude,
                },
                fastPass: {
                  status: fpstate,
                  returnTime: returntime,
                  isVirtQueue: rideData[ridetime.code].meta.isVirtQueue,
                  fastPass: rideData[ridetime.code].meta.fastPass,
                  parent: rideData[ridetime.code].meta.parent,
                },
                meta: {
                  descriptions: {
                    description: rideData[ridetime.code].meta.description,
                    short_description: rideData[ridetime.code].meta.short_description,
                  },
                  type: rideData[ridetime.code].meta.type,
                  single_rider: rideData[ridetime.code].meta.single_rider,
                  tags: rideData[ridetime.code].meta.tags,
                  restrictions: rideData[ridetime.code].meta.restrictions,
                },
              };
              rides.push(rideobj);
            }
          });
          return Promise.resolve(rides);
        });
  };

  /**
  * Get All Operating Hours of EuropaPark
  * This data contains all the Operating Hours in EuropaPark, fetched with currentyear.
  * @return {string} EP park hours
  */
  async getHours() {
    return await this.cache.wrap('season', async () => {
      const token = await this.loginEP();

      return fetch(
          this.config.apiBase +
            `seasons/en`,
          {
            method: 'GET',
            headers: {
              'Content-Type': 'application/json',
              'JWTAuthorization': token,
            },
          },
      )
          .then((res) => res.json())
          .then((json) => {
            return Promise.resolve(json);
          });
    }, 1000 * 60 * 60 * 12 );
  };

  /**
  * Get All Operating Hours of EuropaPark
  * This data contains all the Operating Hours in EuropaPark.
  * @return {string} EP park hours
  * @param {moment} date
  */
  async fetchHours(date) {
    const cal = await this.getHours();
    const parkTimes = cal.seasons.filter(
        // Filter hours in this really strange useless API.
        (x) => !x.closed && x.scopes.indexOf(this.config.parkId) >= 0,
    ).find(
        // Finding valid data
        (x) => date.isBetween(x.startAt, x.endAt, 'day'),
    );

    if (parkTimes !== undefined) {
      const times = [];

      const buildDateString = (inDate) => {
        return moment.tz(inDate, this.config.timezone).set({
          year: date.year(),
          month: date.month(),
          date: date.date(),
        }).format();
      };

      times.push({
        openingTime: buildDateString(parkTimes.startAt),
        closingTime: buildDateString(parkTimes.endAt),
        type: scheduleType.operating,
      });

      // EP provides Hotel Hours
      if (parkTimes.hotelStartAt && parkTimes.hotelEndAt) {
        times.push({
          openingTime: buildDateString(parkTimes.hotelStartAt),
          closingTime: buildDateString(parkTimes.hotelEndAt),
          type: scheduleType.extraHours,
          description: 'Open To Hotel Guests',
        });
      }

      return times;
    }

    return undefined;
  };

  /**
   * Generate the calendar objects
   * @return{object} Object keyed to dates in YYYY-MM-DD format.
   * Each date entry will contain an array of operating hours.
   */
  async buildHours() {
    try {
      // Populate the time object from yesterday on, plus 60 days from no
      const yesterday = this.getTimeNowMoment().subtract(1, 'days');

      const endFillDate = yesterday.clone().add(60 + 1, 'days');

      const now = this.getTimeNowMoment();

      const dates = {};
      // Loop over each day and populate the object with the data
      for (let date = yesterday; date.isSameOrBefore(endFillDate); date.add(1, 'day')) {
        const hours = await this.fetchHours(date);
        if (hours !== undefined) {
          if (!Array.isArray(hours)) {
            this.emit(
                'error',
                new Error(
                    `Hours for ${this.name} date ${date.format('YYYY-MM-DD')} returned invalid non-Array ${JSON.stringify(hours)}`,
                ),
            );
            continue;
          }
          // Ignore if times are from the past, since no one would need that
          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);
    }

    // Nothing is returned, park is probably closed or sth.
    return undefined;
  };

  /**
   * 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 getOpHours() {
    const dates = await this.buildHours();
    const calendar = [];

    Object.keys(dates).forEach((date) => {
      let open = null;
      let close = null;
      let type = null;

      let sopen = null;
      let sclose = null;
      let stype = null;
      let sdesc = null;

      Object.keys(dates[date]).forEach((date123) => {
        if (dates[date][date123].type === 'Operating') {
          open = dates[date][date123].openingTime;
          close = dates[date][date123].closingTime;
          type = dates[date][date123].type;
        } else if (dates[date][date123].type === 'Extra Hours') {
          sopen = dates[date][date123].openingTime;
          sclose = dates[date][date123].closingTime;
          stype = dates[date][date123].type;
          sdesc = dates[date][date123].description;
        }

        calendar.push({
          date: date,
          type: type,
          openingTime: open,
          closingTime: close,
          special: [
            {
              openingTime: sopen,
              closingTime: sclose,
              type: stype,
              description: sdesc,
            },
          ],
        });
      });
    });
    return calendar;
  }
}

export default EuropaParkBase;