package winterwell.jtwitter;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


import winterwell.json.JSONArray;
import winterwell.json.JSONException;
import winterwell.json.JSONObject;
import winterwell.jtwitter.Twitter.KEntityType;
import winterwell.jtwitter.TwitterException.E401;
import winterwell.jtwitter.TwitterException.E403;
import winterwell.jtwitter.TwitterException.SuspendedUser;

/**
 * Java wrapper for the Twitter API version {@value #version}
 * <p>
 * Example usage:<br>
 * First, you should get the user to authorise access via OAuth. There are a
 * couple of ways of doing this -- we show one below -- see
 * {@link OAuthSignpostClient} for more details.
 * <p>
 * Note that you don't need to do this for some operations - e.g. you can look
 * up public posts without logging in (use the {@link #Twitter()} constructor.
 * You can also - for now! - use username and password to login, but Twitter
 * plan to switch this off soon.
 * 
 * <code><pre>
	// First, OAuth to login: Make an oauth client
	OAuthSignpostClient oauthClient = new OAuthSignpostClient(JTWITTER_OAUTH_KEY, JTWITTER_OAUTH_SECRET, "oob");
    // open the authorisation page in the user's browser
    oauthClient.authorizeDesktop(); // Note: this only works on desktop PCs
    // or direct the user to the webpage given jby oauthClient.authorizeUrl()
    // get the pin from the user since we're using "oob" instead of a callback servlet
    String v = oauthClient.askUser("Please enter the verification PIN from Twitter");
    oauthClient.setAuthorizationCode(v);
	// You can store the authorisation token details for future use
    Object accessToken = client.getAccessToken();
</pre></code>
 * 
 * Now we can access Twitter: <code><pre>
	// Make a Twitter object
	Twitter twitter = new Twitter("my-name", oauthClient);
	// Print Winterstein's status
	System.out.println(twitter.getStatus("winterstein"));
	// Set my status
	twitter.updateStatus("Messing about in Java");
</pre></code>
 * 
 * <p>
 * If you can handle callbacks, then the OAuth login can be streamlined. You
 * need a webserver and a servlet (eg. use Jetty or Tomcat) to handle callbacks.
 * Replace "oob" with your callback url. Direct the user to
 * client.authorizeUrl(). Twitter will then call your callback with the request
 * token and verifier (authorisation code).
 * </p>
 * <p>
 * See {@link http://www.winterwell.com/software/jtwitter.php} for more
 * information about this wrapper. See {@link http://dev.twitter.com/doc} for
 * more information about the Twitter API.
 * <p>
 * Notes:
 * <ul>
 * <li>This wrapper takes care of all url-encoding/decoding.
 * <li>This wrapper will throw a runtime exception (TwitterException) if a
 * methods fails, e.g. it cannot connect to Twitter.com or you make a bad
 * request.
 * <li>Note that Twitter treats old-style retweets (those made by sending a
 * normal tweet beginning "RT @whoever") differently from new-style retweets
 * (those made using the retweet API). The differences are documented in various
 * methods.
 * <li>Most methods are available via this class (Twitter), except for list
 * support (in {@link TwitterList} - though {@link #getLists()} is here) and
 * some profile/account settings (in {@link Twitter_Account}).
 * <li>This class is NOT thread safe. If you're using multiple threads, it is
 * best to create separate Twitter objects (which is fine).
 * </ul>
 * 
 * <h4>Copyright and License</h4>
 * This code is copyright (c) Winterwell Associates 2008/2009 and (c) winterwell
 * Mathematics Ltd, 2007 except where otherwise stated. It is released as
 * open-source under the LGPL license. See <a
 * href="http://www.gnu.org/licenses/lgpl.html"
 * >http://www.gnu.org/licenses/lgpl.html</a> for license details. This code
 * comes with no warranty or support.
 * 
 * <h4>Change List</h4>
 * The change list is kept online at: {@link http
 * ://www.winterwell.com/software/changelist.txt}
 * 
 * @author Daniel Winterstein
 */
public class Twitter implements Serializable {
	/**
	 * Use to register per-page callbacks for long-running searches. To stop the
	 * search, return true.
	 * 
	 */
	public interface ICallback {
		public boolean process(List<Status> statuses);
	}
	

	/** TODO
	 * 
	 */
//	If available, returns an array of replies and mentions related to the 
//	specified Tweet. There is no guarantee there will be any replies or 
//	mentions in the response. This method is only available to users who 
//	have access to #newtwitter.
	@Deprecated // TODO
	public List<ITweet> getRelated(ITweet tweet) { 
		// TODO
		String url = TWITTER_URL+"/related_results/show/"+tweet.getId()+".json"; 
		throw new RuntimeException("TODO");
	}
			
	/**
	 * How is the Twitter API today?
	 * See {@link https://dev.twitter.com/status} for more information. 
	 * @return map of {method: %uptime in the last 24 hours}.
	 * An empty map indicates this method itself failed!
	 * 
	 * @throws Exception This method is not officially supported! As such,
	 * it could break at some future point.
	 */
	public static Map<String,Double> getAPIStatus() throws Exception{
		HashMap<String,Double> map = new HashMap();
		// c.f. https://dev.twitter.com/status & https://status.io.watchmouse.com/7617
		// https://api.io.watchmouse.com/synth/current/39657/folder/7617/?fields=info;cur;24h.uptime;24h.status;last.date;daily.avg;daily.uptime;daily.status;daily.period
		String json = null;
		try {
			URLConnectionHttpClient client = new URLConnectionHttpClient();
			json = client.getPage("https://api.io.watchmouse.com/synth/current/39657/folder/7617/?fields=info;cur;24h.uptime", null, false);
			JSONObject jobj = new JSONObject(json);
			JSONArray jarr = jobj.getJSONArray("result");
			for(int i=0; i<jarr.length(); i++) {
				JSONObject jo = jarr.getJSONObject(i);
				String name = jo.getJSONObject("info").getString("name");
				JSONObject h24 = jo.getJSONObject("24h");
				double value = h24.getDouble("uptime");
				map.put(name, value);
			}
			return map;
		} catch (JSONException e) {
			throw new TwitterException.Parsing(json, e);
		} catch (Exception e) {
			return map;
		}		
	}
	
	/**
	 * Interface for an http client - e.g. allows for OAuth to be used instead.
	 * The standard version is {@link OAuthSignpostClient}.
	 * <p>
	 * If creating your own version, please provide support for throwing the
	 * right subclass of TwitterException - see
	 * {@link URLConnectionHttpClient#processError(java.net.HttpURLConnection)}
	 * for example code.
	 * 
	 * @author Daniel Winterstein
	 */
	public static interface IHttpClient {

		/**
		 * Whether this client is setup to do authentication when contacting the
		 * Twitter server. Note: This is a fast method that does not call the
		 * server, so it does not check whether the access token or password is
		 * valid. See {Twitter#isValidLogin()} or
		 * {@link Twitter_Account#verifyCredentials()} if you need to check a
		 * login.
		 * */
		boolean canAuthenticate();

		/**
		 * Lower-level GET method.
		 * 
		 * @param url
		 * @param vars
		 * @param authenticate
		 * @return
		 * @throws IOException
		 */
		HttpURLConnection connect(String url, Map<String, String> vars,
				boolean authenticate) throws IOException;

		/**
		 * @return a copy of this client. The copy can share structure, but it
		 *         MUST be safe for passing to a new thread to be used in
		 *         parallel with the original.
		 */
		IHttpClient copy();

		/**
		 * Fetch a header from the last http request. This is inherently NOT
		 * thread safe. Headers from error messages should (probably) be cached.
		 * 
		 * @param headerName
		 * @return header value, or null if unset
		 */
		String getHeader(String headerName);

		/**
		 * Send an HTTP GET request and return the response body. Note that this
		 * will change all line breaks into system line breaks!
		 * 
		 * @param uri
		 *            The uri to fetch
		 * @param vars
		 *            get arguments to add to the uri
		 * @param authenticate
		 *            If true, use authentication. The authentication method
		 *            used depends on the implementation (basic-auth, OAuth). It
		 *            is an error to use true if no authentication details have
		 *            been set.
		 * 
		 * @throws TwitterException
		 *             for a variety of reasons
		 * @throws TwitterException.E404
		 *             for resource-does-not-exist errors
		 */
		String getPage(String uri, Map<String, String> vars,
				boolean authenticate) throws TwitterException;

		/**
		 * @see Twitter#getRateLimit(KRequestType) This is where the Twitter
		 *      method is implemented.
		 */
		RateLimit getRateLimit(KRequestType reqType);

		/**
		 * Send an HTTP POST request and return the response body.
		 * 
		 * @param uri
		 *            The uri to post to.
		 * @param vars
		 *            The form variables to send. These are URL encoded before
		 *            sending.
		 * @param authenticate
		 *            If true, send user authentication
		 * @return The response from the server.
		 * 
		 * @throws TwitterException
		 *             for a variety of reasons
		 * @throws TwitterException.E404
		 *             for resource-does-not-exist errors
		 */
		String post(String uri, Map<String, String> vars, boolean authenticate)
				throws TwitterException;

		/**
		 * Lower-level POST method.
		 * 
		 * @param uri
		 * @param vars
		 * @return a freshly opened authorised connection
		 * @throws TwitterException
		 */
		HttpURLConnection post2_connect(String uri, Map<String, String> vars)
				throws Exception;

		/**
		 * Set the timeout for a single get/post request. This is an optional
		 * method - implementations can ignore it!
		 * 
		 * @param millisecs
		 */
		void setTimeout(int millisecs);

	}

	/**
	 * This gives common access to features that are common to both
	 * {@link Message}s and {@link Status}es.
	 * 
	 * @author daniel
	 * 
	 */
	public static interface ITweet extends Serializable {

		Date getCreatedAt();

		/**
		 * Twitter IDs are numbers - but they can exceed the range of Java's
		 * signed long.
		 * 
		 * @return The Twitter id for this post. This is used by some API
		 *         methods. This may be a Long or a BigInteger.
		 */
		Number getId();

		/**
		 * @return the location of this tweet. Can be null, never blank. This
		 *         can come from geo-tagging or the user's location. This may be
		 *         a place name, or in the form "latitude,longitude" if it came
		 *         from a geo-tagged source.
		 *         <p>
		 *         Note: This will be set if Twitter supply any geo-information.
		 *         We extract a location from geo and place objects.
		 */
		String getLocation();

		/**
		 * @return list of screen-names this message is to. May be empty, never
		 *         null. For Statuses, this is anyone mentioned in the message.
		 *         For DMs, this is a wrapper round
		 *         {@link Message#getRecipient()}.
		 *         <p>
		 *         Notes: This method is in ITweet as a convenience to allow the
		 *         same code to process both Statuses and Messages where
		 *         possible. It would be better named "getRecipients()", but for
		 *         historical reasons it isn't.
		 */
		List<String> getMentions();

		/**
		 * @return more information on the location of this tweet. This is
		 *         usually null!
		 */
		Place getPlace();

		/** The actual status text. This is also returned by {@link #toString()} */
		String getText();

		/**
		 * Twitter wrap urls with their own url-shortener (as a defence against
		 * malicious tweets). You are recommended to direct people to the
		 * Twitter-url, but use the original url for display.
		 * <p>
		 * Entity support is off by default. Request entity support by setting
		 * {@link Twitter#setIncludeTweetEntities(boolean)}. Twitter do NOT
		 * support entities for search :(
		 * 
		 * @param type
		 *            urls, user_mentions, or hashtags
		 * @return the text entities in this tweet, or null if the info was not
		 *         supplied.
		 */
		List<TweetEntity> getTweetEntities(KEntityType type);

		/** The User who made the tweet */
		User getUser();

		/**
		 * @return text, with the t.co urls replaced.
		 * Use-case: for filtering based on text contents, when we want to
		 * match against the full url.
		 * Note: this does NOT resolve short urls from bit.ly etc. 
		 */
		String getDisplayText();

	}

	public static enum KEntityType {
		hashtags, urls, user_mentions
	}

	/**
	 * The different types of API request. These can have different rate limits.
	 */
	public static enum KRequestType {
		NORMAL(""), SEARCH("Feature"),
		/** this is X-Feature Class "namesearch" in the response headers */
		SEARCH_USERS("Feature"), SHOW_USER(""), UPLOAD_MEDIA("Media");

		/**
		 * USed to find the X-?RateLimit header.
		 */
		final String rateLimit;

		private KRequestType(String rateLimit) {
			this.rateLimit = rateLimit;
		}
	}

	/**
	 * A special slice of text within a tweet.
	 * 
	 * @see Twitter#setIncludeTweetEntities(boolean)
	 */
	public final static class TweetEntity implements Serializable {
		private static final long serialVersionUID = 1L;

		/**
		 * 
		 * @param tweet
		 * @param rawText
		 * @param type
		 * @param jsonEntities
		 * @return Can be null if no entities of this type are specified
		 * @throws JSONException
		 */
		static List<TweetEntity> parse(ITweet tweet, String rawText, KEntityType type,
				JSONObject jsonEntities) throws JSONException 
		{
			assert type != null && tweet != null && rawText != null && jsonEntities!=null
								: tweet+"\t"+rawText+"\t"+type+"\t"+jsonEntities;
			try {
				JSONArray arr = jsonEntities.optJSONArray(type.toString());
				// e.g. "user_mentions":[{"id":19720954,"name":"Lilly Hunter","indices":[0,10],"screen_name":"LillyLyle"}
				if (arr==null || arr.length()==0) {
					return null;
				}
				ArrayList<TweetEntity> list = new ArrayList<TweetEntity>(
						arr.length());
				for (int i = 0; i < arr.length(); i++) {
					JSONObject obj = arr.getJSONObject(i);
					TweetEntity te = new TweetEntity(tweet, rawText, type, obj);
					list.add(te);
				}
				return list;
			} catch (Throwable e) {
				// whatever bogus data Twitter send, don't fail
				return null;
			}
		}

		final String display;
		/**
		 * end of the entity in the contents String, exclusive
		 */
		public final int end;
		/**
		 * start of the entity in the contents String, inclusive
		 */
		public final int start;
		private final ITweet tweet;

		public final KEntityType type;

		/**
		 * 
		 * @param tweet
		 * @param rawText Needed to undo the indexing errors created by entity encoding
		 * @param type
		 * @param obj
		 * @throws JSONException
		 */
		TweetEntity(ITweet tweet, String rawText, KEntityType type, JSONObject obj)
				throws JSONException 
		{
			this.tweet = tweet;
			this.type = type;
			switch (type) {
			case urls:
				Object eu = obj.opt("expanded_url");
				display = JSONObject.NULL.equals(eu) ? null : (String) eu;
				break;
			case user_mentions:
				display = obj.getString("name");
				break;
			default:
				display = null;
			}
			
			// start, end
			JSONArray indices = obj.getJSONArray("indices");
			int _start = indices.getInt(0);
			int _end = indices.getInt(1);
			assert _start >= 0 && _end >= _start : obj;
			// Sadly, due to entity encoding, start/end may be off!
			String text = tweet.getText();
			if (rawText.regionMatches(_start, text, _start, _end - _start)) {
				// normal case: all OK			
				start = _start; end = _end;
				return;
			}
			// oh well - let's correct start/end
			// Note: This correction go wrong in a particular case: 
			// encoding has messed up the indices & we have a repeated entity.
			// ??Do we care enough to fix such a rare corner case with moderately harmless side-effects?
			
			// Protect against (rare) dud data from Twitter
			_end = Math.min(_end, rawText.length());
			_start = Math.min(_start, _end);
			if (_start==_end) { // paranoia
				start = _start; 
				end = _end;
				return;
			}
				
			String entityText = rawText.substring(_start, _end);
			int i = text.indexOf(entityText);
			if (i==-1) {
				// This can't legitimately happen, but handle it anyway 'cos it does (rare & random)
				entityText = InternalUtils.unencode(entityText);
				i = text.indexOf(entityText);
				if (i==-1) i = _start; // give up gracefully
			}
			start = i; 
			end = start + _end - _start;		
		}

		/**
		 * Constructor for when you know exactly what you want (rare).
		 */
		TweetEntity(ITweet tweet, KEntityType type, int start, int end, String display) {
			this.tweet = tweet;
			this.end = end;
			this.start = start;
			this.type = type;			
			this.display = display;
		}

		/**
		 * @return For a url: the expanded version For a user-mention: the
		 *         user's name
		 */
		public String displayVersion() {
			return display == null ? toString() : display;
		}

		/**
		 * The slice of text in the tweet. E.g. for a url, this will be the
		 * *shortened* version.
		 * 
		 * @see #displayVersion()
		 */
		@Override
		public String toString() {
			// There is a strange bug where -- rarely -- end > tweet length!
			// I think this is now fixed (it was an encoding issue).
			String text = tweet.getText();
			int e = Math.min(end, text.length());
			int s = Math.min(start, e);
			return text.substring(s, e);
		}
	}

	/**
	 * This rather dangerous global toggle switches off lower-casing on Twitter
	 * screen-names.
	 * <p>
	 * Screen-names are case insensitive as far as Twitter is concerned. However
	 * you might want to preserve the case people use for display purposes.
	 * <p>
	 * false by default.
	 */
	public static boolean CASE_SENSITIVE_SCREENNAMES;

	static final Pattern contentTag = Pattern.compile(
			"<content>(.+?)<\\/content>", Pattern.DOTALL);

	static final Pattern idTag = Pattern.compile("<id>(.+?)<\\/id>",
			Pattern.DOTALL);

	/**
	 * The length of a url after t.co shortening. Currently 20 characters.
	 * <p>
	 * Use updateConfiguration() if you want to get the latest settings from
	 * Twitter.
	 */
	public static int LINK_LENGTH = 20;

	public static long PHOTO_SIZE_LIMIT;

	public static final String SEARCH_MIXED = "mixed";

	public static final String SEARCH_POPULAR = "popular";

	public static final String SEARCH_RECENT = "recent";

	private static final long serialVersionUID = 1L;

	/**
	 * Search has to go through a separate url (Twitter's decision, June 2010).
	 */
	private static final String TWITTER_SEARCH_URL = "http://search.twitter.com";

	/**
	 * JTwitter version
	 */
	public final static String version = "2.4";

	/**
	 * Convenience method: Finds a user with the given screen-name from the
	 * list.
	 * 
	 * @param screenName
	 *            aka login name
	 * @param users
	 * @return User with the given name, or null.
	 */
	public static User getUser(String screenName, List<User> users) {
		assert screenName != null && users != null;
		for (User user : users) {
			if (screenName.equals(user.screenName))
				return user;
		}
		return null;
	}

	/**
	 * 
	 * @param args
	 *            Can be used as a command-line tweet tool. To do so, enter 3
	 *            arguments: name, password, tweet
	 * 
	 *            If empty, prints version info.
	 */
	public static void main(String[] args) {
		// Post a tweet if we are handed a name, password and tweet
		if (args.length == 3) {
			Twitter tw = new Twitter(args[0], args[1]);
			// int s = 0;
			// List<Long> fids = tw.getFollowerIDs();
			// for (Long fid : fids) {
			// User f = tw.follow(""+fid);
			// if (f!=null) s++;
			// }
			Status s = tw.setStatus(args[2]);
			System.out.println(s);
			return;
		}
		System.out.println("Java interface for Twitter");
		System.out.println("--------------------------");
		System.out.println("Version " + version);
		System.out.println("Released under LGPL by Winterwell Associates Ltd.");
		System.out
				.println("See source code, JavaDoc, or http://winterwell.com for details on how to use.");
	}

	/**
	 * TODO merge with {@link #maxResults}??
	 */
	Integer count;

	/**
	 * Used by search
	 */
	private String geocode;
	final IHttpClient http;

	boolean includeRTs = true;

	private String lang;

	private BigInteger maxId;

	/**
	 * Provides support for fetching many pages
	 */
	private int maxResults = -1;

	private double[] myLatLong;

	/**
	 * Twitter login name. Can be null even if we have authentication when using
	 * OAuth.
	 */
	private String name;

	/**
	 * Gets used once then reset to null by
	 * {@link #addStandardishParameters(Map)}. Gets updated in the while loops
	 * of methods doing a get-all-pages.
	 */
	Integer pageNumber;

	private String resultType;

	/**
	 * The user. Can be null. Can be a "fake-user" (screenname-only) object.
	 */
	User self;

	private Date sinceDate;

	private Number sinceId;

	private String sourceApp = "jtwitterlib";

	boolean tweetEntities = true;

	private String twitlongerApiKey;

	private String twitlongerAppName;

	/**
	 * Change this to access sites other than Twitter that support the Twitter
	 * API. <br>
	 * Note: Does not include the final "/"
	 */
	String TWITTER_URL = "http://api.twitter.com/1";

	private Date untilDate;

	private Number untilId;

	/**
	 * Create a Twitter client without specifying a user. This is an easy way to
	 * access public posts. But you can't post of course.
	 */
	public Twitter() {
		this(null, new URLConnectionHttpClient());
	}

	/**
	 * Java wrapper for the Twitter API.
	 * 
	 * @param name
	 *            the authenticating user's name, if known. Can be null.
	 * @param client
	 * @see OAuthSignpostClient
	 */
	public Twitter(String name, IHttpClient client) {
		this.name = name;
		http = client;
		assert client != null;
	}

	/**
	 * WARNING: Twitter no longer supports name/password basic authentication.
	 * This constructor is only for non-Twitter sites, such as identi.ca.
	 * 
	 * @param screenName
	 *            The name of the user. Only used by some methods.
	 * @param password
	 *            The password of the user.
	 * 
	 * @Deprecated Twitter have switched off basic authentication! Use an OAuth
	 *             client such as {@link OAuthSignpostClient} with
	 *             {@link #Twitter(String, IHttpClient)}
	 */
	@Deprecated
	public Twitter(String screenName, String password) {
		this(screenName, new URLConnectionHttpClient(screenName, password));
	}

	/**
	 * Copy constructor. Use this to pass cloned Twitter objects for
	 * multi-threaded work.
	 * 
	 * @param jtwit
	 */
	public Twitter(Twitter jtwit) {
		this(jtwit.getScreenName(), jtwit.http.copy());
	}

	/**
	 * API methods relating to your account.
	 */
	public Twitter_Account account() {
		return new Twitter_Account(this);
	}

	/**
	 * Add in since_id, page and count, if set. This is called by methods that
	 * return lists of statuses or messages.
	 * 
	 * @param vars
	 * @return vars
	 */
	private Map<String, String> addStandardishParameters(
			Map<String, String> vars) {
		if (sinceId != null) {
			vars.put("since_id", sinceId.toString());
		}
		if (untilId != null) {
			vars.put("max_id", untilId.toString());
		}
		if (pageNumber != null) {
			vars.put("page", pageNumber.toString());
			// this is used once only
			pageNumber = null;
		}
		if (count != null) {
			vars.put("count", count.toString());
		}
		if (tweetEntities) {
			vars.put("include_entities", "1");
		}
		if (includeRTs) {
			vars.put("include_rts", "1");
		}
		return vars;
	}

	/**
	 * Equivalent to {@link #follow(String)}. C.f.
	 * http://apiwiki.twitter.com/Migrating-to-followers-terminology
	 * 
	 * @param username
	 *            Required. The screen name of the user to befriend.
	 * @return The befriended user.
	 * @deprecated Use {@link #follow(String)} instead, which is equivalent.
	 */
	@Deprecated
	public User befriend(String username) throws TwitterException {
		return follow(username);
	}

	/**
	 * Equivalent to {@link #stopFollowing(String)}.
	 * 
	 * @deprecated Please use {@link #stopFollowing(String)} instead.
	 */
	@Deprecated
	public User breakFriendship(String username) {
		return stopFollowing(username);
	}

	/**
	 * @deprecated Use {@link Twitter_Users#show(List)} instead
	 */
	public List<User> bulkShow(List<String> screenNames) {
		return users().show(screenNames);
	}

	/**
	 * @deprecated Use {@link #showById(List)} instead
	 */
	public List<User> bulkShowById(List<? extends Number> userIds) {
		return users().showById(userIds);
	}

	/**
	 * Filter keeping only those messages that come between sinceDate and
	 * untilDate (if either or both are set). The Twitter API used to offer
	 * this, but we now have to do it client side.
	 * 
	 * @see #setSinceId(Number)
	 * 
	 * @param list
	 * @return filtered list (a copy)
	 */
	private <T extends ITweet> List<T> dateFilter(List<T> list) {
		if (sinceDate == null && untilDate == null)
			return list;
		ArrayList<T> filtered = new ArrayList<T>(list.size());
		for (T message : list) {
			// assume OK if Twitter is being stingy on the info
			if (message.getCreatedAt() == null) {
				filtered.add(message);
				continue;
			}
			if (untilDate != null && untilDate.before(message.getCreatedAt())) {
				continue;
			}
			if (sinceDate != null && sinceDate.after(message.getCreatedAt())) {
				continue;
			}
			// ok
			filtered.add(message);
		}
		return filtered;
	}

	/**
	 * Deletes the given Status or Message. The authenticating user must be the
	 * author of the status post.
	 */
	public void destroy(ITweet tweet) throws TwitterException {
		if (tweet instanceof Status) {
			destroyStatus(tweet.getId());
		} else {
			destroyMessage((Message) tweet);
		}
	}

	/**
	 * Destroy a direct message.
	 * 
	 * @param dm
	 */
	private void destroyMessage(Message dm) {
		String page = post(TWITTER_URL + "/direct_messages/destroy/" + dm.id
				+ ".json", null, true);
		assert page != null;
	}

	/**
	 * Deletes the direct message specified by the ID. The authenticating user
	 * must be the author of the specified status.
	 * 
	 * @see #destroy(ITweet)
	 */
	public void destroyMessage(Number id) {
		String page = post(TWITTER_URL + "/direct_messages/destroy/" + id
				+ ".json", null, true);
		assert page != null;
	}

	/**
	 * Deletes the status specified by the required ID parameter. The
	 * authenticating user must be the author of the specified status.
	 * 
	 * @see #destroy(ITweet)
	 */
	public void destroyStatus(Number id) throws TwitterException {
		String page = post(TWITTER_URL + "/statuses/destroy/" + id + ".json",
				null, true);
		// Note: Sends two HTTP requests to Twitter rather than one: Twitter
		// appears
		// not to make deletions visible until the user's status page is
		// requested.
		flush();
		assert page != null;
	}

	/**
	 * Deletes the given status. Equivalent to {@link #destroyStatus(int)}. The
	 * authenticating user must be the author of the status post.
	 * 
	 * @deprecated in favour of {@link #destroy(ITweet)}. This method will be
	 *             removed by the end of 2010.
	 * @see #destroy(ITweet)
	 */
	@Deprecated
	public void destroyStatus(Status status) throws TwitterException {
		destroyStatus(status.getId());
	}

	/**
	 * Have we got enough results for the current search?
	 * 
	 * @param list
	 * @return false if maxResults is set to -1 (ie, unlimited) or if list
	 *         contains less than maxResults results.
	 */
	boolean enoughResults(List list) {
		return (maxResults != -1 && list.size() >= maxResults);
	}

	void flush() {
		// This seems to prompt twitter to update in some cases!
		http.getPage("http://twitter.com/" + name, null, true);
	}

	/**
	 * @see Twitter_Users#follow(String)
	 */
	@Deprecated
	public User follow(String username) throws TwitterException {
		return users().follow(username);
	}
	
	@Override
	public String toString() {
		return name==null? "Twitter" : "Twitter["+name+"]";
	}

	/**
	 * @see Twitter_Users#follow(User)
	 */
	@Deprecated
	public User follow(User user) {
		return follow(user.screenName);
	}

	/**
	 * Geo-location API methods.
	 */
	public Twitter_Geo geo() {
		return new Twitter_Geo(this);
	}

	/**
	 * Returns a list of the direct messages sent to the authenticating user.
	 * <p>
	 * Note: the Twitter API makes this available in rss if that's of interest.
	 */
	public List<Message> getDirectMessages() {
		return getMessages(TWITTER_URL + "/direct_messages.json",
				standardishParameters());
	}

	/**
	 * Returns a list of the direct messages sent *by* the authenticating user.
	 */
	public List<Message> getDirectMessagesSent() {
		return getMessages(TWITTER_URL + "/direct_messages/sent.json",
				standardishParameters());
	}

	/**
	 * The most recent 20 favourite tweets. (Note: This can use page - and page
	 * only - to fetch older favourites).
	 */
	public List<Status> getFavorites() {
		return getStatuses(TWITTER_URL + "/favorites.json",
				standardishParameters(), true);
	}

	/**
	 * The most recent 20 favourite tweets for the given user. (Note: This can
	 * use page - and page only - to fetch older favourites).
	 * 
	 * @param screenName
	 *            login-name.
	 */
	public List<Status> getFavorites(String screenName) {
		Map<String, String> vars = InternalUtils.asMap("screen_name",
				screenName);
		return getStatuses(TWITTER_URL + "/favorites.json",
				addStandardishParameters(vars), http.canAuthenticate());
	}

	/**
	 * @see Twitter_Users#getFollowerIDs()
	 */
	@Deprecated
	public List<Number> getFollowerIDs() throws TwitterException {
		return users().getFollowerIDs();
	}

	/**
	 * @see Twitter_Users#getFollowerIDs(String)
	 */
	@Deprecated
	public List<Number> getFollowerIDs(String screenName)
			throws TwitterException {
		return users().getFollowerIDs(screenName);
	}

	/**
	 * @see Twitter_Users#getFollowers()
	 */
	@Deprecated
	public List<User> getFollowers() throws TwitterException {
		return users().getFollowers();
	}

	/**
	 * @see Twitter_Users#getFollowers(String)
	 */
	@Deprecated
	public List<User> getFollowers(String username) throws TwitterException {
		return users().getFollowers(username);
	}

	/**
	 * @see Twitter_Users#getFriendIDs()
	 */
	@Deprecated
	public List<Number> getFriendIDs() throws TwitterException {
		return users().getFriendIDs();
	}

	/**
	 * @see Twitter_Users#getFriendIDs(String)
	 */
	@Deprecated
	public List<Number> getFriendIDs(String screenName) throws TwitterException {
		return users().getFriendIDs(screenName);
	}

	/**
	 * @see Twitter_Users#getFriends()
	 */
	@Deprecated
	public List<User> getFriends() throws TwitterException {
		return users().getFriends();
	}

	/**
	 * @see Twitter_Users#getFriendss(String)
	 */
	@Deprecated
	public List<User> getFriends(String username) throws TwitterException {
		return users().getFriends(username);
	}

	/**
	 * Returns the 20 most recent statuses posted in the last 24 hours from the
	 * authenticating user and that user's friends.
	 * 
	 * @deprecated Replaced by {@link #getHomeTimeline()}
	 */
	@Deprecated
	public List<Status> getFriendsTimeline() throws TwitterException {
		return getHomeTimeline();
	}

	/**
	 * Returns the 20 most recent statuses posted in the last 24 hours from the
	 * authenticating user and that user's friends, including retweets.
	 */
	public List<Status> getHomeTimeline() throws TwitterException {
		assert http.canAuthenticate();
		return getStatuses(TWITTER_URL + "/statuses/home_timeline.json",
				standardishParameters(), true);
	}

	/**
	 * Provides access to the {@link IHttpClient} which manages the low-level
	 * authentication, posts and gets.
	 */
	public IHttpClient getHttpClient() {
		return http;
	}

	/**
	 * @return your lists, ie. the one's you made.
	 */
	public List<TwitterList> getLists() {
		return getLists(name);
	}
	
	/**
	 * 
		Returns <i>all</i> lists the authenticating or specified user subscribes to, 
		including their own.
	   @param user can be null for the authenticating user.
	   @see #getLists(String)
	 */
	public List<TwitterList> getListsAll(User user) {		
		assert user!=null || http.canAuthenticate() : "No authenticating user";
		try {
			String url = TWITTER_URL + "/lists/all.json";
			Map<String, String> vars = user.screenName==null?
					InternalUtils.asMap("user_id", user.id)
					: InternalUtils.asMap("screen_name", user.screenName);
			String listsJson = http.getPage(url, vars, http.canAuthenticate());
			JSONObject wrapper = new JSONObject(listsJson);
			JSONArray jarr = (JSONArray) wrapper.get("lists");
			List<TwitterList> lists = new ArrayList<TwitterList>();
			for (int i = 0; i < jarr.length(); i++) {
				JSONObject li = jarr.getJSONObject(i);
				TwitterList twList = new TwitterList(li, this);
				lists.add(twList);
			}
			return lists;
		} catch (JSONException e) {
			throw new TwitterException.Parsing(null, e);
		}
	}

	/**
	 * @param screenName
	 * @return the (first 20) lists created by the given user
	 */
	public List<TwitterList> getLists(String screenName) {
		assert screenName != null;
		try {
			String url = TWITTER_URL + "/" + screenName + "/lists.json";
			String listsJson = http.getPage(url, null, true);
			JSONObject wrapper = new JSONObject(listsJson);
			JSONArray jarr = (JSONArray) wrapper.get("lists");
			List<TwitterList> lists = new ArrayList<TwitterList>();
			for (int i = 0; i < jarr.length(); i++) {
				JSONObject li = jarr.getJSONObject(i);
				TwitterList twList = new TwitterList(li, this);
				lists.add(twList);
			}
			return lists;
		} catch (JSONException e) {
			throw new TwitterException.Parsing(null, e);
		}
	}

	/**
	 * @param screenName
	 * @param filterToOwned
	 *            If true, only return lists which the user owns.
	 * @return lists of which screenName is a member. NOTE: currently limited to
	 *         a maximum of 20 lists!
	 */
	public List<TwitterList> getListsContaining(String screenName,
			boolean filterToOwned) {
		assert screenName != null;
		try {
			String url = TWITTER_URL + "/lists/memberships.json";
			Map<String, String> vars = InternalUtils.asMap("screen_name",
					screenName);
			if (filterToOwned) {
				assert http.canAuthenticate();
				vars.put("filter_to_owned_lists", "1");
			}
			String listsJson = http.getPage(url, vars, http.canAuthenticate());
			JSONObject wrapper = new JSONObject(listsJson);
			JSONArray jarr = (JSONArray) wrapper.get("lists");
			List<TwitterList> lists = new ArrayList<TwitterList>();
			for (int i = 0; i < jarr.length(); i++) {
				JSONObject li = jarr.getJSONObject(i);
				TwitterList twList = new TwitterList(li, this);
				lists.add(twList);
			}
			return lists;
		} catch (JSONException e) {
			throw new TwitterException.Parsing(null, e);
		}
	}

	/**
	 * Convenience for {@link #getListsContaining(String, boolean)}.
	 * 
	 * @return lists that you are a member of. Warning: currently limited to a
	 *         maximum of 20 results.
	 */
	public List<TwitterList> getListsContainingMe() {
		return getListsContaining(name, false);
	}

	/**
	 * @param truncatedStatus
	 *            If this is a twitlonger.com truncated status, then call
	 *            twitlonger to fetch the full text.
	 * @return the full status message. If this is not a twitlonger status, this
	 *         will just return the status text as-is.
	 * @see #updateLongStatus(String, long)
	 */
	public String getLongStatus(Status truncatedStatus) {
		// regex for http://tl.gd/ID
		int i = truncatedStatus.text.indexOf("http://tl.gd/");
		if (i == -1)
			return truncatedStatus.text;
		String id = truncatedStatus.text.substring(i + 13).trim();
		String response = http.getPage("http://www.twitlonger.com/api_read/"
				+ id, null, false);
		Matcher m = contentTag.matcher(response);
		boolean ok = m.find();
		if (!ok)
			throw new TwitterException.TwitLongerException(
					"TwitLonger call failed", response);
		String longMsg = m.group(1).trim();
		return longMsg;
	}

	/**
	 * Provides support for fetching many pages. -1 indicates "give me as much
	 * as Twitter will let me have."
	 */
	public int getMaxResults() {
		return maxResults;
	}

	/**
	 * Returns the 20 most recent replies/mentions (status updates with
	 * 
	 * @username) to the authenticating user. Replies are only available to the
	 *            authenticating user; you can not request a list of replies to
	 *            another user whether public or protected.
	 *            <p>
	 *            This is exactly the same as {@link #getReplies()}
	 *            <p>
	 *            When paging, this method can only go back up to 800 statuses.
	 *            <p>
	 *            Does not include new-style retweets.
	 */
	public List<Status> getMentions() {
		return getStatuses(TWITTER_URL + "/statuses/mentions.json",
				standardishParameters(), true);
	}

	/**
	 * 
	 * @param url
	 * @param var
	 * @param isPublic
	 *            Value to set for Message.isPublic
	 * @return
	 */
	private List<Message> getMessages(String url, Map<String, String> var) {
		// Default: 1 page
		if (maxResults < 1) {
			List<Message> msgs = Message.getMessages(http.getPage(url, var,
					true));
			msgs = dateFilter(msgs);
			return msgs;
		}
		// Fetch all pages until we run out
		// -- or Twitter complains in which case you'll get an exception
		pageNumber = 1;
		List<Message> msgs = new ArrayList<Message>();
		while (msgs.size() <= maxResults) {
			String p = http.getPage(url, var, true);
			List<Message> nextpage = Message.getMessages(p);
			nextpage = dateFilter(nextpage);
			msgs.addAll(nextpage);
			if (nextpage.size() < 20) {
				break;
			}
			pageNumber++;
			var.put("page", Integer.toString(pageNumber));
		}
		return msgs;
	}

	/**
	 * Returns the 20 most recent statuses from non-protected users who have set
	 * a custom user icon. Does not require authentication.
	 * <p>
	 * Note: Twitter cache-and-refresh this every 60 seconds, so there is little
	 * point calling it more frequently than that.
	 */
	public List<Status> getPublicTimeline() throws TwitterException {
		return getStatuses(TWITTER_URL + "/statuses/public_timeline.json",
				standardishParameters(), false);
	}

	/**
	 * What is the current rate limit status? Do we need to throttle back our
	 * usage? This is the cached info from the last call of that type.
	 * <p>
	 * Note: The RateLimit object is created using cached info from a previous
	 * Twitter call. So this method is quick (it doesn't require a fresh call to
	 * Twitter), but the RateLimit object isn't available until after you make a
	 * call of the right type to Twitter.
	 * <p>
	 * Status: Headin towards stable, but still a bit experimental.
	 * 
	 * @param reqType
	 *            Different methods have separate rate limits.
	 * @return the last rate limit advice received, or null if unknown.
	 * @see #getRateLimitStatus()
	 */
	public RateLimit getRateLimit(KRequestType reqType) {
		return http.getRateLimit(reqType);
	}

	/**
	 * How many normal rate limit calls do you have left? This calls Twitter,
	 * which makes it slower than {@link #getRateLimit(KRequestType)} but it's
	 * up-to-date and safe against threads and other-programs using the same
	 * allowance.
	 * <p>
	 * This may update getRateLimit(KRequestType) for NORMAL requests, but sadly
	 * it doesn't fetch rate-limit info on other request types.
	 * 
	 * @return the remaining number of API requests available to the
	 *         authenticating user before the API limit is reached for the
	 *         current hour. <i>If this is zero or negative you should stop
	 *         using Twitter with this login for a bit.</i> Note: Calls to
	 *         rate_limit_status do not count against the rate limit.
	 * @see #getRateLimit(KRequestType)
	 */
	public int getRateLimitStatus() {
		
		
		
		String json = http.getPage(TWITTER_URL
				+ "/account/rate_limit_status.json", null,
				http.canAuthenticate());
		try {
			JSONObject obj = new JSONObject(json);
			int hits = obj.getInt("remaining_hits");
			// Update the RateLimit objects
			// http.updateRateLimits(KRequestType.NORMAL); no header info sent!
			if (http instanceof URLConnectionHttpClient) {
				URLConnectionHttpClient _http = (URLConnectionHttpClient) http;
				RateLimit rateLimit = new RateLimit(
						obj.getString("hourly_limit"), Integer.toString(hits),
						obj.getString("reset_time"));
				_http.rateLimits.put(KRequestType.NORMAL, rateLimit);
			}
			return hits;
		} catch (JSONException e) {
			throw new TwitterException.Parsing(json, e);
		}
	}

	/**
	 * Returns the 20 most recent replies/mentions (status updates with
	 * 
	 * @username) to the authenticating user. Replies are only available to the
	 *            authenticating user; you can not request a list of replies to
	 *            another user whether public or protected.
	 *            <p>
	 *            This is exactly the same as {@link #getMentions()}! Twitter
	 *            changed their API & terminology - we are (currently) keeping
	 *            both methods.
	 *            <p>
	 *            When paging, this method can only go back up to 800 statuses.
	 *            <p>
	 *            Does not include new-style retweets.
	 * @deprecated Use #getMentions() for preference
	 */
	public List<Status> getReplies() throws TwitterException {
		return getMentions();
	}

	/**
	 * Show users who (new-style) retweeted the given tweet. Can use count (up
	 * to 100) and page. This does not include old-style retweeters!
	 * 
	 * @param tweet
	 *            You can use a "fake" Status created via
	 *            {@link Status#Status(User, String, long, Date)} if you know
	 *            the id number.
	 */
	public List<User> getRetweeters(Status tweet) {
		String url = TWITTER_URL + "/statuses/" + tweet.id
				+ "/retweeted_by.json";
		Map<String, String> vars = addStandardishParameters(new HashMap<String, String>());
		String json = http.getPage(url, vars, http.canAuthenticate());
		List<User> users = User.getUsers(json);
		return users;
	}

	/**
	 * @return Retweets of this tweet. This attempts to cover new-style and
	 *         old-style "manual" retweets. It does so by making retweet call
	 *         and a search call. It will miss edited retweets though.
	 */
	public List<Status> getRetweets(Status tweet) {
		String url = TWITTER_URL + "/statuses/retweets/" + tweet.id + ".json";
		Map<String, String> vars = addStandardishParameters(new HashMap<String, String>());
		String json = http.getPage(url, vars, true);
		List<Status> newStyle = Status.getStatuses(json);
		try {
			// // Should we also do by search and merge the two lists?
			StringBuilder sq = new StringBuilder();
			sq.append("\"RT @" + tweet.getUser().getScreenName() + ": ");
			if (sq.length() + tweet.text.length() + 1 > 140) {
				int i = tweet.text.lastIndexOf(' ', 140 - sq.length() - 1);
				String words = tweet.text.substring(0, i);
				sq.append(words);
			} else {
				sq.append(tweet.text);
			}
			sq.append('"');
			List<Status> oldStyle = search(sq.toString());
			// merge them
			newStyle.addAll(oldStyle);
			Collections.sort(newStyle, InternalUtils.NEWEST_FIRST);
			return newStyle;
		} catch (TwitterException e) {
			// oh well
			return newStyle;
		}
	}

	/**
	 * @return retweets that you have made using "new-style" retweets rather
	 *         than the RT microfromat. These are your tweets, i.e. they begin
	 *         "RT @whoever: ". You can get the original tweet via
	 *         {@link Status#getOriginal()}
	 */
	public List<Status> getRetweetsByMe() {
		String url = TWITTER_URL + "/statuses/retweeted_by_me.json";
		Map<String, String> vars = addStandardishParameters(new HashMap<String, String>());
		String json = http.getPage(url, vars, true);
		return Status.getStatuses(json);
	}

	/**
	 * @return those of your tweets that have been retweeted. It's a bit of a
	 *         strange one this. You can then query who retweeted you.
	 */
	public List<Status> getRetweetsOfMe() {
		String url = TWITTER_URL + "/statuses/retweets_of_me.json";
		Map<String, String> vars = addStandardishParameters(new HashMap<String, String>());
		String json = http.getPage(url, vars, true);
		return Status.getStatuses(json);
	}

	/**
	 * @return Login name of the authenticating user, or null if not set.
	 *         <p>
	 *         Will call Twitter to find out if null but oauth is set.
	 * @see #getSelf()
	 */
	public String getScreenName() {
		if (name != null)
			return name;
		// load if need be
		getSelf();
		return name;
	}

	/**
	 * @param searchTerm
	 * @param rpp
	 * @return
	 */
	private Map<String, String> getSearchParams(String searchTerm, int rpp) {
		Map<String, String> vars = InternalUtils.asMap("rpp",
				Integer.toString(rpp), "q", searchTerm);
		if (sinceId != null) {
			vars.put("since_id", sinceId.toString());
		}
		if (untilId != null) {
			// It's unclear from the docs whether this will work
			// c.f. https://dev.twitter.com/docs/api/1/get/search
			vars.put("max_id", untilId.toString());
		}
		// since date is no longer supported. until is though?!
		// if (sinceDate != null) vars.put("since", df.format(sinceDate));
		if (untilDate != null) {
			vars.put("until", InternalUtils.df.format(untilDate));
		}
		if (lang != null) {
			vars.put("lang", lang);
		}
		if (geocode != null) {
			vars.put("geocode", geocode);
		}
		if (resultType != null) {
			vars.put("result_type", resultType);
		}
		addStandardishParameters(vars);
		return vars;
	}

	/**
	 * @return you, or null if this is an anonymous Twitter object.
	 *         <p>
	 *         This will cache the result if it makes an API call.
	 */
	public User getSelf() {
		if (self != null)
			return self;
		if (!http.canAuthenticate()) {
			if (name != null) {
				// not sure this case makes sense, but we may as well handle it
				self = new User(name);
				return self;
			}
			return null;
		}
		account().verifyCredentials();
		name = self.getScreenName();
		return self;
	}

	/**
	 * @return The current status of the user. Warning: this is null if (a)
	 *         unset (ie if this user has never tweeted), or (b) their last six
	 *         tweets were all new-style retweets!
	 */
	public Status getStatus() throws TwitterException {
		Map<String, String> vars = InternalUtils.asMap("count", 6);
		String json = http.getPage(
				TWITTER_URL + "/statuses/user_timeline.json", vars, true);
		List<Status> statuses = Status.getStatuses(json);
		if (statuses.size() == 0)
			return null;
		return statuses.get(0);
	}

	/**
	 * Returns a single status, specified by the id parameter below. The
	 * status's author will be returned inline.
	 * 
	 * @param id
	 *            The numerical ID of the status you're trying to retrieve.
	 */
	public Status getStatus(Number id) throws TwitterException {
		Map vars = tweetEntities ? InternalUtils.asMap("include_entities", "1")
				: null;
		String json = http.getPage(TWITTER_URL + "/statuses/show/" + id
				+ ".json", vars, http.canAuthenticate());
		try {
			return new Status(new JSONObject(json), null);
		} catch (JSONException e) {
			throw new TwitterException.Parsing(json, e);
		}
	}

	/**
	 * @return The current status of the given user.
	 *         <p>
	 *         Warning: this can be null if the user has been doing enough
	 *         new-style retweets. This is due to flaws in the Twitter API.
	 */
	public Status getStatus(String username) throws TwitterException {
		assert username != null;
		// new-style retweets can cause blanks in your timeline
		// show(username).status is just as vulnerable
		// grab a few tweets to give some robustness
		Map<String, String> vars = InternalUtils.asMap("id", username, "count",
				6);
		String json = http.getPage(
				TWITTER_URL + "/statuses/user_timeline.json", vars, false);
		List<Status> statuses = Status.getStatuses(json);
		if (statuses.size() == 0)
			return null;
		return statuses.get(0);
	}

	/**
	 * Does the grunt work for paged status fetching
	 * 
	 * @param url
	 * @param var
	 * @param authenticate
	 * @return
	 */
	private List<Status> getStatuses(final String url, Map<String, String> var,
			boolean authenticate) {
		// Default: 1 page
		if (maxResults < 1) {
			List<Status> msgs = Status.getStatuses(http.getPage(url, var,
					authenticate));
			msgs = dateFilter(msgs);
			return msgs;
		}
		// Fetch all pages until we reach the desired maxResults, or run out
		// -- or Twitter complains in which case you'll get an exception
		// Use status ids for paging, rather than page number, because this
		// allows for "drift" when new tweets are posted during the paging.
		maxId = null;
		// pageNumber = 1;
		List<Status> msgs = new ArrayList<Status>();
		while (msgs.size() <= maxResults) {
			String json = http.getPage(url, var, authenticate);
			List<Status> nextpage = Status.getStatuses(json);
			// This test replaces size<20. It requires an extra call to Twitter.
			// But it fixes a bug whereby retweets aren't counted and can thus
			// cause
			// the system to quit early.
			if (nextpage.size() == 0) {
				break;
			}
			// Next page must start strictly before this one
			maxId = nextpage.get(nextpage.size() - 1).id
					.subtract(BigInteger.ONE);
			// System.out.println(maxId + " -> " + nextpage.get(0).id);

			msgs.addAll(dateFilter(nextpage));
			// pageNumber++;
			var.put("max_id", maxId.toString());
		}
		return msgs;
	}

	/**
	 * @return the latest global trending topics on Twitter
	 */
	public List<String> getTrends() {
		return getTrends(1);
	}

	/**
	 * @param a
	 *            Yahoo Where-on-Earth ID. c.f.
	 *            http://developer.yahoo.com/geo/geoplanet/
	 * @return the latest regional trending topics on Twitter
	 * @see Twitter_Geo#getTrendRegions()
	 */
	public List<String> getTrends(Number woeid) {
		String jsonTrends = http.getPage(TWITTER_URL + "/trends/" + woeid
				+ ".json", null, false);
		try {
			JSONArray jarr = new JSONArray(jsonTrends);
			JSONObject json1 = jarr.getJSONObject(0);
			JSONArray json2 = json1.getJSONArray("trends");
			List<String> trends = new ArrayList<String>();
			for (int i = 0; i < json2.length(); i++) {
				JSONObject ti = json2.getJSONObject(i);
				String t = ti.getString("name");
				trends.add(t);
			}
			return trends;
		} catch (JSONException e) {
			throw new TwitterException.Parsing(jsonTrends, e);
		}
	}

	/**
	 * @return the untilDate
	 */
	public Date getUntilDate() {
		return untilDate;
	}

	/**
	 * @see Twitter_Users#getUser(long)
	 */
	@Deprecated
	public User getUser(long userId) {
		return show(userId);
	}

	/**
	 * @see Twitter_Users#getUser(String)
	 */
	@Deprecated
	public User getUser(String screenName) {
		return show(screenName);
	}

	/**
	 * Returns the most recent statuses from the authenticating user. 20 by
	 * default.
	 */
	public List<Status> getUserTimeline() throws TwitterException {
		return getStatuses(TWITTER_URL + "/statuses/user_timeline.json",
				standardishParameters(), true);
	}

	/**
	 * Equivalent to {@link #getUserTimeline(String)}, but takes a numeric
	 * user-id instead of a screen-name.
	 * 
	 * @param userId
	 * @return tweets by userId
	 */
	public List<Status> getUserTimeline(Long userId) throws TwitterException {
		Map<String, String> vars = InternalUtils.asMap("user_id", userId);
		addStandardishParameters(vars);
		// Authenticate if we can (for protected streams)
		boolean authenticate = http.canAuthenticate();
		try {
			return getStatuses(TWITTER_URL + "/statuses/user_timeline.json",
					vars, authenticate);
		} catch (E401 e) {
			// Bug in Twitter: this can be a suspended user...
			// In which case the call below would generate a SuspendedUser exception
			// ...but do we want to conserve our api limit??
			// isSuspended(userId);
			throw e;
		}
	}

	/**
	 * Returns the most recent statuses from the given user.
	 * <p>
	 * This will return 20 results by default, though
	 * {@link #setMaxResults(int)} can be used to fetch multiple pages.
	 * 
	 * Note that if you exclude new-style retweets (via
	 * {@link #setIncludeRTs(boolean)}) then this can return less than 20
	 * results -- it can even return none if the latest 20 are all retweets.
	 * <p>
	 * There is a cap of 3200 tweets - this is the farthest back you can go down
	 * a user timeline!
	 * <p>
	 * This method will authenticate if it can (i.e. if the Twitter object has a
	 * username and password). Authentication is needed to see the posts of a
	 * private user.
	 * 
	 * @param screenName
	 *            Can be null. Specifies the screen name of the user for whom to
	 *            return the user_timeline.
	 * @throws TwitterException.E401
	 *             if the user has protected their tweets, and you do not have
	 *             access.
	 * @throws TwitterException.SuspendedUser
	 *             if the user has been suspended
	 */
	public List<Status> getUserTimeline(String screenName)
			throws TwitterException {
		Map<String, String> vars = InternalUtils.asMap("screen_name",
				screenName);
		addStandardishParameters(vars);
		// Should we authenticate?
		boolean authenticate = http.canAuthenticate();
		try {
			return getStatuses(TWITTER_URL + "/statuses/user_timeline.json",
					vars, authenticate);
		} catch (E401 e) {
			// Bug in Twitter: this can be a suspended user
			// - in which case this will generate a SuspendedUser exception
			isSuspended(screenName);
			throw e;
		}
	}

	/**
	 * @deprecated Use {@link #setIncludeRTs(boolean)} instead to control
	 *             retweet behaviour.
	 * 
	 *             Returns the most recent statuses posted by the given user.
	 *             Unlike {@link #getUserTimeline(String)}, this includes
	 *             new-style retweets.
	 *             <p>
	 *             This will return 20 by default, though
	 *             {@link #setMaxResults(int)} can be used to fetch multiple
	 *             pages. There is a cap of 3200 tweets - this is the farthest
	 *             back you can go down a user timeline!
	 *             <p>
	 *             This method will authenticate if it can (i.e. if the Twitter
	 *             object has a username and password). Authentication is needed
	 *             to see the posts of a private user.
	 * 
	 @param screenName
	 *            Can be null. Specifies the screen name of the user for whom to
	 *            return the user_timeline.
	 * 
	 */
	public List<Status> getUserTimelineWithRetweets(String screenName)
			throws TwitterException {
		Map<String, String> vars = InternalUtils.asMap("screen_name",
				screenName, "include_rts", "1");
		addStandardishParameters(vars);
		// Should we authenticate?
		boolean authenticate = http.canAuthenticate();
		try {
			return getStatuses(TWITTER_URL + "/statuses/user_timeline.json",
					vars, authenticate);
		} catch (E401 e) {
			isSuspended(screenName);
			throw e;
		}
	}

	/**
	 * @see Twitter_Users#isFollower(String)
	 */
	@Deprecated
	public boolean isFollower(String userB) {
		return isFollower(userB, name);
	}

	/**
	 * @see Twitter_Users#isFollower(String, String)
	 */
	@Deprecated
	public boolean isFollower(String followerScreenName,
			String followedScreenName) {
		return users().isFollower(followerScreenName, followedScreenName);
	}

	/**
	 * @see Twitter_Users#isFollowing(String)
	 */
	@Deprecated
	public boolean isFollowing(String userB) {
		return isFollower(name, userB);
	}

	/**
	 * @see Twitter_Users#isFollowing(User)
	 */
	@Deprecated
	public boolean isFollowing(User user) {
		return isFollowing(user.screenName);
	}

	/**
	 * @param type
	 * @param minCalls
	 *            Standard value = 1. The minimum number of calls which should
	 *            be available.
	 * @return true if this is currently rate-limited, & should not be used for
	 *         a while. false = OK
	 * @see #getRateLimit(KRequestType) for more info
	 * @see #getRateLimitStatus() for guaranteed up-to-date info
	 */
	public boolean isRateLimited(KRequestType reqType, int minCalls) {
		// Check NORMAL first
		if (reqType != KRequestType.NORMAL) {
			boolean isLimited = isRateLimited(KRequestType.NORMAL, minCalls);
			if (isLimited)
				return true;
		}
		RateLimit rl = getRateLimit(reqType);
		// assume things are OK (except for NORMAL which we quickly check by
		// calling Twitter)
		if (rl == null) {
			if (reqType == KRequestType.NORMAL) {
				int rls = getRateLimitStatus();
				return rls >= minCalls;
			}
			return false;
		}
		// in credit?
		if (rl.getRemaining() >= minCalls)
			return false;
		// out of date?
		if (rl.getReset().getTime() < System.currentTimeMillis())
			return false;
		// nope - you're over the limit
		return true;
	}

	/**
	 * Generate an exception if the use is suspended. This is used as a
	 * work-around for misleading error codes returned by Twitter.
	 * 
	 * @param screenName
	 * @throws SuspendedUser
	 */
	private void isSuspended(String screenName) throws SuspendedUser {
		show(screenName);
	}

	/**
	 * @return true if {@link #setupTwitlonger(String, String)} has been used to
	 *         provide twitlonger.com details.
	 * @see #updateLongStatus(String, long)
	 */
	public boolean isTwitlongerSetup() {
		return twitlongerApiKey != null && twitlongerAppName != null;
	}

	/**
	 * Are the login details used for authentication valid?
	 * 
	 * @return true if OK, false if unset or invalid
	 * @see Twitter_Account#verifyCredentials() which returns user info
	 */
	public boolean isValidLogin() {
		if (!http.canAuthenticate())
			return false;
		try {
			Twitter_Account ta = new Twitter_Account(this);
			User u = ta.verifyCredentials();
			return true;
		} catch (TwitterException.E403 e) {
			return false;
		} catch (TwitterException.E401 e) {
			return false;
		} catch (TwitterException e) {
			throw e;
		}
	}

	/**
	 * Wrapper for {@link IHttpClient#post(String, Map, boolean)}.
	 */
	private String post(String uri, Map<String, String> vars,
			boolean authenticate) throws TwitterException {
		String page = http.post(uri, vars, authenticate);
		return page;
	}

	/**
	 * Report a user for being a spammer.
	 * 
	 * @param screenName
	 */
	public void reportSpam(String screenName) {
		http.getPage(TWITTER_URL + "/version/report_spam.json",
				InternalUtils.asMap("screen_name", screenName), true);
	}

	/**
	 * Retweet (new-style) a tweet without any edits. You can also retweet by
	 * starting a status using the RT @username microformat. (this is an
	 * old-style retweet).
	 * 
	 * @param tweet
	 *            Note: you cannot retweet your own tweets.
	 * @return your retweet
	 */
	public Status retweet(Status tweet) {
		try {
			String result = post(
					TWITTER_URL + "/statuses/retweet/" + tweet.getId()
							+ ".json", null, true);
			return new Status(new JSONObject(result), null);

			// error handling
		} catch (E403 e) {
			List<Status> rts = getRetweetsByMe();
			for (Status rt : rts) {
				if (tweet.equals(rt.getOriginal()))
					throw new TwitterException.Repetition(rt.getText());
			}
			throw e;
		} catch (JSONException e) {
			throw new TwitterException.Parsing(null, e);
		}
	}

	/**
	 * Perform a search of Twitter. Convenience wrapper for
	 * {@link #search(String, ICallback, int)} with no callback and fetching one
	 * pages worth of results.
	 */
	public List<Status> search(String searchTerm) {
		return search(searchTerm, null, 100);
	}

	/**
	 * Perform a search of Twitter.
	 * <p>
	 * Warning: the User objects returned by a search (as part of the Status
	 * objects) are dummy-users. The only information that is set is the user's
	 * screen-name and a profile image url. This reflects the current behaviour
	 * of the Twitter API. If you need more info, call {@link #show(String)}
	 * with the screen name.
	 * <p>
	 * This supports {@link #maxResults} and pagination. A language filter can
	 * be set via {@link #setLanguage(String)} Location can be set via
	 * {@link #setSearchLocation(double, double, String)}
	 * 
	 * Other advanced search features can be done via the query string. E.g.<br>
	 * "from:winterstein" - tweets from user winterstein<br>
	 * "to:winterstein" - tweets start with @winterstein<br>
	 * "source:jtwitter" - originating from the application JTwitter - your
	 * query must also must contain at least one keyword parameter. <br>
	 * "filter:links" - tweets contain a link<br>
	 * "apples OR pears" - or ("apples pears" would give you apples <i>and</i>
	 * pears).
	 * 
	 * @param searchTerm
	 *            This can include several space-separated keywords, #tags and @username
	 *            (for mentions), and use quotes for \"exact phrase\" searches.
	 * @param callback
	 *            an object whose process() method will be called on each new
	 *            page of results.
	 * @param rpp
	 *            results per page. 100 is the default
	 * @return search results - up to maxResults if maxResults is positive, or
	 *         rpp if maxResults is negative/zero. See
	 *         {@link #setMaxResults(int)} to use > 100.
	 */
	public List<Status> search(String searchTerm, ICallback callback, int rpp) {
		if (rpp > 100 && maxResults < rpp)
			throw new IllegalArgumentException(
					"You need to switch on paging to fetch more than 100 search results. First call setMaxResults() to raise the limit above "
							+ rpp);
		searchTerm = search2_bugHack(searchTerm);
		Map<String, String> vars;
		if (maxResults < 100 && maxResults > 0) {
			// Default: 1 page
			vars = getSearchParams(searchTerm, maxResults);
		} else {
			vars = getSearchParams(searchTerm, rpp);
		}
		// Fetch all pages until we run out
		// -- or Twitter complains in which case you'll get an exception
		List<Status> allResults = new ArrayList<Status>(Math.max(maxResults,
				rpp));
		String url = TWITTER_SEARCH_URL + "/search.json";
		int localPageNumber = 1; // pageNumber is nulled by getSearchParams
		do {
			pageNumber = localPageNumber;
			vars.put("page", Integer.toString(pageNumber));
			String json = http.getPage(url, vars, false);
			List<Status> stati = Status.getStatusesFromSearch(this, json);
			int numResults = stati.size();
			stati = dateFilter(stati);
			allResults.addAll(stati);
			if (callback != null) {
				// the callback may tell us to stop, by returning true
				if (callback.process(stati)) {
					break;
				}
			}
			if (numResults < rpp) { // We've reached the end of the results
				break;
			}
			// paranoia
			localPageNumber++;
		} while (allResults.size() < maxResults);
		// null for the next method
		pageNumber = null;
		return allResults;
	}

	/**
	 * This fixes a couple of bugs in Twitter's search API:
	 * 
	 * 1. Searches using OR and a location return gibberish, unless they also
	 * include a -term. Strangely that seems to fix things. So we just add one
	 * if needed.<br>
	 * 
	 * 2. Searches that start and end with quotes, and use an OR have problems:
	 * they become AND searches with the OR turned into a keyword. E.g. /"apple"
	 * OR "pear"/ acts like /"apple" AND or AND "pear"/
	 * <p>
	 * It should be tested periodically whether we need this. See
	 * {@link TwitterTest#testSearchBug()}, {@link TwitterTest#testSearchBug2()}
	 * 
	 * @param searchTerm
	 * @return e.g. "apples OR pears" (near Edinburgh) goes to
	 *         "apples OR pears -kfz" (near Edinburgh)
	 */
	private String search2_bugHack(String searchTerm) {
		// zero-length is valid with location
		if (searchTerm.length()==0)
			return searchTerm;
		// bug 1: a OR b near X fails
		if (searchTerm.contains(" OR ") && !searchTerm.contains("-")
				&& geocode != null)
			return searchTerm + " -kfz"; // add a -gibberish term
		// bug 2: "a" OR "b" fails
		if (searchTerm.contains(" OR ") && searchTerm.charAt(0) == '"'
				&& searchTerm.charAt(searchTerm.length() - 1) == '"')
			return searchTerm + " -kfz"; // add a -gibberish term
		// hopefully fine as-is
		return searchTerm;
	}

	/**
	 * @see Twitter_Users#searchUsers(String)
	 */
	@Deprecated
	public List<User> searchUsers(String searchTerm) {
		return users().searchUsers(searchTerm);
	}

	/**
	 * Sends a new direct message (DM) to the specified user from the
	 * authenticating user. This is a private message!
	 * 
	 * @param recipient
	 *            Required. The screen name of the recipient user.
	 * @param text
	 *            Required. The text of your direct message. Keep it under 140
	 *            characters! This should *not* include the "d username" portion
	 * @return the sent message
	 * @throws TwitterException.E403
	 *             if the recipient is not following you. (you can \@mention
	 *             anyone but you can only dm people who follow you).
	 */
	public Message sendMessage(String recipient, String text)
			throws TwitterException {
		assert recipient != null && text != null : recipient + " " + text;
		assert !text.startsWith("d " + recipient) : recipient + " " + text;
		if (text.length() > 140)
			throw new IllegalArgumentException("Message is too long.");
		Map<String, String> vars = InternalUtils.asMap("user", recipient,
				"text", text);
		if (tweetEntities) {
			vars.put("include_entities", "1");
		}
		String result = null;
		try {
			// post it
			result = post(TWITTER_URL + "/direct_messages/new.json", vars, true);
			// sadly the response doesn't include rate-limit info
			return new Message(new JSONObject(result));
		} catch (JSONException e) {
			throw new TwitterException.Parsing(result, e);
		} catch (TwitterException.E404 e) {
			// suspended user?? TODO investigate
			throw new TwitterException.E404(e.getMessage() + " with recipient="
					+ recipient + ", text=" + text);
		}
	}

	/**
	 * Set this to access sites other than Twitter that support the Twitter API.
	 * E.g. WordPress or Identi.ca. Note that not all methods may work! Also,
	 * search uses a separate url and is not affected by this method (it will
	 * continue to point to Twitter).
	 * 
	 * @param url
	 *            Format: "http://domain-name", e.g. "http://twitter.com" by
	 *            default.
	 */
	public void setAPIRootUrl(String url) {
		assert url.startsWith("http://") || url.startsWith("https://");
		assert !url.endsWith("/") : "Please remove the trailing / from " + url;
		TWITTER_URL = url;
	}

	/**
	 * *Some* methods - the timeline ones for example - allow a count of
	 * number-of-tweets to return.
	 * 
	 * @param count
	 *            null for default behaviour. 200 is the current maximum.
	 *            Twitter may reject or ignore high counts.
	 */
	public void setCount(Integer count) {
		this.count = count;
	}

	public void setFavorite(Status status, boolean isFavorite) {
		try {
			String uri = isFavorite ? TWITTER_URL + "/favorites/create/"
					+ status.id + ".json" : TWITTER_URL + "/favorites/destroy/"
					+ status.id + ".json";
			http.post(uri, null, true);
		} catch (E403 e) {
			// already a favorite?
			if (e.getMessage() != null
					&& e.getMessage().contains("already favorited"))
				throw new TwitterException.Repetition(
						"You have already favorited this status.");
			// just a normal 403
			throw e;
		}
	}

	/**
	 * true by default. If true, lists of tweets will include new-style
	 * retweets. If false, they won't (execpt for the retweet-specific calls).
	 * 
	 * @param includeRTs
	 */
	public void setIncludeRTs(boolean includeRTs) {
		this.includeRTs = includeRTs;
	}

	/**
	 * Note: does NOT work for search() methods (not supported by Twitter).
	 * 
	 * @param tweetEntities
	 *            Set to true to enable
	 *            {@link Status#getTweetEntities(KEntityType)}, false if you
	 *            don't care. Default is true.
	 */
	public void setIncludeTweetEntities(boolean tweetEntities) {
		this.tweetEntities = tweetEntities;
	}

	/**
	 * Set a language filter for search results. Note: This only applies to
	 * search results.
	 * 
	 * @param language
	 *            ISO code for language. Can be null for all languages.
	 *            <p>
	 *            Note: there are multiple different ISO codes! Twitter supports
	 *            ISO 639-1. http://en.wikipedia.org/wiki/ISO_639-1
	 */
	public void setLanguage(String language) {
		lang = language;
	}

	/**
	 * @param maxResults
	 *            if greater than zero, requests will attempt to fetch as many
	 *            pages as are needed! -1 by default, in which case most methods
	 *            return the first 20 statuses/messages. Zero is not allowed.
	 *            <p>
	 *            If setting a high figure, you should usually also set a
	 *            sinceId or sinceDate to limit your Twitter usage. Otherwise
	 *            you can easily exceed your rate limit.
	 */
	public void setMaxResults(int maxResults) {
		assert maxResults != 0;
		this.maxResults = maxResults;
	}

	/**
	 * Set the location for your tweets.<br>
	 * 
	 * Warning: geo-tagging parameters are ignored if geo_enabled for the user
	 * is false (this is the default setting for all users unless the user has
	 * enabled geolocation in their settings)!
	 * 
	 * @param latitudeLongitude
	 *            Can be null (which is the default), in which case your tweets
	 *            will not carry location data.
	 *            <p>
	 *            The valid ranges for latitude is -90.0 to +90.0 (North is
	 *            positive) inclusive. The valid ranges for longitude is -180.0
	 *            to +180.0 (East is positive) inclusive.
	 * 
	 * @see #setSearchLocation(double, double, String) which is completely
	 *      separate.
	 */
	public void setMyLocation(double[] latitudeLongitude) {
		myLatLong = latitudeLongitude;
		if (myLatLong == null)
			return;
		if (Math.abs(myLatLong[0]) > 90)
			throw new IllegalArgumentException(myLatLong[0]
					+ " is not within +/- 90");
		if (Math.abs(myLatLong[1]) > 180)
			throw new IllegalArgumentException(myLatLong[1]
					+ " is not within +/- 180");
	}

	/**
	 * @param pageNumber
	 *            null (the default) returns the first page. Pages are indexed
	 *            from 1. This is used once only! Then it is reset to null
	 */
	public void setPageNumber(Integer pageNumber) {
		this.pageNumber = pageNumber;
	}

	/**
	 * Restricts {@link #search(String)} to tweets by users located within a
	 * given radius of the given latitude/longitude.
	 * <p>
	 * The location of a tweet is preferably taken from the Geotagging API, but
	 * will fall back to the Twitter profile.
	 * 
	 * @param latitude
	 * @param longitude
	 * @param radius
	 *            E.g. 3.5mi or 2km. Must be <2500km
	 */
	public void setSearchLocation(double latitude, double longitude,
			String radius) {
		assert radius.endsWith("mi") || radius.endsWith("km") : radius;
		geocode = latitude + "," + longitude + "," + radius;
	}

	/**
	 * Optional. Specifies what type of search results you would prefer to
	 * receive. The current default is "mixed." Valid values:<br>
	 * {@link #SEARCH_MIXED}: Include both popular and real time results in the
	 * response.<br> {@link #SEARCH_RECENT}: return only the most recent results in
	 * the response<br> {@link #SEARCH_POPULAR}: return only the most popular
	 * results in the response.<br>
	 * 
	 * @param resultType
	 */
	public void setSearchResultType(String resultType) {
		this.resultType = resultType;
	}

	/**
	 * Date based filter on statuses and messages. This is done client-side as
	 * Twitter have - for their own inscrutable reasons - pulled support for
	 * this feature. Use {@link #setSinceId(Number)} for preference.
	 * <p>
	 * If using this, you probably also want to increase
	 * {@link #setMaxResults(int)} - otherwise you get at most 20, and possibly
	 * less (since the filtering is done client side).
	 * 
	 * @param sinceDate
	 * @see #setSinceId(Number)
	 */
	@Deprecated
	public void setSinceDate(Date sinceDate) {
		this.sinceDate = sinceDate;
	}

	/**
	 * Narrows the returned results to just those statuses created after the
	 * specified status id. This will be used until it is set to null. Default
	 * is null.
	 * <p>
	 * If using this, you probably also want to increase
	 * {@link #setMaxResults(int)} (otherwise you just get the most recent 20).
	 * 
	 * @param statusId
	 */
	public void setSinceId(Number statusId) {
		sinceId = statusId;
	}

	/**
	 * Set the source application. This will be mentioned on Twitter alongside
	 * status updates (with a small label saying source: myapp).
	 * 
	 * <i>In order for this to work, you must first register your app with
	 * Twitter and get a source name from them! You must also use OAuth to
	 * connect.</i>
	 * 
	 * @param sourceApp
	 *            jtwitterlib by default. Set to null for no source.
	 */
	public void setSource(String sourceApp) {
		this.sourceApp = sourceApp;
	}

	/**
	 * Sets the authenticating user's status.
	 * <p>
	 * Identical to {@link #updateStatus(String)}, but with a Java-style name
	 * (updateStatus is the Twitter API name for this method).
	 * 
	 * @param statusText
	 *            The text of your status update. Must not be more than 160
	 *            characters and should not be more than 140 characters to
	 *            ensure optimal display.
	 * @return The posted status when successful.
	 */
	public Status setStatus(String statusText) throws TwitterException {
		return updateStatus(statusText);
	}

	/**
	 * @param untilDate
	 *            the untilDate to set. This is NOT
	 *            properly supported. It operates by post filtering
	 *            results client-side.
	 * @see #setUntilId(Number) which is better
	 */
	@Deprecated
	public void setUntilDate(Date untilDate) {
		this.untilDate = untilDate;
	}

	/**
	 * If set, return results older than this.
	 * 
	 * @param untilId
	 *            aka max_id
	 */
	public void setUntilId(Number untilId) {
		this.untilId = untilId;
	}

	/**
	 * Set this to allow the use of twitlonger via
	 * {@link #updateLongStatus(String, long)}. To get an api-key for your app,
	 * contact twitlonger as described here: http://www.twitlonger.com/api
	 * 
	 * @param twitlongerAppName
	 * @param twitlongerApiKey
	 */
	public void setupTwitlonger(String twitlongerAppName,
			String twitlongerApiKey) {
		this.twitlongerAppName = twitlongerAppName;
		this.twitlongerApiKey = twitlongerApiKey;
	}

	/**
	 * @see Twitter_Users#show(Number)
	 */
	@Deprecated
	public User show(Number userId) {
		return users().show(userId);
	}

	/**
	 * @see Twitter_Users#show(String)
	 */
	@Deprecated
	public User show(String screenName) throws TwitterException,
			TwitterException.SuspendedUser {
		return users().show(screenName);
	}

	/**
	 * Split a long message up into shorter chunks suitable for use with
	 * {@link #setStatus(String)} or {@link #sendMessage(String, String)}.
	 * 
	 * @param longStatus
	 * @return longStatus broken into a list of max 140 char strings
	 */
	public List<String> splitMessage(String longStatus) {
		// Is it really long?
		if (longStatus.length() <= 140)
			return Collections.singletonList(longStatus);
		// Multiple tweets for a longer post
		List<String> sections = new ArrayList<String>(4);
		StringBuilder tweet = new StringBuilder(140);
		String[] words = longStatus.split("\\s+");
		for (String w : words) {
			// messages have a max length of 140
			// plus the last bit of a long tweet tends to be hidden on
			// twitter.com, so best to chop 'em short too
			if (tweet.length() + w.length() + 1 > 140) {
				// Emit
				tweet.append("...");
				sections.add(tweet.toString());
				tweet = new StringBuilder(140);
				tweet.append(w);
			} else {
				if (tweet.length() != 0) {
					tweet.append(" ");
				}
				tweet.append(w);
			}
		}
		// Final bit
		if (tweet.length() != 0) {
			sections.add(tweet.toString());
		}
		return sections;
	}

	/**
	 * Map with since_id, page and count, if set. This is called by methods that
	 * return lists of statuses or messages.
	 */
	private Map<String, String> standardishParameters() {
		return addStandardishParameters(new HashMap<String, String>());
	}

	// /**
	// * The length of an https url after t.co shortening.
	// * This is just 1 more than {@link #LINK_LENGTH}
	// * <p>
	// * Use updateConfiguration() if you want to get the latest settings from
	// Twitter.
	// */
	// public static int LINK_LENGTH_HTTPS = LINK_LENGTH+1;

	/**
	 * @see Twitter_Users#stopFollowing(String)
	 */
	@Deprecated
	public User stopFollowing(String username) {
		return users().stopFollowing(username);
	}

	/**
	 * @see Twitter_Users#stopFollowing(User)
	 */
	@Deprecated
	public User stopFollowing(User user) {
		return stopFollowing(user.screenName);
	}

	/**
	 * Update info on Twitter's configuration -- such as shortened url lengths.
	 */
	public void updateConfiguration() {
		String json = http.getPage(TWITTER_URL + "/help/configuration.json",
				null, false);
		try {
			JSONObject jo = new JSONObject(json);
			LINK_LENGTH = jo.getInt("short_url_length");
			// LINK_LENGTH_HTTPS = jo.getInt("short_url_length_https");
			// LINK_LENGTH + 1
			// characters_reserved_per_media -- this is just LINK_LENGTH
			// max_media_per_upload // 1!
			PHOTO_SIZE_LIMIT = jo.getLong("photo_size_limit");
			// photo_sizes
			// short_url_length_https
		} catch (JSONException e) {
			throw new TwitterException.Parsing(json, e);
		}
	}

	/**
	 * Use twitlonger.com to post a lengthy tweet. See twitlonger.com for more
	 * details on their service.
	 * <p>
	 * Note: You need to have called {@link #setupTwitlonger(String, String)}
	 * before calling this.
	 * 
	 * @param message
	 * @param inReplyToStatusId
	 *            Can be null if this isn't a reply
	 * @return A Twitter status using a truncated message with a link to
	 *         twitlonger.com
	 * @see #setupTwitlonger(String, String)
	 */
	public Status updateLongStatus(String message, Number inReplyToStatusId) {
		if (twitlongerApiKey == null || twitlongerAppName == null)
			throw new IllegalStateException(
					"Twitlonger api details have not been set! Call #setupTwitlonger() first.");
		if (message.length() < 141)
			throw new IllegalArgumentException("Message too short ("
					+ inReplyToStatusId
					+ " chars). Just post a normal Twitter status. ");
		String url = "http://www.twitlonger.com/api_post";
		Map<String, String> vars = InternalUtils.asMap("application",
				twitlongerAppName, "api_key", twitlongerApiKey, "username",
				name, "message", message);
		if (inReplyToStatusId != null && inReplyToStatusId.doubleValue() != 0) {
			vars.put("in_reply", inReplyToStatusId.toString());
		}
		// ?? set direct_message 0/1 as appropriate if allowing long DMs
		String response = http.post(url, vars, false);
		Matcher m = contentTag.matcher(response);
		boolean ok = m.find();
		if (!ok)
			throw new TwitterException.TwitLongerException(
					"TwitLonger call failed", response);
		String shortMsg = m.group(1).trim();

		// Post to Twitter
		Status s = updateStatus(shortMsg, inReplyToStatusId);

		m = idTag.matcher(response);
		ok = m.find();
		if (!ok)
			// weird - but oh well
			return s;
		String id = m.group(1);

		// Once a message has been successfully posted to Twitlonger and
		// Twitter, it would be really useful to send back the Twitter ID for
		// the message. This will allow users to manage their Twitlonger posts
		// and delete not only the Twitlonger post, but also the Twitter post
		// associated with it. It will also makes replies much more effective.
		try {
			url = "http://www.twitlonger.com/api_set_id";
			vars.remove("message");
			vars.remove("in_reply");
			vars.remove("username");
			vars.put("message_id", "" + id);
			vars.put("twitter_id", "" + s.getId());
			http.post(url, vars, false);
		} catch (Exception e) {
			// oh well
		}

		// done
		return s;
	}

	/**
	 * Updates the authenticating user's status.
	 * 
	 * @param statusText
	 *            The text of your status update. Must not be more than 160
	 *            characters and should not be more than 140 characters to
	 *            ensure optimal display.
	 * @return The posted status when successful.
	 */
	public Status updateStatus(String statusText) {
		return updateStatus(statusText, null);
	}

	/**
	 * Updates the authenticating user's status and marks it as a reply to the
	 * tweet with the given ID.
	 * 
	 * @param statusText
	 *            The text of your status update. Must not be more than 160
	 *            characters and should not be more than 140 characters to
	 *            ensure optimal display.
	 * 
	 * 
	 * @param inReplyToStatusId
	 *            The ID of the tweet that this tweet is in response to. The
	 *            statusText must contain the username (with an "@" prefix) of
	 *            the owner of the tweet being replied to for for Twitter to
	 *            agree to mark the tweet as a reply. <i>null</i> to leave this
	 *            unset.
	 * 
	 * @return The posted status when successful.
	 *         <p>
	 *         Warning: the microformat for direct messages is supported. BUT:
	 *         the return value from this method will be null, and not the
	 *         direct message. Other microformats (such as follow) may result in
	 *         an exception being thrown.
	 * 
	 * @throws TwitterException
	 *             if something goes wrong. There is a rare (but not rare
	 *             enough) bug whereby Twitter occasionally returns a success
	 *             code but the wrong tweet. If this happens, the update may or
	 *             may not have worked - wait a bit & check.
	 */
	public Status updateStatus(String statusText, Number inReplyToStatusId)
			throws TwitterException 
	{		
		// check for length
		if (statusText.length() > 160) {
			int shortLength = statusText.length();
			Matcher m = InternalUtils.URL_REGEX.matcher(statusText);
			while(m.find()) {
				shortLength += LINK_LENGTH - m.group().length(); 
			}
			if (shortLength > 140) {
				// bogus - send a helpful error
				if (statusText.startsWith("RT")) {
					throw new IllegalArgumentException(
							"Status text must be 140 characters or less -- use Twitter.retweet() to do new-style retweets which can be a bit longer: "
									+ statusText.length() + " " + statusText);
				}
				throw new IllegalArgumentException(
						"Status text must be 140 characters or less: "
								+ statusText.length() + " " + statusText);
			}
		}
		
		Map<String, String> vars = InternalUtils.asMap("status", statusText);
		if (tweetEntities) vars.put("include_entities", "1");

		// add in long/lat if set
		if (myLatLong != null) {
			vars.put("lat", Double.toString(myLatLong[0]));
			vars.put("long", Double.toString(myLatLong[1]));
		}

		if (sourceApp != null) {
			vars.put("source", sourceApp);
		}
		if (inReplyToStatusId != null) {
			// TODO remove this legacy check
			double v = inReplyToStatusId.doubleValue();
			assert v != 0 && v != -1;
			vars.put("in_reply_to_status_id", inReplyToStatusId.toString());
		}
		String result = http.post(TWITTER_URL + "/statuses/update.json", vars,
					true);
		try {
			Status s = new Status(new JSONObject(result), null);
			s = updateStatus2_safetyCheck(statusText, s);
			return s;
		} catch (JSONException e) {
			throw new TwitterException.Parsing(result, e);
		}
	}

	private Status updateStatus2_safetyCheck(String statusText, Status s) {
		// Weird bug: Twitter occasionally rejects tweets?!
		// TODO does this still happen or have they fixed it? Hard to know
		// with an intermittent bug!
		// Sanity check...
		String targetText = statusText.trim();
		String returnedStatusText = s.text.trim();
		// strip the urls to remove the effects of the t.co shortener
		// (obviously this weakens the safety test, but failure would be
		// a corner case of a corner case).
		// TODO Twitter also shorten some not-quite-urls, such as "www.google.com", which stripUrls() won't catch.		
		targetText = InternalUtils.stripUrls(targetText);
		returnedStatusText = InternalUtils.stripUrls(returnedStatusText);
		if (returnedStatusText.equals(targetText))
			return s;
		{ // is it a direct message? - which doesn't return the true status
			String st = statusText.toLowerCase();
			if (st.startsWith("dm") || st.startsWith("d"))
				return null;
		}
		// Assume Twitter have fixed this bug -- TODO check this periodically
		if (true) return s;
		// try waiting and rechecking - maybe it did work after all
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			// igore the interruption
		}
		Status s2 = getStatus();			
		if (s2 != null) {
			returnedStatusText = InternalUtils.stripUrls(s2.text.trim());
			if (targetText.equals(returnedStatusText)) {			
				return s2;
			}
		}
		throw new TwitterException.Unexplained(
				"Unexplained failure for tweet: expected \"" + statusText
						+ "\" but got " + s2);
	}

	// TODO
	// c.f. https://dev.twitter.com/discussions/1059
	Status updateStatusWithMedia(String statusText, Number inReplyToStatusId,
			File media) {

		// should we trim statusText??
		// TODO support URL shortening
		if (statusText.length() > 160)
			throw new IllegalArgumentException(
					"Status text must be 160 characters or less: "
							+ statusText.length() + " " + statusText);
		Map<String, String> vars = InternalUtils.asMap("status", statusText);

		// add in long/lat if set
		if (myLatLong != null) {
			vars.put("lat", Double.toString(myLatLong[0]));
			vars.put("long", Double.toString(myLatLong[1]));
		}

		if (sourceApp != null) {
			vars.put("source", sourceApp);
		}
		if (inReplyToStatusId != null) {
			// TODO remove this legacy check
			double v = inReplyToStatusId.doubleValue();
			assert v != 0 && v != -1;
			vars.put("in_reply_to_status_id", inReplyToStatusId.toString());
		}
		// media[]
		// possibly_sensitive
		// place_id
		// display_coordinates
		String result = null;
		try {
			result = http
					.post( // WithMedia
					// TWITTER_URL +
					"http://upload.twitter.com/1/statuses/update_with_media.json",
							vars, true);
			Status s = new Status(new JSONObject(result), null);
			return s;
		} catch (E403 e) {
			// test for repetition (which gets a 403)
			Status s = getStatus();
			if (s != null && s.getText().equals(statusText))
				throw new TwitterException.Repetition(s.getText());
			throw e;
		} catch (JSONException e) {
			throw new TwitterException.Parsing(result, e);
		}
	}

	/**
	 * User and social-network related API methods.
	 */
	public Twitter_Users users() {
		return new Twitter_Users(this);
	}

}

