package WebGUI::Asset::Shortcut;

#-------------------------------------------------------------------
# WebGUI is Copyright 2001-2009 Plain Black Corporation.
#-------------------------------------------------------------------
# Please read the legal notices (docs/legal.txt) and the license
# (docs/license.txt) that came with this distribution before using
# this software.
#-------------------------------------------------------------------
# http://www.plainblack.com                     info@plainblack.com
#-------------------------------------------------------------------

use strict;
use Carp;
use Tie::IxHash;
use WebGUI::Asset;
use WebGUI::International;
use WebGUI::Operation::Profile;
use WebGUI::ProfileField;
use WebGUI::ProfileCategory;
use WebGUI::Macro;
use HTML::Entities qw(encode_entities);
use Data::Dumper;

our @ISA = qw(WebGUI::Asset);

#-------------------------------------------------------------------
sub _drawQueryBuilder {
	my $self    = shift;
    my $session = $self->session;
	# Initialize operators
	my @textFields = qw|text yesNo selectBox radioList|;
	my $i18n = WebGUI::International->new($session,"Asset_Shortcut");
	my %operator;
	foreach (@textFields) {
		$operator{$_} = {
			"=" => $i18n->get("is"),
			"!=" => $i18n->get("isnt")
		};
	}
	$operator{checkList} = {
		"+~" => $i18n->get("contains"),
	};
	$operator{integer} = {
		"=" => $i18n->get("equal to"),
		"!=" => $i18n->get("not equal to"),
		"<" => $i18n->get("less than"),
		">" => $i18n->get("greater than")
	};

	# Get the fields and count them
	my $fields = $self->getMetaDataFields();
	my $fieldCount = scalar(keys %$fields);

	unless ($fieldCount) {	# No fields found....
		return sprintf $i18n->get('no metadata yet'), $session->url->page('func=manageMetaData');
	}

	# Static form fields
	my $shortcutCriteriaField = WebGUI::Form::textarea($session, {
		name=>"shortcutCriteria",
		value=>$self->getValue("shortcutCriteria"),
		extras=>'style="width: 100%" '.$self->{_disabled}
	});
	my $conjunctionField = WebGUI::Form::selectBox($session, {
		name=>"conjunction",
		options=>{
			"AND" => $i18n->get("AND"),
			"OR" => $i18n->get("OR")},
			value=>["OR"],
			extras=>'class="qbselect" '. $self->{_disabled},
		}
	);

	# html
	$session->style->setScript($session->url->extras('wobject/Shortcut/querybuilder.js'), {type=>"text/javascript"});
	$session->style->setLink($session->url->extras('wobject/Shortcut/querybuilder.css'), {type=>"text/css", rel=>"stylesheet"});
	my $output;
	$output .= qq|<table cellspacing="0" cellpadding="0" border="0"><tr><td colspan="5" align="right">$shortcutCriteriaField</td></tr><tr><td></td><td></td><td></td><td></td><td class="qbtdright"></td></tr><tr><td></td><td></td><td></td><td></td><td class="qbtdright">$conjunctionField</td></tr>|;

	# Here starts the field loop
	my $i = 1;
    my $addLabel = $i18n->get('add', 'Asset_Wobject');
	foreach my $field (keys %$fields) {
		my $fieldLabel = $fields->{$field}{fieldName};
		my $fieldType = $fields->{$field}{fieldType} || "text";

		# The operator select field
		my $opFieldName = "op_field".$i;
		my $opField = WebGUI::Form::selectBox($self->session, {
			name=>$opFieldName,
			uiLevel=>5,
			options=>$operator{$fieldType},
			extras=>'class="qbselect" '. $self->{_disabled},
		});
		# The value select field
		my $valFieldName = "val_field".$i;
        my $options = $fields->{$field}{possibleValues};
        ##Only allow one at a time to be selected, and work with current JS for choosing which one.
        $fieldType = 'radioList' if $fieldType eq 'checkList';
		my $valueField = WebGUI::Form::dynamicField($session,
			fieldType=>$fieldType,
			name=>$valFieldName,
			uiLevel=>5,
			extras=>qq/title="$fields->{$field}{description}" class="qbselect" /. $self->{_disabled},
			options=>$options,
		);
		# An empty row
		$output .= qq|<tr><td></td><td></td><td></td><td></td><td class="qbtdright"></td></tr>|;

		# Table row with field info
        my $disabled = $self->{_disabled};
        my $odd_row = $i % 2 ? q{class="qbtr_alt"} : "";
		$output .= qq|
		<tr $odd_row>
		<td class="qbtdleft"><p class="qbfieldLabel">$fieldLabel</p></td>
		<td class="qbtd">
		$opField
		</td>
		<td class="qbtd">
		<span class="qbText">$valueField</span>
		</td>
		<td class="qbtd"></td>
		<td class="qbtdright">
		<input class="qbButton" type=button value=$addLabel onclick="addCriteria('$fieldLabel', this.form.$opFieldName, this.form.$valFieldName)" $disabled></td>
		</tr>
		|;
		$i++;
	}
	# Close the table
	$output .= "</table>";

	return $output;
}

#-------------------------------------------------------------------
sub _submenu {
	my $self = shift;
	my $workarea = shift;
	my $title = shift;
	my $help = shift;
	my $ac = WebGUI::AdminConsole->new($self->session,"shortcutmanager");
	$ac->setIcon($self->getIcon);
	my $i18n = WebGUI::International->new($self->session,"Asset_Shortcut");
	$ac->addSubmenuItem($self->getUrl('func=edit'), $i18n->get("Back to Edit Shortcut"));
	$ac->addSubmenuItem($self->getUrl("func=manageOverrides"),$i18n->get("Manage Shortcut Overrides"));
	return $ac->render($workarea, $title);
}

#-------------------------------------------------------------------

=head2 canEdit 

Extend the base method to handle dashlets.  If this Shortcut is a dashlet, then if you
can manage the parent you can edit this Shortcut.

=cut

sub canEdit {
	my $self = shift;
return 1 if ($self->SUPER::canEdit || ($self->isDashlet && $self->getParent->canManage));
	return 0;
}

#-------------------------------------------------------------------

=head2 canManage 

An alias for canEdit.

=cut

sub canManage {
	my $self = shift;
	return $self->canEdit;
}

#-------------------------------------------------------------------
sub definition {
	my $class = shift;
	my $session = shift;
	my $definition = shift;
	my $i18n = WebGUI::International->new($session,"Asset_Shortcut");
	push(@{$definition}, {
		assetName=>$i18n->get('assetName'),
		icon=>'shortcut.gif',
		tableName=>'Shortcut',
		className=>'WebGUI::Asset::Shortcut',
		properties=>{
			shortcutToAssetId=>{
				noFormPost=>1,
				fieldType=>"hidden",
				defaultValue=>undef
			},
			shortcutByCriteria=>{
				fieldType=>"yesNo",
				defaultValue=>0,
			},
			disableContentLock=>{
				fieldType=>"yesNo",
				defaultValue=>0
			},
			resolveMultiples=>{
				fieldType=>"selectBox",
				defaultValue=>"mostRecent",
			},
			shortcutCriteria=>{
				fieldType=>"textarea",
				defaultValue=>"",
			},
			templateId=>{
				fieldType=>"template",
				defaultValue=>"PBtmpl0000000000000140"
			},
			prefFieldsToShow=>{
				fieldType=>"checkList",
				defaultValue=>undef
			},
			prefFieldsToImport=>{
				fieldType=>"checkList",
				defaultValue=>undef
			},
			showReloadIcon=>{
				fieldType=>"yesNo",
				defaultValue=>1
			}
		}
	});
	return $class->SUPER::definition($session,$definition);
}

#-------------------------------------------------------------------

=head2 discernUserId 

This utility method is used to determine if the user should be shown the view of the
Shortcut that the Visitor would see, or their own.

=cut

sub discernUserId {
	my $self = shift;
	return ($self->canManage && $self->session->var->isAdminOn) ? '1' : $self->session->user->userId;
}

#-------------------------------------------------------------------

=head2 duplicate

Extend the base method to duplicate shortcut overrides.

=cut

sub duplicate {
    my $self = shift;
    my $newAsset = $self->SUPER::duplicate(@_);
    $self->session->db->write(<<'END_SQL', [$newAsset->getId, $self->getId]);
INSERT INTO Shortcut_overrides (assetId, fieldName, newValue)
SELECT ?, fieldName, newValue
FROM Shortcut_overrides 
WHERE assetId = ?
END_SQL
    return $newAsset;
}

#-------------------------------------------------------------------

=head2 getContentLastModified 

Extend the base method to consider the revisionDate of the Asset being shortcutted.
Return the largest of either the asset revision date, or the shortcut revision date.

=cut

sub getContentLastModified {
    my $self = shift;
    my $assetRev = $self->get('revisionDate');
    my $shortcut = $self->getShortcut;
    my $shortcuttedRev;
    if (defined $shortcut) {
        $shortcuttedRev = $shortcut->getContentLastModified;
        return $assetRev > $shortcuttedRev ? $assetRev : $shortcuttedRev;
    } else {
        return 0;
    }
}

#-------------------------------------------------------------------

=head2 getEditForm 

Extend the base class to handle hand drawing the query build and other pieces.

=cut

sub getEditForm {
	my $self = shift;
	my $tabform = $self->SUPER::getEditForm();
	my $originalTemplate;
	my $i18n = WebGUI::International->new($self->session, "Asset_Shortcut");
	my $shortcut = $self->getShortcut;
	if (defined $shortcut) {
		$tabform->getTab("properties")->readOnly(
			-label=>$i18n->get(1),
			-hoverHelp=>$i18n->get('1 description'),
			-value=>'<a href="'.$shortcut->getUrl.'">'.$shortcut->get('title').'</a> ('.$shortcut->getId.')'
			);
	} else {
		$tabform->getTab("properties")->readOnly(
			value=>'<a href="'.$self->getUrl("func=delete").'"><span style="font-weight: bold; color: red;">'.$self->notLinked.'</span></a>'
			);
	}
	$tabform->getTab("display")->template(
		-value=>$self->getValue("templateId"),
		-label=>$i18n->get('shortcut template title'),
		-hoverHelp=>$i18n->get('shortcut template title description'),
		-namespace=>"Shortcut"
	);
	if($self->session->setting->get("metaDataEnabled")) {
		$tabform->getTab("properties")->yesNo(
			-name     => "shortcutByCriteria",
			-value    => $self->getValue("shortcutByCriteria"),
			-label    => $i18n->get("Shortcut by alternate criteria"),
			-hoverHelp=> $i18n->get("Shortcut by alternate criteria description"),
			-extras   => q|onchange="wgCriteriaDisable(this.form, this.form.shortcutByCriteria[0].checked)"|,
        );
		$tabform->getTab("properties")->yesNo(
			-name=>"disableContentLock",
			-value=>$self->getValue("disableContentLock"),
			-label=>$i18n->get("disable content lock"),
			-hoverHelp=>$i18n->get("disable content lock description")
			);
		if ($self->getValue("shortcutByCriteria") == 0) {
			$self->{_disabled} = 'disabled=true';
		}
		$tabform->getTab("properties")->selectBox(
			-name=>"resolveMultiples",
			-value=>[ $self->getValue("resolveMultiples") ],
			-label=>$i18n->get("Resolve Multiples"),
			-hoverHelp=>$i18n->get("Resolve Multiples description"),
			-options=>{
				mostRecent=>$i18n->get("Most Recent"),
				random=>$i18n->get("Random"),
			},
			-extras=>$self->{_disabled}
		);
		$tabform->getTab("properties")->readOnly(
			-value=>$self->_drawQueryBuilder(),
			-label=>$i18n->get("Criteria"),
			-hoverHelp=>$i18n->get("Criteria description")
		);
	}
	$tabform->addTab('overrides',$i18n->get('Overrides'));
	$tabform->getTab('overrides')->raw('<tr><td>' . $self->getOverridesList . '</td></tr>');
	if ($self->isDashlet) {
		$tabform->addTab('preferences',$i18n->get('Preferences'), 9);
		$tabform->getTab('preferences')->raw($self->getFieldsList);
		$tabform->getTab("properties")->yesNo(
			-value=>$self->getValue("showReloadIcon"),
			-name=>"showReloadIcon",
			-label=>$i18n->get("show reload icon"),
			-hoverHelp=>$i18n->get("show reload icon description")
		);
	}
	return $tabform;
}


#-------------------------------------------------------------------

=head2 getFieldsList 

Return a form with a list of profile fields to show to the user, and a list of fields
to import as variables for use in overrides.

=cut

sub getFieldsList {
	my $self    = shift;
    my $session = $self->session;
	my $i18n    = WebGUI::International->new($session, "Asset_Shortcut");
	my $output  = '<a href="'.$self->getUrl('op=editProfileSettings').'" class="formLink">'.$i18n->get('Manage Profile Fields').'</a><br /><br />';
	my %fieldNames;
	tie %fieldNames, 'Tie::IxHash';
	foreach my $field (@{WebGUI::ProfileField->getFields($session)}) {
		my $fieldId = $field->getId;
		next if $fieldId =~ /contentPositions/;
		$fieldNames{$fieldId} = $field->getLabel.' ['.$fieldId.']';
	}
	$output .= '<table cellspacing="0" cellpadding="3" border="1"><tr><td><table cellspacing="0" cellpadding="3" border="0">';
	my @prefFieldsToShow = $self->getPrefFieldsToShow;
	my $list = WebGUI::Form::CheckList->new($session,
		-name=>"prefFieldsToShow",
		-value=>\@prefFieldsToShow,
		-options=>\%fieldNames,
		-label=>$i18n->get('pref fields to show'),
		-hoverHelp=>$i18n->get('pref fields to show description'),
		-vertical=>1,
		-uiLevel=>9
	);
	$output .= $list->toHtmlWithWrapper;
	$output .= '</table></td><td><table cellspacing="0" cellpadding="3" border="0">';
	my @prefFieldsToImport = $self->getPrefFieldsToImport;
	$output .= WebGUI::Form::CheckList->new($session,
		-name=>"prefFieldsToImport",
		-value=>\@prefFieldsToImport,
		-options=>\%fieldNames,
		-label=>$i18n->get('pref fields to import'),
		-hoverHelp=>$i18n->get('pref fields to import description'),
		-vertical=>1,
		-uiLevel=>9
	)->toHtmlWithWrapper;
	$output .= '</table></td></tr></table>';
	return $output;
}

#-------------------------------------------------------------------

=head2 getOverridesList

Generates a table of overrides, based on the shortcutted Asset's definition sub.
If the field type is hidden, or the field label is empty, then an override is not
generated for that field

=cut

sub getOverridesList {
	my $self = shift;
	my $output = '';
	my $i18n = WebGUI::International->new($self->session, "Asset_Shortcut");
	my %overrides = $self->getOverrides;
	$output .= '<table cellspacing="0" cellpadding="3" border="1">';
	$output .= '<tr class="tableHeader"><td>'.$i18n->get('fieldName').'</td><td>'.$i18n->get('edit delete fieldname').'</td><td>'.$i18n->get('Original Value').'</td><td>'.$i18n->get('New value').'</td><td>'.$i18n->get('Replacement value').'</td></tr>';
	my $shortcut = $self->getShortcutOriginal;
	return undef unless defined $shortcut;
	foreach my $definition (@{$shortcut->definition($self->session)}) {
		foreach my $prop (keys %{$definition->{properties}}) {
			next if $definition->{properties}{$prop}{fieldType} eq 'hidden';
            next if $definition->{properties}{$prop}{label} eq '';
			$output .= '<tr>';
            $output .= '<td class="tableData">'.$definition->{properties}{$prop}{label}.'</td>';
			$output .= '<td class="tableData">';
			$output .= $self->session->icon->edit('func=editOverride;fieldName='.$prop,$self->get("url"));
			$output .= $self->session->icon->delete('func=deleteOverride;fieldName='.$prop,$self->get("url")) if exists $overrides{overrides}{$prop};
			$output .= '</td><td>';
			$output .= $overrides{overrides}{$prop}{origValue};
			$output .= '</td><td>';
			$output .= encode_entities($overrides{overrides}{$prop}{newValue}, '<>&"^');
			$output .= '</td><td>';
			$output .= $overrides{overrides}{$prop}{parsedValue};
			$output .= "</td></tr>\n";
		}
	}
	$output .= '</table>';
	return $output;
}

#-------------------------------------------------------------------

=head2 _overridesCacheTag

Overrides are cached by this shortcut's assetId, the session user userId, and whether or not
admin mode is on.

=cut

sub _overridesCacheTag {
	my $self = shift;
	#cache by userId, assetId of this shortcut, and whether adminMode is on or not.
	return ["shortcutOverrides", $self->getId, $self->session->user->userId, $self->session->var->isAdminOn];
}

#-------------------------------------------------------------------

=head2 getOverrides

Return overrides for the original asset.  If this is a dashlet (child of a Dashboard), then
overrides can pick up values from the user's profile.

Overrides are cached, unless you are in admin mode.  The cache is invalidated if it has
expired, or if the user's profile field has changed.

=cut

sub getOverrides {
	my $self    = shift;
    my $session = $self->session;
	my $cache   = WebGUI::Cache->new($self->session,$self->_overridesCacheTag);
    my $u       = WebGUI::User->new($self->session, $self->discernUserId);

	my $overridesRef = $cache->get;
    ##If admin mode is not on, and the cache is valid, and not expired, and the user object was not updated,
    ##return the cached value.
    if ( ! $session->var->isAdminOn
        && $overridesRef
        && $overridesRef->{cacheNotExpired}
        && $overridesRef->{userLastUpdated} >= $u->get('lastUpdated')) {
        return %{ $overridesRef };
    }
    my %overrides;
    my $orig = $self->getShortcutOriginal;
    if (defined $orig) {
        unless ( exists $orig->{_propertyDefinitions}) {
        my %properties;
            foreach my $definition (@{$orig->definition($self->session)}) {
                %properties = (%properties, %{$definition->{properties}});
            }
            $orig->{_propertyDefinitions} = \%properties;
        }
        $overrides{cacheNotExpired} = 1;
        my $sth = $self->session->db->read("select fieldName, newValue from Shortcut_overrides where assetId=? order by fieldName",[$self->getId]);
        while (my ($fieldName, $newValue) = $sth->array) {
            $overrides{overrides}{$fieldName}{fieldType}   = $orig->{_propertyDefinitions}{$fieldName}{fieldType};
            $overrides{overrides}{$fieldName}{origValue}   = $orig->get($fieldName);
            $overrides{overrides}{$fieldName}{newValue}    = $newValue;
            $overrides{overrides}{$fieldName}{parsedValue} = $newValue;
        }
        $sth->finish;
    }
    else {
        $self->session->errorHandler->warn("Original asset could not be instanciated by shortcut ".$self->getId);
    }
    if ($self->isDashlet) {
        my @userPrefs = $self->getPrefFieldsToImport;
        foreach my $fieldId (@userPrefs) {
            my $field = WebGUI::ProfileField->new($self->session,$fieldId);
            next unless $field;
            my $fieldName  = $field->getId;
            my $fieldValue = $u->profileField($field->getId);
            $overrides{userPrefs}{$fieldName}{value}       = $fieldValue;
            $overrides{userPrefs}{$fieldName}{parsedValue} = $fieldValue;
            #  'myTemplateId is ##userPref:myTemplateId##', for example.
            foreach my $overr (keys %{$overrides{overrides}}) {
                $overrides{overrides}{$overr}{parsedValue} =~ s/\#\#userPref\:${fieldName}\#\#/$fieldValue/gm;
            }
        }
    }
    $overrides{userLastUpdated} = $session->user->get('lastUpdated');
    $cache->set(\%overrides, 60*60);
    $overridesRef = \%overrides;
	return %{ $overridesRef };
}

#-------------------------------------------------------------------

=head2 getShortcut 

Return a copy of the Asset that this Shortcut points to.  Overrides are
processed if set.

=cut

sub getShortcut {
	my $self = shift;
	unless ($self->{_shortcut}) {
		$self->{_shortcut} = $self->getShortcutOriginal;
	}
	$self->{_shortcut}{_properties}{displayTitle} = undef if ($self->isDashlet);
	# Hide title by default.  If you want, you can create an override
	# to display it.  But it's being shown in the dragheader by default.
	my %overhash = $self->getOverrides;
	if (exists $overhash{overrides}) {
		my %overrides = %{$overhash{overrides}};
		foreach my $override (keys %overrides) {
			$self->{_shortcut}{_properties}{$override} = $overrides{$override}{parsedValue};
		}
		$self->{_shortcut}{_properties}{showReloadIcon} = $self->get("showReloadIcon");
	}
	return $self->{_shortcut};
}

#-------------------------------------------------------------------

=head2 getShortcutByCriteria ( hashRef )

This function will search for a asset that match a metadata criteria set.
If no asset is found, undef will be returned.

=cut

sub getShortcutByCriteria {
	my $self = shift;
	my $assetProxy = shift;
	my $criteria = $self->get("shortcutCriteria");
	my $order = $self->get("resolveMultiples");
	my $assetId = $self->getId;

	# Parse macro's in criteria
	WebGUI::Macro::process($self->session,\$criteria);

	# Once a asset is found, we will stick to that asset, 
	# to prevent the proxying of multiple- depth assets like Surveys and USS.
	my $scratchId;
	if ($assetId) {
		$scratchId = "Shortcut_" . $assetId;
		if($self->session->scratch->get($scratchId) && !$self->getValue("disableContentLock")) {
			unless ($self->session->var->isAdminOn) {
				return WebGUI::Asset->newByDynamicClass($self->session, $self->session->scratch->get($scratchId));
			}
		}
	}

	# $criteria = "State = Wisconsin AND Country != Sauk";
	#
	# State          =             Wisconsin AND Country != Sauk
	# |              |             |
	# |- $field      |_ $operator  |- $value
	# |_ $attribute                |_ $attribute
    my $operator = qr/<>|!=|=|>=|<=|>|<|like|\+~/i;
    my $attribute = qr/['"][^()|=><!]+['"]|[^()|=><!\s]+/i; 
                                                                                                      
	my $constraint = $criteria;
	
	# Get each expression from $criteria
	my $db = $self->session->db;
	my $counter = "b";
	my @joins = ();
    ##Transform the expression into valid SQL
	foreach my $expression ($criteria =~ /($attribute\s*$operator\s*$attribute)/gi) {
		# $expression will match "State = Wisconsin"

        	my $replacement = $expression;	# We don't want to modify $expression.
						# We need it later.
		my $alias = $counter . '_v';
		push(@joins," left join metaData_values $alias on a.assetId=$alias.assetId and d.revisionDate = $alias.revisionDate ");
		# Get the field (State) and the value (Wisconsin) from the $expression.
	        $expression =~ /($attribute)\s*($operator)\s*($attribute)/gi;
	        my $field = $1;
            my $current_operator = $2;
	        my $value = $3;

		# quote the field / value variables.
		my $quotedField = $field;
		my $quotedValue = $value;
		unless ($field =~ /^\s*['"].*['"]\s*/) {
			$quotedField = $db->quote($field);
		}
        unless ($value =~ /^\s*['"].*['"]\s*/) {
            $quotedValue = $db->quote($value);
        }
		
		# transform replacement from "State = Wisconsin" to 
		# "(fieldname=State and value = Wisconsin)"
		my $clause = "(".$counter."_p.fieldName=".$quotedField." and ".$counter."_v.value ";
        $replacement =~ s/\Q$field/$clause/;
        $replacement =~ s/\Q$value/$quotedValue )/i;

		# replace $expression with the new $replacement in $constraint.
        $constraint =~ s/\Q$expression/$replacement/;
        if ($current_operator eq '+~') {
            $constraint =~ s/and ${counter}_v\.value\s*\+~\s*$quotedValue/and FIND_IN_SET($quotedValue, REPLACE(${counter}_v.value,"\\n",","))/;
        }
		push (@joins, " left join metaData_properties ".$counter."_p on ".$counter."_p.fieldId=".$counter."_v.fieldId ");
		$counter++;
	}

	my $sql = "select a.assetId from asset a
               left join assetData d on a.assetId = d.assetId
                   and d.revisionDate=(
                       select max(revisionDate)
                       from assetData d2
                       where d2.assetId = a.assetId
                   ) ".join("\n", @joins)."
			where a.className = ".$db->quote($self->getShortcutDefault->get("className"));
	# Add constraint only if it has been modified.
	$sql .= " and ".$constraint if (($constraint ne $criteria) && $constraint ne "");
	$sql .= " order by a.creationDate desc";

	# Execute the query with an unconditional read
	my @ids;
        my $sth = $db->unconditionalRead($sql);
        while (my ($data) = $sth->array) {
		push (@ids, $data);
        }
        $sth->finish;

	# No matching assets found.
        if (scalar(@ids) == 0) {
                return $self->getShortcutDefault; # fall back to the originally mirrored asset.
	}
	my $id;
	# Grab a wid from the results
	if ($order eq 'random') {
		$id = $ids[ rand @ids ];
	} else { 
				 #default order is mostRecent
		$id = $ids[0]; # 1st element in list is most recent.
	}

	# Store the matching assetId in user scratch. 
	$self->session->scratch->set($scratchId,$id) if ($scratchId);

	return WebGUI::Asset->newByDynamicClass($self->session, $id);
}

#-------------------------------------------------------------------

=head2 getShortcutDefault 

Return the asset that this Shortcut points to.

=cut

sub getShortcutDefault {
	my $self = shift;
	return WebGUI::Asset->newByDynamicClass($self->session, $self->get("shortcutToAssetId"));
}

#-------------------------------------------------------------------

=head2 getShortcutOriginal 

If shortcutByCriteria is set, return the Shortcut that matches the criteria.  Otherwise,
just return the asset the shortcut points to.

=cut

sub getShortcutOriginal {
	my $self = shift;
	if ($self->get("shortcutByCriteria")) {
		return $self->getShortcutByCriteria;
	} else {
		return $self->getShortcutDefault;
	}
}

#-------------------------------------------------------------------

=head2 getPrefFieldsToShow 

Returns an array of profile fields to show to the user as preferences.

=cut

sub getPrefFieldsToShow {
	my $self = shift;
	return split("\n",$self->getValue("prefFieldsToShow"));
}

#-------------------------------------------------------------------

=head2 getPrefFieldsToImport 

Returns an array of profile fields to import from the user's profile as variables
for overrides.

=cut

sub getPrefFieldsToImport {
	my $self = shift;
	return split("\n",$self->getValue("prefFieldsToImport"));
}

#----------------------------------------------------------------------------

=head2 getTemplateVars

Gets the template vars for the asset we're a shortcut to, with any overrides
applied.

=cut

sub getTemplateVars {
    my $self            = shift;

    my $shortcut        = $self->getShortcut;
    if ( $shortcut->can('getTemplateVars') ) {
        return $shortcut->getTemplateVars;
    }
    else {
        return $shortcut->get;
    }
}

#-------------------------------------------------------------------

=head2 isDashlet 

Returns true if this Shortcut is a Dashlet.  Dashlets are chilren of Dashboards.

=cut

sub isDashlet {
	my $self = shift;
	return 1 if ref $self->getParent eq 'WebGUI::Asset::Wobject::Dashboard';
	return 0;
}

#-------------------------------------------------------------------

=head2 notLinked 

Returns an I18n'ed error message that the Asset that this Shortcut points to no longer exists.

=cut

sub notLinked {
	my $self = shift;
	$self->session->errorHandler->warn("Shortcut ".$self->getId." is linked to an asset ".$self->get("shortcutToAssetId").", which no longer exists.");
    my $i18n = WebGUI::International->new($self->session, 'Asset_Shortcut');
	return $i18n->get('no longer exists');
}

#-------------------------------------------------------------------

=head2 paste ( )

Pasting assets under a Shortcut can cause lots of problems, including infinite loops
for operations like paste, and purge.

=cut

sub paste {
    return 0;
}

#-------------------------------------------------------------------

=head2 prepareView ( )

See WebGUI::Asset::prepareView() for details.  Extends the base class to call prepareView
on the Asset that is shortcutted.

=cut

sub prepareView {
	my $self = shift;
	$self->SUPER::prepareView();
	my $template = WebGUI::Asset::Template->new($self->session, $self->get("templateId"));
	$template->prepare($self->getMetaDataAsTemplateVariables);
	$self->{_viewTemplate} = $template;
	my $shortcut = $self->getShortcut;
	$shortcut->prepareView if defined $shortcut;
}


#-------------------------------------------------------------------

=head2 processPropertiesFromFormPost ( )

See WebGUI::Asset::processPropertiesFromFormPost () for details.  Extends the base class to delete
the scratch variables, and to uncache the overrides.

=cut

sub processPropertiesFromFormPost {
	my $self = shift;
	$self->SUPER::processPropertiesFromFormPost;
	my $scratchId = "Shortcut_" . $self->getId;
	$self->session->scratch->delete($scratchId);
	$self->uncacheOverrides;
}

#----------------------------------------------------------------------------

=head2 setOverride ( overrides )

Set this shortcut's overrides. C<overrides> is a hash reference of overrides
to set.

=cut

sub setOverride {
    my $self        = shift;
    my $override    = shift;

    croak "Shortcut->setOverride - first argument must be hash reference" 
        unless $override && ref $override eq "HASH";
    
    for my $key ( %$override ) {
        $self->session->db->write(
            "DELETE FROM Shortcut_overrides WHERE assetId=? AND fieldName=?",
            [$self->getId, $key],
        );
        $self->session->db->write(
            "INSERT INTO Shortcut_overrides VALUES (?,?,?)",
            [$self->getId, $key, $override->{$key}],
        );
    }
}

#-------------------------------------------------------------------

=head2 purge ( )

See Asset::purge for details.  Extends the base method to delete this Shortcut's
overrides.

=cut

sub purge {
    my $self = shift;
    $self->session->db->write(<<'END_SQL', [$self->getId]);
DELETE FROM Shortcut_overrides
WHERE assetId = ?
END_SQL
    return $self->SUPER::purge(@_);
}

#-------------------------------------------------------------------

=head2 uncacheOverrides 

Delete any cached overrides.

=cut

sub uncacheOverrides {
	my $self = shift;
	WebGUI::Cache->new($self->session,$self->_overridesCacheTag)->delete;
}

#-------------------------------------------------------------------

=head2 view 

Render the Shortcut.

=cut

sub view {
	my $self = shift;
	my $content;
	my $i18n = WebGUI::International->new($self->session,"Asset_Shortcut");
	my $shortcut = $self->getShortcut;

	unless (defined $shortcut) {
		if ($self->canEdit) {
			return $self->session->style->userStyle('<a href="'.$self->getUrl("func=delete").'">'.$self->notLinked.'</a>');
		} else {
			return $self->notLinked;
		}
	}
	
    if ($self->get("shortcutToAssetId") eq $self->get("parentId")) {
        $content = $i18n->get("Displaying this shortcut would cause a feedback loop");
    }
    elsif (! $shortcut->canView) {
        $content = '';
    }
    else {
        # Make sure the www_view method won't be skipped b/c the asset is cached.
        $shortcut->purgeCache();

        my $t = ($self->session->log->canShowPerformanceIndicators()) ? [Time::HiRes::gettimeofday()] : undef;
        $content = $shortcut->view;
        $content .= "Shortcut:" . Time::HiRes::tv_interval($t)
            if $t;

        # Make sure the overrides are not cached by the original asset.
        $shortcut->purgeCache();
	}

	my %var = (
		isShortcut => 1,
		'shortcut.content' => $content,
		'shortcut.label' => $i18n->get('assetName'),
		originalURL => $shortcut->getUrl,
		'shortcut.url'=>$self->getUrl
		);
	foreach my $prop (keys %{$self->{_shortcut}{_properties}}) {
		next if ($prop eq 'content' || $prop eq 'label' || $prop eq 'url');
		$var{'shortcut.'.$prop} = $self->{_shortcut}{_properties}{$prop};
	}
	return $self->processTemplate(\%var,undef, $self->{_viewTemplate});
}

#-------------------------------------------------------------------

=head2 www_edit 

Override the base class to handle adding a menu entry for Manage Overrides.

=cut

sub www_edit {
	my $self = shift;
	return $self->session->privilege->insufficient() unless $self->canEdit;
	return $self->session->privilege->locked() unless $self->canEditIfLocked;
	my $i18n = WebGUI::International->new($self->session,"Asset_Shortcut");
	$self->getAdminConsole->addSubmenuItem($self->getUrl("func=manageOverrides"),$i18n->get("Manage Shortcut Overrides"));
	return $self->getAdminConsole->render($self->getEditForm->print,$i18n->get(2));
}

#-------------------------------------------------------------------

=head2 www_getUserPrefsForm 

Returns a form displaying all user profile fields to show to the user, that they
are allowed to edit.

=cut

sub www_getUserPrefsForm {
	#This is a form retrieved by "ajax".
	my $self    = shift;
    my $session = $self->session;
    my $i18n = WebGUI::International->new($session, 'Asset_Shortcut');
	return $i18n->get('not logged in') if $session->user->isVisitor;
	return $i18n->get('cannot personalize') unless $self->getParent->canPersonalize;
	my $output;
	my @fielden = $self->getPrefFieldsToShow;
	my $f = WebGUI::HTMLForm->new($self->session,extras=>' onsubmit="submitForm(this,\''.$self->getId.'\',\''.$self->getUrl.'\');return false;"');
	$f->raw('<table cellspacing="0" cellpadding="3" border="0">');
    my $allowedToSave = ( ! $session->var->isAdminOn && $self->getParent->canPersonalize )
                     || (   $session->var->isAdminOn && $session->user->isInGroup($session->setting->get('groupIdAdminUser')) );
    if ($allowedToSave) {
        $f->hidden(  
            -name => 'func', 
            -value => 'saveUserPrefs'
        );
    }
	my $u = WebGUI::User->new($session, $self->discernUserId);
	FIELD: foreach my $fieldId (@fielden) {
		my $field = WebGUI::ProfileField->new($session,$fieldId);
		next FIELD unless $field;
		my $params = {};
		if (lc($field->get("fieldType")) eq 'textarea') {
			$params->{rows} = 4;
			$params->{columns} = 20;
		}
		if (lc($field->get("fieldType")) eq 'text') {
			$params->{size} = 20;
		}
        if (! $allowedToSave) {
            $params->{extras} = ' disabled ';
        }
		$f->raw($field->formField($params,1, $u));
	}
    if ($allowedToSave) {
        $f->submit({extras=>'className="nothing"'});
    }
	$f->raw('</table>');
	my $tags = $session->style->generateAdditionalHeadTags();
	$output .= $tags.$f->print;

	return $output;
}

#-------------------------------------------------------------------

=head2 www_manageOverrides 

Web facing method for getOverridesList.  Returns insufficient unless the current
user canEdit this Shortcut.

=cut

sub www_manageOverrides {
	my $self = shift;
	return $self->session->privilege->insufficient() unless $self->canEdit;
	my $i18n = WebGUI::International->new($self->session,"Asset_Shortcut");
	return $self->_submenu($self->getOverridesList,$i18n->get("Manage Shortcut Overrides"));
}

#-------------------------------------------------------------------

=head2 www_purgeOverrideCache 

Web facing method for purgeOverrideCache.  Returns insufficient unless the current
user canEdit this Shortcut.  Returns the user to the manageOverrides screen.

=cut

sub www_purgeOverrideCache {
	my $self = shift;
	return $self->session->privilege->insufficient() unless $self->canEdit;
	$self->uncacheOverrides;
	return $self->www_manageOverrides;
}

#-------------------------------------------------------------------

=head2 www_deleteOverride 

Delete an override by fieldName, as set by the form variable C<fieldName>.  Then it
uncaches the overrides and returns the user to the manageOverrides screen.

=cut

sub www_deleteOverride {
	my $self = shift;
	return $self->session->privilege->insufficient() unless $self->canEdit;
	$self->session->db->write('delete from Shortcut_overrides where assetId='.$self->session->db->quote($self->getId).' and fieldName='.$self->session->db->quote($self->session->form->process("fieldName")));
	$self->uncacheOverrides;
	return $self->www_manageOverrides;
}

#-------------------------------------------------------------------

=head2 www_saveUserPrefs

Updates the user's profile from the inline form displayed on the dashlet.  Only users
who are allowed to personalize the Dashboard, or who are in admin mode and are in groupIdAdminUser
are allowed to call this method.  The last restriction is due to the following:
When admin mode is turned on, the form displays the profile fields for Visitor.  Saving
the form would allow someone who is not a User Admin to alter Visitor's profile.

=cut

sub www_saveUserPrefs {
	my $self    = shift;
    my $session = $self->session;
	return '' unless $self->getParent->canPersonalize
                  || ( $session->var->isAdminOn && $session->user->isInGroup($session->setting->get('groupIdAdminUser')) );
	my @fellowFields = $self->getPrefFieldsToShow;
	my %data = ();
	$self->uncacheOverrides;
	my $i18n = WebGUI::International->new($session);
	my $u = WebGUI::User->new($session, $self->discernUserId);
	foreach my $fieldId ($session->form->param) {
		my $field = WebGUI::ProfileField->new($session,$fieldId);
		next unless $field;
		$data{$field->getId} = $field->formProcess;
        if ($field->getId eq 'email' && $field->isDuplicate($data{$field->getId})) {
			return '<li>'.$i18n->get(1072).'</li>';
		}
		if ($field->isRequired && !$data{$field->getId}) {
			return '<li>'.$field->getLabel.' '.$i18n->get(451).'</li>';
		}
		$u->profileField($field->getId,$data{$field->getId});
	}
	return $self->getParent->www_view;
}

#-------------------------------------------------------------------

=head2 www_getNewTitle 

Returns the title of the shortcut, with overrides applied.  If the user cannot
personalize the Dashboard, returns ''.

=cut

sub www_getNewTitle {
	my $self = shift;
	return '' unless $self->getParent->canPersonalize;
	my $foo = $self->getShortcut;
	return $foo->{_properties}{title};
}

#-------------------------------------------------------------------

=head2 www_editOverride 

Renders a form for overrides to be entered.  The override to be edited is
set by the form variable C<fieldName>.

=cut

sub www_editOverride {
	my $self = shift;
	return $self->session->privilege->insufficient() unless $self->canEdit;
	my $i18n = WebGUI::International->new($self->session, "Asset_Shortcut");
	my $fieldName = $self->session->form->process("fieldName");

	# Using getOverrides is not the most efficient way to get the properties of only
	# one override, since it'll return all overrides (that have been set)
	my %overrides = $self->getOverrides;

	# Cannot fetch the original value from the overrides hash b/c it will be empty if
	# the override has not been set before. Also getOverrides uses a cached version of
	# the origValue, which can be out of date.
	my $origValue = $self->getShortcutOriginal->getValue($fieldName); 

	my $output = '';
	$output .= '</table>';
	
	my $f = WebGUI::HTMLForm->new($self->session,-action=>$self->getUrl);
	$f->hidden(
		-name		=> "func",
		-value		=> "saveOverride"
	);
	$f->hidden(
		-name		=> "overrideFieldName",
		-value		=> $fieldName
	);
	$f->readOnly(
		-label		=> $i18n->get("fieldName"),
		-value		=> $fieldName
	);
	$f->readOnly(
		-label		=> $i18n->get("Original Value"),
		-value		=> $origValue
	);

	# Fetch the parameters for the dynamic field.
	my (%params, %props);
	foreach my $def (@{$self->getShortcutOriginal->definition($self->session)}) {
		%props = (%props,%{$def->{properties}});
	}
	foreach my $key (keys %{$props{$fieldName}}) {
		next if ($key eq "tab");
		$params{$key} = $props{$fieldName}{$key};
	}
	$params{value} = $origValue;
	$params{name} = $fieldName;
	$params{label} = $params{label} || $i18n->get("Edit Field Directly");
	$params{hoverHelp} = $params{hoverHelp} || $i18n->get("Use this field to edit the override using the native form handler for this field type");

	if ($params{fieldType} eq 'template') {$params{namespace} = $params{namespace} || WebGUI::Asset->newByDynamicClass($self->session, $origValue)->get("namespace");}

	$f->dynamicField(%params);
	$f->textarea(
		-name		=> "newOverrideValueText",
		-label		=> $i18n->get("New Override Value"),
		-value		=> $overrides{overrides}{$fieldName}{newValue},
		-hoverHelp	=> $i18n->get("Place something in this box if you dont want to use the automatically generated field")
	);
	$f->readOnly(
		-label		=> $i18n->get("Replacement Value"),
		-value		=> $overrides{overrides}{$fieldName}{parsedValue},
		-hoverHelp	=> $i18n->get("This is the example output of the field when parsed for user preference macros")
	) if $self->isDashlet;
	$f->submit;

	$output .= $f->print;
	
	return $self->_submenu($output,$i18n->get('Edit Override'));
}

#-------------------------------------------------------------------

=head2 www_saveOverride 

Process the editOverride form.

=cut

sub www_saveOverride {
    my $self = shift;
    return $self->session->privilege->insufficient() unless $self->canEdit;
    my $fieldName = $self->session->form->process("overrideFieldName");
    my %overrides = $self->getOverrides;
    my $output = '';
    my %props;
    foreach my $def (@{$self->getShortcutOriginal->definition($self->session)}) {
        %props = (%props,%{$def->{properties}});
    }
    my $fieldType = $props{$fieldName}{fieldType};
    my $value = $self->session->form->process($fieldName,$fieldType);
    $value = $self->session->form->process("newOverrideValueText") || $value;
    $self->session->db->write("delete from Shortcut_overrides where assetId=".$self->session->db->quote($self->getId)." and fieldName=".$self->session->db->quote($fieldName));
    $self->session->db->write("insert into Shortcut_overrides values (".$self->session->db->quote($self->getId).",".$self->session->db->quote($fieldName).",".$self->session->db->quote($value).")");
    $self->uncacheOverrides;
    $self->getShortcutOriginal->purgeCache();
    return $self->www_manageOverrides;
}

#-------------------------------------------------------------------

=head2 www_view 

Render the shortcut in standalone mode.

=cut

sub www_view {
        my $self    = shift;
        my $session = $self->session;
        my $check   = $self->checkView;
        return $check if defined $check;
        my $shortcut = $self->getShortcut;
        return $session->privilege->noAccess() unless $shortcut->canView;
        $self->prepareView;

        # Make sure the www_view method won't be skipped b/c the asset is cached.
        $shortcut->purgeCache();

        if ($shortcut->isa('WebGUI::Asset::Wobject')) {
                $self->session->http->setLastModified($self->getContentLastModified);
                $self->session->http->sendHeader;
                ##Tell processStyle not to set the h
                my $style = $shortcut->processStyle($shortcut->getSeparator, { noHeadTags => 1 });
                my ($head, $foot) = split($shortcut->getSeparator,$style);
                $self->session->output->print($head, 1);
                $self->session->output->print($self->view);
                $self->session->output->print($foot, 1);
                return "chunked";
        }
        my $output = $shortcut->www_view;
        
        # Make sure the overrides are not cached by the original asset.
        $shortcut->purgeCache();
        return $output;
}

#----------------------------------------------------------------------------

=head1 STATIC METHODS

These methods are called using CLASS->method

#----------------------------------------------------------------------------

=head2 getShortcutsForAssetId ( session, assetId [, properties] )

Get an arrayref of assetIds of all the shortcuts for the passed-in assetId.

"properties" is a hash reference of properties to give to getLineage. 
Probably the only useful key will be "returnObjects".

=cut

sub getShortcutsForAssetId {
    my $class       = shift;
    my $session     = shift;
    my $assetId     = shift;
    my $properties  = shift || {};
    
    croak "First argument to getShortcutsForAssetId must be WebGUI::Session"
        unless $session && $session->isa("WebGUI::Session");
    croak "Second argument to getShortcutsForAssetId must be assetId"
        unless $assetId;
    croak "Third argument to getShortcutsForAssetId must be hash reference"
        if $properties && !ref $properties eq "HASH";

    my $db      = $session->db;

    $properties->{ joinClass            } = 'WebGUI::Asset::Shortcut';
    $properties->{ whereClause          } = 'Shortcut.shortcutToAssetId = ' . $db->quote($assetId);
    $properties->{ statesToInclude      } = ['published', 'trash', 'clipboard', 'clipboard-limbo', 'trash-limbo'];
    $properties->{ statusToInclude      } = ['approved', 'pending', 'archived'];

    return WebGUI::Asset->getRoot($session)->getLineage(['descendants'], $properties); 
}

1;

