using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Expressive;
using InABox.Clients;

namespace InABox.Core
{
    public interface IDFRenderer
    {
        /// <summary>
        /// Retrieve the value of a field, for use in expressions.
        /// </summary>
        /// <param name="field"></param>
        /// <returns></returns>
        object? GetFieldValue(string field);

        /// <summary>
        /// Retrieve a piece of additional data for a field.
        /// </summary>
        /// <param name="fieldName">The field name, which is the variable code.</param>
        /// <param name="dataField">The specific field to be retrieved from the variable.</param>
        /// <returns>A value, which is specific to the type of <paramref name="fieldName"/> and the specific <paramref name="dataField"/> being
        /// retrieved.</returns>
        object? GetFieldData(string fieldName, string dataField);

        void SetFieldValue(string field, object? value);

        /// <summary>
        /// Set the background colour for a field.
        /// </summary>
        void SetFieldColour(string field, Color? colour = null);
    }

    public class DFLayout
    {
        public DFLayout()
        {
            ColumnWidths = new List<string>();
            RowHeights = new List<string>();
            Elements = new List<DFLayoutControl>();
            HiddenElements = new List<DFLayoutControl>();

            Expressions = new Dictionary<string, CoreExpression>();
            ColourExpressions = new Dictionary<string, CoreExpression>();
            VariableReferences = new Dictionary<string, List<Tuple<ReferenceType, string>>>();
        }

        public List<string> ColumnWidths { get; }

        public List<string> RowHeights { get; }

        public List<DFLayoutControl> Elements { get; }
        public List<DFLayoutControl> HiddenElements { get; }

        private enum ReferenceType
        {
            Value,
            Colour
        }

        private Dictionary<string, CoreExpression> Expressions;
        private Dictionary<string, CoreExpression> ColourExpressions;
        private Dictionary<string, List<Tuple<ReferenceType, string>>> VariableReferences;
        public IDFRenderer? Renderer;

        public IEnumerable<DFLayoutControl> GetElements(bool includeHidden = false)
        {
            foreach (var element in Elements)
            {
                yield return element;
            }
            if (includeHidden)
            {
                foreach (var element in HiddenElements)
                {
                    yield return element;
                }
            }
        }

        public string SaveLayout()
        {
            var sb = new StringBuilder();
            foreach (var column in ColumnWidths)
                sb.AppendFormat("C {0}\n", column);
            foreach (var row in RowHeights)
                sb.AppendFormat("R {0}\n", row);
            foreach (var element in Elements)
                sb.AppendFormat("E {0} {1}\n", element.GetType().EntityName(), element.SaveToString());
            var result = sb.ToString();
            return result;
        }

        private static Dictionary<string, Type>? _controls;
        /// <returns>A type which is a <see cref="DFLayoutControl"/></returns>
        private Type? GetElementType(string typeName)
        {
            _controls ??= CoreUtils.Entities.Where(x => x.IsClass && !x.IsGenericType && typeof(DFLayoutControl).IsAssignableFrom(x)).ToDictionary(
                x => x.EntityName(),
                x => x);
            return _controls.GetValueOrDefault(typeName);
        }

        private static bool IsHidden(DFLayoutControl element)
            => element is DFLayoutField field && field.GetPropertyValue<bool>("Hidden");

        public void LoadLayout(string layout)
        {
            ColumnWidths.Clear();
            RowHeights.Clear();
            Elements.Clear();
            var lines = layout.Split('\n');
            foreach (var line in lines)
                if (line.StartsWith("C "))
                {
                    ColumnWidths.Add(line.Substring(2));
                }
                else if (line.StartsWith("R "))
                {
                    RowHeights.Add(line.Substring(2));
                }
                else if (line.StartsWith("E ") || line.StartsWith("O "))
                {
                    var typename = line.Split(' ').Skip(1).FirstOrDefault()
                        ?.Replace("InABox.Core.Design", "InABox.Core.DFLayout")
                        ?.Replace("DFLayoutChoiceField", "DFLayoutOptionField");
                    if (!string.IsNullOrWhiteSpace(typename))
                    {
                        var type = GetElementType(typename);
                        if(type != null)
                        {
                            var element = (Activator.CreateInstance(type) as DFLayoutControl)!;
                            var json = string.Join(" ", line.Split(' ').Skip(2));
                            element.LoadFromString(json);
                            //Serialization.DeserializeInto(json, element);

                            if(IsHidden(element))
                            {
                                HiddenElements.Add(element);
                            }
                            else
                            {
                                Elements.Add(element);
                            }
                        }
                        else
                        {
                            Logger.Send(LogType.Error, ClientFactory.UserID, $"{typename} is not the name of any concrete DFLayoutControls!");
                        }
                    }
                }

            //else if (line.StartsWith("O "))
            //{
            //    String typename = line.Split(' ').Skip(1).FirstOrDefault()?.Replace("PRSDesktop", "InABox.Core");
            //    if (!String.IsNullOrWhiteSpace(typename))
            //    {
            //        Type type = Type.GetType(typename);
            //        DesignControl element = Activator.CreateInstance(type) as DesignControl;
            //        if (element != null)
            //        {
            //            String json = String.Join(" ", line.Split(' ').Skip(2));
            //            element.LoadFromString(json);
            //            //CoreUtils.DeserializeInto(json, element);
            //        }
            //        Elements.Add(element);
            //    }
            //}
            // Invalid Line Hmmm..
            if (!ColumnWidths.Any())
                ColumnWidths.AddRange(new[] { "*", "Auto" });
            if (!RowHeights.Any())
                RowHeights.AddRange(new[] { "Auto" });
        }

        private void AddVariableReference(string reference, string fieldName, ReferenceType referenceType)
        {
            if (reference.Contains('.'))
                reference = reference.Split('.')[0];

            if(!VariableReferences.TryGetValue(reference, out var refs))
            {
                refs = new List<Tuple<ReferenceType, string>>();
                VariableReferences[reference] = refs;
            }
            refs.Add(new Tuple<ReferenceType, string>(referenceType, fieldName));
        }

        private object? GetFieldValue(string field)
        {
            if (field.Contains('.'))
            {
                var parts = field.Split('.');
                return Renderer?.GetFieldData(parts[0], string.Join('.', parts.Skip(1)));
            }
            else
            {
                return Renderer?.GetFieldValue(field);
            }
        }

        private void EvaluateValueExpression(string name)
        {
            var expression = Expressions[name];
            var values = new Dictionary<string, object?>();
            foreach (var field in expression.ReferencedVariables)
            {
                values[field] = GetFieldValue(field);
            }
            var oldValue = Renderer?.GetFieldValue(name);
            try
            {
                var value = expression?.Evaluate(values);
                if(value != oldValue)
                {
                    Renderer?.SetFieldValue(name, value);
                }
            }
            catch (Exception e)
            {
                Logger.Send(LogType.Error, ClientFactory.UserID, $"Error in Expression field '{name}': {CoreUtils.FormatException(e)}");
            }
        }

        private void EvaluateColourExpression(string name)
        {
            var expression = ColourExpressions[name];
            var values = new Dictionary<string, object?>();
            foreach (var field in expression.ReferencedVariables)
            {
                values[field] = GetFieldValue(field);
            }

            try
            {
                var colour = expression?.Evaluate(values);
                Renderer?.SetFieldColour(name, DFLayoutUtils.ConvertObjectToColour(colour));
            }
            catch (Exception e)
            {
                Logger.Send(LogType.Error, ClientFactory.UserID, $"Error in Expression field '{name}': {CoreUtils.FormatException(e)}");
            }
        }

        private void LoadExpression(string fieldName, string? expressionStr, ReferenceType referenceType)
        {
            if (string.IsNullOrWhiteSpace(expressionStr))
                return;

            var expression = new CoreExpression(expressionStr);
            foreach (var reference in expression.ReferencedVariables)
            {
                AddVariableReference(reference, fieldName, referenceType);
            }
            switch (referenceType)
            {
                case ReferenceType.Value:
                    Expressions[fieldName] = expression;
                    break;
                case ReferenceType.Colour:
                    ColourExpressions[fieldName] = expression;
                    break;
            }
        }

        public void LoadVariable(DigitalFormVariable variable, DFLayoutField field)
        {
            var properties = variable.LoadProperties(field);
            LoadExpression(field.Name, properties?.Expression, ReferenceType.Value);
            LoadExpression(field.Name, properties?.ColourExpression, ReferenceType.Colour);
        }

        public void LoadVariables(IEnumerable<DigitalFormVariable> variables)
        {
            foreach (var field in Elements.Where(x => x is DFLayoutField).Cast<DFLayoutField>())
            {
                var variable = variables.FirstOrDefault(x => string.Equals(x.Code, field.Name));
                if (variable != null)
                {
                    LoadVariable(variable, field);
                }
            }
        }

        public static DFLayout FromLayoutString(string layoutString)
        {
            var layout = new DFLayout();
            layout.LoadLayout(layoutString);
            return layout;
        }

        #region Expression Fields

        public void ChangeField(string fieldName)
        {
            if (!VariableReferences.TryGetValue(fieldName, out var refs)) return;
            foreach(var (refType, refName) in refs)
            {
                switch (refType)
                {
                    case ReferenceType.Value:
                        EvaluateValueExpression(refName);
                        break;
                    case ReferenceType.Colour:
                        EvaluateColourExpression(refName);
                        break;
                }
            }
        }

        public void EvaluateExpressions()
        {
            foreach(var name in Expressions.Keys)
            {
                EvaluateValueExpression(name);
            }
            foreach(var name in ColourExpressions.Keys)
            {
                EvaluateColourExpression(name);
            }
        }

        #endregion

        #region Auto-generated Layouts

        public static string GetLayoutFieldDefaultHeight(DFLayoutField field)
        {
            if (field is DFLayoutSignaturePad || field is DFLayoutMultiSignaturePad)
                return "200";
            return "Auto";
        }

        public static DFLayoutField? GenerateLayoutFieldFromVariable(DigitalFormVariable variable)
        {
            DFLayoutField? field = Activator.CreateInstance(variable.FieldType()) as DFLayoutField;
            if(field == null)
            {
                return null;
            }

            field.Name = variable.Code;
            return field;
        }

        public static DFLayout GenerateAutoDesktopLayout(
            IList<DigitalFormVariable> variables)
        {
            var layout = new DFLayout();

            layout.ColumnWidths.Add("Auto");
            layout.ColumnWidths.Add("Auto");
            layout.ColumnWidths.Add("*");

            int row = 1;
            var group = "";
            foreach(var variable in variables)
            {
                
                if (!String.IsNullOrWhiteSpace(variable.Group) && !String.Equals(variable.Group, group))
                {
                    layout.RowHeights.Add("Auto");
                    var header = new DFLayoutHeader()
                        { Header = variable.Group, Row = row, Column = 1, ColumnSpan = 3, Collapsed = !String.IsNullOrWhiteSpace(group) };
                    layout.Elements.Add(header);
                    group = variable.Group;
                    row++;                
                }
                
                var rowHeight = "Auto";

                var rowNum = new DFLayoutLabel { Caption = row.ToString(), Row = row, Column = 1 };
                var label = new DFLayoutLabel { Caption = variable.Code, Row = row, Column = 2 };

                layout.Elements.Add(rowNum);
                layout.Elements.Add(label);

                var field = GenerateLayoutFieldFromVariable(variable);
                if(field != null)
                {
                    field.Row = row;
                    field.Column = 3;

                    layout.Elements.Add(field);

                    rowHeight = GetLayoutFieldDefaultHeight(field);
                }
                layout.RowHeights.Add(rowHeight);

                ++row;
            }

            return layout;
        }

        public static DFLayout GenerateAutoMobileLayout(
            IList<DigitalFormVariable> variables)
        {
            var layout = new DFLayout();

            layout.ColumnWidths.Add("Auto");
            layout.ColumnWidths.Add("*");

            var row = 1;
            var i = 0;
            var group = "";
            foreach(var variable in variables)
            {
                if (!String.IsNullOrWhiteSpace(variable.Group) && !String.Equals(variable.Group, group))
                {
                    layout.RowHeights.Add("Auto");
                    var header = new DFLayoutHeader()
                        { Header = variable.Group, Row = row, Column = 1, ColumnSpan = 2, Collapsed = !String.IsNullOrWhiteSpace(group) };
                    layout.Elements.Add(header);
                    group = variable.Group;
                    row++;
                }
                
                var rowHeight = "Auto";
                layout.RowHeights.Add("Auto");

                var rowNum = new DFLayoutLabel { Caption = i + 1 + ".", Row = row, Column = 1 };
                var label = new DFLayoutLabel { Caption = variable.Code, Row = row, Column = 2 };

                layout.Elements.Add(rowNum);
                layout.Elements.Add(label);

                var field = GenerateLayoutFieldFromVariable(variable);
                if(field != null)
                {
                    field.Row = row + 1;
                    field.Column = 1;
                    field.ColumnSpan = 2;

                    layout.Elements.Add(field);

                    rowHeight = GetLayoutFieldDefaultHeight(field);
                }
                layout.RowHeights.Add(rowHeight);

                row += 2;
                ++i;
            }

            return layout;
        }

        public static DFLayout GenerateAutoLayout(DFLayoutType type, IList<DigitalFormVariable> variables)
        {
            return type switch
            {
                DFLayoutType.Mobile => GenerateAutoDesktopLayout(variables),
                _ => GenerateAutoDesktopLayout(variables),
            };
        }

        public static DFLayoutField? GenerateLayoutFieldFromEditor(BaseEditor editor)
        {
            // TODO: Finish
            switch (editor)
            {
                case CheckBoxEditor _:
                    var newField = new DFLayoutBooleanField();
                    newField.Properties.Type = DesignBooleanFieldType.Checkbox;
                    return newField;

                case CheckListEditor _:
                    // TODO: At this point, it seems CheckListEditor is unused.
                    throw new NotImplementedException();

                case UniqueCodeEditor _:
                case CodeEditor _:
                    return new DFLayoutCodeField();

                /* Not implemented because we don't like it.
                case PopupEditor v:*/
                case CodePopupEditor codePopupEditor:
                    // TODO: Let's look at this later. For now, using a lookup.
                    var newLookupFieldPopup = new DFLayoutLookupField();
                    newLookupFieldPopup.Properties.LookupType = codePopupEditor.Type.EntityName();
                    return newLookupFieldPopup;

                case ColorEditor _:
                    return new DFLayoutColorField();
                case CurrencyEditor _:
                    // TODO: Make this a specialised editor
                    return new DFLayoutDoubleField();

                case DateEditor _:
                    return new DFLayoutDateField();
                case DateTimeEditor _:
                    return new DFLayoutDateTimeField();
                case DoubleEditor _:
                    return new DFLayoutDoubleField();

                case DurationEditor _:
                    return new DFLayoutTimeField();

                case EmbeddedImageEditor _:
                    return new DFLayoutEmbeddedImage();

                case FileNameEditor _:
                case FolderEditor _:
                    // Unimplemented because these editors only apply to properties for server engine configuration; it
                    // doesn't make sense to store filenames in the database, and hence no entity will ever try to be saved
                    // with a property with these editors.
                    throw new NotImplementedException("This has intentionally been left unimplemented.");

                case IntegerEditor _:
                    return new DFLayoutIntegerField();

                case ComboLookupEditor _:
                case EnumLookupEditor _:
                    var newComboLookupField = new DFLayoutOptionField();
                    var comboValuesTable = (editor as StaticLookupEditor)!.Values("Key");
                    newComboLookupField.Properties.Options = string.Join(",", comboValuesTable.ExtractValues<string>("Key"));
                    return newComboLookupField;

                case LookupEditor lookupEditor:
                    var newLookupField = new DFLayoutLookupField();
                    newLookupField.Properties.LookupType = lookupEditor.Type.EntityName();
                    return newLookupField;

                case ImageDocumentEditor _:
                case MiscellaneousDocumentEditor _:
                case VectorDocumentEditor _:
                case PDFDocumentEditor _:
                    var newDocField = new DFLayoutDocumentField();
                    newDocField.Properties.FileMask = (editor as BaseDocumentEditor)!.FileMask;
                    return newDocField;

                case NotesEditor _:
                    return new DFLayoutNotesField();
                case NullEditor _:
                    return null;
                case PasswordEditor _:
                    return new DFLayoutPasswordField();
                case PINEditor _:
                    var newPINField = new DFLayoutPINField();
                    newPINField.Properties.Length = ClientFactory.PINLength;
                    return newPINField;

                // TODO: Implement JSON editors and RichText editors.
                case JsonEditor _:
                case MemoEditor _:
                case RichTextEditor _:
                case ButtonEditor _:
                case ScriptEditor _:
                    return new DFLayoutTextField();
                
                case TextBoxEditor _:
                    return new DFLayoutStringField();

                case TimestampEditor _:
                    return new DFLayoutTimeStampField();
                case TimeOfDayEditor _:
                    return new DFLayoutTimeField();

                case URLEditor _:
                    return new DFLayoutURLField();
            }

            return null;
        }

        public static DFLayout GenerateEntityLayout(Type entityType)
        {
            var layout = new DFLayout();

            layout.ColumnWidths.Add("Auto");
            layout.ColumnWidths.Add("*");

            var properties = DatabaseSchema.Properties(entityType);

            var Row = 1;
            foreach (var property in properties)
            {
                var editor = property.Editor;
                if (editor.Editable.EditorVisible())
                {
                    var field = GenerateLayoutFieldFromEditor(editor);
                    if (field != null)
                    {
                        var label = new DFLayoutLabel { Caption = editor.Caption };

                        label.Row = Row;
                        label.Column = 1;

                        field.Row = Row;
                        field.Column = 2;
                        field.Name = property.Name;

                        layout.Elements.Add(label);
                        layout.Elements.Add(field);

                        layout.RowHeights.Add("Auto");
                        Row++;
                    }
                }
            }

            return layout;
        }

        public static DFLayout GenerateEntityLayout<T>()
        {
            return GenerateEntityLayout(typeof(T));
        }

        #endregion
    }
}