/*
 * SmugMugApi.cs
 *
 * Authors:
 *   Thomas Van Machelen <thomas.vanmachelen@gmail.com>
 *
 * Copyright (C) 2006 Thomas Van Machelen
 * This is free software. See COPYING for details.
 *
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Xml;
using System.Collections.Specialized;

namespace SmugMugNet
{
	public struct Credentials
	{
		public string session_id;
		public int user_id;
		public string password_hash;

		public string SessionID {
			get { return session_id; } 
			set { session_id = value; }
		}

		public int UserID {
			get { return user_id; } 
		}

		public string PasswordHash {
			get { return password_hash; } 
		}

		public Credentials(string session_id, int user_id, string password_hash)
		{
			this.session_id = session_id;
			this.user_id = user_id;
			this.password_hash = password_hash;
		}
	}

	public struct Category
	{
		public Category( string title, int id)
		{
			this.title = title;
			this.category_id = id;
		}

		private int category_id;
		public int CategoryID
		{
			get { return category_id; }
			set { category_id = value; }
		}

		private string title;
		public string Title
		{
			get { return title; }
			set { title = value; }
		}
	}

	public struct Album
	{
		public Album(string title, int id)
		{
			this.album_id = id;
			this.title = title;
		}

		private int album_id;
		public int AlbumID
		{
			get { return album_id; }
			set { album_id = value; }
		}

		private string title;
		public string Title
		{
			get { return title; }
			set { title = value; }
		}
	}

	public class SmugMugApi
	{
		private string username = String.Empty;
		private string password = String.Empty;
		private bool connected = false;

		private Credentials credentials;
		private const string VERSION = "1.1.1";
		private Category[] categories;

		public SmugMugApi (string email_address, string password)
		{
			this.username = email_address;
			this.password = password;
		}

		public bool Login ()
		{
			if (this.username.Length == 0 | this.password.Length == 0)
			{
				throw new SmugMugException("There is no username or password.");
			}

			if (this.connected == false && this.credentials.UserID == 0)
			{
				try
				{
					this.credentials = SmugMugProxy.LoginWithPassword (this.username, this.password);
					this.connected = true;
				}
				catch
				{
					return false;
				}
			}
			else
			{
				LoginWithHash ();
			}

			return true;
		}

		private void LoginWithHash ()
		{ 
			try {
				string session_id = SmugMugProxy.LoginWithHash (this.credentials.UserID, this.credentials.PasswordHash);
				
				if (session_id != null && session_id.Length > 0)
				{
					this.credentials.SessionID = session_id;
				}
				else
				{
					throw new SmugMugException ("SessionID was empty");
				}
			}
			catch (Exception ex) {
				throw new SmugMugException ("A login error occured, SessionID may be invalid.", ex.InnerException);
			}
		}

		public void Logout () 
		{
			if (!connected)
			 	return; 

			if (this.credentials.SessionID == null && this.credentials.SessionID.Length == 0)
			 	return;

			SmugMugProxy.Logout (this.credentials.SessionID);
			connected = false;
			this.credentials = new Credentials (null, 0, null);
		}

		public Category[] GetCategories ()
		{
			if (this.categories == null)
			{
				try {
					this.categories = SmugMugProxy.GetCategories (credentials.SessionID);
				}
				catch (Exception ex) {
					throw new SmugMugException ("Could not retrieve Categories", ex.InnerException);
				}
			}
			return this.categories;
		}

		public Album CreateAlbum (string title, int category_id, bool is_public)
		{
		 	try {
				return SmugMugProxy.CreateAlbum (title, category_id, credentials.SessionID, is_public);
			}
			catch (Exception ex) {
				throw new SmugMugException ("Could not create album", ex.InnerException);
			}
		}

		public Album[] GetAlbums ()
		{
		 	try {
				return SmugMugProxy.GetAlbums(credentials.SessionID);
			}
			catch (Exception ex) {
				throw new SmugMugException ("Could not get albums", ex.InnerException);
			}
		}

		public Uri GetAlbumUrl (int image_id)
		{
			try {
				return SmugMugProxy.GetAlbumUrl (image_id, credentials.SessionID);
			}
			catch (Exception ex) {
				throw new SmugMugException ("Could not get album url", ex.InnerException);
			}
		}

		public int Upload (string path, int album_id)
		{
		 	try {
				return SmugMugProxy.Upload (path, album_id, credentials.SessionID);
			}
			catch (Exception ex) {
				throw new SmugMugException ("Could not upload file", ex.InnerException);
			}
		}
	}

	public class SmugMugProxy
	{
	 	// FIXME: this getting should be done over https
		private const string GET_URL = "https://api.SmugMug.com/hack/rest/";
		private const string POST_URL = "https://upload.SmugMug.com/hack/rest/";
		// key from massis
		private const string APIKEY = "umtr0zB2wzwTZDhF2BySidg0hY0le3K6";
		private const string VERSION = "1.1.1";

		// rest methods
		private const string LOGIN_WITHPASS_METHOD = "smugmug.login.withPassword";
		private const string LOGIN_WITHHASH_METHOD = "smugmug.login.withHash";
		private const string LOGOUT_METHOD = "smugmug.logout";
		private const string ALBUMS_CREATE_METHOD = "smugmug.albums.create";
		private const string ALBUMS_GET_URLS_METHOD = "smugmug.images.getURLs";
		private const string ALBUMS_GET_METHOD = "smugmug.albums.get";
		private const string CATEGORIES_GET_METHOD = "smugmug.categories.get";

		// parameter constants
		private const string EMAIL = "EmailAddress";
		private const string PASSWORD = "Password";
		private const string USER_ID = "UserID";
		private const string PASSWORD_HASH = "PasswordHash";
		private const string SESSION_ID = "SessionID";
		private const string CATEGORY_ID = "CategoryID";
		private const string IMAGE_ID = "ImageID";
		private const string TITLE = "Title";
		private const string ID = "id";

		public static Credentials LoginWithPassword (string username, string password)
		{
			string url = FormatGetUrl (LOGIN_WITHPASS_METHOD, new SmugMugParam (EMAIL, username), new SmugMugParam (PASSWORD, password));
			XmlDocument doc = GetResponseXml (url);

			string sessionId = doc.SelectSingleNode ("/rsp/Login/SessionID").InnerText;
			int userId = int.Parse (doc.SelectSingleNode ("/rsp/Login/UserID").InnerText);
			string passwordHash = doc.SelectSingleNode ("/rsp/Login/PasswordHash").InnerText;
			
			return new Credentials (sessionId, userId, passwordHash);
		}

		public static string LoginWithHash (int user_id, string password_hash)
		{
			string url = FormatGetUrl (LOGIN_WITHHASH_METHOD, new SmugMugParam (USER_ID, user_id), new SmugMugParam (PASSWORD_HASH, password_hash));
			XmlDocument doc = GetResponseXml(url);

			return doc.SelectSingleNode ("/rsp/Login/SessionID").InnerText;
		}

		public static void Logout (string session_id)
		{
			string url = FormatGetUrl (LOGOUT_METHOD, new SmugMugParam (SESSION_ID, session_id));
			GetResponseXml (url); 
		}

		public static Album[] GetAlbums (string session_id)
		{
			string url = FormatGetUrl (ALBUMS_GET_METHOD, new SmugMugParam(SESSION_ID, session_id));
			XmlDocument doc = GetResponseXml (url);
			XmlNodeList albumNodes = doc.SelectNodes ("/rsp/Albums/Album");

			Album[] albums = new Album[albumNodes.Count];

			for (int i = 0; i < albumNodes.Count; i++)
			{
				XmlNode current = albumNodes[i];
				albums[i] = new Album (current.SelectSingleNode (TITLE).InnerText, int.Parse (current.Attributes[ID].Value));
			}
			return albums;
		}

		public static Uri GetAlbumUrl (int image_id, string session_id)
		{
			string url = FormatGetUrl(ALBUMS_GET_URLS_METHOD, new SmugMugParam(IMAGE_ID, image_id), new SmugMugParam(SESSION_ID, session_id));
			XmlDocument doc = GetResponseXml(url);

			string album_url = doc.SelectSingleNode("/rsp/ImageURLs/Image/AlbumURL").InnerText;

			return new Uri(album_url);
		}

		public static Category[] GetCategories (string session_id)
		{
			string url = FormatGetUrl(CATEGORIES_GET_METHOD, new SmugMugParam (SESSION_ID, session_id));
			XmlDocument doc = GetResponseXml (url);

			XmlNodeList categoryNodes = doc.SelectNodes ("/rsp/Categories/Category");
			Category[] categories = new Category[categoryNodes.Count];

			for (int i = 0; i < categoryNodes.Count; i++)
			{
				XmlNode current = categoryNodes[i];
				categories[i] = new Category (current.SelectSingleNode (TITLE).InnerText, int.Parse (current.Attributes[ID].Value));
			}
			return categories;
		}

		public static Album CreateAlbum (string title, int category_id, string session_id)
		{
			return CreateAlbum (title, category_id, session_id, true);	 
		}

		public static Album CreateAlbum (string title, int category_id, string session_id, bool is_public)
		{
		 	int public_int = is_public ? 1 : 0;
			string url = FormatGetUrl (ALBUMS_CREATE_METHOD, new SmugMugParam (TITLE, title), new SmugMugParam (CATEGORY_ID, category_id), new SmugMugParam (SESSION_ID, session_id), new SmugMugParam ("Public", public_int));
			XmlDocument doc = GetResponseXml (url);

			int id = int.Parse(doc.SelectSingleNode("/rsp/Create/Album").Attributes[ID].Value);

			return new Album(title, id);
		}

		public static int Upload (string path, int album_id, string session_id)
		{
			FileInfo file = new FileInfo(path);

			if (!file.Exists)
				throw new ArgumentException("Image does not exist: " + file.FullName);

			try
			{
				WebClient client = new WebClient ();
				client.BaseAddress = "http://upload.smugmug.com";
				client.Headers.Add ("Cookie:SMSESS=" + session_id);

				NameValueCollection queryStringCollection = new NameValueCollection ();
				queryStringCollection.Add ("AlbumID", album_id.ToString());
				queryStringCollection.Add ("ResponseType", "REST");
				client.QueryString = queryStringCollection;

				byte[] responseArray = client.UploadFile ("http://upload.smugmug.com/photos/xmladd.mg", "POST", file.FullName);
				string response = Encoding.ASCII.GetString (responseArray);

				XmlDocument doc = new XmlDocument ();
				doc.LoadXml (response);

				return int.Parse (doc.SelectSingleNode ("/rsp/ImageID").InnerText);
			}
			catch (Exception ex)
			{
				throw new SmugMugUploadException ("Error uploading image: " + file.FullName, ex.InnerException);
			}
		}

		private static string FormatGetUrl(string method_name, params SmugMugParam[] parameters)
		{
			StringBuilder builder = new StringBuilder (string.Format ("{0}{1}/?method={2}", GET_URL, VERSION, method_name));

			foreach (SmugMugParam param in parameters)
				builder.Append (param.ToString ());

			builder.Append (new SmugMugParam ("APIKey", APIKEY));
			return builder.ToString();
		}

		private static XmlDocument GetResponseXml (string url)
		{
			HttpWebRequest request = HttpWebRequest.Create (url) as HttpWebRequest;
			request.Credentials = CredentialCache.DefaultCredentials;
			WebResponse response = request.GetResponse ();

			XmlDocument doc = new XmlDocument ();
			doc.LoadXml (new StreamReader (response.GetResponseStream ()).ReadToEnd ());
			CheckResponseXml (doc);

			response.Close ();
			return doc;
		}

		private static void CheckResponseXml (XmlDocument doc)
		{
	 		if (doc.SelectSingleNode("/rsp").Attributes["stat"].Value == "ok")
		 		return;

			string message = doc.SelectSingleNode ("/rsp/err").Attributes["msg"].Value;
			throw new SmugMugException (message);
		}

		private class SmugMugParam
		{
			string name;
			object value;

			public SmugMugParam (string name, object value)
			{
				this.name = name;
				this.value = value;
			}

			public string Name 
			{
				get {return name;}
			}

			public object Value
			{
				get {return value;}
			}

			public override string ToString()
			{
				return string.Format("&{0}={1}", Name, Value);
			}
		}
	}

	public class SmugMugException : ApplicationException 
	{
		public SmugMugException(string message) : base (message) 
		{
			Console.WriteLine (message);
		}

		public SmugMugException (string message, Exception innerException) : base (message, innerException) 
		{
			Console.WriteLine (message, innerException.ToString());
		}
	}

	public sealed class SmugMugUploadException : ApplicationException 
	{
		public SmugMugUploadException (string message, Exception innerException) : base (message, innerException) 
		{
			Console.WriteLine (message, innerException.ToString ());
		}
	}
}
