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

namespace Marr.Data.QGen
{
    /// <summary>
    /// Decorates the SelectQuery by wrapping it in a paging query.
    /// </summary>
    public class PagingQueryDecorator : IQuery
    {
        private SelectQuery _innerQuery;
        private int _firstRow;
        private int _lastRow;

        public PagingQueryDecorator(SelectQuery innerQuery, int skip, int take)
        {
            if (string.IsNullOrEmpty(innerQuery.OrderBy.ToString()))
            {
                throw new DataMappingException("A paged query must specify an order by clause.");
            }

            _innerQuery = innerQuery;
            _firstRow = skip + 1;
            _lastRow = skip + take;
        }

        public string Generate()
        {
            // Decide which type of paging query to create
            
            if (_innerQuery.IsView || _innerQuery.IsJoin)
            {
                return ComplexPaging();
            }
            return SimplePaging();
        }

        /// <summary>
        /// Generates a query that pages a simple inner query.
        /// </summary>
        /// <returns></returns>
        private string SimplePaging()
        {
            // Create paged query
            StringBuilder sql = new StringBuilder();

            sql.AppendLine("WITH RowNumCTE AS");
            sql.AppendLine("(");
            _innerQuery.BuildSelectClause(sql);
            BuildRowNumberColumn(sql);
            _innerQuery.BuildFromClause(sql);
            _innerQuery.BuildJoinClauses(sql);
            _innerQuery.BuildWhereClause(sql);
            sql.AppendLine(")");
            BuildSimpleOuterSelect(sql);

            return sql.ToString();
        }
        
        /// <summary>
        /// Generates a query that pages a view or joined inner query.
        /// </summary>
        /// <returns></returns>
        private string ComplexPaging()
        {
            // Create paged query
            StringBuilder sql = new StringBuilder();

            sql.AppendLine("WITH GroupCTE AS (");
            BuildSelectClause(sql);
            BuildGroupColumn(sql);
            _innerQuery.BuildFromClause(sql);
            _innerQuery.BuildJoinClauses(sql);
            _innerQuery.BuildWhereClause(sql);
            sql.AppendLine("),");
            sql.AppendLine("RowNumCTE AS (");
            sql.AppendLine("SELECT *");
            BuildRowNumberColumn(sql);
            sql.AppendLine("FROM GroupCTE");
            sql.AppendLine("WHERE GroupRow = 1");
            sql.AppendLine(")");
            _innerQuery.BuildSelectClause(sql);
            _innerQuery.BuildFromClause(sql);
            _innerQuery.BuildJoinClauses(sql);
            BuildJoinBackToCTE(sql);
            sql.AppendFormat("WHERE RowNumber BETWEEN {0} AND {1}", _firstRow, _lastRow);

            return sql.ToString();
        }

        private void BuildJoinBackToCTE(StringBuilder sql)
        {
            Table baseTable = GetBaseTable();
            sql.AppendLine("INNER JOIN RowNumCTE cte");
            int pksAdded = 0;
            foreach (var pk in baseTable.Columns.PrimaryKeys)
            {
                if (pksAdded > 0)
                    sql.Append(" AND ");

                string cteQueryPkName = _innerQuery.NameOrAltName(pk.ColumnInfo);
                string outerQueryPkName = _innerQuery.IsJoin ? pk.ColumnInfo.Name : _innerQuery.NameOrAltName(pk.ColumnInfo);
                sql.AppendFormat("ON cte.{0} = {1} ", cteQueryPkName, _innerQuery.Dialect.CreateToken(string.Concat("t0", ".", outerQueryPkName)));
                pksAdded++;
            }
            sql.AppendLine();
        }

        private void BuildSimpleOuterSelect(StringBuilder sql)
        {
            sql.Append("SELECT ");
            int startIndex = sql.Length;

            // COLUMNS
            foreach (Table join in _innerQuery.Tables)
            {
                for (int i = 0; i < join.Columns.Count; i++)
                {
                    var c = join.Columns[i];

                    if (sql.Length > startIndex)
                        sql.Append(",");

                    string token = _innerQuery.NameOrAltName(c.ColumnInfo);
                    sql.Append(_innerQuery.Dialect.CreateToken(token));
                }
            }

            sql.AppendLine("FROM RowNumCTE");
            sql.AppendFormat("WHERE RowNumber BETWEEN {0} AND {1}", _firstRow, _lastRow).AppendLine();
            sql.AppendLine("ORDER BY RowNumber ASC;");
        }

        private void BuildGroupColumn(StringBuilder sql)
        {
            bool isView = _innerQuery.IsView;
            sql.AppendFormat(", ROW_NUMBER() OVER (PARTITION BY {0} {1}) As GroupRow ", BuildBaseTablePKColumns(isView), _innerQuery.OrderBy.BuildQuery(isView));
        }

        private string BuildBaseTablePKColumns(bool useAltName = true)
        {
            Table baseTable = GetBaseTable();

            StringBuilder sb = new StringBuilder();
            foreach (var col in baseTable.Columns.PrimaryKeys)
            {
                if (sb.Length > 0)
                    sb.AppendLine(", ");

                string columnName = useAltName ?
                    _innerQuery.NameOrAltName(col.ColumnInfo) :
                    col.ColumnInfo.Name;

                sb.AppendFormat(_innerQuery.Dialect.CreateToken(string.Concat(baseTable.Alias, ".", columnName)));
            }

            return sb.ToString();
        }

        private void BuildRowNumberColumn(StringBuilder sql)
        {
            string orderBy = _innerQuery.OrderBy.ToString();
            // Remove table prefixes from order columns
            foreach (Table t in _innerQuery.Tables)
            {
                orderBy = orderBy.Replace(string.Format("[{0}].", t.Alias), "");
            }
            
            sql.AppendFormat(", ROW_NUMBER() OVER ({0}) As RowNumber ", orderBy);
        }

        private Table GetBaseTable()
        {
            Table baseTable = null;
            if (_innerQuery.Tables[0] is View)
            {
                baseTable = (_innerQuery.Tables[0] as View).Tables[0];
            }
            else
            {
                baseTable = _innerQuery.Tables[0];
            }
            return baseTable;
        }

        public void BuildSelectClause(StringBuilder sql)
        {
            List<string> appended = new List<string>();

            sql.Append("SELECT ");

            int startIndex = sql.Length;

            // COLUMNS
            foreach (Table join in _innerQuery.Tables)
            {
                for (int i = 0; i < join.Columns.Count; i++)
                {
                    var c = join.Columns[i];

                    if (sql.Length > startIndex && sql[sql.Length - 1] != ',')
                        sql.Append(",");

                    if (join is View)
                    {
                        string token = _innerQuery.Dialect.CreateToken(string.Concat(join.Alias, ".", _innerQuery.NameOrAltName(c.ColumnInfo)));
                        if (appended.Contains(token))
                            continue;

                        sql.Append(token);
                        appended.Add(token);
                    }
                    else
                    {
                        string token = string.Concat(join.Alias, ".", c.ColumnInfo.Name);
                        if (appended.Contains(token))
                            continue;

                        sql.Append(_innerQuery.Dialect.CreateToken(token));

                        if (_innerQuery.UseAltName && c.ColumnInfo.AltName != null && c.ColumnInfo.AltName != c.ColumnInfo.Name)
                        {
                            string altName = c.ColumnInfo.AltName;
                            sql.AppendFormat(" AS {0}", altName);
                        }
                    }
                }
            }
        }

    }
}
