// 
// Copyright (c) 2013-2016 Carsten Sonne Larsen <cs@innolan.dk>
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Ntp.Analyzer.Data.Log;
using Ntp.Analyzer.Objects;
using Ntp.Common.Log;
using Ntp.Data;
using Ntp.Data.Log;
using Ntp.Data.Provider;

namespace Ntp.Analyzer.Data.Sql
{
    /// <summary>
    /// Base class for OR/M mappers. Can be used for mapping objects stored in SQL databases.
    /// </summary>
    public abstract class SqlDatabaseMapper<T> : DataMapper<T>, ITableInitializer
        where T : PersistentObject
    {
        protected SqlDatabaseMapper(LogBase log)
            : base(log)
        {
        }

        // ReSharper disable once StaticMemberInGenericType
        private static readonly AutoResetEvent InsertEvent = new AutoResetEvent(true);
        private readonly Dictionary<int, T> items = new Dictionary<int, T>();
        protected readonly object MapperLocker = new object();
        private IDbConnection connection;
        protected IDataReader Reader;
        private bool tableFetched;

        protected IEnumerable<T> Content => items.Values;

        protected IDbCommand Command { get; private set; }

        protected abstract string TableName { get; }

        protected abstract string CreateSql { get; }

        protected abstract bool UseCache { get; }

        public T this[int id]
        {
            get
            {
                if (!UseCache)
                    throw new NotSupportedException(LogMessages.DatabaseCacheError);

                FetchTable();

                T item;

                lock (items)
                {
                    item = items.ContainsKey(id) ? items[id] : null;
                }

                return item ?? FetchExternal(id);
            }
        }

        [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
        public void CheckTable()
        {
            string sql = SqlDatabaseFactory.Instance.PrepareCheckTableSql(TableName);
            bool exists;

            try
            {
                Open();
                Command.CommandText = sql;
                Log.SqlExecute(Command.CommandText);
                Reader = Command.ExecuteReader();
                exists = Reader.Read();
            }
            finally
            {
                Close();
            }

            if (exists)
            {
                Log.TableExists(TableName);
                return;
            }

            Log.CreateTable(TableName);

            try
            {
                Open();
                Command.CommandText = SqlDatabaseFactory.Instance.PrepareCreateTableSql(CreateSql);
                Log.SqlExecute(Command.CommandText);
                Command.Prepare();
                Command.ExecuteNonQuery();
            }
            finally
            {
                Close();
            }
        }

        public override IEnumerator<T> GetEnumerator()
        {
            FetchTable();
            return Content.ToList().GetEnumerator();
        }

        public void Save(T item)
        {
            if (item.NewObject)
            {
                Insert(item);

                if (UseCache)
                {
                    AddItem(item);
                }
            }
            else
            {
                Update(item);
            }
        }

        protected void AddItem(T item)
        {
            lock (items)
            {
                items.Add(item.Id, item);
            }
        }

        protected void Close()
        {
            connection?.Close();
            connection = null;
            InsertEvent.Set();
        }

        protected IDbDataParameter CreateParameter(string name, object value)
        {
            return SqlDatabaseFactory.Instance.CreateParameter(name, value ?? DBNull.Value);
        }

        protected virtual T FetchExternal(int id)
        {
            return null;
        }

        protected abstract void Insert(T item);

        protected void Open()
        {
            InsertEvent.WaitOne();

            if (Reader != null && !Reader.IsClosed)
            {
                Reader.Close();
                Reader = null;
            }

            // TODO: Implement monitoring of database link
            if (connection == null)
            {
                connection = SqlDatabaseFactory.Instance.CreateConnection();
            }

            if (connection.State == ConnectionState.Closed)
            {
                connection.Open();
            }

            Command = SqlDatabaseFactory.Instance.CreateCommand();
            Command.Connection = connection;
        }

        protected string PrepareInsertSql(string sql)
        {
            return SqlDatabaseFactory.Instance.PrepareInsertSql(sql);
        }

        protected virtual string PrepareSql(string sql)
        {
            return SqlDatabaseFactory.Instance.PrepareSql(sql);
        }

        protected abstract void ReadContent();

        protected void RemoveItem(T item)
        {
            lock (items)
            {
                items.Remove(item.Id);
            }
        }

        protected abstract void Update(T item);

        private void FetchTable()
        {
            lock (MapperLocker)
            {
                if (tableFetched)
                    return;

                ReadContent();
                tableFetched = true;
            }
        }
    }
}