/*  Copyright (C) 2008 - 2011 Jordan Marr

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>. */

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.Common;
using System.Reflection;
using System.Collections;
using Marr.Data.Mapping;
using Marr.Data.Parameters;
using Marr.Data.QGen;
using System.Linq.Expressions;
using System.Diagnostics;

namespace Marr.Data
{
    /// <summary>
    /// This class is the main access point for making database related calls.
    /// </summary>
    public class DataMapper : IDataMapper
    {

        #region - Contructor, Members -

        private DbCommand _command;

        /// <summary>
        /// Initializes a DataMapper for the given provider type and connection string.
        /// </summary>
        /// <param name="providerName">Ex: </param>
        /// <param name="connectionString">The database connection string.</param>
        public DataMapper(string providerName, string connectionString)
            : this(DbProviderFactories.GetFactory(providerName), connectionString)
        { }

        /// <summary>
        /// A database provider agnostic initialization.
        /// </summary>
        /// <param name="connectionString">The database connection string.</param>
        public DataMapper(DbProviderFactory dbProviderFactory, string connectionString)
        {
            SqlMode = SqlModes.StoredProcedure;
            if (dbProviderFactory == null)
                throw new ArgumentNullException("dbProviderFactory");

            if (string.IsNullOrEmpty(connectionString))
                throw new ArgumentNullException("connectionString");

            ProviderFactory = dbProviderFactory;

            ConnectionString = connectionString;
        }

        public string ConnectionString { get; private set; }

        public DbProviderFactory ProviderFactory { get; private set; }

        /// <summary>
        /// Creates a new command utilizing the connection string.
        /// </summary>
        private DbCommand CreateNewCommand()
        {
            DbConnection conn = ProviderFactory.CreateConnection();
            conn.ConnectionString = ConnectionString;
            DbCommand cmd = conn.CreateCommand();
            SetSqlMode(cmd);
            return cmd;
        }

        /// <summary>
        /// Creates a new command utilizing the connection string with a given SQL command.
        /// </summary>
        private DbCommand CreateNewCommand(string sql)
        {
            DbCommand cmd = CreateNewCommand();
            cmd.CommandText = sql;
            return cmd;
        }

        /// <summary>
        /// Gets or creates a DbCommand object.
        /// </summary>
        public DbCommand Command
        {
            get
            {
                // Lazy load
                if (_command == null)
                    _command = CreateNewCommand();
                else
                    SetSqlMode(_command); // Set SqlMode every time.

                return _command;
            }
        }

        #endregion

        #region - Parameters -

        public DbParameterCollection Parameters
        {
            get
            {
                return Command.Parameters;
            }
        }

        public ParameterChainMethods AddParameter(string name, object value)
        {
            return new ParameterChainMethods(Command, name, value);
        }

        public IDbDataParameter AddParameter(IDbDataParameter parameter)
        {
            // Convert null values to DBNull.Value
            if (parameter.Value == null)
                parameter.Value = DBNull.Value;

            Parameters.Add(parameter);
            return parameter;
        }

        #endregion

        #region - SP / SQL Mode -

        /// <summary>
        /// Gets or sets a value that determines whether the DataMapper will 
        /// use a stored procedure or a sql text command to access 
        /// the database.  The default is stored procedure.
        /// </summary>
        public SqlModes SqlMode { get; set; }

        /// <summary>
        /// Sets the DbCommand objects CommandType to the current SqlMode.
        /// </summary>
        /// <param name="command">The DbCommand object we are modifying.</param>
        /// <returns>Returns the same DbCommand that was passed in.</returns>
        private DbCommand SetSqlMode(DbCommand command)
        {
            if (SqlMode == SqlModes.StoredProcedure)
                command.CommandType = CommandType.StoredProcedure;
            else
                command.CommandType = CommandType.Text;

            return command;
        }

        #endregion

        #region - ExecuteScalar, ExecuteNonQuery, ExecuteReader -

        /// <summary>
        /// Executes a stored procedure that returns a scalar value.
        /// </summary>
        /// <param name="sql">The SQL command to execute.</param>
        /// <returns>A scalar value</returns>
        public object ExecuteScalar(string sql)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
            Command.CommandText = sql;

            try
            {
                OpenConnection();
                return Command.ExecuteScalar();
            }
            finally
            {
                CloseConnection();
            }
        }

        /// <summary>
        /// Executes a non query that returns an integer.
        /// </summary>
        /// <param name="sql">The SQL command to execute.</param>
        /// <returns>An integer value</returns>
        public int ExecuteNonQuery(string sql)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
            Command.CommandText = sql;

            try
            {
                OpenConnection();
                return Command.ExecuteNonQuery();
            }
            finally
            {
                CloseConnection();
            }
        }

        /// <summary>
        /// Executes a DataReader that can be controlled using a Func delegate.
        /// (Note that reader.Read() will be called automatically).
        /// </summary>
        /// <typeparam name="TResult">The type that will be return in the result set.</typeparam>
        /// <param name="sql">The sql statement that will be executed.</param>
        /// <param name="func">The function that will build the the TResult set.</param>
        /// <returns>An IEnumerable of TResult.</returns>
        public IEnumerable<TResult> ExecuteReader<TResult>(string sql, Func<DbDataReader, TResult> func)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
            Command.CommandText = sql;

            try
            {
                OpenConnection();

                var list = new List<TResult>();
                DbDataReader reader = null;
                try
                {
                    reader = Command.ExecuteReader();

                    while (reader.Read())
                    {
                        list.Add(func(reader));
                    }

                    return list;
                }
                finally
                {
                    if (reader != null) reader.Close();
                }
            }
            finally
            {
                CloseConnection();
            }
        }

        /// <summary>
        /// Executes a DataReader that can be controlled using an Action delegate.
        /// </summary>
        /// <param name="sql">The sql statement that will be executed.</param>
        /// <param name="action">The delegate that will work with the result set.</param>
        public void ExecuteReader(string sql, Action<DbDataReader> action)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");

            Command.CommandText = sql;

            try
            {
                OpenConnection();

                DbDataReader reader = null;
                try
                {
                    reader = Command.ExecuteReader();

                    while (reader.Read())
                    {
                        action(reader);
                    }
                }
                finally
                {
                    if (reader != null) reader.Close();
                }
            }
            finally
            {
                CloseConnection();
            }
        }

        #endregion

        #region - DataSets -

        public DataSet GetDataSet(string sql)
        {
            return GetDataSet(sql, new DataSet(), null);
        }

        public DataSet GetDataSet(string sql, DataSet ds, string tableName)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");

            try
            {
                using (DbDataAdapter adapter = ProviderFactory.CreateDataAdapter())
                {
                    Command.CommandText = sql;
                    adapter.SelectCommand = Command;

                    if (ds == null)
                        ds = new DataSet();

                    OpenConnection();

                    if (string.IsNullOrEmpty(tableName))
                        adapter.Fill(ds);
                    else
                        adapter.Fill(ds, tableName);

                    return ds;
                }
            }
            finally
            {
                CloseConnection();  // Clears parameters
            }
        }

        public DataTable GetDataTable(string sql)
        {
            return GetDataTable(sql, null, null);
        }

        public DataTable GetDataTable(string sql, DataTable dt, string tableName)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");

            try
            {
                using (DbDataAdapter adapter = ProviderFactory.CreateDataAdapter())
                {
                    Command.CommandText = sql;
                    adapter.SelectCommand = Command;

                    if (dt == null)
                        dt = new DataTable();

                    adapter.Fill(dt);

                    if (!string.IsNullOrEmpty(tableName))
                        dt.TableName = tableName;

                    return dt;
                }
            }
            finally
            {
                CloseConnection();  // Clears parameters
            }
        }

        public int UpdateDataSet(DataSet ds, string sql)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");

            if (ds == null)
                throw new ArgumentNullException("ds", "DataSet cannot be null.");

            DbDataAdapter adapter = null;

            try
            {
                adapter = ProviderFactory.CreateDataAdapter();

                adapter.UpdateCommand = Command;
                adapter.UpdateCommand.CommandText = sql;

                return adapter.Update(ds);
            }
            finally
            {
                if (adapter.UpdateCommand != null)
                    adapter.UpdateCommand.Dispose();

                adapter.Dispose();
            }
        }

        public int InsertDataTable(DataTable table, string insertSP)
        {
            return InsertDataTable(table, insertSP, UpdateRowSource.None);
        }

        public int InsertDataTable(DataTable dt, string sql, UpdateRowSource updateRowSource)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");

            if (dt == null)
                throw new ArgumentNullException("dt", "DataTable cannot be null.");

            DbDataAdapter adapter = null;

            try
            {
                adapter = ProviderFactory.CreateDataAdapter();

                adapter.InsertCommand = Command;
                adapter.InsertCommand.CommandText = sql;

                adapter.InsertCommand.UpdatedRowSource = updateRowSource;

                return adapter.Update(dt);
            }
            finally
            {
                if (adapter.InsertCommand != null)
                    adapter.InsertCommand.Dispose();

                adapter.Dispose();
            }
        }

        public int DeleteDataTable(DataTable dt, string sql)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");

            if (dt == null)
                throw new ArgumentNullException("dt", "DataSet cannot be null.");

            DbDataAdapter adapter = null;

            try
            {
                adapter = ProviderFactory.CreateDataAdapter();

                adapter.DeleteCommand = Command;
                adapter.DeleteCommand.CommandText = sql;

                return adapter.Update(dt);
            }
            finally
            {
                if (adapter.DeleteCommand != null)
                    adapter.DeleteCommand.Dispose();

                adapter.Dispose();
            }
        }

        #endregion

        #region - Find -

        public T Find<T>(string sql)
        {
            return Find<T>(sql, default(T));
        }

        /// <summary>
        /// Returns an entity of type T.
        /// </summary>
        /// <typeparam name="T">The type of entity that is to be instantiated and loaded with values.</typeparam>
        /// <param name="sql">The SQL command to execute.</param>
        /// <returns>An instantiated and loaded entity of type T.</returns>
        public T Find<T>(string sql, T ent)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A stored procedure name has not been specified for 'Find'.");

            Type entityType = typeof(T);
            Command.CommandText = sql;

            MapRepository repository = MapRepository.Instance;
            ColumnMapCollection mappings = repository.GetColumns(entityType);

            bool isSimpleType = DataHelper.IsSimpleType(typeof(T));

            try
            {
                OpenConnection();
                var mappingHelper = new MappingHelper(this);

                using (DbDataReader reader = Command.ExecuteReader())
                {
                    if (reader.Read())
                    {
                        if (isSimpleType)
                        {
                            return mappingHelper.LoadSimpleValueFromFirstColumn<T>(reader);
                        }
                        else
                        {
                            if (ent == null)
                                ent = (T)mappingHelper.CreateAndLoadEntity<T>(mappings, reader, false);
                            else
                                mappingHelper.LoadExistingEntity(mappings, reader, ent, false);
                        }
                    }
                }
            }
            finally
            {
                CloseConnection();
            }

            return ent;
        }

        #endregion

        #region - Query -

        /// <summary>
        /// Creates a QueryBuilder that allows you to build a query.
        /// </summary>
        /// <typeparam name="T">The type of object that will be queried.</typeparam>
        /// <returns>Returns a QueryBuilder of T.</returns>
        public QueryBuilder<T> Query<T>()
        {
            var dialect = QueryFactory.CreateDialect(this);
            return new QueryBuilder<T>(this, dialect);
        }

        /// <summary>
        /// Returns the results of a query.
        /// Uses a List of type T to return the data.
        /// </summary>
        /// <returns>Returns a list of the specified type.</returns>
        public List<T> Query<T>(string sql)
        {
            return (List<T>)Query<T>(sql, new List<T>());
        }

        /// <summary>
        /// Returns the results of a SP query.
        /// </summary>
        /// <returns>Returns a list of the specified type.</returns>
        public ICollection<T> Query<T>(string sql, ICollection<T> entityList)
        {
            return Query<T>(sql, entityList, false);
        }

        internal ICollection<T> Query<T>(string sql, ICollection<T> entityList, bool useAltName)
        {
            if (entityList == null)
                throw new ArgumentNullException("entityList", "ICollection instance cannot be null.");

            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "A query or stored procedure has not been specified for 'Query'.");

            var mappingHelper = new MappingHelper(this);
            Type entityType = typeof(T);
            Command.CommandText = sql;
            ColumnMapCollection mappings = MapRepository.Instance.GetColumns(entityType);

            bool isSimpleType = DataHelper.IsSimpleType(typeof(T));

            try
            {
                OpenConnection();
                using (DbDataReader reader = Command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        if (isSimpleType)
                        {
                            entityList.Add(mappingHelper.LoadSimpleValueFromFirstColumn<T>(reader));
                        }
                        else
                        {
                            entityList.Add((T)mappingHelper.CreateAndLoadEntity<T>(mappings, reader, useAltName));
                        }
                    }
                }
            }
            finally
            {
                CloseConnection();
            }

            return entityList;
        }

        #endregion

        #region - Query to Graph -

        public List<T> QueryToGraph<T>(string sql)
        {
            return (List<T>)QueryToGraph<T>(sql, new List<T>());
        }

        public ICollection<T> QueryToGraph<T>(string sql, ICollection<T> entityList)
        {
            EntityGraph graph = new EntityGraph(typeof(T), (IList)entityList);
            return QueryToGraph<T>(sql, graph, new List<MemberInfo>());
        }

        /// <summary>
        /// Queries a view that joins multiple tables and returns an object graph.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sql"></param>
        /// <param name="entityList"></param>
        /// <param name="entityGraph">Coordinates loading all objects in the graph..</param>
        /// <returns></returns>
        internal ICollection<T> QueryToGraph<T>(string sql, EntityGraph graph, List<MemberInfo> childrenToLoad)
        {
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentNullException("sql", "sql");

            var mappingHelper = new MappingHelper(this);
            Type parentType = typeof(T);
            Command.CommandText = sql;

            try
            {
                OpenConnection();
                using (DbDataReader reader = Command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        // The entire EntityGraph is traversed for each record, 
                        // and multiple entities are created from each view record.
                        foreach (EntityGraph lvl in graph)
                        {
                            if (lvl.IsParentReference)
                            {
                                // A child specified a circular reference to its previously loaded parent
                                lvl.AddParentReference();
                            }
                            else if (childrenToLoad.Count > 0 && !lvl.IsRoot && !childrenToLoad.ContainsMember(lvl.Member))
                            {
                                // A list of relationships-to-load was specified and this relationship was not included
                                continue;
                            }
                            else if (lvl.IsNewGroup(reader))
                            {
                                // Create a new entity with the data reader
                                var newEntity = mappingHelper.CreateAndLoadEntity(lvl.EntityType, lvl.Columns, reader, true);

                                // Add entity to the appropriate place in the object graph
                                lvl.AddEntity(newEntity);
                            }
                        }
                    }
                }
            }
            finally
            {
                CloseConnection();
            }

            return (ICollection<T>)graph.RootList;
        }

        #endregion

        #region - Update -

        public UpdateQueryBuilder<T> Update<T>()
        {
            return new UpdateQueryBuilder<T>(this);
        }

        public int Update<T>(T entity, Expression<Func<T, bool>> filter)
        {
            return Update<T>()
                .Entity(entity)
                .Where(filter)
                .Execute();
        }

        public int Update<T>(string tableName, T entity, Expression<Func<T, bool>> filter)
        {
            return Update<T>()
                .TableName(tableName)
                .Entity(entity)
                .Where(filter)
                .Execute();
        }

        public int Update<T>(T entity, string sql)
        {
            return Update<T>()
                .Entity(entity)
                .QueryText(sql)
                .Execute();
        }

        #endregion

        #region - Insert -

        /// <summary>
        /// Creates an InsertQueryBuilder that allows you to build an insert statement.
        /// This method gives you the flexibility to manually configure all options of your insert statement.
        /// Note: You must manually call the Execute() chaining method to run the query.
        /// </summary>
        public InsertQueryBuilder<T> Insert<T>()
        {
            return new InsertQueryBuilder<T>(this);
        }

        /// <summary>
        /// Generates and executes an insert query for the given entity.
        /// This overload will automatically run an identity query if you have mapped an auto-incrementing column,
        /// and if an identity query has been implemented for your current database dialect.
        /// </summary>
        public object Insert<T>(T entity)
        {
            var columns = MapRepository.Instance.GetColumns(typeof(T));
            var dialect = QueryFactory.CreateDialect(this);
            var builder = Insert<T>().Entity(entity);

            // If an auto-increment column exists and this dialect has an identity query...
            if (columns.Exists(c => c.ColumnInfo.IsAutoIncrement) && dialect.HasIdentityQuery)
            {
                builder.GetIdentity();
            }

            return builder.Execute();
        }

        /// <summary>
        /// Generates and executes an insert query for the given entity.
        /// This overload will automatically run an identity query if you have mapped an auto-incrementing column,
        /// and if an identity query has been implemented for your current database dialect.
        /// </summary>
        public object Insert<T>(string tableName, T entity)
        {
            var columns = MapRepository.Instance.GetColumns(typeof(T));
            var dialect = QueryFactory.CreateDialect(this);
            var builder = Insert<T>().Entity(entity).TableName(tableName);

            // If an auto-increment column exists and this dialect has an identity query...
            if (columns.Exists(c => c.ColumnInfo.IsAutoIncrement) && dialect.HasIdentityQuery)
            {
                builder.GetIdentity();
            }

            return builder.Execute();
        }

        /// <summary>
        /// Executes an insert query for the given entity using the given sql insert statement.
        /// This overload will automatically run an identity query if you have mapped an auto-incrementing column,
        /// and if an identity query has been implemented for your current database dialect.
        /// </summary>
        public object Insert<T>(T entity, string sql)
        {
            var columns = MapRepository.Instance.GetColumns(typeof(T));
            var dialect = QueryFactory.CreateDialect(this);
            var builder = Insert<T>().Entity(entity).QueryText(sql);

            // If an auto-increment column exists and this dialect has an identity query...
            if (columns.Exists(c => c.ColumnInfo.IsAutoIncrement) && dialect.HasIdentityQuery)
            {
                builder.GetIdentity();
            }

            return builder.Execute();
        }

        #endregion

        #region - Delete -

        public int Delete<T>(Expression<Func<T, bool>> filter)
        {
            return Delete<T>(null, filter);
        }

        public int Delete<T>(string tableName, Expression<Func<T, bool>> filter)
        {
            // Remember sql mode
            var previousSqlMode = SqlMode;
            SqlMode = SqlModes.Text;

            var mappingHelper = new MappingHelper(this);
            if (tableName == null)
            {
                tableName = MapRepository.Instance.GetTableName(typeof(T));
            }
            var dialect = QueryFactory.CreateDialect(this);
            TableCollection tables = new TableCollection();
            tables.Add(new Table(typeof(T)));
            var where = new WhereBuilder<T>(Command, dialect, filter, tables, false, false);
            IQuery query = QueryFactory.CreateDeleteQuery(dialect, tables[0], where.ToString());
            Command.CommandText = query.Generate();

            int rowsAffected = 0;

            try
            {
                OpenConnection();
                rowsAffected = Command.ExecuteNonQuery();
            }
            finally
            {
                CloseConnection();
            }

            // Return to previous sql mode
            SqlMode = previousSqlMode;

            return rowsAffected;
        }

        #endregion

        #region - Events -

        public event EventHandler OpeningConnection;

        public event EventHandler ClosingConnection;

        #endregion

        #region - Connections / Transactions -

        protected virtual void OnOpeningConnection()
        {
            if (OpeningConnection != null)
                OpeningConnection(this, EventArgs.Empty);
        }

        protected virtual void OnClosingConnection()
        {
            WriteToTraceLog();

            if (ClosingConnection != null)
                ClosingConnection(this, EventArgs.Empty);
        }

        protected internal void OpenConnection()
        {
            OnOpeningConnection();

            if (Command.Connection.State != ConnectionState.Open)
                Command.Connection.Open();
        }

        protected internal void CloseConnection()
        {
            OnClosingConnection();

            Command.Parameters.Clear();
            Command.CommandText = string.Empty;

            if (Command.Transaction == null)
                Command.Connection.Close(); // Only close if no transaction is present

            UnbindEvents();
        }

        private void WriteToTraceLog()
        {
            if (MapRepository.Instance.EnableTraceLogging)
            {
                var sb = new StringBuilder();
                sb.AppendLine();
                sb.AppendLine("==== Begin Query Trace ====");
                sb.AppendLine();
                sb.AppendLine("QUERY TYPE:");
                sb.AppendLine(Command.CommandType.ToString());
                sb.AppendLine();
                sb.AppendLine("QUERY TEXT:");
                sb.AppendLine(Command.CommandText);
                sb.AppendLine();
                sb.AppendLine("PARAMETERS:");
                foreach (IDbDataParameter p in Parameters)
                {
                    object val = (p.Value != null && p.Value is string) ? string.Format("\"{0}\"", p.Value) : p.Value;
                    sb.AppendFormat("{0} = [{1}]", p.ParameterName, val ?? "NULL").AppendLine();
                }
                sb.AppendLine();
                sb.AppendLine("==== End Query Trace ====");
                sb.AppendLine();

                Trace.Write(sb.ToString());
            }
        }

        private void UnbindEvents()
        {
            OpeningConnection = null;
            ClosingConnection = null;
        }

        public void BeginTransaction(IsolationLevel isolationLevel)
        {
            OpenConnection();
            DbTransaction trans = Command.Connection.BeginTransaction(isolationLevel);
            Command.Transaction = trans;
        }

        public void RollBack()
        {
            try
            {
                if (Command.Transaction != null)
                    Command.Transaction.Rollback();
            }
            finally
            {
                Command.Connection.Close();
            }
        }

        public void Commit()
        {
            try
            {
                if (Command.Transaction != null)
                    Command.Transaction.Commit();
            }
            finally
            {
                Command.Connection.Close();
            }
        }

        #endregion

        #region - IDisposable Members -

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this); // In case a derived class implements a finalizer
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (Command.Transaction != null)
                {
                    Command.Transaction.Dispose();
                    Command.Transaction = null;
                }

                if (Command.Connection != null)
                {
                    Command.Connection.Dispose();
                    Command.Connection = null;
                }

                if (Command != null)
                {
                    Command.Dispose();
                    _command = null;
                }
            }
        }

        #endregion

    }
}
