//
// SqlDatabaseMapper.cs
//
// Author:
//       Carsten Sonne Larsen <cs@innolan.dk>
//
// Copyright (c) 2013-2016 Carsten Sonne Larsen
//
// 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.Data.Common;
using System.Linq;
using System.Threading;
using Ntp.Analyzer.Localize;
using Ntp.Log;
using Ntp.Analyzer.Objects;
using Ntp.Data;
using Ntp.Data.Provider;

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

        private static AutoResetEvent autoEvent = new AutoResetEvent(true);
        private readonly Dictionary<int, T> items = new Dictionary<int, T>();
        protected Object MapperLocker = new Object();
        protected DbDataReader Reader;
        private DbCommand command;
        private DbConnection connection;
        private bool tableFetched;

        protected IEnumerable<T> Content
        {
            get { return items.Values; }
        }

        protected DbCommand Command
        {
            get { return command; }
        }

        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(LogMessage.DatabaseCacheError);

                FetchTable();

                T item;

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

                return item ?? (FetchExternal(id));
            }
        }

        public void CheckTable()
        {
            string sql = SqlDatabaseFactory.Instance.PrepareCheckTableSql(TableName);
            bool exists;

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

            if (!exists)
            {
                Log.WriteLine(String.Format(
                    LogMessage.DatabaseCreateTable, TableName),
                    Severity.Notice
                    );

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

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

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

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

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

        protected void FetchTable()
        {
            lock (MapperLocker)
            {
                if (!tableFetched)
                {
                    ReadContent();
                    tableFetched = true;
                }
            }
        }

        protected abstract void ReadContent();

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

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

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

        protected void Open()
        {
            autoEvent.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 void Close()
        {
            if (connection != null)
                connection.Close();

            connection = null;

            autoEvent.Set();
        }

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

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

        protected abstract void Insert(T item);

        protected abstract void Update(T item);
    }
}