﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
using log4net;
using Palmmedia.ReportGenerator.Common;
using Palmmedia.ReportGenerator.Parser.Analysis;
using Palmmedia.ReportGenerator.Properties;

namespace Palmmedia.ReportGenerator.Parser
{
    using System.Diagnostics.Contracts;

    /// <summary>
    /// Parser for XML reports generated by PartCover 2.2.
    /// </summary>
    public class PartCover22Parser : ParserBase
    {
        private static readonly ILog logger = LogManager.GetLogger(typeof(PartCover22Parser));
        private Dictionary<string, string> fileIdByFilenameDictionary;
        private readonly XElement[] types;
        private readonly XElement[] files;

        /// <summary>
        /// Initializes a new instance of the <see cref="PartCover22Parser"/> class.
        /// </summary>
        /// <param name="report">The report file as XContainer.</param>
        public PartCover22Parser(XContainer report)
        {
            Contract.Requires<ArgumentNullException>(report != null);

            this.types = report.Descendants("type").ToArray();
            this.files = report.Descendants("file").ToArray();

            Process();

            this.types = null;
            this.files = null;
            this.fileIdByFilenameDictionary = null;
        }

        private async void Process()
        {
            this.fileIdByFilenameDictionary = this.files.ToDictionary(f => f.Attribute("url").Value, f => f.Attribute("id").Value);

            var assemblyNames = this.types.Select(type => type.Attribute("asm").Value).Distinct().OrderBy(value => value).ToArray();

            //Parallel.ForEach(assemblyNames, assemblyName => this.AddAssembly(this.ProcessAssembly(assemblyName)));

            foreach (var assemblyName in assemblyNames)
            {
                var processed = await this.ProcessAssembly(assemblyName);
                this.AddAssembly(processed);
            }
        }
        
        private async Task<Assembly> ProcessAssembly(string assemblyName)
        {
            logger.DebugFormat("  " + Resources.CurrentAssembly, assemblyName);

            var classNames = this.types
                .Where(type => type.Attribute("asm").Value.Equals(assemblyName) && !type.Attribute("name").Value.Contains("__"))
                .Select(type => type.Attribute("name").Value)
                .OrderBy(name => name)
                .Distinct()
                .ToArray();

            var assembly = new Assembly(assemblyName);

            foreach (var className in classNames)
            {
                var processedClass = await this.ProcessClass(assembly, className);
                assembly.AddClass(processedClass);
            }
            return assembly;
        }

        private async Task<Class> ProcessClass(Assembly assembly, string className)
        {
            var fileIdsOfClass = this.types
                .Where(type => type.Attribute("asm").Value.Equals(assembly.Name) 
                    && (type.Attribute("name").Value.Equals(className, StringComparison.Ordinal)
                    || type.Attribute("name").Value.StartsWith(className + "<", StringComparison.Ordinal)))
                .Elements("method")
                .Elements("code")
                .Elements("pt")
                .Where(pt => pt.Attribute("fid") != null)
                .Select(pt => pt.Attribute("fid").Value)
                .Distinct()
                .ToHashSet();

            var filesOfClass = this.files
                .Where(file => fileIdsOfClass.Contains(file.Attribute("id").Value))
                .Select(file => file.Attribute("url").Value)
                .ToArray();

            var processedClass = new Class(className, assembly);

            foreach (var file in filesOfClass)
            {
                processedClass.AddFile(this.ProcessFile(processedClass, file));
            }

            return processedClass;
        }

        private CodeFile ProcessFile(Class processedClass, string filePath)
        {
            string fileId = this.fileIdByFilenameDictionary[filePath];

            var seqpntsOfFile = this.types
                .Where(type => type.Attribute("asm").Value.Equals(processedClass.Assembly.Name) 
                    && (type.Attribute("name").Value.Equals(processedClass.Name, StringComparison.Ordinal)
                        || type.Attribute("name").Value.StartsWith(processedClass.Name + "<", StringComparison.Ordinal)))
                .Elements("method")
                .Elements("code")
                .Elements("pt")
                .Where(seqpnt => seqpnt.HasAttributeWithValue("fid", fileId))
                .Select(seqpnt => new
                {
                    LineNumberStart = int.Parse(seqpnt.Attribute("sl").Value, CultureInfo.InvariantCulture),
                    LineNumberEnd = seqpnt.Attribute("el") != null ? int.Parse(seqpnt.Attribute("el").Value, CultureInfo.InvariantCulture) : int.Parse(seqpnt.Attribute("sl").Value, CultureInfo.InvariantCulture),
                    Visits = int.Parse(seqpnt.Attribute("visit").Value, CultureInfo.InvariantCulture)
                })
                .OrderBy(seqpnt => seqpnt.LineNumberEnd)
                .ToArray();

            int[] coverage = new int[] { };

            if (seqpntsOfFile.Length > 0)
            {
                coverage = new int[seqpntsOfFile[seqpntsOfFile.LongLength - 1].LineNumberEnd + 1];

                for (int i = 0; i < coverage.Length; i++)
                {
                    coverage[i] = -1;
                }

                foreach (var seqpnt in seqpntsOfFile)
                {
                    for (int lineNumber = seqpnt.LineNumberStart; lineNumber <= seqpnt.LineNumberEnd; lineNumber++)
                    {
                        coverage[lineNumber] = coverage[lineNumber] == -1 ? seqpnt.Visits : coverage[lineNumber] + seqpnt.Visits;
                    }
                }
            }

            return new CodeFile(filePath, coverage);
        }
    }
}
