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

/**
* Represents a blog entry resource.
*/
class Blog_Entry extends Resource
{

    /**
    * Regular expression that will match a blank line, i.e., a vertical
    * whitespace character or a sequence of HTML that is rendered as whitespace.
    */
    const BLANK_LINE_REGEX = '(<br ?\/?>|<p>(&nbsp;|\s|\xC2\xA0)*<\/p>|\r\n|\r|\n)';

    /**
    * Get the blog entry comments in ascending order.
    * @return Returns an array of blog entry comments as Message objects.
    */
    public function Comments()
    {
        # read in comments if not already loaded
        if (!isset($this->Comments))
        {
            $Database = new Database();
            $Database->Query("
                SELECT MessageId FROM Messages
                WHERE ParentId = ".$this->Id()."
                AND ParentType = 2
                ORDER BY DatePosted ASC");

            while ($MessageId = $Database->FetchField("MessageId"))
            {
                $this->Comments[] = new Message($MessageId);
            }
        }

        # return array of comments to caller
        return $this->Comments;
    }

    /**
    * Get the URL to the blog entry 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 blog entry relative to the CWIS root.
    */
    public function EntryUrl(array $Get=array(), $Fragment=NULL)
    {
        $Blog = $GLOBALS["G_PluginManager"]->GetPlugin("Blog");
        $UrlPrefix = $Blog->BlogSetting($this->GetBlogId(), "CleanUrlPrefix");

        # if clean URLs are available
        if ($GLOBALS["AF"]->HtaccessSupport() && strlen($UrlPrefix)>0)
        {
            # base part of the URL
            $Url = $UrlPrefix . "/" . urlencode($this->Id()) . "/";

            # add the title if the blog entry is okay
            if ($this->Status() === 1)
            {
                $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_Blog_Entry";
            $Get["ID"] = $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 title field value for the blog entry.
    * @return Returns the blog entry title.
    */
    public function Title()
    {
        return $this->Get(Blog::TITLE_FIELD_NAME);
    }

    /**
    * Get the body field value for the blog entry. This removes the teaser
    * marker, if found.
    * @param int $MaxTeaserLength The maximum teaser length, used to remove the
    *      teaser marker from the body.
    * @return Returns the blog entry body.
    */
    public function Body($MaxTeaserLength=1200)
    {
        $Body = $this->Get(Blog::BODY_FIELD_NAME);
        $EndOfTeaserPosition = $this->GetEndOfTeaserPosition($MaxTeaserLength);

        # just return the body as-is if a teaser marker wasn't found
        if ($EndOfTeaserPosition === FALSE)
        {
            return $Body;
        }

        # get the teaser
        $Teaser = substr($Body, 0, $EndOfTeaserPosition);
        $RestOfBody = substr($Body, $EndOfTeaserPosition);

        # replace the teaser marker from the rest of the body
        $RestOfBody = preg_replace(
            '/'.self::BLANK_LINE_REGEX.'{5,}/',
            "\n\n",
            $RestOfBody,
            1);

        # put the teaser and the rest of the body back together and return
        return $Teaser . $RestOfBody;
    }

    /**
    * Get the body field value for the blog entry but truncated. This tries to
    * use paragraph tags first, then multiple newlines second, and
    * NeatlyTruncateString() last.
    * @param int $MaxLength The maximum length of the teaser.
    * @return Returns the truncated blog entry body.
    */
    public function Teaser($MaxLength=1200)
    {
        $Body = $this->Get(Blog::BODY_FIELD_NAME);
        $Position = $this->GetEndOfTeaserPosition($MaxLength);

        # if a good position could be found
        if ($Position !== FALSE)
        {
            return substr($Body, 0, $Position);
        }

        return NeatlyTruncateString(trim($Body), $MaxLength);
    }

    /**
    * Get the best image insertion point for the blog entry.
    * @param int $MaxLength The maximum length of the teaser.
    * @return Returns the best image insertion point of the blog entry.
    */
    public function GetImageInsertionPoint($MaxLength=1200)
    {
        $Position = $this->GetEndOfTeaserPosition($MaxLength);

        # if a good position was found
        if ($Position !== FALSE)
        {
            return $Position;
        }

        # just return the beginning if a good position wasn't found
        return 0;
    }

    /**
    * Get the best image insertion points for the blog entry.
    * @return Returns the best image insertion points of the blog entry.
    */
    public function GetImageInsertionPoints()
    {
        return $this->GetAllImageMarkerPositions();
    }

    /**
    * Determine if the image for the blog entry has an associated caption.
    * @param SPTImage $Image An image to use for context.
    * @return Returns TRUE if there is a caption and FALSE otherwise.
    */
    public function HasCaption(SPTImage $Image=NULL)
    {
        $Schema = new MetadataSchema($this->SchemaId());
        $ImageField = $Schema->GetFieldByName(Blog::IMAGE_FIELD_NAME);

        # get the default image if not given an image for context
        if (is_null($Image))
        {
            $Image = $this->Image();
        }

        $DefaultAltText = $ImageField->DefaultAltText();
        $AltText = $Image->AltText();

        return $AltText != $DefaultAltText && strlen(trim($AltText));
    }

    /**
    * Get the author field value for the blog entry.
    * @return Returns the blog entry author as a User object.
    */
    public function Author()
    {
        return $this->Get(Blog::AUTHOR_FIELD_NAME, TRUE);
    }

    /**
    * Get the editor field value for the blog entry.
    * @return Returns the last editor of the blog entry as a User object.
    */
    public function Editor()
    {
        return $this->Get(Blog::EDITOR_FIELD_NAME, TRUE);
    }

    /**
    * Get the creation date field value for the blog entry.
    * @return Returns the blog entry creation date.
    */
    public function CreationDate()
    {
        return $this->Get(Blog::CREATION_DATE_FIELD_NAME);
    }

    /**
    * Get the modification date field value for the blog entry.
    * @return Returns the blog entry modification date.
    */
    public function ModificationDate()
    {
        return $this->Get(Blog::MODIFICATION_DATE_FIELD_NAME);
    }

    /**
    * Get the publication date field value for the blog entry.
    * @return Returns the blog entry publication date.
    */
    public function PublicationDate()
    {
        return $this->Get(Blog::PUBLICATION_DATE_FIELD_NAME);
    }

    /**
    * Get the categories field value for the blog entry.
    * @return Returns the blog entry categories as an array of ControlledName
    *     objects.
    */
    public function Categories()
    {
        $Categories = $this->Get(Blog::CATEGORIES_FIELD_NAME, TRUE);
        return is_null($Categories) ? array() : $Categories;
    }

    /**
    * Get the the first image field value for the blog entry.
    * @return Returns the blog entry image as an Image object.
    */
    public function Image()
    {
        $Images = $this->Images();
        return count($Images) ? array_shift($Images) : NULL;
    }

    /**
    * Get the image field value for the blog entry.
    * @return Returns the blog entry image as an Image object.
    */
    public function Images()
    {
        return $this->Get(Blog::IMAGE_FIELD_NAME, TRUE);
    }

    /**
     * Get the Blog name that this entry is from
     * @return Returns the blog name as string
     */
    public function BlogName()
    {
        return current($this->Get(Blog::BLOG_NAME_FIELD_NAME));
    }

    /**
    * Get the title field value for displaying to users.
    * @return Returns the title field value for display to users.
    */
    public function TitleForDisplay()
    {
        $SafeTitle = StripTagsAttributes($this->Title(), array(
                "StripTags" => TRUE, "StripAttributes" => TRUE,
                "Tags" => "b i u s em del sub sup br"));
        return $SafeTitle;
    }

    /**
    * Get the body field value for displaying to users.
    * @return Returns the body field value for display to users.
    */
    public function BodyForDisplay()
    {
        return $this->Body();
    }

    /**
    * 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->Author());
    }

    /**
    * 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->Editor());
    }

    /**
    * 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 $this->FormatTimestampForDisplay($this->CreationDate());
    }

    /**
    * 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 $this->FormatTimestampForDisplay($this->ModificationDate());
    }

    /**
    * Get the publication date field value for displaying to users.
    * @return Returns the publication date field value for display to users.
    */
    public function PublicationDateForDisplay()
    {
        return $this->FormatTimestampForDisplay($this->PublicationDate());
    }

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

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

        return $Categories;
    }

    /**
    * Get the first image field value for displaying to users.
    * @return Returns the image field value for display to users.
    */
    public function ImageForDisplay()
    {
        return $this->Image()->PreviewUrl();
    }

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

        foreach ($this->Images() as $Image)
        {
            $Images[] = $Image->PreviewUrl();
        }

        return $Images;
    }

    /**
    * Get the image field value as a thumbnail for displaying to users.
    * @return Returns the image field value as a thumbnail for display to users.
    */
    public function ThumbnailForDisplay()
    {
        return $this->Image()->ThumbnailUrl();
    }

    /**
    * Get the image field alt value for displaying to users.
    * @return Returns the image field alt value for display to users.
    */
    public function ImageAltForDisplay()
    {
        return $this->Image()->AltText();
    }

    /**
    * Get the creation date field value for machine parsing.
    * @return Returns the creation date field value for machine parsing.
    */
    public function CreationDateForParsing()
    {
        return $this->FormatTimestampForParsing($this->CreationDate());
    }

    /**
    * Get the modification date field value for machine parsing.
    * @return Returns the modification date field value for machine parsing.
    */
    public function ModificationDateForParsing()
    {
        return $this->FormatTimestampForParsing($this->ModificationDate());
    }

    /**
    * Get the publication date field value for machine parsing.
    * @return Returns the publication date field value for machine parsing.
    */
    public function PublicationDateForParsing()
    {
        return $this->FormatTimestampForParsing($this->PublicationDate());
    }

    /**
    * 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 = strip_tags($this->Title());
        $SafeTitle = str_replace(" ", "-", $SafeTitle);
        $SafeTitle = preg_replace('/[^a-zA-Z0-9-]/', "", $SafeTitle);
        $SafeTitle = strtolower(trim($SafeTitle));

        return $SafeTitle;
    }

    /**
    * Get the date prefix for the creation date field value for displaying to
    * users.
    * @return Returns the date prefix for the creation date field value for
    *      displaying to users.
    */
    public function CreationDateDisplayPrefix()
    {
        return $this->GetTimestampPrefix($this->CreationDate());
    }

    /**
    * Get the date prefix for the modification date field value for displaying
    * to users.
    * @return Returns the date prefix for the modification date field value for
    *      displaying to users.
    */
    public function ModificationDateDisplayPrefix()
    {
        return $this->GetTimestampPrefix($this->ModificationDate());
    }

    /**
    * Get the date prefix for the publication date field value for displaying to
    * users.
    * @return Returns the date prefix for the publication date field value for
    *      displaying to users.
    */
    public function PublicationDateDisplayPrefix()
    {
        return $this->GetTimestampPrefix($this->PublicationDate());
    }

    /**
    * Get BlogId
    * @return the BlogId assocaited with this entry
    */
    public function GetBlogId()
    {
        return current(array_keys($this->Get(Blog::BLOG_NAME_FIELD_NAME)));
    }

    /**
    * Format a user's name for display, using the real name if available and the
    * user name otherwise.
    * @param array $Users Users to format names for (from $this->Get()).
    * @return Returns the user's name for display.
    */
    protected function FormatUserNameForDisplay($Users)
    {
        # blog schema does not allow multiple users, so just grab
        #  the first (and only) entry in the array
        $User = array_shift($Users);

        # 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;
    }

    /**
    * Format a timestamp for displaying to users.
    * @param string $Timestamp Timestamp to format.
    * @return Returns a formatted timestamp.
    */
    protected function FormatTimestampForDisplay($Timestamp)
    {
        return GetPrettyTimestamp($Timestamp, TRUE);
    }

    /**
    * Format a timestamp for machine parsing.
    * @param string $Timestamp Timestamp to format.
    * @return Returns a formatted timestamp.
    */
    protected function FormatTimestampForParsing($Timestamp)
    {
        $Timestamp = strtotime($Timestamp);

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

        return date("c", $Timestamp);
    }

    /**
    * 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) == date("n/j/Y", strtotime("-1 day")))
        {
            return "";
        }

        # before yesterday
        return "on";
    }

    /**
    * Get all image marker positions.
    * @return Returns an array of all image marker positions found.
    */
    protected function GetAllImageMarkerPositions()
    {
        $Body = $this->Get(Blog::BODY_FIELD_NAME);
        $Offset = 0;
        $Positions = array();

        # put a hard limit on the number of loops
        for ($i = 0; $i < 20; $i++)
        {
            # search for an image marker
            list($Position, $Length) =
                $this->GetEndOfFirstParagraphPositionWithLines($Body, $Offset);

            # didn't find a marker so stop
            if ($Position === FALSE)
            {
                break;
            }

            # save the position and update the offset
            $Positions[] = $Position;
            $Offset = $Position+$Length;
        }

        return $Positions;
    }

    /**
    * Get a good position to end the teaser of a blog entry.
    * @param int $MaxLength The maximum length of the teaser.
    * @return Returns the position if a good one is found or FALSE otherwise.
    */
    protected function GetEndOfTeaserPosition($MaxLength=1200)
    {
        $Body = $this->Get(Blog::BODY_FIELD_NAME);

        # try using lines first
        list($Position) = $this->GetEndOfFirstParagraphPositionWithLines($Body);

        # return the line-based excerpt if it's short enough
        if ($Position !== FALSE)
        {
            return $Position;
        }

        # couldn't find a good position
        return FALSE;
    }

    /**
    * Try to find the end of the first paragraph in some HTML using blank lines.
    * @param string $Html HTML in which to search.
    * @param int $Offset Position in the string to begin searching (OPTIONAL, default 0).
    * @return Returns the position and length if found or FALSE otherwise.
    */
    protected function GetEndOfFirstParagraphPositionWithLines($Html, $Offset=0)
    {
        # save the initial length so that the offset of the HTML in the original
        # HTML can be found after trimming
        $InitialLength = strlen($Html);

        # strip beginning whitespace and what is rendered as whitespace in HTML
        $Html = $this->LeftTrimHtml($Html);

        # find the next double (or more) blank line
        preg_match(
            '/' . self::BLANK_LINE_REGEX . '{5,}/',
            $Html,
            $Matches,
            PREG_OFFSET_CAPTURE,
            $Offset);

        # a double (or more) blank line wasn't found
        if (!count($Matches))
        {
            return array(FALSE, 0);
        }

        # return the position before the blank lines and their length
        return array(
            $Matches[0][1] + ($InitialLength - strlen($Html)),
            strlen($Matches[0][0]));
    }

    /**
    * Try to find the end of the first paragraph in some HTML using paragraph
    * tags.
    * @param string $Html HTML in which to search.
    * @return Returns the position if found or FALSE otherwise.
    */
    protected function GetEndOfFirstParagraphPositionWithHtml($Html)
    {
        $Position = strpos($Html, "</p>");

        # there are no paragraphs
        if ($Position === FALSE)
        {
            return FALSE;
        }

        # add four to include the characters in "</p>"
        return $Position + 4;
    }

    /**
    * Try to find the end of the first paragraph in some HTML using newlines and
    * break tags.
    * @param string $Html HTML in which to search.
    * @param int $Offset Position in the string to begin searching (OPTIONAL, default 0).
    * @return Returns the position if found or FALSE otherwise.
    */
    protected function GetEndOfFirstParagraphPositionWithNewlines($Html, $Offset=0)
    {
        # find the next double (or more) newline
        preg_match(
            '/' . self::BLANK_LINE_REGEX . '{2,}/',
            $Html,
            $Matches,
            PREG_OFFSET_CAPTURE,
            $Offset);

        # a double (or more) newline wasn't found
        if (!count($Matches))
        {
            return FALSE;
        }

        $Position = $Matches[0][1];
        $FinalPosition = $Position;

        # reverse to the first period found because the last bit should be a
        # complete sentence
        while ($FinalPosition >= $Offset && !preg_match('/\./', $Html{$FinalPosition}))
        {
            $FinalPosition -= 1;
        }

        # try to find another spot if nothing could be found
        if ($FinalPosition < $Offset)
        {
            return $this->GetEndOfFirstParagraphPositionWithNewlines($Html, $Position+1);
        }

        # return the position found (add one to include the period)
        return $FinalPosition + 1;
    }

    /**
    * Removes whitespace and most HTML that is rendered as whitespace from the
    * beginning of some HTML.
    * @param string $Html HTML to trim.
    * @return Returns the trimmed HTML.
    */
    protected function LeftTrimHtml($Html)
    {
        # remove whitespace from the beginning
        $Html = ltrim($Html);

        # now remove items that act as whitespace in HTML
        $Html = preg_replace('/^'.self::BLANK_LINE_REGEX.'+/', "", $Html);

        # do one last left trim
        $Html = ltrim($Html);

        # return the new HTML
        return $Html;
    }

    /**
    * Removes whitespace and most HTML that is rendered as whitespace from the
    * end of some HTML.
    * @param string $Html HTML to trim.
    * @return Returns the trimmed HTML.
    */
    protected function RightTrimHtml($Html)
    {
        # remove whitespace from the end
        $Html = rtrim($Html);

        # now remove items that act as whitespace in HTML
        $Html = preg_replace('/'.self::BLANK_LINE_REGEX.'+$/', "", $Html);

        # do one last right trim
        $Html = rtrim($Html);

        # return the new HTML
        return $Html;
    }

    /**
    * Cached blog entry comments as Message objects.
    */
    protected $Comments;
}
