using Microsoft.Owin;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Owin.Security.Providers.OpenIDBase.Infrastructure
{
    public class Message
    {
        public Message(IReadableStringCollection parameters, bool strict)
        {
            Namespaces = new Dictionary<string, Property>(StringComparer.Ordinal);
            Properties = new Dictionary<string, Property>(parameters.Count(), StringComparer.Ordinal);
            Add(parameters, strict);
        }

        public Dictionary<string, Property> Namespaces { get; }
        public Dictionary<string, Property> Properties { get; }

        /// <summary>
        /// Adds the openid parameters from querystring or form body into Namespaces and Properties collections.
        /// This normalizes the parameter name, by replacing the variable namespace alias with the 
        /// actual namespace in the collection's key, and will optionally skip any parameters that are
        /// not signed if the strict argument is true.
        /// </summary>
        /// <param name="parameters">The keys and values of the incoming querystring or form body</param>
        /// <param name="strict">True if keys that are not signed should be ignored</param>
        private void Add(IReadableStringCollection parameters, bool strict)
        {
            IEnumerable<KeyValuePair<string, string[]>> addingParameters;

            // strict is true if keys that are not signed should be strict
            if (strict)
            {
                var signed = parameters.GetValues("openid.signed");
                if (signed == null ||
                    signed.Count != 1)
                {
                    // nothing is added if the signed parameter is not present
                    return;
                }

                // determine the set of keys that are signed, or which may be used without
                // signing. ns, mode, signed, and sig each may be used without signing.
                var strictKeys = new HashSet<string>(signed[0]
                    .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                    .Select(value => "openid." + value)
                    .Concat(new[] { "openid.ns", "openid.mode", "openid.signed", "openid.sig" }));

                // the parameters to add are only the parameters what are in this set
                addingParameters = parameters.Where(kv => strictKeys.Contains(kv.Key));
            }
            else
            {
                // when strict is false all of the incoming parameters are to be added 
                addingParameters = parameters;
            }

            // convert the incoming parameter strings into Property objects. the
            // Key is the raw key name. The Name starts of being equal to Key with a 
            // trailing dot appended. The Value is the query or form value, with a comma delimiter
            // inserted between multiply occuring values.
            var addingProperties = addingParameters.Select(kv => new Property
            {
                Key = kv.Key,
                Name = kv.Key + ".",
                Value = string.Join(",", kv.Value)
            }).ToArray();

            // first, recognize which parameters are namespace declarations 

            var namespacePrefixes = new Dictionary<string, Property>(StringComparer.Ordinal);
            foreach (var item in addingProperties.Where(item => item.Name.StartsWith("openid.ns.", StringComparison.Ordinal)))
            {
                // the value of the parameter is the uri of the namespace
                item.Namespace = item.Value;
                item.Name = "openid." + item.Name.Substring("openid.ns.".Length);

                // the namespaces collection is keyed by the ns uri
                Namespaces.Add(item.Namespace, item);

                // and the prefixes collection is keyed by "openid.alias." 
                namespacePrefixes.Add(item.Name, item);
            }

            // second, recognize which parameters are property values 

            foreach (var item in addingProperties)
            {
                // anything with a namespace was already added to Namespaces
                if (item.Namespace != null) continue;
                // look for the namespace match for this property. 
                Property match = null;

                // try finding where openid.alias.arg2 matches openid.ns.alies namespace
                if (item.Name.StartsWith("openid.", StringComparison.Ordinal))
                {
                    var dotIndex = item.Name.IndexOf('.', "openid.".Length);
                    if (dotIndex != -1)
                    {
                        var namespacePrefix = item.Name.Substring(0, dotIndex + 1);
                        namespacePrefixes.TryGetValue(namespacePrefix, out match);
                    }
                }

                // then try finding where openid.arg1 should match openid.ns namespace
                if (match == null)
                {
                    namespacePrefixes.TryGetValue("openid.", out match);
                }

                // when a namespace is found
                if (match != null)
                {
                    // the property's namespace is defined, and the namespace's prefix is removed
                    item.Namespace = match.Namespace;
                    item.Name = item.Name.Substring(match.Name.Length);
                }

                // the resulting property key is keyed by the local name and namespace
                // so "openid.arg1" becomes "arg1.namespace-uri-of-openid"
                // and "openid.alias.arg2" becomes "arg2.namespace-uri-of-alias"
                Properties.Add(item.Name + item.Namespace, item);
            }
        }

        public bool TryGetValue(string key, out string value)
        {
            Property property;
            if (Properties.TryGetValue(key, out property))
            {
                value = property.Value;
                return true;
            }
            value = null;
            return false;
        }

        public IEnumerable<KeyValuePair<string, string>> ToFormValues()
        {
            return Namespaces.Concat(Properties).Select(pair => new KeyValuePair<string, string>(pair.Value.Key, pair.Value.Value));
        }
    }
}
