using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Newtonsoft.Json.Linq;
using TextFieldParserStandard;

namespace InABox.Core
{
    // public abstract class DigitalFormsCount : CoreAggregate<DigitalForm, DigitalFormLayout, Guid>
    // {
    //     public override Expression<Func<DigitalFormLayout, Guid>> Aggregate => x => x.ID;
    //
    //     public override Filter<DigitalFormLayout> Filter => 
    //         new Filter<DigitalFormLayout>(x => x.Type).IsEqualTo(DFLayoutType.Mobile)
    //             .And(x=>x.Active).IsEqualTo(true);
    //
    //     public override Dictionary<Expression<Func<DigitalFormLayout, object>>, Expression<Func<DigitalForm, object>>> Links =>
    //         new Dictionary<Expression<Func<DigitalFormLayout, object>>, Expression<Func<DigitalForm, object>>>()
    //         {
    //             { DigitalFormLayout => DigitalFormLayout.Form.ID, DigitalForm => DigitalForm.ID }
    //         };
    //
    //     public override AggregateCalculation Calculation => AggregateCalculation.Count;
    // }
    //
    // public class ActiveMobileFormsCount : DigitalFormsCount
    // {
    //     public override Filter<DigitalFormLayout> Filter => 
    //         new Filter<DigitalFormLayout>(x => x.Type).IsEqualTo(DFLayoutType.Mobile)
    //             .And(x=>x.Active).IsEqualTo(true);
    // }
    //
    // public class ActiveFormsCount : DigitalFormsCount
    // {
    //     public override Filter<DigitalFormLayout> Filter => 
    //         new Filter<DigitalFormLayout>(x=>x.Active).IsEqualTo(true);
    // }
    
    [UserTracking("Digital Forms")]
    public class DigitalForm : Entity, IRemotable, IPersistent, ILicense<DigitalFormsLicense>//, IDuplicatable
    {
        /// <summary>
        ///     The following functions support PNG, BMP and JPEG
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private static readonly byte[] bmpHeader = Encoding.ASCII.GetBytes("BM");

        private static readonly byte[] pngHeader = { 137, 80, 78, 71 };
        private static readonly byte[] jpegHeader = { 255, 216, 255, 224 };
        private static readonly byte[] jpeg2Header = { 255, 216, 255, 225 };

        [UniqueCodeEditor(Visible = Visible.Default, Editable = Editable.Enabled)]
        [EditorSequence(1)]
        public string Code { get; set; }

        [TextBoxEditor]
        [EditorSequence(2)]
        public string Description { get; set; }

        [ComboLookupEditor(typeof(DigitalFormCategoryLookups))]
        [EditorSequence(3)]
        public string AppliesTo { get; set; }

        [CheckBoxEditor]
        [EditorSequence(4)]
        public bool Active { get; set; } = true;

        [CheckBoxEditor]
        [EditorSequence(5)]
        public bool Secure { get; set; } = false;

        [CheckBoxEditor]
        [EditorSequence(6)]
        public bool Final { get; set; } = false;

        [EditorSequence(7)]
        public DigitalFormGroupLink Group => InitializeField(ref _group, nameof(Group));
        private DigitalFormGroupLink _group;

        [EditorSequence(8)]
        [Comment("Expression to calculate 'Description' of form.")]
        [ExpressionEditor(null, ToolTip = "Evaluates to a string which becomes the description of the form when saved.")]
        public string DescriptionExpression { get; set; } = "";
        
        [EditorSequence(9)]
        [Comment("Expression to calculate 'Filename' of form.")]
        [ExpressionEditor(null, ToolTip = "Evaluates to a string which becomes the filename of the form when saved.")]
        public string ExportExpression { get; set; } = "";

        [NullEditor]
        public string Report { get; set; }
        
        // [NullEditor]
        // [Aggregate(typeof(ActiveFormsCount))]
        // public int ActiveForms { get; set; }
        //
        // [NullEditor]
        // [Aggregate(typeof(ActiveMobileFormsCount))]
        // public int ActiveMobileForms { get; set; }

        public IEntityDuplicator GetDuplicator()
        {
            var result = new EntityDuplicator<DigitalForm>();
            result.AddChild<DigitalForm, DigitalFormVariable, DigitalFormLink>(x => x.Form);
            result.AddChild<DigitalForm, DigitalFormLayout, DigitalFormLink>(x => x.Form);
            return result;
        }

        public override string ToString()
        {
            return string.Format("{0}: {1}", Code, Description);
        }

        private static bool IsValidImage(byte[] data)
        {
            return bmpHeader.SequenceEqual(data.Take(bmpHeader.Length))
                   || pngHeader.SequenceEqual(data.Take(pngHeader.Length))
                   || jpegHeader.SequenceEqual(data.Take(jpegHeader.Length))
                   || jpeg2Header.SequenceEqual(data.Take(jpeg2Header.Length));
        }

        private static bool IsValidImage(string base64Data)
        {
            var firstBytes = base64Data.Substring(0, Math.Min(8, base64Data.Length));
            return IsValidImage(Convert.FromBase64String(firstBytes));
        }

        
        /// <summary>
        /// Takes a serialized FormData string and BlobData string and deserializes into one dictionary.
        /// The result of this is just as if you were deserializing formData as per usual.
        /// </summary>
        /// <remarks>
        ///     Returns <see langword="null"/> if <paramref name="formData"/> is not a valid JSON object.
        /// </remarks>
        /// <param name="formData"></param>
        /// <param name="blobData"></param>
        /// <returns></returns>
        public static DFLoadStorage? DeserializeFormData(string formData, string? blobData)
        {
            var values = Serialization.Deserialize<Dictionary<string, object?>>(formData);
            if (values is null)
                return null;
            if (blobData.IsNullOrWhiteSpace())
                return new DFLoadStorage(values, null);
            var blobs = Serialization.Deserialize<Dictionary<string, object?>>(blobData);
            return new DFLoadStorage(values, blobs);
        }

        /// <summary>
        /// Like <see cref="DeserializeFormData(string, string?)"/>, but takes the FormData and BlobData from <paramref name="form"/>,
        /// rather than having to specify them manually.
        /// </summary>
        /// <remarks>
        ///     Returns <see langword="null"/> if <c>form.FormData</c> is not a valid JSON object.
        /// </remarks>
        /// <param name="form"></param>
        /// <returns></returns>
        public static DFLoadStorage? DeserializeFormData(IDigitalFormInstance form)
            => DeserializeFormData(form.FormData, form.BlobData);

        /// <summary>
        /// Takes a form instance, set of variables and some saved values, and serializes the FormData and BlobData into the respective
        /// fields of <paramref name="form"/>.
        /// </summary>
        /// <remarks>
        /// Doesn't return anything, but saves the serialized results directly into form.FormData and form.BlobData.
        /// It choose which fields should be saved into BlobData based on whether the form field has the <see cref="IDFBlobField"/> interface.
        /// <br/>
        /// <paramref name="values"/> should be the same as what you pass into <see cref="Serialization.Serialize(object?, bool)"/>.
        /// </remarks>
        /// <param name="form">The form instance.</param>
        /// <param name="variables">The variables associated with the digital form.</param>
        /// <param name="storage">The values to save.</param>
        public static void SerializeFormData(IDigitalFormInstance form, ICollection<DigitalFormVariable> variables, DFSaveStorage storage)
        {
            form.FormData = Serialization.Serialize(storage.FormData);
            form.BlobData = Serialization.Serialize(storage.BlobData);
        }


        private static string? GetVariableData(DigitalFormVariable variable, object value)
        {
            // TODO: Replace with a function on DFLayoutField or something
            var fieldType = variable.FieldType();
            if (fieldType == typeof(DFLayoutEmbeddedImage)
                || fieldType == typeof(DFLayoutSignaturePad))
            {
                if (value is byte[]) return IsValidImage((byte[])value) ? Convert.ToBase64String((byte[])value) : null;
                var str = value.ToString();
                return IsValidImage(str) ? str : null;
            }
            else if(fieldType == typeof(DFLayoutMultiImage))
            {
                if (value is JArray || value is IEnumerable<string>) return Serialization.Serialize(value);
                return null;
            }
            return variable.FormatValue(value);
        }

        /// <summary>
        ///     Generates a database FormData from a dictionary of objects. Returns null if the data is invalid (specifically if
        ///     any required fields are not present.
        /// </summary>
        /// <param name="values">.NET objects</param>
        /// <param name="variables">The variables of the form needed to be encoded</param>
        /// <returns>A string with JSON-encoded FormData, or null if validation requirements are not met.</returns>
        public static string? GenerateFormData(Dictionary<string, object> values, IEnumerable<DigitalFormVariable> variables, Entity entity)
        {
            var data = new Dictionary<string, string>();
            foreach (var variable in variables)
                if (values.TryGetValue(variable.Code, out var value))
                {
                    var properties = variable.CreateProperties();
                    if (!string.IsNullOrWhiteSpace(properties.Property))
                    {
                        if (variable.FieldType() == typeof(DFLayoutLookupField))
                        {
                            if (values.TryGetValue($"{variable.Code}$ID", out var idStr) && Guid.TryParse((string)idStr, out var id))
                                CoreUtils.SetPropertyValue(entity, properties.Property, id);
                        }
                        else
                        {
                            CoreUtils.SetPropertyValue(entity, properties.Property, value);
                        }
                    }

                    if (value != null)
                    {
                        var varData = GetVariableData(variable, value);
                        if (varData != null)
                            data[variable.Code] = varData;
                    }
                }
                else if (variable.Required)
                {
                    return null;
                }

            return Serialization.Serialize(data);
        }

        /// <summary>
        ///     Creates a dictionary of objects from a database FormData
        /// </summary>
        /// <param name="formData">A string with JSON-encoded FormData</param>
        /// <param name="variables">The variables of the form needed to be encoded</param>
        /// <returns></returns>
        public static Dictionary<string, object?> ParseFormData(string formData, IEnumerable<DigitalFormVariable> variables, Entity entity)
        {
            var data = new Dictionary<string, object?>();

            // Could be null
            var formObject = new DFLoadStorage(Serialization.Deserialize<Dictionary<string, object?>>(formData) ?? new Dictionary<string, object?>(), null);

            foreach (var variable in variables)
            {
                object? value = null;

                var code = variable.Code;

                var properties = variable.CreateProperties();
                if (!string.IsNullOrWhiteSpace(properties.Property))
                {
                    value = CoreUtils.GetPropertyValue(entity, properties.Property);
                    if (variable.FieldType() == typeof(DFLayoutLookupField))
                    {
                        code = variable.Code + "$ID";
                    }
                }
                else if (value == null)
                {
                    value = properties.Deserialize(formObject.GetEntry(code));
                }

                if (value != null)
                {
                    data[code] = value;
                }
            }

            return data;
        }
    }
}