using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using InABox.Clients;
using InABox.Core;
using Xamarin.Forms;

namespace InABox.Mobile
{
    public abstract class Shell<TParent,TEntity> : BindableObject, INotifyPropertyChanged, IShell, IShell<TEntity>
        where TParent : ICoreRepository
        where TEntity : Entity, IPersistent, IRemotable, new()
    {
        
        #region INotifyPropertyChanged
        
        public new event PropertyChangedEventHandler PropertyChanged;
        
        protected void DoPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        
        protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
            field = value;
            DoPropertyChanged(propertyName);
            return true;
        }

        
        #endregion

        private TEntity _entity;
        
        private TEntity CheckEntity()
        {
            _entity ??= Row.ToObject<TEntity>();
            return _entity;
        }

        public TEntity Entity => CheckEntity();
        
        protected virtual void RowChanged()
        {
        }

        public bool IsChanged() => _entity?.IsChanged() ?? false;

        public virtual void Save(string auditmessage)
        {
            if (_entity != null)
                new Client<TEntity>().Save(_entity, auditmessage);
        }

        public void Cancel()
        {
            _entity?.CancelChanges();
            _entity = null;
        }
        
        private CoreRow _row;
        public CoreRow Row
        {
            get => _row;
            set
            {
                _row = value;
                RowChanged();
            }
        }
        
        public TParent Parent { get; set; }

        ICoreRepository IShell.Parent => this.Parent;

        public Guid ID => Get<Guid>();
        
        #region Row Get/Set Caching
        
        // We do this for three reasons:
        // 1. Rather than define properties in once class and columns in another,
        // we can define and link properties and columns in the one class,
        // using a _static_ constructor, which reduces complexity
        // 2. By caching based on the property name, we eliminate the need to convert
        // expressions to strings (expensive), while still retaining type-safety
        // 3. Using the Get/Set helper functions reduces code complexity when defining 
        // shell properties, and distinguishes between data and calculated properties

        private static ShellColumns<TParent, TEntity> _columns;
        
        public ShellColumns<TParent, TEntity> Columns
        {
            get
            {
                if (_columns == null)
                {
                    _columns = new ShellColumns<TParent, TEntity>();
                    _columns.Map(nameof(ID), x => x.ID);
                    ConfigureColumns(_columns);
                }
                return _columns;
            }
        }

        protected abstract void ConfigureColumns(ShellColumns<TParent, TEntity> columns);

        protected virtual T Get<T>([CallerMemberName] string property = null)
        {
            if (_entity != null)
            {
                return (T)CoreUtils.GetPropertyValue(
                    _entity,
                    CoreUtils.GetFullPropertyName(Columns[property], ".")
                );
            }

            if (_row != null)
            {
                var col = _columns?.IndexOf(property);
                if (col != null)
                    return Row.Get<T>(col.Value); //() Row.Values[];
            }

            return CoreUtils.GetDefault<T>();

            //return value != null ? (T)CoreUtils.ChangeType(value, typeof(T)) : CoreUtils.GetDefault<T>();        
        }
        
        protected virtual void Set<T>(T value, bool notify = true, [CallerMemberName] string property = null)
        {
            CheckEntity();
            CoreUtils.SetPropertyValue(
                _entity,
                CoreUtils.GetFullPropertyName(Columns[property], "."),
                value
            );
            //Row.Values[Columns.IndexOf(property)] = value;
            if (notify)
                DoPropertyChanged(property);
        }

        #endregion
        
    }

}