using System;
using System.Collections.Generic;
using System.Text;
using System.Data;

namespace MessageBoard.Storage
{
    class Database : SharedLibrary._Database
    {
        public Database(String FN, SharedLibrary.Interfaces.ILogger logger) : base(FN, logger) { }

        public override void Init()
        {
            if (!System.IO.File.Exists(FileName))
            {
                string createClientTable = @"CREATE TABLE IF NOT EXISTS [USERS] ( 
                [id] INTEGER PRIMARY KEY AUTOINCREMENT,
                [ranking] INTEGER DEFAULT 0,
                [username] TEXT COLLATE NOCASE NOT NULL,
                [email] TEXT NOT NULL, 
                [passwordhash] TEXT NOT NULL,
                [passwordsalt] TEXT NOT NULL,
                [lastlogin] TEXT NOT NULL,
                [creationdate] TEXT NOT NULL,
                [subscribedthreads] TEXT DEFAULT 0,
                [avatarurl] TEXT
                );";

                string createSessionTable = @"CREATE TABLE IF NOT EXISTS [SESSIONS] ( 
                [sessionid] TEXT NOT NULL,
                [sessionuserid] INTEGER NOT NULL,
                FOREIGN KEY(sessionuserid) REFERENCES USERS(id)
                );";

                string createRankingTable = @"CREATE TABLE IF NOT EXISTS [RANKS] (
                [id] INTEGER PRIMARY KEY AUTOINCREMENT,
                [name] TEXT UNIQUE NOT NULL,
                [equivalentrank] INTEGER DEFAULT 0
                );";

                string createCategoryTable = @"CREATE TABLE IF NOT EXISTS [CATEGORIES] (
                [id] INTEGER PRIMARY KEY AUTOINCREMENT,
                [title] TEXT NOT NULL,
                [description] TEXT NOT NULL,
                [permissions] BLOB
                );";
    
                string createThreadTable = @"CREATE TABLE IF NOT EXISTS [THREADS] ( 
                [id] INTEGER PRIMARY KEY AUTOINCREMENT,
                [title] TEXT NOT NULL,
                [categoryid] INTEGER NOT NULL,
                [replies] INTEGER DEFAULT 0,
                [authorid] INTEGER NOT NULL,
                [creationdate] TEXT NOT NULL,
                [updateddate] TEXT NOT NULL,
                [content] TEXT NOT NULL,
                [visible] INTEGER DEFAULT 1,
                FOREIGN KEY(authorid) REFERENCES USERS(id),
                FOREIGN KEY(categoryid) REFERENCES CATEGORIES(id)
                );";

                string createReplyTable = @"CREATE TABLE IF NOT EXISTS [REPLIES] (
                [id] INTEGER PRIMARY KEY AUTOINCREMENT,
                [title] TEXT NOT NULL,
                [authorid] INT NOT NULL,
                [threadid] INT NOT NULL,
                [creationdate] TEXT NOT NULL,
                [updateddate] TEXT NOT NULL,
                [content] TEXT NOT NULL,
                [visible] INTEGER DEFAULT 1,
                FOREIGN KEY(authorid) REFERENCES USERS(id),
                FOREIGN KEY(threadid) REFERENCES THREADS(id)
                );";

                string createUserProfileTable = @"CREATE TABLE IF NOT EXISTS [PROFILES] (
                [id] INTEGER PRIMARY KEY AUTOINCREMENT,
                [userid] INTEGER NOT NULL,
                [showemail] INTEGER DEFAULT 1,
                [bannercolor] TEXT DEFAULT '#ff6633',
                [birthday] TEXT,
                [showage] INTEGER DEFAULT 0
                );";  


                ExecuteNonQuery(createClientTable);
                ExecuteNonQuery(createSessionTable);
                ExecuteNonQuery(createRankingTable);
                ExecuteNonQuery(createCategoryTable);
                ExecuteNonQuery(createThreadTable);
                ExecuteNonQuery(createReplyTable);
                ExecuteNonQuery(createUserProfileTable);

                Rank guestRank  = new Rank(1, "Guest", SharedLibrary.Player.Permission.User);
                Rank userRank   = new Rank(2, "User", SharedLibrary.Player.Permission.Trusted);
                Rank modRank    = new Rank(3, "Moderator", SharedLibrary.Player.Permission.Moderator);
                Rank adminRank  = new Rank(4, "Administrator", SharedLibrary.Player.Permission.Owner);

                addRank(guestRank);
                addRank(userRank);
                addRank(modRank);
                addRank(adminRank);

                List<Permission> defaultCatPerms = new List<Permission> {
                    new Permission(guestRank.getID(), Permission.Action.READ),
                    new Permission(userRank.getID(), Permission.Action.READ | Permission.Action.WRITE),
                    new Permission(modRank.getID(), Permission.Action.READ | Permission.Action.WRITE | Permission.Action.MODIFY),
                    new Permission(adminRank.getID(), Permission.Action.READ | Permission.Action.WRITE | Permission.Action.MODIFY | Permission.Action.DELETE)
                };

                Category defaultCat = new Category(1, "Default Category", "This is the default category.", defaultCatPerms);
                addCategory(defaultCat);
            }
        }

        #region SESSIONS
        public Session getSession(string sessionID)
        {
            DataTable Result = GetDataTable("SESSIONS", new KeyValuePair<string, object>("sessionid", sessionID));

            if (Result != null && Result.Rows.Count > 0)
            {
                DataRow ResponseRow = Result.Rows[0];
                int userID = Int32.Parse(ResponseRow["sessionuserid"].ToString());
                User sessionUser = getUser(userID);

                // this shouldn't happen.. but it might :c
                if (sessionUser == null)
                    return null;

                Session foundSession = new Session(sessionUser, sessionID);
                return foundSession;
            }

            else
                return null;
        }

        public Session setSession(int userID, string sessionID)
        {
            // prevent duplicated tuples
            if (getSession(sessionID) != null)
            {
                updateSession(sessionID, userID);
                return getSession(sessionID);
            }

            Dictionary<String, object> newSession = new Dictionary<String, object>();

            newSession.Add("sessionid", sessionID);
            newSession.Add("sessionuserid", userID);

            Insert("SESSIONS", newSession);

            return getSession(sessionID);
        }

        public void removeSession(string sessionID)
        {
            ExecuteNonQuery(String.Format("DELETE FROM SESSIONS WHERE sessionid = '{0}'", sessionID));
        }

        public bool updateSession(string sessionID, int userID)
        {
            if (getSession(sessionID) == null)
                return false;

            Dictionary<string, object> updatedSession = new Dictionary<string, object>();
            updatedSession.Add("sessionuserid", userID);

            Update("SESSIONS", updatedSession, new KeyValuePair<string, object>("sessionid", sessionID));
            return true;
        }
        #endregion

        #region USERS
        private User getUserFromDataTable(DataTable Result)
        {
            if (Result != null && Result.Rows.Count > 0)
            {
                DataRow ResponseRow = Result.Rows[0];
                int id = Convert.ToInt32(ResponseRow["id"].ToString());
                string passwordHash = ResponseRow["passwordhash"].ToString();
                string passwordSalt = ResponseRow["passwordsalt"].ToString();
                string username = ResponseRow["username"].ToString();
                string email = ResponseRow["email"].ToString();
                DateTime lastLogon = DateTime.Parse(ResponseRow["lastlogin"].ToString());
                DateTime creationDate = DateTime.Parse(ResponseRow["creationdate"].ToString());
                Rank ranking = getRank(Convert.ToInt32(ResponseRow["ranking"]));
                string avatarURL = ResponseRow["avatarurl"].ToString();
                string posts = GetDataTable(String.Format("select (select count(*) from THREADS where authorid = {0}) + (select count(*) from REPLIES where authorid = {0}) as posts;", id)).Rows[0]["posts"].ToString();

                User foundUser = new User(id, passwordHash, passwordSalt, username, email, Convert.ToInt32(posts), lastLogon, creationDate, ranking, avatarURL);
                return foundUser;
            }

            return null;
        }

        private Dictionary<string, object> getDataTableFromUser(User addedUser)
        {
            Dictionary<String, object> newUser = new Dictionary<String, object>();

            newUser.Add("username", addedUser.username);
            newUser.Add("email", addedUser.email);
            newUser.Add("passwordhash", addedUser.getPasswordHash());
            newUser.Add("passwordsalt", addedUser.getPasswordSalt());
            newUser.Add("lastlogin", SharedLibrary.Utilities.DateTimeSQLite(addedUser.lastLogin));
            newUser.Add("creationdate", SharedLibrary.Utilities.DateTimeSQLite(addedUser.creationDate));
            //newUser.Add("subscribedthreads", String.Join<int>(",", addedUser.subscribedThreads));
            newUser.Add("ranking", addedUser.ranking.getID());
            newUser.Add("avatarurl", addedUser.avatarURL);

            return newUser;
        }

        public User getUser(int userid)
        {
            DataTable Result = GetDataTable("USERS", new KeyValuePair<string, object>("id", userid));

            return getUserFromDataTable(Result);
        }

        public User getUser(string username)
        {
            DataTable Result = GetDataTable("USERS", new KeyValuePair<string, object>("username", username));

            return getUserFromDataTable(Result);
        }

        public bool userExists(string username, string email)
        {
            String Query = String.Format("SELECT * FROM USERS WHERE username = '{0}' or email = '{1}'", username, email);
            DataTable Result = GetDataTable(Query);

            return Result.Rows.Count > 0;
        }

        /// <summary>
        /// Returns ID of added user
        /// </summary>
        /// <param name="addedUser"></param>
        /// <param name="userSession"></param>
        /// <returns></returns>
        public User addUser(User addedUser, Session userSession)
        {
            var newUser = getDataTableFromUser(addedUser);
            Insert("USERS", newUser);

            // fixme
            User createdUser = getUser(addedUser.username);
            return createdUser;           
        }

        public bool updateUser(User updatedUser)
        {
            var user = getDataTableFromUser(updatedUser);
            Update("USERS", user, new KeyValuePair<string, object>("id", updatedUser.getID()));

            return true;
        }

        public int getNumUsers()
        {
            var Result = GetDataTable("SELECT COUNT(id) AS userCount FROM `USERS`;");
            return Convert.ToInt32(Result.Rows[0]["userCount"]);
        }
        #endregion

        #region CATEGORIES
        private Category getCategoryFromDataTable(DataTable Result)
        {
            if (Result != null && Result.Rows.Count > 0)
            {
                DataRow ResponseRow = Result.Rows[0];

                int id = Convert.ToInt32(ResponseRow["id"]);
                string title = ResponseRow["title"].ToString();
                string description = ResponseRow["description"].ToString();
                string permissions = Encoding.UTF8.GetString((byte[])ResponseRow["permissions"]);
                List<Permission> perms = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Permission>>(permissions);

                Category requestedCategory = new Category(id, title, description, perms);
                return requestedCategory;
            }

            return null;
        }

        public void addCategory(Category addingCategory)
        {
            Dictionary<String, object> newCategory = new Dictionary<string, object>();

            newCategory.Add("title", addingCategory.title);
            newCategory.Add("description", addingCategory.description);
            newCategory.Add("permissions", Newtonsoft.Json.JsonConvert.SerializeObject(addingCategory.permissions));

            Insert("CATEGORIES", newCategory);
        }

        public Category getCategory(int id)
        {
            string Query = String.Format("SELECT * FROM CATEGORIES WHERE id = {0}", id);
            DataTable Result = GetDataTable(Query);

            return getCategoryFromDataTable(Result);
        }

        public List<Category> getAllCategories()
        {
            string Query = String.Format("SELECT id FROM CATEGORIES");
            List<Category> cats = new List<Category>();
            DataTable Result = GetDataTable(Query);

            if (Result != null && Result.Rows.Count > 0)
            {
                for (int i = 0; i < Result.Rows.Count; i++)
                    cats.Add(getCategory(Convert.ToInt32(Result.Rows[i]["id"])));
            }

            return cats;
        }
        #endregion

        #region THREADS

        public Dictionary<string, object> getDataTableFromThread(ForumThread Thread)
        {
            Dictionary<string, object> newThread = new Dictionary<string, object>();
            newThread.Add("title", Thread.title);
            newThread.Add("categoryid", Thread.threadCategory.getID());
            newThread.Add("replies", Thread.replies);
            newThread.Add("authorid", Thread.author.getID());
            newThread.Add("creationdate", SharedLibrary.Utilities.DateTimeSQLite(Thread.creationDate));
            newThread.Add("updateddate", SharedLibrary.Utilities.DateTimeSQLite(Thread.updatedDate));
            newThread.Add("content", Thread.content);
            newThread.Add("visible", Convert.ToInt32(Thread.visible));

            return newThread;
        }

        public int addThread(ForumThread Thread)
        {
            Insert("THREADS", getDataTableFromThread(Thread));
            return getThreadID(Thread.creationDate);
        }


        public bool updateThread(ForumThread updatedThread)
        {
            var user = getDataTableFromThread(updatedThread);
            Update("THREADS", user, new KeyValuePair<string, object>("id", updatedThread.getID()));

            return true;
        }

        public ForumThread getThread(int id)
        {
            DataTable Result = GetDataTable("THREADS", new KeyValuePair<string, object>("id", id));

            return getThreadFromDataTable(Result);     
        }

        private ForumThread getThreadFromDataTable(DataTable Result)
        {
            if (Result != null && Result.Rows.Count > 0)
            {
                DataRow ResponseRow = Result.Rows[0];
                int id = Convert.ToInt32(ResponseRow["id"].ToString());
                int categoryid = Convert.ToInt32(ResponseRow["categoryid"].ToString());
                int authorid = Convert.ToInt32(ResponseRow["authorid"].ToString());
                int replies = Convert.ToInt32(ResponseRow["replies"].ToString());
                string title = ResponseRow["title"].ToString();

                var category = getCategory(categoryid);
                var author = getUser(authorid);

                bool visible = Convert.ToBoolean((Convert.ToInt32(ResponseRow["visible"])));

                DateTime creationDate = DateTime.Parse(ResponseRow["creationdate"].ToString());
                DateTime updatedDate = DateTime.Parse(ResponseRow["updateddate"].ToString());
                string content = ResponseRow["content"].ToString();

                ForumThread retrievedThread = new ForumThread(id, title, visible, content, replies, author, category, creationDate, updatedDate);
                return retrievedThread;
            }

            return null;
        }

        // we have no other unique id yet
        private int getThreadID(DateTime creationDate)
        {
            string Query = String.Format("SELECT * FROM THREADS WHERE creationdate = \"{0}\"", SharedLibrary.Utilities.DateTimeSQLite(creationDate));
            DataTable Result = GetDataTable(Query);

            if (Result != null && Result.Rows.Count > 0)
                return Convert.ToInt32(Result.Rows[0]["id"].ToString());

            return 0;
        }

        public List<ForumThread> getRecentThreads(int categoryID)
        {
            List<ForumThread> threads = new List<ForumThread>();
            string Query = String.Format("SELECT id FROM THREADS WHERE categoryid = {0} AND visible = 1 ORDER BY `updateddate` DESC LIMIT 3", categoryID);
            DataTable Result = GetDataTable(Query);

            if (Result != null && Result.Rows.Count > 0)
            {
                for (int i = 0; i < Result.Rows.Count; i++)
                    threads.Add(getThread(Convert.ToInt32(Result.Rows[i]["id"])));
            }

            return threads;
        }

        public List<ForumThread> getCategoryThreads(int categoryID)
        {
            List<ForumThread> threads = new List<ForumThread>();
            string Query = String.Format("SELECT id FROM THREADS WHERE categoryid = {0} and visible = 1 ORDER BY `updateddate` DESC", categoryID);
            DataTable Result = GetDataTable(Query);

            if (Result != null && Result.Rows.Count > 0)
            {
                for (int i = 0; i < Result.Rows.Count; i++)
                    threads.Add(getThread(Convert.ToInt32(Result.Rows[i]["id"])));
            }

            return threads;
        }
        #endregion

        #region RANKING
        public int addRank(Rank newRank)
        {
            Dictionary<string, object> rank = new Dictionary<string, object>();
            rank.Add("name", newRank.name);
            rank.Add("equivalentrank", (int)newRank.equivalentRank);

            Insert("RANKS", rank);

            Rank r = getRank(newRank.name);

            if (r == null)
                return 0;

            return r.getID();
        }

        public Rank getRank(string rankName)
        {
            DataTable Result = GetDataTable("RANKS", new KeyValuePair<string, object>("name", rankName));

            if (Result != null && Result.Rows.Count > 0)
            {
                DataRow ResponseRow = Result.Rows[0];
                string name = ResponseRow["name"].ToString();
                int equivRank = Convert.ToInt32(ResponseRow["equivalentrank"].ToString());
                int id = Convert.ToInt32(ResponseRow["id"].ToString());

                Rank retrievedRank = new Rank(id, name, (SharedLibrary.Player.Permission)equivRank);
                return retrievedRank;
            }

            return null;
        }

        public Rank getRank(int rankID)
        {
            DataTable Result = GetDataTable("RANKS", new KeyValuePair<string, object>("id", rankID));

            if (Result != null && Result.Rows.Count > 0)
            {
                DataRow ResponseRow = Result.Rows[0];
                string name = ResponseRow["name"].ToString();
                int equivRank = Convert.ToInt32(ResponseRow["equivalentrank"].ToString());

                Rank retrievedRank = new Rank(rankID, name, (SharedLibrary.Player.Permission)equivRank);
                return retrievedRank;
            }

            return null;
        }
        #endregion

        #region REPLIES
        public int addReply(Post reply)
        {
            Insert("REPLIES", getDataTableFromReply(reply));
            return getReplyID(reply.creationDate);
        }

        public bool updateReply(Post reply)
        {
            return Update("REPLIES", getDataTableFromReply(reply), new KeyValuePair<string, object>("id", reply.id));
        }

        public Post getReply(int id)
        {
            DataTable Result = GetDataTable("REPLIES", new KeyValuePair<string, object>("id", id));

            return getReplyFromDataTable(Result);
        }

        public List<Post> getRepliesFromThreadID(int threadID)
        {
            List<Post> replies = new List<Post>();
            //var Result = GetDataTable("REPLIES", new KeyValuePair<string, object>("threadid", threadID));
            var Result = GetDataTable("SELECT * FROM REPLIES WHERE threadid = " + threadID + " AND visible = 1");

            foreach (DataRow row in Result.Rows)
            {
                replies.Add(getReply(Convert.ToInt32(row["id"].ToString())));
            }

            return replies;
        }

        private Dictionary<string, object> getDataTableFromReply(Post reply)
        {
            Dictionary<string, object> newReply = new Dictionary<string, object>();
            newReply.Add("title", reply.title);
            newReply.Add("authorid", reply.author.getID());
            newReply.Add("threadid", reply.threadid);
            newReply.Add("creationdate", SharedLibrary.Utilities.DateTimeSQLite(reply.creationDate));
            newReply.Add("updateddate", SharedLibrary.Utilities.DateTimeSQLite(reply.updatedDate));
            newReply.Add("content", reply.content);
            newReply.Add("visible", Convert.ToInt32(reply.visible));

            return newReply;
        }

        private Post getReplyFromDataTable(DataTable Result)
        {
            if (Result != null && Result.Rows.Count > 0)
            {
                DataRow ResponseRow = Result.Rows[0];
                int id = Convert.ToInt32(ResponseRow["id"].ToString());
                int threadid = Convert.ToInt32(ResponseRow["threadid"].ToString());
                int authorid = Convert.ToInt32(ResponseRow["authorid"].ToString());
                string title = ResponseRow["title"].ToString();
                var author = getUser(authorid);

                DateTime creationDate = DateTime.Parse(ResponseRow["creationdate"].ToString());
                DateTime updatedDate = DateTime.Parse(ResponseRow["updateddate"].ToString());
                string content = ResponseRow["content"].ToString();

                bool visible = Convert.ToBoolean((Convert.ToInt32(ResponseRow["visible"])));

                Post retrievedPost = new Post(id, threadid, visible, title, content, author, creationDate, updatedDate);
                return retrievedPost;
            }

            return null;
        }

        // we have no other unique id yet
        private int getReplyID(DateTime creationDate)
        {
            DataTable Result = GetDataTable("REPLIES", new KeyValuePair<string, object>("creationdate", SharedLibrary.Utilities.DateTimeSQLite(creationDate)));

            if (Result != null && Result.Rows.Count > 0)
                return Convert.ToInt32(Result.Rows[0]["id"].ToString());

            return 0;
        }
        #endregion

        #region PROFILES
        private ProfileSettings getProfileFromDataTable(DataTable Result)
        {
            if (Result != null && Result.Rows.Count > 0)
            {
                DataRow ResponseRow = Result.Rows[0];
                int id = Convert.ToInt32(ResponseRow["id"].ToString());
                int userID = Convert.ToInt32(ResponseRow["userid"].ToString());
                bool showEmail = Convert.ToBoolean(Convert.ToInt32(ResponseRow["showemail"].ToString()));
                string bannerColor = ResponseRow["bannercolor"].ToString();
                DateTime birthday = DateTime.Parse(ResponseRow["birthday"].ToString());
                bool showAge = Convert.ToBoolean(Convert.ToInt32(ResponseRow["showage"].ToString()));

                ProfileSettings foundProfile = new ProfileSettings(id, userID, showEmail, bannerColor, birthday, showAge);
                return foundProfile;
            }

            return null;
        }

        private Dictionary<string, object> getDataTableFromProfile(ProfileSettings addedSettings)
        {
            Dictionary<String, object> newSettings = new Dictionary<String, object>();

            if(addedSettings.id > 0)
                newSettings.Add("id", addedSettings.id);
            newSettings.Add("userid", addedSettings.userid);
            newSettings.Add("showemail", Convert.ToInt32(addedSettings.showEmail));
            newSettings.Add("bannercolor", addedSettings.bannerColor);
            newSettings.Add("birthday", SharedLibrary.Utilities.DateTimeSQLite(addedSettings.birthday));
            newSettings.Add("showage", Convert.ToInt32(addedSettings.showAge));

            return newSettings;
        }

        public ProfileSettings getProfileSettings(int userid)
        {
            DataTable Result = GetDataTable("PROFILES", new KeyValuePair<string, object>("userid", userid));

            return getProfileFromDataTable(Result);
        }

        public bool addProfileSettings(ProfileSettings newProfile)
        {
            return Insert("PROFILES", getDataTableFromProfile(newProfile));
        }

        public bool updateProfileSettings(ProfileSettings updatedProfile)
        {
            return Update("PROFILES", getDataTableFromProfile(updatedProfile), new KeyValuePair<string, object>("userid", updatedProfile.userid));
        }

        #endregion
    }
}