using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Text.RegularExpressions;

/// <summary>
/// MySQLCommands are embedded in SQL Scripts as follows:
///--# [Command] [Version] [Arg1] [Arg2] ... [Arg3]
///--# 
///~ Serves as the Command End Marker!
///-- Possible Commands are, PRE_CREATE,CREATE,POST_CREATE,PRE_ALTER,ALTER,POST_ALTER,PRE_DROP,DROP,POST_DROP,RENAME
///-- Command Execution is as follows.
///
///-- CREATE must exists.
///-- For New Install: *Script ID does not exist* -- WILL ALWAYS TAKE ONLY LATEST VERSION for CREATE, runs PRE-CREATEs and POST-CREATEs accordingly
///-- PRE-CREATE, CREATE, POST-CREATE (If you want nothing done, leave command blank)
///-- Allow Versions to use *, so that we can say for all [0.*.*] call this PRE-CREATE, or POST-CREATE
///
///-- For Update Install: *Script ID exists* - Will check db where to start and execute until to highest version of CREATE was reached
///-- PRE-DROP, DROP, POST-DROP, PRE-RENAME, RENAME, POST-RENAME, PRE-ALTER, ALTER, POST-ALTER, for each respective version
///
///-- Versioning is done as follows:
/// (Major version).(Minor version).(Revision number)
/// 
/// MySQLIdentifiers are embedded in SQL Scripts as follows:
///--$ [Identifier] [Value], Identifier structure
///
///--$ [ScriptId] [Guid]!
///-- Each script is versioned on it's own, so that is what we store in the db.
///-- Give each script a UNIQUE GUID.
///-- We will then store the ScriptID guid in the versioning table, along side the highest version of the script that most recently ran!,
///-- ~this way we can always know if something has to be done for each database object,
/// </summary>
namespace Sdaleo.Internal
{
    /// <summary>
    /// 
    /// </summary>
    public enum MySQLCommandType
    {
        PRE_CREATE,
        CREATE,
        POST_CREATE,
        PRE_ALTER,
        ALTER,
        POST_ALTER,
        PRE_DROP,
        DROP,
        POST_DROP,
        PRE_RENAME,
        RENAME,
        POST_RENAME,
    }

    /// <summary>
    /// Custom SQL Commands that are part of an SQL Script.
    /// They start as follows:
    /// --# [Command] [Version] [Arg1] [Arg2] ... [Arg3]
    /// 
    /// and end as follows:
    /// --#
    /// ~Serves as the Command End Marker!
    /// Everything in between is SQL.
    /// </summary>
    internal class MySQLCommand
    {
        #region Members N' Properties

        // Line Specifics
        internal uint Line_CommandStart { get; private set; }
        internal uint Line_CommandEnd { get; private set; }
        internal string[] SQLScriptBlock { get; private set; }

        // Command Specifics
        internal MySQLCommandType Command { get; private set; }
        internal string Version
        { 
            get 
            { 
                if (_Versioning != null) 
                    return _Versioning.Version; 
                else 
                    return String.Empty; 
            } 
        }
        internal Versioning _Versioning = null;
        internal string[] Arguments = null;

        /// <summary>
        /// Returns true if there are Lines between CommandStart and CommandEnd,
        /// this indicates that it contains sql in between
        /// </summary>
        internal bool ContainsSQLScript
        {
            get
            {
                if (Line_CommandStart >= Line_CommandEnd)
                    return false;
                else if ((Line_CommandStart + 1) == Line_CommandEnd)
                    return false;
                else if ((SQLScriptBlock != null) && (SQLScriptBlock.Length > 0))
                    return true;
                else
                    return false;
            }
        }

        #endregion

        #region Internal Execute

        internal DBError ExecuteScriptBlock(IConnectDb credential)
        {
            DBError dbError = DBError.Create("No Script Block to Execute");
            if ((SQLScriptBlock != null) && (SQLScriptBlock.Length > 0))
            {                
                foreach (string sqlLine in SQLScriptBlock)
                {
                    DB db = DB.Create(credential);
                    dbError = db.ExecuteNonQuery(sqlLine);
                    if (dbError.ErrorOccured)
                        break;
                }
            }
            return dbError;
        }

        #endregion

        #region Construction

        /// <summary>
        /// Used to Parse a SQLCommand from an SQL Script
        /// </summary>
        /// <param name="Line_CommandStart">Specifies the line of code where the Command is located --# [Command] [Version] [Arg1] [Arg2] ... [Arg3] </param>
        /// <param name="Line_CommandStartString">The String of Line_CommandStart, that contains all the Command Values the --# [Command] [Version] [Arg1] [Arg2] ... [Arg3] string</param>
        /// <param name="Line_CommandEnd">Specifies the line of code where the Command ends --#</param>
        internal MySQLCommand(uint Line_CommandStart, string Line_CommandStartString, uint Line_CommandEnd, string[] sqlScriptBlock)
        {            
            this.Line_CommandStart = Line_CommandStart;
            this.Line_CommandEnd = Line_CommandEnd;
            ParseLine_CommandStartString(Line_CommandStartString);
            this.SQLScriptBlock = sqlScriptBlock;
        }

        /// <summary>
        /// Parses a CommmandStartString and fills the values for this class
        /// </summary>
        /// <param name="LineCommandStartString">a line that contains a CommandStartString</param>
        private void ParseLine_CommandStartString(string LineCommandStartString)
        {                        
            /// --# [Command] [Version] [Arg1] [Arg2] ... [Arg3]
            /// --# [Create] [1.0.0] [Arg1] [Arg2]
            Regex rx = new Regex(@"\[([\w\a-\?\.\*\+]+)\]");
            MatchCollection matches = rx.Matches(LineCommandStartString);
            if (matches.Count < 2)
                throw new ArgumentException("LineIdentifierString is Invalid");

            // Get the Command and Command Version
            string m1 = matches[0].ToString().Trim(new char[] { '[', ']' });
            string m2 = matches[1].ToString().Trim(new char[] { '[', ']' });
            this.Command = (MySQLCommandType)Enum.Parse(typeof(MySQLCommandType), m1.ToUpper());
            this._Versioning = new Versioning(m2);                        
            
            // Get Optional Arguments
            int nArguments = matches.Count - 2;
            if (nArguments > 0)
            {
                Arguments = new string[nArguments];
                for (int i = 0; i < nArguments; ++i)
                    Arguments[i] = matches[2 + i].ToString().Trim(new char[] { '[', ']' });
            }
        }

        #endregion

        #region Internal Statics

        internal const string IDENTIFY_SQLSCRIPT_COMMAND = "--#";

        /// <summary>
        /// Returns true if the passed in line is a MySQLCommandStartLine
        /// </summary>
        /// <param name="line">line to check</param>
        /// <returns>true if MySQLCommandStartLine, false otherwise</returns>
        internal static bool IsMySQLCommandStartLine(string line)
        {
            string lineTrimed = line.Trim();
            if (lineTrimed.StartsWith(IDENTIFY_SQLSCRIPT_COMMAND) && lineTrimed.Length != IDENTIFY_SQLSCRIPT_COMMAND.Length)
                return true;
            return false;
        }

        /// <summary>
        /// Returns ture if the passed in line is a MySQLCommandEndLine
        /// </summary>
        /// <param name="line">line to check</param>
        /// <returns>true if MySQLCommandEndLine, false otherwise</returns>
        internal static bool IsMySQLCommandEndLine(string line)
        {
            string lineTrimed = line.Trim();
            if (lineTrimed.StartsWith(IDENTIFY_SQLSCRIPT_COMMAND) && lineTrimed.Length == IDENTIFY_SQLSCRIPT_COMMAND.Length)
                return true;
            return false;
        }

        #endregion
    }

    /// <summary>
    /// 
    /// </summary>
    public enum MySQLIdentifierType
    {
        SCRIPTID,
        SCRIPTVERSION,
    }

    /// <summary>
    /// Custom SQL Identifiers that are part of an SQL Script.
    /// They start as follows:
    /// --$ [Identifier] [Value]
    /// </summary>
    public class MySQLIdentifier
    {
        #region Members N' Properties

        // Line Specifics
        internal uint Line_IdentifierStart { get; private set; }

        // Identifier Specifics
        internal MySQLIdentifierType Identity { get; private set; }
        internal string Value { get; private set; }

        #endregion

        #region Construction

        /// <summary>
        /// Used to Parse a SQLCommand from an SQL Script
        /// </summary>
        /// <param name="Line_Identifier">Specifies the line of code where the Identifier is located --$ [Identifier] [Value] </param>
        /// <param name="Line_IdentifierString">The String of Line_IdentifierString, that contains all the Identifier Values the --$ [Identifier] [Value] string</param>
        internal MySQLIdentifier(uint Line_IdentifierStart, string Line_IdentifierString)
        {
            this.Line_IdentifierStart = Line_IdentifierStart;
            ParseLine_IndentifierStartString(Line_IdentifierString);            
        }

        /// <summary>
        /// Parses a LineIdentifierString and fills the values for this class
        /// </summary>
        /// <param name="LineIdentifierString">a line that contains a LineIdentifierString</param>
        private void ParseLine_IndentifierStartString(string LineIdentifierString)
        {
            /// --$ [Identifier] [Value]
            /// --$ [ScriptID] [12312-312321-3213]            
            Regex rx = new Regex(@"\[([\w\a-\?\.\*\+]+)\]");
            MatchCollection matches = rx.Matches(LineIdentifierString);                                   
            if (matches.Count < 2)
                throw new ArgumentException("LineIdentifierString is Invalid");

            // Get the Identity and Value
            string m1 = matches[0].ToString().Trim(new char[] { '[', ']' });
            string m2 = matches[1].ToString().Trim(new char[] { '[', ']' });            
            this.Identity = (MySQLIdentifierType)Enum.Parse(typeof(MySQLIdentifierType), m1.ToUpper());
            this.Value = m2;
        }

        #endregion

        #region Internal Statics

        internal const string IDENTIFY_SQLSCRIPT_IDENTIFIER = "--$";

        /// <summary>
        /// Returns true if the passed in line is a MySQLIdentifier
        /// </summary>
        /// <param name="line">line to check</param>
        /// <returns>true if MySQLIdentifier, false otherwise</returns>
        internal static bool IsMySQLIdentifier(string line)
        {
            string lineTrimed = line.Trim();
            if (lineTrimed.StartsWith(IDENTIFY_SQLSCRIPT_IDENTIFIER) && lineTrimed.Length != IDENTIFY_SQLSCRIPT_IDENTIFIER.Length)
                return true;
            return false;
        }        

        #endregion
    }

    /// <summary>
    /// Helper class that parses an SQL Script for Commands and Identifiers,
    /// for processing.
    /// </summary>
    internal static class SQLParser
    {
        /// <summary>
        /// Iterates thru the Identifiers and returns the value of the first identifier that matches the identity.
        /// </summary>
        /// <returns>the value of the first matched identity, String.Empty if not found</returns>
        internal static string GetValueForFirstFoundMySQLIdentifierType(MySQLIdentifierType Identity, MySQLIdentifier[] identifiers)
        {
            if (identifiers != null && identifiers.Length > 0)
            {
                foreach (MySQLIdentifier identifier in identifiers)
                {
                    if (identifier.Identity == Identity)
                        return identifier.Value;
                }
            }
            return String.Empty;
        }

        /// <summary>
        /// Parses an SQLScript for MySQLCommand Objects
        /// </summary>
        /// <param name="sqlscript">a string array from an sql script</param>
        /// <param name="commands">all MySQLCommand objects found, if any, otherwise, empty []</param>
        /// <param name="identifiers">all MySQLIdentifier objects found, if any, otherwise, empty []</param>
        internal static void ParseSQLScriptForMySQLCommandsAndIdentifiers(string[] sqlscript, out MySQLCommand[] commands, out MySQLIdentifier[] identifiers)
        {
            // Result set
            commands = null;
            identifiers = null;
            List<MySQLCommand> CommandsFound = new List<MySQLCommand>();
            List<MySQLIdentifier> IdentifiersFound = new List<MySQLIdentifier>();

            // intermediary variables 
            int nStartLine = -1;
            string strStartLine = String.Empty;
            
            // Iterate thru all lines
            for (int i = 0; i < sqlscript.Length; ++i)
            {
                string curLine = sqlscript[i];

                /// Parse for
                /// --$ [Identifier] [Value]
                if (MySQLIdentifier.IsMySQLIdentifier(curLine))
                {
                    IdentifiersFound.Add(new MySQLIdentifier((uint)i, curLine));
                    continue;
                }

                /// Parse for
                /// --# [Command] [Version] [Arg1] [Arg2] ... [Arg3]
                /// ...
                /// --#
                if (MySQLCommand.IsMySQLCommandStartLine(curLine))
                {
                    nStartLine = i;
                    strStartLine = curLine;
                    continue;
                }
                else if (MySQLCommand.IsMySQLCommandEndLine(curLine))
                {
                    // Something is wrong with the script!
                    if ((nStartLine == -1) || String.IsNullOrEmpty(strStartLine))
                    {
                        Debug.Assert(false);
                    }
                    else
                    {
                        if (nStartLine == i || nStartLine == i + 1)
                        {
                            Debug.Assert(false); // There is no SQLCodeBlock! = something is wrong with the script
                            nStartLine = -1;
                            strStartLine = String.Empty;
                            continue;
                        }

                        // Create the SQLScriptBlock
                        List<String> sqlScriptBlock = new List<String>();
                        for (int j = nStartLine; j < i; ++j)
                            sqlScriptBlock.Add(sqlscript[j]);

                        // Convert the SQLScriptBlock into Executable Code
                        string[] executableSQLCodeBlock = GetExecutableSqlTextFromSQLScriptBlock(sqlScriptBlock.ToArray());

                        // Create the Commands Object that now contains the Executable SQL Code
                        CommandsFound.Add(new MySQLCommand((uint)nStartLine, strStartLine, (uint)i, executableSQLCodeBlock));
                        nStartLine = -1;
                        strStartLine = String.Empty;
                    }
                    continue;
                }                
            }

            // Delegate to Propertly Sort the MySQLCommands, First, by Command, then by Version
            System.Comparison<MySQLCommand> comparer = delegate(MySQLCommand x, MySQLCommand y)
            {
                // First Compare the Command, 
                int nCompare = x.Command.CompareTo(y.Command);

                // If equal, compare the versioning
                if(nCompare == 0)
                    nCompare = x._Versioning.CompareTo(y._Versioning);
                
                return nCompare;
            };
            CommandsFound.Sort(comparer);

            // Pass out Result set
            commands = CommandsFound.ToArray();
            identifiers = IdentifiersFound.ToArray();
        }

        /// <summary>
        /// Use this to Parse an SQL Script Block (a block of code, contained inside a MySQLCommand.
        /// It Ignores Comment Lines, and also breaks out the SQL Script by GO Statements.
        /// </summary>
        /// <param name="sqlscriptBlock">an unprocessed Script block from an SQL Script</param>
        /// <returns>a ADO.Net Friend sql script block that can be executed</returns>
        internal static string[] GetExecutableSqlTextFromSQLScriptBlock(string[] sqlscriptBlock)
        {

            bool bIsInCommentMode = false;
            StringBuilder sbSQLLines = new StringBuilder();
            List<String> sbSQLBrokenOut = new List<String>();
            foreach (string line in sqlscriptBlock)
            {
                // Let's deal with comments, which allows us to put comments in our SQL Scripts
                if (bIsInCommentMode)
                {
                    if (line.StartsWith("*/") || line.EndsWith("*/"))
                        bIsInCommentMode = false;
                    continue;
                }
                else if (line.StartsWith("/*") && !line.EndsWith("*/"))
                {
                    bIsInCommentMode = true; // skip lines until end of comment
                    continue;
                }
                else if (line.StartsWith("/*") && line.EndsWith("*/"))
                    continue; // skip single comment line
                else if (line.StartsWith("--"))
                    continue; // skip single comment line

                // We break out GO stmts to allow execution seperatly
                if ((line.Length == 2) && line.ToUpper().StartsWith("GO"))
                {
                    if (sbSQLLines.Length > 0)
                        sbSQLBrokenOut.Add(sbSQLLines.ToString());
                    sbSQLLines.Remove(0, sbSQLLines.Length); // clear sbSQLLines
                    continue;
                }

                // Ready to use the SQL Line
                sbSQLLines.Append((line + " "));
            }

            if (sbSQLLines.Length > 0)
                sbSQLBrokenOut.Add(sbSQLLines.ToString());

            // Return the 'GO' Broken out SQL Script Block
            return sbSQLBrokenOut.ToArray();
        }
    }
}
