﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.Serialization;

namespace ExtensibilityFramework.Entities
{
    [CollectionDataContract]
    public class Flow : BindingList<Step>
    {
        [DataMember]
        public IEnumerable<Step> OrderedContents
        {
            get
            {
                return from x in Items
                       orderby x.Order
                       select x;
            }
        }

        protected override void InsertItem(int index, Step item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
            if (index < 0)
                throw new ArgumentOutOfRangeException("index", "Item index cannot be negative");

            Step existing = Items.FirstOrDefault(x => x.Id == item.Id);
            if (existing != null)
                throw new ArgumentException("duplicate step ID detected");

            base.InsertItem(index, item);
            item.Order = Count;
        }

        protected override void RemoveItem(int index)
        {
            if (index < 0)
                throw new ArgumentOutOfRangeException("index", "Item index cannot be negative");

            Step step = Items[index];
            base.RemoveItem(index);
            foreach (var x in Items)
            {
                if (x.Order > step.Order)
                    x.Order--;

                // invalidate parameters pointing to the removed step
                foreach (Parameter one in x.Parameters.Values)
                    if (one.StepId == step.Id)
                        one.UndoProcessing();
            }
            Sort();
        }

        public void Sort()
        {
            List<Step> steps = Items as List<Step>;
            steps.Sort(Step.Comparer);
            OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
        }

        public void Swap(int order1, int order2)
        {
            Step one = GetByOrder(order1);
            if (one == null)
                throw new ArgumentOutOfRangeException("order1");
            Step two = GetByOrder(order2);
            if (two == null)
                throw new ArgumentOutOfRangeException("order2");
            one.Order = order2;
            two.Order = order1;

            // invalidate parameters pointing to the changed steps
            foreach (var x in Items)
                foreach (Parameter prm in x.Parameters.Values)
                    if (prm.StepId == one.Id || prm.StepId == two.Id)
                        prm.UndoProcessing();

            Sort();
        }

        public Step GetByOrder(int order)
        {
            return Items.FirstOrDefault(x => x.Order == order);
        }

        public List<StepError> Validate(Dictionary<string, ActionDefinition> actions)
        {
            var retVal = new List<StepError>();
            var publishedValues = new HashSet<string>();

            if (Items == null || Items.Count == 0)
                retVal.Add(new StepError("Nothing to do", Severity.Fatal));
            else
            {
                if (Items.Any(step => step == null))
                    throw new ArgumentNullException("step", "Null step in Items");

                foreach (var step in OrderedContents)
                {
                    if (string.IsNullOrEmpty(step.Name))
                        retVal.Add(new StepError("Step is not named", Severity.Warning, step));

                    if (!actions.ContainsKey(step.ActionName))
                    {
                        retVal.Add(new StepError("Unknown action used", Severity.Error, step));
                        continue;
                    }

                    ActionDefinition definition = actions[step.ActionName];
                    if (definition.Inputs.Count != step.Parameters.Count)
                    {
                        retVal.Add(new StepError("Number of input parameters does not match number of action consumables", Severity.Error, step));
                        continue;
                    }

                    // check each step parameter individually
                    var inputsFromDefinition = definition.Inputs.ToArray();
                    var inputsFromStep = step.Parameters.Values.ToArray();
                    Array.Sort(inputsFromStep, Parameter.Comparer);
                    Array.Sort(inputsFromDefinition, Public.Comparer);
                    for (int i = 0; i < definition.Inputs.Count; i++)
                    {
                        // is parameter supplied of the appropriate type?
                        if (inputsFromStep[i].Category != inputsFromDefinition[i])
                            retVal.Add(new StepError(string.Format("Input parameter is of type [{0}] while action needs [{1}]",
                                                                   inputsFromStep[i].Category.Name, inputsFromDefinition[i].Name),
                                                     Severity.Error, step));

                        // is index in range according to what has been already published?
                        if (!publishedValues.Contains(inputsFromStep[i].Key))
                            retVal.Add(new StepError(string.Format("Input parameter {0} uses a value that has not been previously published",
                                                                   inputsFromStep[i].Key), Severity.Error, step));
                    }

                    // update published values up to now
                    foreach (PublicWithId one in definition.Outputs)
                    {
                        string key = one.Name + "#" + one.Id + "@" + step.Id;
                        if (publishedValues.Contains(key))
                            throw new InvalidOperationException("Duplicate key found in output parameters: " + key);

                        publishedValues.Add(key);
                    }
                }
            }
            return retVal;
        }
    }
}
