Code/Expression.cs
//Copyright(c) 2019 Shaun Lawrence

//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 NON INFRINGEMENT. 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 Expressive.Exceptions;
using Expressive.Expressions;
using Expressive.Functions;
using Expressive.Operators;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Expressive
{
    /// <summary>
    /// Class definition for an Expression that can be evaluated.
    /// </summary>
    public sealed class Expression : IExpression
    {
        #region Fields

        private IExpression compiledExpression;
        private readonly Context context;
        private readonly string originalExpression;
        private readonly ExpressionParser parser;
        private string[] referencedVariables;

        #endregion

        #region Properties

        /// <summary>
        /// Gets a list of the Variable names that are contained within this Expression.
        /// </summary>
        public IReadOnlyCollection<string> ReferencedVariables
        {
            get
            {
                this.CompileExpression();

                return this.referencedVariables;
            }
        }

        /// <summary>
        /// Gets the currently registered functions described by <see cref="IFunctionMetadata"/>.
        /// </summary>
        public IEnumerable<IFunctionMetadata> RegisteredFunctions => this.context.RegisteredFunctions;

        /// <summary>
        /// Gets the currently registered operators described by <see cref="IOperatorMetadata"/>.
        /// </summary>
        public IEnumerable<IOperatorMetadata> RegisteredOperators => this.context.RegisteredOperators;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="Expression"/> class with the specified <paramref name="options"/>.
        /// </summary>
        /// <param name="expression">The expression to be evaluated.</param>
        /// <param name="options">The <see cref="ExpressiveOptions"/> to use when evaluating.</param>
        public Expression(string expression, ExpressiveOptions options = ExpressiveOptions.None) : this(expression, new Context(options))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Expression"/> class with the specified <paramref name="context"/>.
        /// </summary>
        /// <param name="expression">The expression to be evaluated.</param>
        /// <param name="context">The <see cref="Context"/> to use when evaluating.</param>
        public Expression(string expression, Context context)
        {
            this.originalExpression = expression;
            this.context = context ?? throw new ArgumentNullException(nameof(context));
            this.parser = new ExpressionParser(this.context);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Evaluates the expression using the supplied <paramref name="variables"/> and returns the result.
        /// </summary>
        /// <exception cref="Exceptions.ExpressiveException">Thrown when there is a break in the evaluation process, check the InnerException for further information.</exception>
        /// <param name="variables">The variables to be used in the evaluation.</param>
        /// <returns>The result of the evaluation.</returns>
        public object Evaluate(IDictionary<string, object> variables = null)
        {
            try
            {
                this.CompileExpression();

                return this.compiledExpression?.Evaluate(ApplyStringComparerSettings(variables, this.context.ParsingStringComparer));
            }
            catch (Exception ex)
            {
                throw new ExpressiveException(ex);
            }
        }

        /// <summary>
        /// Evaluates the expression using the supplied <paramref name="variables"/> and returns the result.
        /// </summary>
        /// <exception cref="Exceptions.ExpressiveException">Thrown when there is a break in the evaluation process, check the InnerException for further information.</exception>
        /// <param name="variables">The variables to be used in the evaluation.</param>
        /// <returns>The result of the evaluation.</returns>
        public T Evaluate<T>(IDictionary<string, object> variables = null)
        {
            try
            {
                return (T)this.Evaluate(variables);
            }
            catch (ExpressiveException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new ExpressiveException(ex);
            }
        }

        /// <summary>
        /// Evaluates the expression using the supplied <paramref name="variableProvider"/> and returns the result.
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown when <paramref name="variableProvider"/> is null.</exception>
        /// <exception cref="Exceptions.ExpressiveException">Thrown when there is a break in the evaluation process, check the InnerException for further information.</exception>
        /// <param name="variableProvider">The <see cref="IVariableProvider"/> implementation to provide variable values during evaluation.</param>
        /// <returns>The result of the evaluation.</returns>
        public object Evaluate(IVariableProvider variableProvider)
        {
            if (variableProvider is null)
            {
                throw new ArgumentNullException(nameof(variableProvider));
            }

            return this.Evaluate(new VariableProviderDictionary(variableProvider));
        }

        /// <summary>
        /// Evaluates the expression using the supplied <paramref name="variableProvider"/> and returns the result.
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown when <paramref name="variableProvider"/> is null.</exception>
        /// <exception cref="Exceptions.ExpressiveException">Thrown when there is a break in the evaluation process, check the InnerException for further information.</exception>
        /// <param name="variableProvider">The <see cref="IVariableProvider"/> implementation to provide variable values during evaluation.</param>
        /// <returns>The result of the evaluation.</returns>
        public T Evaluate<T>(IVariableProvider variableProvider)
        {
            if (variableProvider is null)
            {
                throw new ArgumentNullException(nameof(variableProvider));
            }

            try
            {
                return (T)this.Evaluate(variableProvider);
            }
            catch (ExpressiveException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new ExpressiveException(ex);
            }
        }

        /*/// <summary>
        /// Evaluates the expression using the supplied variables asynchronously and returns the result via the callback.
        /// </summary>
        /// <exception cref="System.ArgumentNullException">Thrown if the callback is not supplied.</exception>
        /// <param name="callback">Provides the result once the evaluation has completed.</param>
        /// <param name="variables">The variables to be used in the evaluation.</param>
        public void EvaluateAsync(Action<string, object> callback, IDictionary<string, object> variables = null)
        {
            this.EvaluateAsync<object>(callback, variables);
        }

        /// <summary>
        /// Evaluates the expression using the supplied variables asynchronously and returns the result via the callback.
        /// </summary>
        /// <exception cref="System.ArgumentNullException">Thrown if the callback is not supplied.</exception>
        /// <param name="callback">Provides the result once the evaluation has completed.</param>
        /// <param name="variables">The variables to be used in the evaluation.</param>
        public void EvaluateAsync<T>(Action<string, T> callback, IDictionary<string, object> variables = null)
        {
            if (callback is null)
            {
                throw new ArgumentNullException(nameof(callback));
            }

            Task.Run(() =>
            {
                var result = default(T);
                string message = null;

                try
                {
                    result = this.Evaluate<T>(variables);
                }
                catch (ExpressiveException ex)
                {
                    message = ex.Message;
                }

                callback.Invoke(message, result);
            });
        }*/

        /// <summary>
        /// Registers a custom function for use in evaluating an expression.
        /// </summary>
        /// <param name="functionName">The name of the function (NOTE this is also the tag that will be used to extract the function from an expression).</param>
        /// <param name="function">The method of evaluating the function.</param>
        /// <exception cref="Exceptions.FunctionNameAlreadyRegisteredException">Thrown when the name supplied has already been registered.</exception>
        public void RegisterFunction(string functionName, Func<IExpression[], IDictionary<string, object>, object> function) => 
            this.context.RegisterFunction(functionName, function);

        /// <summary>
        /// Registers a custom function inheriting from <see cref="IFunction"/> for use in evaluating an expression.
        /// </summary>
        /// <param name="function">The <see cref="IFunction"/> implementation.</param>
        /// <exception cref="Exceptions.FunctionNameAlreadyRegisteredException">Thrown when the name supplied has already been registered.</exception>
        public void RegisterFunction(IFunction function) => 
            this.context.RegisterFunction(function);

        /// <summary>
        /// Registers the supplied <paramref name="op"/> for use within compiling and evaluating an <see cref="Expression"/>.
        /// </summary>
        /// <param name="op">The <see cref="IOperator"/> implementation to register.</param>
        /// <param name="force">Whether to forcefully override any existing <see cref="IOperator"/>.</param>
        /// <remarks>
        /// Please if you are calling this with your own <see cref="IOperator"/> implementations do seriously consider raising an issue to add it in to the general framework:
        /// https://github.com/bijington/expressive
        /// </remarks>
        public void RegisterOperator(IOperator op, bool force = false) => 
            this.context.RegisterOperator(op, force);

        /// <summary>
        /// Removes the function from the available set of functions when evaluating. 
        /// </summary>
        /// <param name="functionName">The name of the function to remove.</param>
        public void UnregisterFunction(string functionName) => 
            this.context.UnregisterFunction(functionName);

        /// <summary>
        /// Removes the operator from the available set of operators when evaluating. 
        /// </summary>
        /// <param name="tag">The tag of the operator to remove.</param>
        public void UnregisterOperator(string tag) =>
            this.context.UnregisterOperator(tag);

        #endregion

        #region Private Methods

        private void CompileExpression()
        {
            // Cache the expression to save us having to recompile.
            if (!(this.compiledExpression is null) && !this.context.Options.HasFlag(ExpressiveOptions.NoCache))
            {
                return;
            }

            var variables = new List<string>();

            this.compiledExpression = this.parser.CompileExpression(this.originalExpression, variables);

            this.referencedVariables = variables.ToArray();
        }

        private static IDictionary<string, object> ApplyStringComparerSettings(IDictionary<string, object> variables, IEqualityComparer<string> desiredStringComparer)
        {
            switch (variables)
            {
                case null:
                    return null;
                case Dictionary<string, object> dictionary when dictionary.Comparer.Equals(desiredStringComparer):
                    return dictionary;
                case VariableProviderDictionary _:
                    return variables;
                default:
                    return new Dictionary<string, object>(variables, desiredStringComparer);
            }
        }

        #endregion
    }
}