<?PHP
#
#   FILE:  CalendarEvents_Event.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2013 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu/cwis/
#

/**
* Represents an event resource.
*/
class CalendarEvents_Event extends Resource
{

    /**
    * The format for a date for display.
    */
    const DATE_FOR_DISPLAY = 'F jS, Y';

    /**
    * The format for a date/time for display.
    */
    const DATETIME_FOR_DISPLAY = 'F jS, Y \a\t g:i A';

    /**
    * The format for a time for display.
    */
    const TIME_FOR_DISPLAY = 'g:i A';

    /**
    * The format for a date for machine parsing.
    */
    const DATE_FOR_PARSING = 'Y-m-d';

    /**
    * The format for a date/time for machine parsing.
    */
    const DATETIME_FOR_PARSING = 'c';

    /**
    * The URL to the Google Maps service.
    */
    const MAP_URL = "http://maps.google.com/";

    /**
    * The URL to the Google Maps static map API.
    */
    const STATIC_MAP_URL = "http://maps.googleapis.com/maps/api/staticmap";

    /**
    * Get the URL to the event relative to the CWIS root.
    * @param array $Get Optional GET parameters to add.
    * @param string $Fragment Optional fragment ID to add.
    * @return Returns the URL to the event relative to the CWIS root.
    */
    public function EventUrl(array $Get=array(), $Fragment=NULL)
    {
        # if clean URLs are available
        if ($GLOBALS["AF"]->HtaccessSupport())
        {
            $Plugin = $GLOBALS["G_PluginManager"]->GetPlugin("CalendarEvents");

            # base part of the URL
            $Url = $Plugin->CleanUrlPrefix() . "/" . urlencode($this->Id()) . "/";

            # add the title
            $Url .= urlencode($this->TitleForUrl());
        }

        # clean URLs aren't available
        else
        {
            # base part of the URL
            $Url = "index.php";

            # add the page to the GET parameters
            $Get["P"] = "P_CalendarEvents_Event";
            $Get["EntryId"] = $this->Id();
        }

        # tack on the GET parameters, if necessary
        if (count($Get))
        {
            $Url .= "?" . http_build_query($Get);
        }

        # tack on the fragment identifier, if necessary
        if (!is_null($Fragment))
        {
            $Url .= "#" . urlencode($Fragment);
        }

        return $Url;
    }

    /**
    * Get the URL to the event in iCalendar format relative to the CWIS root.
    * @param array $Get Optional GET parameters to add.
    * @param string $Fragment Optional fragment ID to add.
    * @return Returns the URL to the event in iCalendar format relative to the
    *      CWIS root.
    */
    public function iCalUrl(array $Get=array(), $Fragment=NULL)
    {
        # if clean URLs are available
        if ($GLOBALS["AF"]->HtaccessSupport())
        {
            $Plugin = $GLOBALS["G_PluginManager"]->GetPlugin("CalendarEvents");

            # base part of the URL
            $Url = $Plugin->CleanUrlPrefix() . "/ical/" . urlencode($this->Id()) . "/";

            # add the title
            $Url .= urlencode($this->TitleForUrl());
        }

        # clean URLs aren't available
        else
        {
            # base part of the URL
            $Url = "index.php";

            # add the page to the GET parameters
            $Get["P"] = "P_CalendarEvents_iCal";
            $Get["EntryId"] = $this->Id();
        }

        # tack on the GET parameters, if necessary
        if (count($Get))
        {
            $Url .= "?" . http_build_query($Get);
        }

        # tack on the fragment identifier, if necessary
        if (!is_null($Fragment))
        {
            $Url .= "#" . urlencode($Fragment);
        }

        return $Url;
    }

    /**
    * Get the URL to the Google Maps service centered on the event location.
    * @return Returns a URL to the map for the event or NULL if there is no
    *      location to map.
    */
    public function MapUrl()
    {
        $Location = $this->LocationString();

        # return NULL if there is no location to map
        if (!$Location)
        {
            return NULL;
        }

        return self::MAP_URL . "?" . http_build_query(array("q" => $Location));
    }

    /**
    * Get the URL to the image to use for the static map for the event.
    * @param int $Width Optional width parameter.
    * @param int $Height Optional height parameter.
    * @return Returns a URL to the static map for the event or NULL if there is
    *      no location to map.
    */
    public function StaticMapUrl($Width=500, $Height=300)
    {
        $Coordinates = $this->Get("Coordinates");

        # use the coordinates if possible
        if ($Coordinates["X"] && $Coordinates["Y"])
        {
            $Location = $Coordinates["X"] . ", " . $Coordinates["Y"];
        }

        # otherwise concatenate the parts of the location
        else
        {
            $Location = $this->LocationString();
        }

        # return NULL if there is no location to map
        if (!$Location)
        {
            return NULL;
        }

        $QueryParameters = array(
            "center" => $Location,
            "markers" => $Location,
            "size" => $Width."x".$Height,
            "sensor" => "false");

        return self::STATIC_MAP_URL . "?" . http_build_query($QueryParameters);
    }

    /**
    * Get the URL to the image to use for the national static map for the event.
    * @param int $Width Optional width parameter.
    * @param int $Height Optional height parameter.
    * @return Returns a URL to the static map for the event or NULL if there is
    *      no location to map.
    */
    public function NationalStaticMapUrl($Width=200, $Height=200)
    {
        $Coordinates = $this->Get("Coordinates");

        # use the coordinates if possible
        if ($Coordinates["X"] && $Coordinates["Y"])
        {
            $Location = $Coordinates["X"] . ", " . $Coordinates["Y"];
        }

        # otherwise concatenate the parts of the location
        else
        {
            $Location = $this->LocationString();
        }

        # return NULL if there is no location to map
        if (!$Location)
        {
            return NULL;
        }

        $QueryParameters = array(
            "center" => $Location,
            "markers" => $Location,
            "size" => $Width."x".$Height,
            "zoom" => 3,
            "sensor" => "false");

        return self::STATIC_MAP_URL . "?" . http_build_query($QueryParameters);
    }

    /**
    * Get the best URL to use for the event, meaning the Url field when set and
    * the URL to the event record otherwise.
    * @return Returns the best URL to use for the event.
    */
    public function GetBestUrl()
    {
        $Url = $this->Get("Url");

        if ($Url)
        {
            return $Url;
        }

        return OurBaseUrl() . $this->EventUrl();
    }

    /**
    * Determine if the event occurs in the future.
    * @return Returns TRUE if the event occurs in the future.
    */
    public function IsInFuture()
    {
        # make the date precise only to the day if the event occurs all day
        if ($this->Get("All Day"))
        {
            $Date = date("Y-m-d", strtotime($this->Get("Start Date")));
            return strtotime($Date) > time();
        }

        return $this->StartDateAsObject()->getTimestamp() > time();
    }

    /**
    * Determine if the event is currently occuring.
    * @return Returns TRUE if the event is currently occuring.
    */
    public function IsOccurring()
    {
        return !($this->IsInFuture() || $this->IsInPast());
    }

    /**
    * Determine if the event occurs in the past.
    * @return Returns TRUE if the event occurs in the past.
    */
    public function IsInPast()
    {
        # make the date precise only to the day if the event occurs all day
        if ($this->Get("All Day"))
        {
            $Date = date("Y-m-d", strtotime($this->Get("End Date")));
            return strtotime($Date) < time();
        }

        return $this->EndDateAsObject()->getTimestamp() < time();
    }

    /**
    * Determine if the event starts at some point today.
    * @return Returns TRUE if the event starts at some point today.
    */
    public function StartsToday()
    {
        return date("Y-m-d") == date("Y-m-d", strtotime($this->Get("Start Date")));
    }

    /**
    * Determine if the event ends at some point today.
    * @return Returns TRUE if the event ends at some point today.
    */
    public function EndsToday()
    {
        return date("Y-m-d") == date("Y-m-d", strtotime($this->Get("End Date")));
    }

    /**
    * Get the start date as a DateTime object.
    * @return Returns the start date as a DateTime object.
    */
    public function StartDateAsObject()
    {
        return $this->ConvertDateStringToObject($this->Get("Start Date"));
    }

    /**
    * Get the end date as a DateTime object.
    * @return Returns the end date as a DateTime object.
    */
    public function EndDateAsObject()
    {
        return $this->ConvertDateStringToObject($this->Get("End Date"));
    }

    /**
    * Get the creation date as a DateTime object.
    * @return Returns the creation date as a DateTime object.
    */
    public function CreationDateAsObject()
    {
        return $this->ConvertDateStringToObject($this->Get("Date of Creation"));
    }

    /**
    * Get the modification date as a DateTime object.
    * @return Returns the modification date as a DateTime object.
    */
    public function ModificationDateAsObject()
    {
        return $this->ConvertDateStringToObject($this->Get("Date of Modification"));
    }

    /**
    * Get the release date as a DateTime object.
    * @return Returns the release date as a DateTime object.
    */
    public function ReleaseDateAsObject()
    {
        return $this->ConvertDateStringToObject($this->Get("Release Date"));
    }

    /**
    * Get the categories field value for displaying to users.
    * @return Returns the categories field value for display to users.
    */
    public function CategoriesForDisplay()
    {
        $ControlledNames = $this->Get("Categories", TRUE);

        # there are no categories assigned to the event
        if (is_null($ControlledNames))
        {
            return array();
        }

        $Categories = array();

        foreach ($ControlledNames as $Id => $Category)
        {
            $Categories[$Id] = $Category->Name();
        }

        return $Categories;
    }

    /**
    * Get the attachments field value for displaying to users.
    * @return Returns the attachments field value for display to users.
    */
    public function AttachmentsForDisplay()
    {
        $Attachments = array();

        foreach ($this->Get("Attachments", TRUE) as $Id => $Attachment)
        {
            $Attachments[$Id] = array($Attachment->Name(), $Attachment->GetLink());
        }

        return $Attachments;
    }

    /**
    * Get the release flage field value for displaying to users.
    * @return Returns the release flag field value for display to users.
    */
    public function ReleaseFlagForDisplay()
    {
        $Schema = new MetadataSchema($this->SchemaId());
        $Field = $Schema->GetFieldByName("Release Flag");

        if ($this->Get("Release Flag"))
        {
            return $Field->FlagOnLabel();
        }

        return $Field->FlagOffLabel();
    }

    /**
    * Get the start date field value for displaying to users.
    * @return Returns the start date field value for display to users.
    */
    public function StartDateForDisplay()
    {
        if ($this->Get("All Day"))
        {
            $DateRange = GetPrettyDateRangeInParts(
                $this->Get("Start Date"),
                $this->Get("End Date"));

            return $DateRange["Start"];
        }

        return str_replace('12:00am', '',
                           GetPrettyTimestamp($this->Get("Start Date"), FALSE) );
    }

    /**
    * Get the end date field value for displaying to users.
    * @return Returns the end date field value for display to users.
    */
    public function EndDateForDisplay()
    {
        if ($this->Get("All Day"))
        {
            $DateRange = GetPrettyDateRangeInParts(
                $this->Get("Start Date"),
                $this->Get("End Date"));

            return $DateRange["End"];
        }

        return str_replace('12:00am', '',
                           GetPrettyTimestamp($this->Get("End Date"), FALSE) );
    }

    /**
    * Get the start date time for displaying to users.
    * @return Returns the start date time for display to users.
    */
    public function StartDateTimeForDisplay()
    {
        return str_replace('12:00am', '',
                           GetPrettyTimestamp($this->Get("Start Date")) );
    }

    /**
    * Get the end date time for displaying to users.
    * @return Returns the end date time for display to users.
    */
    public function EndDateTimeForDisplay()
    {
        return str_replace('12:00am', '',
                           GetPrettyTimestamp($this->Get("End Date")) ) ;
    }

    /**
    * Get the author field value for displaying to users.
    * @return Returns the author field value for display to users.
    */
    public function AuthorForDisplay()
    {
        return $this->FormatUserNameForDisplay($this->Get("Authored By", TRUE));
    }

    /**
    * Get the editor field value for displaying to users.
    * @return Returns the editor field value for display to users.
    */
    public function EditorForDisplay()
    {
        return $this->FormatUserNameForDisplay($this->Get("Last Modified By", TRUE));
    }

    /**
    * Get the creation date field value for displaying to users.
    * @return Returns the creation date field value for display to users.
    */
    public function CreationDateForDisplay()
    {
        return GetPrettyTimestamp( $this->Get("Date of Creation") );
    }

    /**
    * Get the modification date field value for displaying to users.
    * @return Returns the modification date field value for display to users.
    */
    public function ModificationDateForDisplay()
    {
        return GetPrettyTimestamp( $this->Get("Date of Modification") );
    }

    /**
    * Get the release date field value for displaying to users.
    * @return Returns the release date field value for display to users.
    */
    public function ReleaseDateForDisplay()
    {
        return GetPrettyTimestamp( $this->Get("Release Date") );
    }

    /**
    * Get the start date field value for machine parsing.
    * @return Returns the start date field value for machine parsing.
    */
    public function StartDateForParsing()
    {
        $Format = $this->Get("All Day")
            ? self::DATE_FOR_PARSING : self::DATETIME_FOR_PARSING;
        return $this->StartDateAsObject()->format($Format);
    }

    /**
    * Get the end date field value for machine parsing.
    * @return Returns the end date field value for machine parsing.
    */
    public function EndDateForParsing()
    {
        $Format = $this->Get("All Day")
            ? self::DATE_FOR_PARSING : self::DATETIME_FOR_PARSING;
        return $this->EndDateAsObject()->format($Format);
    }

    /**
    * Get the entire location as a space-separated string.
    * @return Returns the entire location as a space-separated string
    */
    public function LocationString()
    {
        # start out with an empty location string
        $Location = "";

        # add the venue name if given
        if ($this->Get("Venue"))
        {
            $Location .= $this->Get("Venue") . " ";
        }

        # add the street address if given
        if ($this->Get("Street Address"))
        {
            $Location .= $this->Get("Street Address") . " ";
        }

        # add the locality if given
        if ($this->Get("Locality"))
        {
            $Location .= $this->Get("Locality") . " ";
        }

        # add the region if given
        if ($this->Get("Region"))
        {
            $Location .= $this->Get("Region") . " ";
        }

        # add the postal code if given
        if ($this->Get("Postal Code"))
        {
            $Location .= $this->Get("Postal Code") . " ";
        }

        # add the country if given
        if ($this->Get("Country"))
        {
            $Location .= $this->Get("Country");
        }

        # remove trailing whitespace
        $Location = trim($Location);

        # return the location
        return $Location;
    }

    /**
     * Get the summary of the event location in the order of Locality, Reigion
     * -> Country.
     *
     * @return Summary of the event location, or empty string if none provided.
     */
    public function LocationSummaryString()
    {
        if ($this->Get("Region") && $this->Get("Locality"))
        {
            return $this->Get("Locality") . ", " . $this->Get("Region");
        }
        elseif ($this->Get("Country"))
        {
            return $this->Get("Country");
        }
        else
        {
            return "";
        }
    }

    /**
    * Get the location of the event as one line of plain text with most tokens
    * separated by commas as in normal US format.
    * @return The location of the event as one line of plain text.
    */
    public function OneLineLocation()
    {
        # start out with an empty location string
        $Location = "";

        # add the venue name if given
        if ($this->Get("Venue"))
        {
            $Location .= $this->Get("Venue") . ", ";
        }

        # don't add the street address if given
        if ($this->Get("Street Address"))
        {
            $Location .= $this->Get("Street Address") . ", ";
        }

        # add the locality if given
        if ($this->Get("Locality"))
        {
            $Location .= $this->Get("Locality") . ", ";
        }

        # add the region if given
        if ($this->Get("Region"))
        {
            $Suffix = $this->Get("Postal Code") ? " " : ", ";
            $Location .= $this->Get("Region") . $Suffix;
        }

        # add the postal code if given
        if ($this->Get("Postal Code"))
        {
            $Location .= $this->Get("Postal Code") . ", ";
        }

        # add the country if given
        if ($this->Get("Country"))
        {
            $Location .= $this->Get("Country");
        }

        # remove trailing whitespace and commas
        $Location = trim($Location, " \t\n\r\0\x0B,");

        # return the location
        return $Location;
    }

    /**
    * Get the entire location as HTML.
    * @return Returns the entire location as HTML.
    */
    public function LocationForHtml()
    {
        # start out with an empty location string
        $Location = "";

        # add the venue name if given
        if ($this->Get("Venue"))
        {
            $Location .=
                '<span class="calendar_events-venue" itemprop="name">'
                .defaulthtmlentities($this->Get("Venue"))
                .'</span>';
        }

        # add the street address if given
        if ($this->Get("Street Address"))
        {
            $Location .=
                '<span class="calendar_events-street_address" itemprop="streetAddress">'
                .defaulthtmlentities($this->Get("Street Address"))
                .'</span>';
        }

        # add the locality if given
        if ($this->Get("Locality"))
        {
            $Location .=
                '<span class="calendar_events-locality" itemprop="addressLocality">'
                .defaulthtmlentities($this->Get("Locality"))
                .'</span>';
        }

        # add the region if given
        if ($this->Get("Region"))
        {
            $Location .=
                '<span class="calendar_events-region" itemprop="addressRegion">'
                .defaulthtmlentities($this->Get("Region"))
                .'</span>';
        }

        # add the postal code if given, but only if there's a locality or region
        if ($this->Get("Postal Code") && ($this->Get("Locality") || $this->Get("Region")))
        {
            $Location .=
                '<span class="calendar_events-postal_code" itemprop="postalCode">'
                .defaulthtmlentities($this->Get("Postal Code"))
                .'</span>';
        }

        # add the country if given
        if ($this->Get("Country"))
        {
            $Location .=
                '<span class="calendar_events-country" itemprop="addressCountry">'
                .defaulthtmlentities($this->Get("Country"))
                .'</span>';
        }

        # wrap the address if anything has been added so far
        if (strlen($Location) > 0)
        {
            $Location = '<span class="calendar_events-location" itemprop="location"'
                            .' itemscope="itemscope" itemtype="http://schema.org/Place">'
                          .$Location
                       .'</span>';
        }

        # return the location
        return $Location;
    }

    /**
    * Get the span of the event in days.
    * @return Returns the span of the event in days.
    */
    public function SpanInDays()
    {
        $StartDate = $this->StartDateAsObject();
        $EndDate = $this->EndDateAsObject();

        # get the difference between the dates
        $Difference = intval($EndDate->diff($StartDate, TRUE)->format('%a'));

        # set the value to two days if the difference is less than a day but the
        # interval crosses a day boundary
        if ($Difference === 0 && $StartDate->format("d") != $EndDate->format("d"))
        {
            return 2;
        }

        # otherwise the difference is plus one day because the interval doesn't
        # account for the event happening throughout the day
        return $Difference + 1;
    }

    /**
    * Get the title field value for inserting into a URL.
    * @return Returns the title field value for inserting into a URL.
    */
    public function TitleForUrl()
    {
        $SafeTitle = str_replace(" ", "-", $this->Get("Title"));
        $SafeTitle = preg_replace('/[^a-zA-Z0-9-]/', "", $SafeTitle);
        $SafeTitle = strtolower(trim($SafeTitle));

        return $SafeTitle;
    }

    /**
    * Get the date prefix for the start date field value for displaying to
    * users.
    * @return Returns the date prefix for the start date field value for
    *      displaying to users.
    */
    public function StartDateDisplayPrefix()
    {
        # use the date prefix if it's a full-day event
        if ($this->Get("All Day"))
        {
            return $this->GetDatePrefix($this->Get("Start Date"));
        }

        # otherwise use the timestamp prefix
        return $this->GetTimestampPrefix($this->Get("Start Date"));
    }

    /**
    * Get the date prefix for the end date field value for displaying to
    * users.
    * @return Returns the date prefix for the end date field value for
    *      displaying to users.
    */
    public function EndDateDisplayPrefix()
    {
        # use the date prefix if it's a full-day event
        if ($this->Get("All Day"))
        {
            return $this->GetDatePrefix($this->Get("End Date"));
        }

        # otherwise use the timestamp prefix
        return $this->GetTimestampPrefix($this->Get("End Date"));
    }

    /**
    * Format a user's name for display, using the real name if available and the
    * user name otherwise.
    * @param User $User User of which to get the name.
    * @return Returns the user's name for display.
    */
    protected function FormatUserNameForDisplay($User)
    {
        $User = array_shift($User);

        # the user isn't set
        if (!($User instanceof User))
        {
            return "-";
        }

        # the user is invalid
        if ($User->Status() !== U_OKAY)
        {
            return "-";
        }

        # get the real name or user name if it isn't available
        $BestName = $User->GetBestName();

        # blank best name
        if (!strlen($BestName))
        {
            return "-";
        }

        return $BestName;
    }

    /**
    * Get the date prefix for a timestamp for displaying to users, e.g., "on",
    * "at", etc.
    * @param string $Timestamp Timestamp for which to get a date prefix
    * @return Returns the date prefix for a timestamp.
    */
    protected function GetDatePrefix($Timestamp)
    {
        # convert timestamp to seconds
        $Timestamp = strtotime($Timestamp);

        # invalid timestamp
        if ($Timestamp === FALSE)
        {
            return "";
        }

        # up until yesterday
        if ($Timestamp < strtotime("yesterday"))
        {
            return "";
        }

        # before yesterday
        return "on";
    }

    /**
    * Get the date prefix for a timestamp for displaying to users, e.g., "on",
    * "at", etc.
    * @param string $Timestamp Timestamp for which to get a date prefix
    * @return Returns the date prefix for a timestamp.
    */
    protected function GetTimestampPrefix($Timestamp)
    {
        # convert timestamp to seconds
        $Timestamp = strtotime($Timestamp);

        # invalid timestamp
        if ($Timestamp === FALSE)
        {
            return "";
        }

        # today
        if (date("z Y", $Timestamp) == date("z Y"))
        {
            return "at";
        }

        # yesterday
        if (date("n/j/Y", ($Timestamp - (24 * 60 * 60))) == date("n/j/Y"))
        {
            return "";
        }

        # before yesterday
        return "on";
    }

    /**
    * Convert a date or date/time string to a DateTime object.
    * @param string $DateOrDateTime A date or date/time string.
    * @param string $TZID An optional time zone ID.
    * @return Returns a DateTime object.
    */
    protected function ConvertDateStringToObject($DateOrDateTime, $TZID=NULL)
    {
        # if a time zone ID isn't available
        if (is_null($TZID))
        {
            return new DateTime($DateOrDateTime);
        }

        # a time zone ID is available, so use it
        else
        {
            try
            {
                return new DateTime($DateOrDateTime, new DateTimeZone($TZID));
            }

            # the time zone ID is invalid, so just use the date
            catch (Exception $Exception)
            {
                return new DateTime($DateOrDateTime);
            }
        }
    }

    /**
    * Set database access values; overridden here because Events are
    * stored in the Resources table.
    *@param string $ClassName Class name to set values for.
    */
    static protected function SetDatabaseAccessValues($ClassName)
    {
        if (!isset(self::$ItemIdColumnNames[$ClassName]))
        {
            self::$ItemIdColumnNames[$ClassName] = "ResourceId";
            self::$ItemNameColumnNames[$ClassName] = "ResourceName";
            self::$ItemTableNames[$ClassName] = "Resources";
        }
    }
}
