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

/**
* Class to provide support for transport controls (used for paging back
* and forth through a list) in the user interface.  This is an abstract base
* class, that provides everything but the constants defining the $_GET
* variable names for values and the method that actually prints the HTML for
* the controls.  The intent is to provide the ability to customize that HTML
* by replacing just the child class in a different (custom, active) interface.
*/
abstract class TransportControlsUI_Base
{
    # ---- PUBLIC INTERFACE --------------------------------------------------

    /** Constant to use when no item types available. */
    const NO_ITEM_TYPE = PHP_INT_MAX;

    /**
    * Class constructor.  Retrieves values for starting index, sort field,
    * and reverse sort order from $_GET using the indexes "SI", "SF", and
    * "RS", respectively (indexes defined in TransportControlsUI class).
    * @param mixed $ItemTypes Item type or array of item types.
    * @param mixed $ItemsPerPage Number of items displayed per page or array
    *       of number of items displayed per page, indexed by item type.  If
    *       an array, there must be an entry for every item type present
    *       in $ItemCounts.
    */
    public function __construct($ItemTypes, $ItemsPerPage)
    {
        # normalize and store item types
        if (is_array($ItemTypes))
        {
            $this->ItemTypes = $ItemTypes;
        }
        else
        {
            $this->ItemTypes = array($ItemTypes);
        }

        # normalize and store items per page
        if (is_array($ItemsPerPage))
        {
            $this->ItemsPerPage = $ItemsPerPage;
        }
        else
        {
            foreach ($this->ItemTypes as $ItemType)
            {
                $this->ItemsPerPage[$ItemType] = $ItemsPerPage;
            }
        }

        # retrieve current position (if any) from URL
        $this->StartingIndex(isset($_GET[static::PNAME_STARTINGINDEX])
                ? $_GET[static::PNAME_STARTINGINDEX] : array());

        # retrieve sort fields (if any) from URL
        $this->SortField(isset($_GET[static::PNAME_SORTFIELD])
                ? $_GET[static::PNAME_SORTFIELD] : array());
        $this->ReverseSortFlag(isset($_GET[static::PNAME_REVERSESORT])
                ? $_GET[static::PNAME_REVERSESORT] : array());

        # retrieve active tab (if any) from URL
        if (isset($_GET[static::PNAME_ACTIVETAB]))
        {
            $this->ActiveTab($_GET[static::PNAME_ACTIVETAB]);
        }
    }

    /**
    * Get/set count of items in search results.
    * @param mixed $ItemCounts Count of total items, array of counts of total
    *       items (indexed by item type), or array of arrays of items (top
    *       level indexed by item type).  If an array of arrays (like search
    *       results) is passed in, counts will be taken from the size of the
    *       inner arrays.
    */
    public function ItemCount($ItemCounts)
    {
        # make sure incoming data makes sense based on known item types
        if (!is_array($ItemCounts)
                && (count($this->ItemCounts) > 1))
        {
            throw InvalidArgumentException("Single item count supplied"
                    ." when multiple item types were supplied to constructor.");
        }

        # normalize and store item counts
        foreach ($this->ItemTypes as $ItemType)
        {
            if (is_array($ItemCounts))
            {
                if (isset($ItemCounts[$ItemType]))
                {
                    $this->ItemCounts[$ItemType] =
                            is_array($ItemCounts[$ItemType])
                            ? count($ItemCounts[$ItemType])
                            : $ItemCounts[$ItemType];
                }
                else
                {
                    $this->ItemCounts[$ItemType] = 0;
                }
            }
            else
            {
                $this->ItemCounts[$ItemType] = $ItemCounts;
            }
        }

        # determine index of first item on the first and last page
        foreach ($this->ItemTypes as $ItemType)
        {
            $this->LastPageStartIndexes[$ItemType] = $this->ItemCounts[$ItemType]
                    - ($this->ItemCounts[$ItemType] % $this->ItemsPerPage[$ItemType]);
        }

        # make sure starting indexes are within bounds
        foreach ($this->ItemTypes as $ItemType)
        {
            if ($this->StartingIndexes[$ItemType]
                    > $this->LastPageStartIndexes[$ItemType])
            {
                $this->StartingIndexes[$ItemType] =
                        $this->LastPageStartIndexes[$ItemType];
            }
        }

        # if active tab not already specified
        if (!isset($this->ActiveTab))
        {
            # if there are item counts available
            if (count($this->ItemCounts))
            {
                # set active tab based on item type with the highest count
                $SortedCounts = $this->ItemCounts;
                arsort($SortedCounts);
                reset($SortedCounts);
                $this->ActiveTab = key($SortedCounts);
            }
        }
    }

    /**
    * Get/set current starting index values.  If new value(s) are supplied,
    * they will replace all current values, resetting any values not supplied
    * back to the default.
    * @param mixed $NewValue New starting index value or array of index
    *       values with item types for indexes.  (OPTIONAL)
    * @return mixed Starting index value or array of values with item types
    *       for the indexes.  A single value is passed back when a single
    *       value was passed into the constructor for item types.
    */
    public function StartingIndex($NewValue = NULL)
    {
        # if new starting index supplied
        if ($NewValue !== NULL)
        {
            # start with empty (all default value) indexes
            $this->StartingIndexes = array();

            # for each item type
            foreach ($this->ItemTypes as $ItemType)
            {
                if (is_array($NewValue))
                {
                    $this->StartingIndexes[$ItemType] =
                            isset($NewValue[$ItemType])
                            ? $NewValue[$ItemType]
                            : 0;
                }
                else
                {
                    $this->StartingIndexes[$ItemType] = $NewValue;
                }

                # make sure starting index is within bounds
                if (isset($this->LastPageStartIndexes[$ItemType]))
                {
                    if ($this->StartingIndexes[$ItemType]
                            > $this->LastPageStartIndexes[$ItemType])
                    {
                        $this->StartingIndexes[$ItemType] =
                                $this->LastPageStartIndexes[$ItemType];
                    }
                }
            }
        }

        # return array of values or single value to caller, depending on
        #       whether we are using multiple item types
        return (count($this->ItemTypes) > 1)
                ? $this->StartingIndexes
                : reset($this->StartingIndexes);
    }

    /**
    * Get/set ID of field(s) currently used for sorting.  If new value(s) are
    * supplied, they will replace all current values, resetting any values not
    * supplied back to the default.
    * @param mixed $NewValue Field ID or array of field IDs (indexed by item type).
    * @return mixed Sort field ID or array of IDs with item types
    *       for the indexes.  A single ID is passed back when a single
    *       value was passed into the constructor for item types.
    */
    public function SortField($NewValue = NULL)
    {
        # if new sort field supplied
        if ($NewValue !== NULL)
        {
            # start with empty (all default) sort field list
            $this->SortFields = array();

            # for each item type
            foreach ($this->ItemTypes as $ItemType)
            {
                # if multiple new values supplied
                if (is_array($NewValue))
                {
                    # if valid value supplied for this item type
                    if (isset($NewValue[$ItemType])
                            && $this->IsValidField($NewValue[$ItemType], $ItemType))
                    {
                        # use supplied value
                        $this->SortFields[$ItemType] = $NewValue[$ItemType];
                    }
                    else
                    {
                        # set to default
                        $this->SortFields[$ItemType] = self::$DefaultSortField;
                    }
                }
                else
                {
                    # if supplied value looks valid
                    if ($this->IsValidField($NewValue, $ItemType))
                    {
                        # set value for item type to this value
                        $this->SortFields[$ItemType] = $NewValue;
                    }
                    else
                    {
                        # set to default
                        $this->SortFields[$ItemType] = self::$DefaultSortField;
                    }
                }
            }
        }

        # return array of values or single value to caller, depending on
        #       whether we are using multiple item types
        return (count($this->ItemTypes) > 1)
                ? $this->SortFields
                : reset($this->SortFields);
    }

    /**
    * Get/set default sort field value.
    * @param mixed $NewValue New default sort field.
    * @return mixed Current default sort field.
    */
    public static function DefaultSortField($NewValue = NULL)
    {
        if ($NewValue !== NULL)
        {
            self::$DefaultSortField = $NewValue;
        }
        return self::$DefaultSortField;
    }

    /**
    * Get/set the active tab value (usually an item type).  This can be used
    * to override the value obtained from $_GET (if any).
    * @param int $NewValue New active tab.  (OPTIONAL)
    * @return int Current active tab.
    */
    public function ActiveTab($NewValue = NULL)
    {
        if ($NewValue !== NULL)
        {
            $this->ActiveTab = $NewValue;
        }
        return isset($this->ActiveTab)
                ? $this->ActiveTab
                : self::$DefaultActiveTab;
    }

    /**
    * Get/set the default active tab value (usually an item type).
    * @param int $NewValue New default active tab.  (OPTIONAL)
    * @return int Current default active tab.
    */
    public static function DefaultActiveTab($NewValue = NULL)
    {
        if ($NewValue !== NULL)
        {
            self::$DefaultActiveTab = $NewValue;
        }
        return self::$DefaultActiveTab;
    }

    /**
    * Get/set whether to reverse the sort order from normal.  If new values are
    * supplied, they will replace all current values, resetting any values not
    * supplied back to the default.
    * @param mixed $NewValue New value or array of values (indexed by item type).
    * @return mixed TRUE to reverse sort order or FALSE to use normal order
    *       for field.  A single value is passed back when a single value was
    *       passed into the constructor for item types.
    */
    public function ReverseSortFlag($NewValue = NULL)
    {
        # if new value(s) supplied
        if ($NewValue !== NULL)
        {
            # start with empty (all default value)
            $this->ReverseSortFlags = array();

            # for each item type
            foreach ($this->ItemTypes as $ItemType)
            {
                # if multiple new values supplied
                if (is_array($NewValue))
                {
                    # if value supplied for this item type
                    if (isset($NewValue[$ItemType]))
                    {
                        # use supplied value
                        $this->ReverseSortFlags[$ItemType] =
                                ($NewValue[$ItemType] ? TRUE : FALSE);
                    }
                    else
                    {
                        # assume no reverse sort for item type
                        $this->ReverseSortFlags[$ItemType] = FALSE;
                    }
                }
                else
                {
                    # set value for item type to supplied value
                    $this->ReverseSortFlags[$ItemType] =
                            ($NewValue ? TRUE : FALSE);
                }
            }
        }

        # return array of values or single value to caller, depending on
        #       whether we are using multiple item types
        return (count($this->ItemTypes) > 1)
                ? $this->ReverseSortFlags
                : reset($this->ReverseSortFlags);
    }

    /**
    * Get string containing URL parameters, ready for inclusion in URL.
    * @param bool $EncodeSeparators If TRUE, "&amp;" is used to separate
    *       arguments, otherwise "&" is used.  (OPTIONAL, defaults to TRUE)
    * @param array $ExcludeParameters Array with one or more parameters to
    *       exclude from the parameter string.  (OPTIONAL)
    * @return string URL parameter string, with leading "&amp;" if
    *       parameters are included.
    */
    public function UrlParameterString(
            $EncodeSeparators = TRUE, $ExcludeParameters = NULL)
    {
        # add any non-default starting indexes to query data
        foreach ($this->StartingIndexes as $ItemType => $Index)
        {
            if ($Index != 0)
            {
                $QData[static::PNAME_STARTINGINDEX][$ItemType] = $Index;
            }
        }

        # add any non-default sort fields to query data
        foreach ($this->SortFields as $ItemType => $Field)
        {
            if ($Field != self::$DefaultSortField)
            {
                $QData[static::PNAME_SORTFIELD][$ItemType] = $Field;
            }
        }

        # add any non-default sort directions to query data
        foreach ($this->ReverseSortFlags as $ItemType => $Flag)
        {
            if ($Flag)
            {
                $QData[static::PNAME_REVERSESORT][$ItemType] = 1;
            }
        }

        # add active tab to query data if set and meaningful
        if (isset($this->ActiveTab) && (count($this->ItemTypes) > 1))
        {
            $QData[static::PNAME_ACTIVETAB] = $this->ActiveTab;
        }

        # remove any requested exclusions
        if (isset($QData))
        {
            if ($ExcludeParameters)
            {
                foreach ($ExcludeParameters as $Param)
                {
                    if (isset($QData[$Param]))
                    {
                        unset($QData[$Param]);
                    }
                }
            }
        }

        # collapse down parameters if only one item type
        if (count($this->ItemTypes) == 1)
        {
            $Params = array(static::PNAME_STARTINGINDEX,
                    static::PNAME_SORTFIELD,
                    static::PNAME_REVERSESORT);
            foreach ($Params as $Param)
            {
                if (isset($QData[$Param]))
                {
                    $QData[$Param] = reset($QData[$Param]);
                }
            }
        }

        # if no non-default transport parameters
        if (!isset($QData) || !count($QData))
        {
            # return empty string
            return "";
        }
        else
        {
            # build parameter string and return it to caller
            $Sep = $EncodeSeparators ? "&amp;" : "&";
            return $Sep.http_build_query($QData, "", $Sep);
        }
    }

    # ---- TEST/LINK METHODS ------------------------------------------------
    # (useful if constructing your own interface rather than using PrintControls()

    /**
    * Set current item type for Show or Link methods.
    * @param int $ItemType Item type for operations.
    */
    public function SetItemType($ItemType)
    {
        $this->CurrentItemType = $ItemType;
    }

    /**
    * Set current base link for Link methods.
    * @param string $BaseLink New base URL.
    */
    public function SetBaseLink($BaseLink)
    {
        $this->CurrentBaseLink = $BaseLink;
    }

    /**
    * Report whether any forward buttons should be displayed.  The item
    * type must be set via SetItemType() before using this.
    * @return bool TRUE if at least one forward button should be displayed,
    *       otherwise FALSE.
    * @see SetItemType()
    */
    public function ShowAnyForwardButtons()
    {
        return (($this->StartingIndexes[$this->CurrentItemType]
                        + $this->ItemsPerPage[$this->CurrentItemType])
                < ($this->LastPageStartIndexes[$this->CurrentItemType] + 1))
                ? TRUE : FALSE;
    }

    /**
    * Report whether any reverse buttons should be displayed.  The item
    * type must be set via SetItemType() before using this.
    * @return bool TRUE if at least one reverse button should be displayed,
    *       otherwise FALSE.
    * @see SetItemType()
    */
    public function ShowAnyReverseButtons()
    {
        return ($this->StartingIndexes[$this->CurrentItemType] > 0)
                ? TRUE : FALSE;
    }

    /**
    * Report whether forward button should be displayed.  The item
    * type must be set via SetItemType() before using this.
    * @return bool TRUE if forward button should be displayed, otherwise FALSE.
    * @see SetItemType()
    */
    public function ShowForwardButton()
    {
        return ($this->StartingIndexes[$this->CurrentItemType]
                        < ($this->LastPageStartIndexes[$this->CurrentItemType]
                                - $this->ItemsPerPage[$this->CurrentItemType]))
                ? TRUE : FALSE;
    }

    /**
    * Report whether reverse button should be displayed.  The item
    * type must be set via SetItemType() before using this.
    * @return bool TRUE if reverse button should be displayed, otherwise FALSE.
    * @see SetItemType()
    */
    public function ShowReverseButton()
    {
        return (($this->StartingIndexes[$this->CurrentItemType] + 1)
                        >= ($this->ItemsPerPage[$this->CurrentItemType] * 2))
                ? TRUE : FALSE;
    }

    /**
    * Report whether fast forward button should be displayed.  The item
    * type must be set via SetItemType() before using this.
    * @return bool TRUE if fast forward button should be displayed,
    *       otherwise FALSE.
    * @see SetItemType()
    */
    public function ShowFastForwardButton()
    {
        return (($this->FastDistance() > $this->ItemsPerPage[$this->CurrentItemType])
                && (($this->StartingIndexes[$this->CurrentItemType]
                        + $this->FastDistance())
                                < $this->LastPageStartIndexes[$this->CurrentItemType]))
                ? TRUE : FALSE;
    }

    /**
    * Report whether fast reverse button should be displayed.  The item
    * type must be set via SetItemType() before using this.
    * @return bool TRUE if fast reverse button should be displayed,
    *       otherwise FALSE.
    * @see SetItemType()
    */
    public function ShowFastReverseButton()
    {
        return (($this->FastDistance() > $this->ItemsPerPage[$this->CurrentItemType])
                        && ($this->StartingIndexes[$this->CurrentItemType]
                                >= $this->FastDistance()))
                ? TRUE : FALSE;
    }

    /**
    * Get link for forward button.
    * @return string Link URL.
    */
    public function ForwardLink()
    {
        return $this->GetLinkWithStartingIndex(
                min($this->LastPageStartIndexes[$this->CurrentItemType],
                        ($this->StartingIndexes[$this->CurrentItemType]
                        + $this->ItemsPerPage[$this->CurrentItemType])));
    }

    /**
    * Get link for reverse button.
    * @return string Link URL.
    */
    public function ReverseLink()
    {
        return $this->GetLinkWithStartingIndex(
                max(0, ($this->StartingIndexes[$this->CurrentItemType]
                        - $this->ItemsPerPage[$this->CurrentItemType])));
    }

    /**
    * Get link for fast forward button.
    * @return string Link URL.
    */
    public function FastForwardLink()
    {
        return $this->GetLinkWithStartingIndex(
                min($this->LastPageStartIndexes[$this->CurrentItemType],
                        ($this->StartingIndexes[$this->CurrentItemType]
                        + $this->FastDistance())));
    }

    /**
    * Get link for fast reverse button.
    * @return string Link URL.
    */
    public function FastReverseLink()
    {
        return $this->GetLinkWithStartingIndex(
                max(0, ($this->StartingIndexes[$this->CurrentItemType]
                        - $this->FastDistance())));
    }

    /**
    * Get link for button to go to end.
    * @return string Link URL.
    */
    public function GoToEndLink()
    {
        return $this->GetLinkWithStartingIndex(
                $this->LastPageStartIndexes[$this->CurrentItemType]);
    }

    /**
    * Get link for button to go to start.
    * @return string Link URL.
    */
    public function GoToStartLink()
    {
        return $this->GetLinkWithStartingIndex(0);
    }


    # ---- PRIVATE INTERFACE -------------------------------------------------

    protected $ActiveTab;
    protected $CurrentBaseLink;
    protected $CurrentItemType;
    protected $ItemCounts;
    protected $ItemsPerPage;
    protected $ItemTypes;
    protected $LastPageStartIndexes;
    protected $ReverseSortFlags;
    protected $SortFields;
    protected $StartingIndexes;

    protected static $DefaultActiveTab = MetadataSchema::SCHEMAID_DEFAULT;
    protected static $DefaultSortField = "R";

    /**
    * Get distance to jump for fast forward/reverse.
    * @return int Number of items to jump.
    */
    protected function FastDistance()
    {
        return floor($this->ItemCounts[$this->CurrentItemType] / 5)
                - (floor($this->ItemCounts[$this->CurrentItemType] / 5)
                        % $this->ItemsPerPage[$this->CurrentItemType]);
    }

    /**
    * Generate link with specified modified starting index.
    * @param int $StartingIndex Index to use for current item type.
    * @return string Generated link.
    */
    protected function GetLinkWithStartingIndex($StartingIndex)
    {
        # temporarily swap in supplied starting index
        $SavedStartingIndex = $this->StartingIndexes[$this->CurrentItemType];
        $this->StartingIndexes[$this->CurrentItemType] = $StartingIndex;

        # get link with parameters
        $Link = $this->CurrentBaseLink.$this->UrlParameterString();

        # restore starting index
        $this->StartingIndexes[$this->CurrentItemType] = $SavedStartingIndex;

        # return link to caller
        return $Link;
    }

    /**
    * Check whether specified field looks valid for specified item type.
    * @param int $Field Field to check.
    * @param int $ItemType Type of item to check against.
    * @return bool TRUE if field looks valid, otherwise FALSE.
    */
    protected function IsValidField($Field, $ItemType)
    {
        # default field is assumed to always be valid
        if ($Field === self::$DefaultSortField)
        {
            return TRUE;
        }

        # fields are assumed to be always valid for no item type
        if ($ItemType === self::NO_ITEM_TYPE)
        {
            return TRUE;
        }

        # load metadata schema to check against (if not already loaded)
        static $Schemas;
        if (!isset($Schemas[$ItemType]))
        {
            $Schemas[$ItemType] = new MetadataSchema($ItemType);
        }

        # report to caller whether field exists for this schema
        return $Schemas[$ItemType]->FieldExists($Field);
    }
}

