//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.IO;
using System.Linq;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.LanguageServices;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.Utility;
using ConnectionType = Microsoft.SqlTools.ServiceLayer.Connection.ConnectionType;
using Location = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Location;
using Microsoft.SqlServer.Management.Sdk.Sfc;
using System.Text;
using System.Data;
using System.Diagnostics;
using Range = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Range;

namespace Microsoft.SqlTools.ServiceLayer.Scripting
{
    internal partial class Scripter
    {
        private bool error;
        private string errorMessage;
        private ServerConnection serverConnection;
        private ConnectionInfo connectionInfo;
        private Database database;
        private string tempPath;

        // Dictionary that holds the object name (as appears on the TSQL create statement)
        private Dictionary<DeclarationType, string> sqlObjectTypes = new Dictionary<DeclarationType, string>();

        private Dictionary<string, string> sqlObjectTypesFromQuickInfo = new Dictionary<string, string>();

        private Dictionary<DatabaseEngineEdition, string> targetDatabaseEngineEditionMap = new Dictionary<DatabaseEngineEdition, string>();

        private Dictionary<int, string> serverVersionMap = new Dictionary<int, string>();

        private Dictionary<string, string> objectScriptMap = new Dictionary<string, string>();

        internal Scripter() {}

        /// <summary>
        /// Initialize a Peek Definition helper object
        /// </summary>
        /// <param name="serverConnection">SMO Server connection</param>
        internal Scripter(ServerConnection serverConnection, ConnectionInfo connInfo)
        {
            this.serverConnection = serverConnection;
            this.connectionInfo = connInfo;
            this.tempPath = FileUtilities.GetPeekDefinitionTempFolder();
            Initialize();
        }
        
        internal Database Database
        {
            get
            {
                if (this.database == null)
                {
                    if (this.serverConnection != null && !string.IsNullOrEmpty(this.serverConnection.DatabaseName))
                    {
                        try
                        {
                            // Reuse existing connection
                            Server server = new Server(this.serverConnection);
                            // The default database name is the database name of the server connection
                            string dbName = this.serverConnection.DatabaseName;
                            if (this.connectionInfo != null)
                            {
                                // If there is a query DbConnection, use that connection to get the database name
                                // This is preferred since it has the most current database name (in case of database switching)
                                DbConnection connection;
                                if (connectionInfo.TryGetConnection(ConnectionType.Query, out connection))
                                {
                                    if (!string.IsNullOrEmpty(connection.Database))
                                    {
                                        dbName = connection.Database;
                                    }
                                }
                            }
                            this.database = new Database(server, dbName);
                            this.database.Refresh();
                        }
                        catch (ConnectionFailureException cfe)
                        {
                            Logger.Write(TraceEventType.Error, "Exception at PeekDefinition Database.get() : " + cfe.Message);
                            this.error = true;
                            this.errorMessage = (connectionInfo != null && connectionInfo.IsCloud) ? SR.PeekDefinitionAzureError(cfe.Message) : SR.PeekDefinitionError(cfe.Message);
                            return null;
                        }
                        catch (Exception ex)
                        {
                            Logger.Write(TraceEventType.Error, "Exception at PeekDefinition Database.get() : " + ex.Message);
                            this.error = true;
                            this.errorMessage = SR.PeekDefinitionError(ex.Message);
                            return null;
                        }
                    }
                }
                return this.database;
            }
        }

        /// <summary>
        /// Add the given type, scriptgetter and the typeName string to the respective dictionaries
        /// </summary>
        private void AddSupportedType(DeclarationType type, string typeName, string quickInfoType, Type smoObjectType)
        {
            sqlObjectTypes.Add(type, typeName);
            if (!string.IsNullOrEmpty(quickInfoType))
            {
                sqlObjectTypesFromQuickInfo.Add(quickInfoType.ToLowerInvariant(), typeName);
            }
        }

        /// <summary>
        /// Get the script of the selected token based on the type of the token
        /// </summary>
        /// <param name="declarationItems"></param>
        /// <param name="tokenText"></param>
        /// <param name="schemaName"></param>
        /// <returns>Location object of the script file</returns>
        internal DefinitionResult GetScript(ParseResult parseResult, Position position, IMetadataDisplayInfoProvider metadataDisplayInfoProvider, string tokenText, string schemaName)
        {
            int parserLine = position.Line;
            int parserColumn = position.Character;
            // Get DeclarationItems from The Intellisense Resolver for the selected token. The type of the selected token is extracted from the declarationItem.
            IEnumerable<Declaration> declarationItems = GetCompletionsForToken(parseResult, parserLine, parserColumn, metadataDisplayInfoProvider);
            if (declarationItems != null && declarationItems.Count() > 0)
            {
                foreach (Declaration declarationItem in declarationItems)
                {
                    if (declarationItem.Title == null)
                    {
                        continue;
                    }
                    if (this.Database == null)
                    {
                        return GetDefinitionErrorResult(SR.PeekDefinitionDatabaseError);
                    }
                    StringComparison caseSensitivity = this.Database.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
                    // if declarationItem matches the selected token, script SMO using that type

                    if (declarationItem.Title.Equals(tokenText, caseSensitivity))
                    {
                        return GetDefinitionUsingDeclarationType(declarationItem.Type, declarationItem.DatabaseQualifiedName, tokenText, schemaName);
                    }
                }
            }
            else
            {
                // if no declarationItem matched the selected token, we try to find the type of the token using QuickInfo.Text
                string quickInfoText = GetQuickInfoForToken(parseResult, parserLine, parserColumn, metadataDisplayInfoProvider);
                return GetDefinitionUsingQuickInfoText(quickInfoText, tokenText, schemaName);
            }
            // no definition found
            return GetDefinitionErrorResult(SR.PeekDefinitionNoResultsError);
        }

        /// <summary>
        /// Script an object using the type extracted from quickInfo Text
        /// </summary>
        /// <param name="quickInfoText">the text from the quickInfo for the selected token</param>
        /// <param name="tokenText">The text of the selected token</param>
        /// <param name="schemaName">Schema name</param>
        /// <returns></returns>
        internal DefinitionResult GetDefinitionUsingQuickInfoText(string quickInfoText, string tokenText, string schemaName)
        {
            if (this.Database == null)
            {
                return GetDefinitionErrorResult(SR.PeekDefinitionDatabaseError);
            }
            StringComparison caseSensitivity = this.Database.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
            string tokenType = GetTokenTypeFromQuickInfo(quickInfoText, tokenText, caseSensitivity);
            if (tokenType != null)
            {
                if (sqlObjectTypesFromQuickInfo.ContainsKey(tokenType.ToLowerInvariant()))
                {
                    // With SqlLogin authentication, the defaultSchema property throws an Exception when accessed.
                    // This workaround ensures that a schema name is present by attempting
                    // to get the schema name from the declaration item.
                    // If all fails, the default schema name is assumed to be "dbo"
                    if ((connectionInfo != null && connectionInfo.ConnectionDetails.AuthenticationType.Equals(Constants.SqlLoginAuthenticationType)) && string.IsNullOrEmpty(schemaName))
                    {
                        string fullObjectName = this.GetFullObjectNameFromQuickInfo(quickInfoText, tokenText, caseSensitivity);
                        schemaName = this.GetSchemaFromDatabaseQualifiedName(fullObjectName, tokenText);
                    }
                    Location[] locations = GetSqlObjectDefinition(
                                tokenText,
                                schemaName,
                                sqlObjectTypesFromQuickInfo[tokenType.ToLowerInvariant()]
                            );
                    DefinitionResult result = new DefinitionResult
                    {
                        IsErrorResult = this.error,
                        Message = this.errorMessage,
                        Locations = locations
                    };
                    return result;
                }
                else
                {
                    // If a type was found but is not in sqlScriptGettersFromQuickInfo, then the type is not supported
                    return GetDefinitionErrorResult(SR.PeekDefinitionTypeNotSupportedError);
                }
            }
            // no definition found
            return GetDefinitionErrorResult(SR.PeekDefinitionNoResultsError);
        }

        /// <summary>
        /// Script a object using the type extracted from declarationItem
        /// </summary>
        /// <param name="declarationItem">The Declaration object that matched with the selected token</param>
        /// <param name="tokenText">The text of the selected token</param>
        /// <param name="schemaName">Schema name</param>
        /// <returns></returns>
        internal DefinitionResult GetDefinitionUsingDeclarationType(DeclarationType type, string databaseQualifiedName, string tokenText, string schemaName)
        {
            if (sqlObjectTypes.ContainsKey(type))
            {
                // With SqlLogin authentication, the defaultSchema property throws an Exception when accessed.
                // This workaround ensures that a schema name is present by attempting
                // to get the schema name from the declaration item.
                // If all fails, the default schema name is assumed to be "dbo"
                if ((connectionInfo != null && connectionInfo.ConnectionDetails.AuthenticationType.Equals(Constants.SqlLoginAuthenticationType)) && string.IsNullOrEmpty(schemaName))
                {
                    string fullObjectName = databaseQualifiedName;
                    schemaName = this.GetSchemaFromDatabaseQualifiedName(fullObjectName, tokenText);
                }
                Location[] locations = GetSqlObjectDefinition(
                            tokenText,
                            schemaName,
                            sqlObjectTypes[type]
                        );
                DefinitionResult result = new DefinitionResult
                {
                    IsErrorResult = this.error,
                    Message = this.errorMessage,
                    Locations = locations
                };
                return result;
            }
            // If a type was found but is not in sqlScriptGetters, then the type is not supported
            return GetDefinitionErrorResult(SR.PeekDefinitionTypeNotSupportedError);
        }

        /// <summary>
        /// Script a object using SMO and write to a file.
        /// </summary>
        /// <param name="sqlScriptGetter">Function that returns the SMO scripts for an object</param>
        /// <param name="objectName">SQL object name</param>
        /// <param name="schemaName">Schema name or null</param>
        /// <param name="objectType">Type of SQL object</param>
        /// <returns>Location object representing URI and range of the script file</returns>
        internal Location[] GetSqlObjectDefinition(
                string objectName,
                string schemaName,
                string objectType)
        {
            // script file destination
            string tempFileName = (schemaName != null) ? Path.Combine(this.tempPath, string.Format("{0}.{1}.sql", schemaName, objectName))
                                                : Path.Combine(this.tempPath, string.Format("{0}.sql", objectName));

            SmoScriptingOperation operation = InitScriptOperation(objectName, schemaName, objectType);
            operation.Execute();
            string script = operation.ScriptText;

            bool objectFound = false;
            int createStatementLineNumber = 0;

            File.WriteAllText(tempFileName, script);
            string[] lines = File.ReadAllLines(tempFileName);
            int lineCount = 0;
            string createSyntax = null;
            if (objectScriptMap.ContainsKey(objectType.ToLower()))
            {
                createSyntax = string.Format("CREATE");
                foreach (string line in lines)
                {
                    if (LineContainsObject(line, objectName, createSyntax))
                    {
                        createStatementLineNumber = lineCount;
                        objectFound = true;
                        break;
                    }
                    lineCount++;
                }
            }
            if (objectFound)
            {
                Location[] locations = GetLocationFromFile(tempFileName, createStatementLineNumber);
                return locations;
            }
            else
            {
                this.error = true;
                this.errorMessage = SR.PeekDefinitionNoResultsError;
                return null;
            }
        }

        #region Helper Methods
        /// <summary>
        /// Return schema name from the full name of the database. If schema is missing return dbo as schema name.
        /// </summary>
        /// <param name="fullObjectName"> The full database qualified name(database.schema.object)</param>
        /// <param name="objectName"> Object name</param>
        /// <returns>Schema name</returns>
        internal string GetSchemaFromDatabaseQualifiedName(string fullObjectName, string objectName)
        {
            if (!string.IsNullOrEmpty(fullObjectName))
            {
                string[] tokens = fullObjectName.Split('.');
                for (int i = tokens.Length - 1; i > 0; i--)
                {
                    if (tokens[i].Equals(objectName))
                    {
                        return tokens[i - 1];
                    }
                }
            }
            return "dbo";
        }

        /// <summary>
        /// Convert a file to a location array containing a location object as expected by the extension
        /// </summary>
        internal Location[] GetLocationFromFile(string tempFileName, int lineNumber)
        {
            // Get absolute Uri based on uri format. This works around a dotnetcore URI bug for linux paths.
            if (Path.DirectorySeparatorChar.Equals('/'))
            {
                tempFileName = "file:" + tempFileName;
            }
            else
            {
                tempFileName = new Uri(tempFileName).AbsoluteUri;
            }
            // Create a location array containing the tempFile Uri, as expected by VSCode.
            Location[] locations = new[] 
            {
                    new Location 
                    {
                        Uri = tempFileName,
                        Range = new Range 
                        {
                            Start = new Position { Line = lineNumber, Character = 0},
                            End = new Position { Line = lineNumber + 1, Character = 0}
                        }
                    }
            };
            return locations;
        }

        /// <summary>
        /// Helper method to create definition error result object
        /// </summary>
        /// <param name="errorMessage">Error message</param>
        /// <returns> DefinitionResult</returns>
        internal DefinitionResult GetDefinitionErrorResult(string errorMessage)
        {
            return new DefinitionResult
            {
                IsErrorResult = true,
                Message = errorMessage,
                Locations = null
            };
        }

        /// <summary>
        /// Return full object name(database.schema.objectName) from the quickInfo text("type database.schema.objectName")
        /// </summary>
        /// <param name="quickInfoText">QuickInfo Text for this token</param>
        /// <param name="tokenText">Token Text</param>
        /// <param name="caseSensitivity">StringComparison enum</param>
        /// <returns></returns>
        internal string GetFullObjectNameFromQuickInfo(string quickInfoText, string tokenText, StringComparison caseSensitivity)
        {
            if (string.IsNullOrEmpty(quickInfoText) || string.IsNullOrEmpty(tokenText))
            {
                return null;
            }
            // extract full object name from quickInfo text
            string[] tokens = quickInfoText.Split(' ');
            List<string> tokenList = tokens.Where(el => el.IndexOf(tokenText, caseSensitivity) >= 0).ToList();
            return (tokenList?.Count() > 0) ? tokenList[0] : null;
        }

        /// <summary>
        /// Return token type from the quickInfo text("type database.schema.objectName")
        /// </summary>
        /// <param name="quickInfoText">QuickInfo Text for this token</param>
        /// <param name="tokenText"Token Text></param>
        /// <param name="caseSensitivity">StringComparison enum</param>
        /// <returns></returns>
        internal string GetTokenTypeFromQuickInfo(string quickInfoText, string tokenText, StringComparison caseSensitivity)
        {
            if (string.IsNullOrEmpty(quickInfoText) || string.IsNullOrEmpty(tokenText))
            {
                return null;
            }
            // extract string denoting the token type from quickInfo text
            string[] tokens = quickInfoText.Split(' ');
            List<int> indexList = tokens.Select((s, i) => new { i, s }).Where(el => (el.s).IndexOf(tokenText, caseSensitivity) >= 0).Select(el => el.i).ToList();
            return (indexList?.Count() > 0) ? String.Join(" ", tokens.Take(indexList[0])) : null;
        }


        /// <summary>
        /// Wrapper method that calls Resolver.GetQuickInfo
        /// </summary>
        internal string GetQuickInfoForToken(ParseResult parseResult, int parserLine, int parserColumn, IMetadataDisplayInfoProvider metadataDisplayInfoProvider)
        {
            if (parseResult == null || metadataDisplayInfoProvider == null)
            {
                return null;
            }
            Babel.CodeObjectQuickInfo quickInfo = Resolver.GetQuickInfo(
                parseResult, parserLine, parserColumn, metadataDisplayInfoProvider);
            return quickInfo?.Text;
        }

        /// <summary>
        /// Wrapper method that calls Resolver.FindCompletions
        /// </summary>
        /// <param name="parseResult"></param>
        /// <param name="parserLine"></param>
        /// <param name="parserColumn"></param>
        /// <param name="metadataDisplayInfoProvider"></param>
        /// <returns></returns>
        internal IEnumerable<Declaration> GetCompletionsForToken(ParseResult parseResult, int parserLine, int parserColumn, IMetadataDisplayInfoProvider metadataDisplayInfoProvider)
        {
            if (parseResult == null || metadataDisplayInfoProvider == null)
            {
                return null;
            }
            return Resolver.FindCompletions(
                parseResult, parserLine, parserColumn, metadataDisplayInfoProvider);
        }

        /// <summary>
        /// Wrapper method that calls Resolver.FindCompletions
        /// </summary>
        /// <param name="objectName"></param>
        /// <param name="schemaName"></param>
        /// <param name="objectType"></param>
        /// <param name="tempFileName"></param>
        /// <returns></returns>
        internal SmoScriptingOperation InitScriptOperation(string objectName, string schemaName, string objectType)
        {            
            // object that has to be scripted
            ScriptingObject scriptingObject = new ScriptingObject 
            {
                Name = objectName,
                Schema = schemaName,
                Type = objectType
            };

            // scripting options
            ScriptOptions options = new ScriptOptions 
            {
                ScriptCreateDrop = "ScriptCreate",
                TypeOfDataToScript = "SchemaOnly",
                ScriptStatistics = "ScriptStatsNone",
                TargetDatabaseEngineEdition = GetTargetDatabaseEngineEdition(),
                TargetDatabaseEngineType = GetTargetDatabaseEngineType(),
                ScriptCompatibilityOption = GetScriptCompatibilityOption(),
                ScriptExtendedProperties = false,
                ScriptUseDatabase = false,
                IncludeIfNotExists = false,
                GenerateScriptForDependentObjects = false,
                IncludeDescriptiveHeaders = false,
                ScriptCheckConstraints = false,
                ScriptChangeTracking = false,
                ScriptDataCompressionOptions = false,
                ScriptForeignKeys = false,
                ScriptFullTextIndexes = false,
                ScriptIndexes = false,
                ScriptPrimaryKeys = false,
                ScriptTriggers = false,
                UniqueKeys = false

            };

            List<ScriptingObject> objectList = new List<ScriptingObject>();
            objectList.Add(scriptingObject);

            // create parameters for the scripting operation

            ScriptingParams parameters = new ScriptingParams 
            {
                ConnectionString = ConnectionService.BuildConnectionString(this.connectionInfo.ConnectionDetails),
                ScriptingObjects = objectList,
                ScriptOptions = options,
                ScriptDestination = "ToEditor"
            };

            return new ScriptAsScriptingOperation(parameters, serverConnection);
        }

        internal string GetTargetDatabaseEngineEdition() 
        {
            DatabaseEngineEdition dbEngineEdition = this.serverConnection.DatabaseEngineEdition;
            string dbEngineEditionString;
            targetDatabaseEngineEditionMap.TryGetValue(dbEngineEdition, out dbEngineEditionString);
            return (dbEngineEditionString != null) ? dbEngineEditionString : "SqlServerEnterpriseEdition";
        }

        internal string GetScriptCompatibilityOption()
        {
            int serverVersion = this.serverConnection.ServerVersion.Major;
            string dbEngineTypeString = serverVersionMap[serverVersion];
            return (dbEngineTypeString != null) ? dbEngineTypeString : "Script140Compat";
        }

        internal string GetTargetDatabaseEngineType()
        {
           return connectionInfo.IsCloud ? "SqlAzure" : "SingleInstance";
        }

        internal bool LineContainsObject(string line, string objectName, string createSyntax)
        {
            if (line.IndexOf(createSyntax, StringComparison.OrdinalIgnoreCase) >= 0 &&
                line.IndexOf(objectName, StringComparison.OrdinalIgnoreCase) >=0)
            {
                return true;
            }
            return false;
        }

        internal static class ScriptingGlobals
        {
            /// <summary>
            /// Left delimiter for an named object
            /// </summary>
            public const char LeftDelimiter = '[';

            /// <summary>
            /// right delimiter for a named object
            /// </summary>
            public const char RightDelimiter = ']';
        }

        internal static class ScriptingUtils
        {
            /// <summary>
            /// Quote the name of a given sql object.
            /// </summary>
            /// <param name="sqlObject">object</param>
            /// <returns>quoted object name</returns>
            internal static string QuoteObjectName(string sqlObject)
            {
                return QuoteObjectName(sqlObject, ']');
            }

            /// <summary>
            /// Quotes the name of a given sql object
            /// </summary>
            /// <param name="sqlObject">object</param>
            /// <param name="quote">quote to use</param>
            /// <returns></returns>
            internal static string QuoteObjectName(string sqlObject, char quote)
            {
                int len = sqlObject.Length;
                StringBuilder result = new StringBuilder(sqlObject.Length);
                for (int i = 0; i < len; i++)
                {
                    if (sqlObject[i] == quote)
                    {
                        result.Append(quote);
                    }
                    result.Append(sqlObject[i]);
                }
                return result.ToString();
            }

            /// <summary>
            /// Returns the value whether the server supports XTP or not s
            internal static bool IsXTPSupportedOnServer(Server server)
            {
                bool isXTPSupported = false;
                if (server.ConnectionContext.ExecuteScalar("SELECT SERVERPROPERTY('IsXTPSupported')") != DBNull.Value)
                {
                    isXTPSupported = server.IsXTPSupported;
                }
                return isXTPSupported;
            }
        }

        internal static string SelectAllValuesFromTransmissionQueue(Urn urn)
        {
            string script = string.Empty;
            StringBuilder selectQuery = new StringBuilder();

            /*
             SELECT TOP *, casted_message_body =
              CASE MESSAGE_TYPE_NAME WHEN 'X'
                 THEN CAST(MESSAGE_BODY AS NVARCHAR(MAX))
                 ELSE MESSAGE_BODY
              END
              FROM [new].[sys].[transmission_queue]
             */
            selectQuery.Append("SELECT TOP (1000) ");
            selectQuery.Append("*, casted_message_body = \r\nCASE message_type_name WHEN 'X' \r\n  THEN CAST(message_body AS NVARCHAR(MAX)) \r\n  ELSE message_body \r\nEND \r\n");

            // from clause
            selectQuery.Append("FROM ");
            Urn dbUrn = urn;

            // database
            while (dbUrn.Parent != null && dbUrn.Type != "Database")
            {
                dbUrn = dbUrn.Parent;
            }
            selectQuery.AppendFormat("{0}{1}{2}",
                            ScriptingGlobals.LeftDelimiter,
                            ScriptingUtils.QuoteObjectName(dbUrn.GetAttribute("Name"), ScriptingGlobals.RightDelimiter),
                            ScriptingGlobals.RightDelimiter);
            //SYS
            selectQuery.AppendFormat(".{0}sys{1}",
                                     ScriptingGlobals.LeftDelimiter,
                                     ScriptingGlobals.RightDelimiter);
            //TRANSMISSION QUEUE
            selectQuery.AppendFormat(".{0}transmission_queue{1}",
                                      ScriptingGlobals.LeftDelimiter,
                                      ScriptingGlobals.RightDelimiter);

            script = selectQuery.ToString();
            return script;
        }

        internal static string SelectAllValues(Urn urn)
        {
            string script = string.Empty;
            StringBuilder selectQuery = new StringBuilder();
            selectQuery.Append("SELECT TOP (1000) ");
            selectQuery.Append("*, casted_message_body = \r\nCASE message_type_name WHEN 'X' \r\n  THEN CAST(message_body AS NVARCHAR(MAX)) \r\n  ELSE message_body \r\nEND \r\n");

            // from clause
            selectQuery.Append("FROM ");
            Urn dbUrn = urn;

            // database
            while (dbUrn.Parent != null && dbUrn.Type != "Database")
            {
                dbUrn = dbUrn.Parent;
            }
            selectQuery.AppendFormat("{0}{1}{2}",
                ScriptingGlobals.LeftDelimiter,
                ScriptingUtils.QuoteObjectName(dbUrn.GetAttribute("Name"), ScriptingGlobals.RightDelimiter),
                ScriptingGlobals.RightDelimiter);
            // schema
            selectQuery.AppendFormat(".{0}{1}{2}",
                ScriptingGlobals.LeftDelimiter,
                ScriptingUtils.QuoteObjectName(urn.GetAttribute("Schema"), ScriptingGlobals.RightDelimiter),
                ScriptingGlobals.RightDelimiter);
            // object
            selectQuery.AppendFormat(".{0}{1}{2}",
                ScriptingGlobals.LeftDelimiter,
                ScriptingUtils.QuoteObjectName(urn.GetAttribute("Name"), ScriptingGlobals.RightDelimiter),
                ScriptingGlobals.RightDelimiter);

            //Adding no lock in the end.
            selectQuery.AppendFormat(" WITH(NOLOCK)");

            script = selectQuery.ToString();
            return script;
        }

        internal DataTable GetColumnNames(Server server, Urn urn, bool isDw)
        {
            List<string> filterExpressions = new List<string>();
            if (server.Version.Major >= 10)
            {
                // We don't have to include sparce columns as all the sparce columns data.
                // Can be obtain from column set columns.
                filterExpressions.Add("@IsSparse=0");
            }

            // Check if we're called for EDIT for SQL2016+/Sterling+.
            // We need to omit temporal columns if such are present on this table.
            if (server.Version.Major >= 13 || (DatabaseEngineType.SqlAzureDatabase == server.DatabaseEngineType && server.Version.Major >= 12))
            {
                // We're called in order to generate a list of columns for EDIT TOP N rows.
                // Don't return auto-generated, auto-populated, read-only temporal columns.
                filterExpressions.Add("@GeneratedAlwaysType=0");
            }

            if (server.Version.Major >= 16 || (DatabaseEngineType.SqlAzureDatabase == server.DatabaseEngineType && server.Version.Major >= 12))
            {
                filterExpressions.Add("@IsDroppedLedgerColumn=0");
            }

            // Check if we're called for SQL2017/Sterling+.
            // We need to omit graph internal columns if such are present on this table.
            if (server.Version.Major >= 14 || (DatabaseEngineType.SqlAzureDatabase == server.DatabaseEngineType && !isDw))
            {
                // from Smo.GraphType:
                // 0 = None
                // 1 = GraphId
                // 2 = GraphIdComputed
                // 3 = GraphFromId
                // 4 = GraphFromObjId
                // 5 = GraphFromIdComputed
                // 6 = GraphToId
                // 7 = GraphToObjId
                // 8 = GraphToIdComputed
                //
                // We only want to show types 0, 2, 5, and 8:
                filterExpressions.Add("(@GraphType=0 or @GraphType=2 or @GraphType=5 or @GraphType=8)");
            }
            
            Request request = new Request();
            // If we have any filters on the columns, add them.
            if (filterExpressions.Count > 0)
            {
                request.Urn = String.Format("{0}/Column[{1}]", urn.ToString(), string.Join(" and ", filterExpressions.ToArray()));
            }
            else
            {
                request.Urn = String.Format("{0}/Column", urn.ToString());
            }

            request.Fields = new String[] { "Name" };

            // get the columns in the order they were created
            OrderBy order = new OrderBy();
            order.Dir = OrderBy.Direction.Asc;
            order.Field = "ID";
            request.OrderByList = new OrderBy[] { order };

            Enumerator en = new Enumerator();

            // perform the query.
            DataTable dt = null;
            EnumResult result = en.Process(server.ConnectionContext, request);

            if (result.Type == ResultType.DataTable)
            {
                dt = result;
            }
            else
            {
                dt = ((DataSet)result).Tables[0];
            }
            return dt;    
        }

        internal string SelectFromTableOrView(Server server, Urn urn, bool isDw)
        {
            DataTable dt = GetColumnNames(server, urn, isDw);
            StringBuilder selectQuery = new StringBuilder();

            // build the first line
            if (dt != null && dt.Rows.Count > 0)
            {

                selectQuery.Append("SELECT TOP (1000) ");

                // first column
                selectQuery.AppendFormat("{0}{1}{2}\r\n",
                                         ScriptingGlobals.LeftDelimiter,
                                         ScriptingUtils.QuoteObjectName(dt.Rows[0][0] as string, ScriptingGlobals.RightDelimiter),
                                         ScriptingGlobals.RightDelimiter);
                // add all other columns on separate lines. Make the names align.
                for (int i = 1; i < dt.Rows.Count; i++)
                {
                    selectQuery.AppendFormat("      ,{0}{1}{2}\r\n",
                                             ScriptingGlobals.LeftDelimiter,
                                             ScriptingUtils.QuoteObjectName(dt.Rows[i][0] as string, ScriptingGlobals.RightDelimiter),
                                             ScriptingGlobals.RightDelimiter);
                }
            }
            else 
            {
                selectQuery.Append("SELECT TOP (1000) * ");
            }

            // from clause
            selectQuery.Append("  FROM ");

            if(server.ServerType != DatabaseEngineType.SqlAzureDatabase)
            {   
                // Azure doesn't allow qualifying object names with the DB, so only add it on if we're not in Azure database URN
                Urn dbUrn = urn.Parent;
                selectQuery.AppendFormat("{0}{1}{2}.",
                                     ScriptingGlobals.LeftDelimiter,
                                     ScriptingUtils.QuoteObjectName(dbUrn.GetAttribute("Name"), ScriptingGlobals.RightDelimiter),
                                     ScriptingGlobals.RightDelimiter);
            }

            // schema
            selectQuery.AppendFormat("{0}{1}{2}.",
                                     ScriptingGlobals.LeftDelimiter,
                                     ScriptingUtils.QuoteObjectName(urn.GetAttribute("Schema"), ScriptingGlobals.RightDelimiter),
                                     ScriptingGlobals.RightDelimiter);
            // object
            selectQuery.AppendFormat("{0}{1}{2}",
                                     ScriptingGlobals.LeftDelimiter,
                                     ScriptingUtils.QuoteObjectName(urn.GetAttribute("Name"), ScriptingGlobals.RightDelimiter),
                                     ScriptingGlobals.RightDelimiter);

            // In Hekaton M5, if it's a memory optimized table, we need to provide SNAPSHOT hint for SELECT.
            if (urn.Type.Equals("Table") && ScriptingUtils.IsXTPSupportedOnServer(server))
            {
                try
                {
                    Table table = (Table)server.GetSmoObject(urn);
                    table.Refresh();
                    if (table.IsMemoryOptimized)
                    {
                        selectQuery.Append(" WITH (SNAPSHOT)");
                    }
                }
                catch (Exception ex)
                {
                    // log any exceptions determining if InMemory, but don't treat as fatal exception
                    Logger.Write(TraceEventType.Error, "Could not determine if is InMemory table " + ex.ToString());
                }
            }

            return selectQuery.ToString();
        }

        #endregion
    }
}