using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;

namespace XLAB
{
    internal sealed class PsvPrintService
    {
        private const int PrintDialogId = 88;
        private const int WordAlertsNone = 0;
        private const int WordCloseDoNotSaveChanges = 0;

        public void PrintDocument(PsvDocumentSummary document, IReadOnlyList<PsvDocumentLine> lines)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }

            if (lines == null)
            {
                throw new ArgumentNullException("lines");
            }

            if (lines.Count == 0)
            {
                throw new InvalidOperationException("В выбранном ПСВ нет строк для печати.");
            }

            var templateFileName = document.IssuedOn.HasValue ? "ClosePsv.docx" : "OpenPsv.docx";
            PrintWordTemplate(
                templateFileName,
                document.DocumentNumber,
                delegate(object wordDocument)
                {
                    PopulatePsvTemplate((dynamic)wordDocument, document, lines);
                });
        }

        public void PrintVerificationDocument(PsvDocumentLine line)
        {
            if (line == null)
            {
                throw new ArgumentNullException("line");
            }

            if (!line.IsPassed.HasValue)
            {
                throw new InvalidOperationException("Для печати документа о поверке не указан результат поверки.");
            }

            if (string.IsNullOrWhiteSpace(line.VerificationDocumentNumber))
            {
                throw new InvalidOperationException("Для печати документа о поверке не указан номер документа.");
            }

            if (!line.VerificationPerformedOn.HasValue && !line.VerificationDocumentDate.HasValue)
            {
                throw new InvalidOperationException("Для печати документа о поверке не указана дата поверки.");
            }

            if (!line.IsPassed.Value && string.IsNullOrWhiteSpace(line.RejectionReason))
            {
                throw new InvalidOperationException("Для печати извещения о непригодности не указана причина непригодности.");
            }

            var templateFileName = line.IsPassed.Value ? "Svid.docx" : "Izv.docx";
            PrintWordTemplate(
                templateFileName,
                line.VerificationDocumentNumber,
                delegate(object wordDocument)
                {
                    PopulateVerificationTemplate((dynamic)wordDocument, line);
                });
        }

        private static void PrintWordTemplate(string templateFileName, string printNumber, Action<object> populateDocument)
        {
            if (string.IsNullOrWhiteSpace(templateFileName))
            {
                throw new ArgumentException("Не указано имя шаблона печати.", "templateFileName");
            }

            if (populateDocument == null)
            {
                throw new ArgumentNullException("populateDocument");
            }

            var templatePath = ResolveTemplatePath(templateFileName);
            var workingCopyPath = CreateWorkingCopy(templatePath, printNumber);
            object wordApplication = null;
            object wordDocument = null;
            object dialogs = null;
            object printDialog = null;

            try
            {
                var wordType = Type.GetTypeFromProgID("Word.Application");
                if (wordType == null)
                {
                    throw new InvalidOperationException("Microsoft Word не найден. Печать ПСВ по шаблону DOCX недоступна.");
                }

                wordApplication = Activator.CreateInstance(wordType);
                dynamic word = wordApplication;
                word.Visible = true;
                word.DisplayAlerts = WordAlertsNone;

                wordDocument = word.Documents.Open(
                    FileName: workingCopyPath,
                    ConfirmConversions: false,
                    ReadOnly: false,
                    AddToRecentFiles: false,
                    Visible: true);

                populateDocument(wordDocument);
                ((dynamic)wordDocument).Save();
                word.Activate();
                ((dynamic)wordDocument).Activate();

                dialogs = word.Dialogs;
                printDialog = ((dynamic)dialogs).Item(PrintDialogId);
                ((dynamic)printDialog).Show();

                ((dynamic)wordDocument).Close(WordCloseDoNotSaveChanges);
                wordDocument = null;

                word.Quit();
                wordApplication = null;
            }
            finally
            {
                ReleaseComObject(printDialog);
                ReleaseComObject(dialogs);
                ReleaseComObject(wordDocument);
                ReleaseComObject(wordApplication);
                TryDeleteFile(workingCopyPath);
            }
        }

        private static void PopulatePsvTemplate(dynamic document, PsvDocumentSummary summary, IReadOnlyList<PsvDocumentLine> lines)
        {
            var groupedLines = BuildPrintedGroups(lines, summary.IssuedOn.HasValue);

            ReplacePlaceholder(document, "number", NormalizeText(summary.DocumentNumber));
            ReplacePlaceholder(document, "div", NormalizeText(summary.CustomerName));
            ReplacePlaceholder(document, "date", FormatDate(summary.AcceptedOn));
            ReplacePlaceholder(document, "count", lines.Count.ToString(CultureInfo.InvariantCulture));
            ReplacePlaceholder(document, "person", string.Empty);

            if (summary.IssuedOn.HasValue)
            {
                ReplacePlaceholder(document, "today", FormatDate(summary.IssuedOn));
                ReplacePlaceholder(document, "good", lines.Count(delegate(PsvDocumentLine line) { return line.IsPassed == true; }).ToString(CultureInfo.InvariantCulture));
                ReplacePlaceholder(document, "bad", lines.Count(delegate(PsvDocumentLine line) { return line.IsPassed == false; }).ToString(CultureInfo.InvariantCulture));
            }
            else
            {
                var dueDate = summary.AcceptedOn.HasValue
                    ? summary.AcceptedOn.Value.AddDays(30)
                    : (DateTime?)null;
                ReplacePlaceholder(document, "next", FormatDate(dueDate));
            }

            FillTable(document, groupedLines);
        }

        private static void PopulateVerificationTemplate(dynamic document, PsvDocumentLine line)
        {
            var verificationDate = ResolveVerificationDate(line);

            ReplacePlaceholder(document, "number", NormalizeText(line.VerificationDocumentNumber));
            ReplacePlaceholder(document, "nextpovdate", FormatDate(ResolveNextVerificationDate(line, verificationDate)));
            ReplacePlaceholder(document, "name", NormalizeText(line.InstrumentName));
            ReplacePlaceholder(document, "type (рег. № gr)", BuildVerificationTypeText(line));
            ReplacePlaceholder(document, "serial", NormalizeText(line.SerialNumber));
            ReplacePlaceholder(document, "erial", NormalizeText(line.SerialNumber));
            ReplacePlaceholder(document, "method", string.Empty);
            ReplacePlaceholder(document, "temp", string.Empty);
            ReplacePlaceholder(document, "hum", string.Empty);
            ReplacePlaceholder(document, "press", string.Empty);
            ReplacePlaceholder(document, "person", NormalizeText(line.VerifierName));
            ReplacePlaceholder(document, "reason", NormalizeText(line.RejectionReason));
            ReplacePlaceholder(document, "date", FormatDate(verificationDate));
        }

        private static void FillTable(dynamic document, IReadOnlyList<PrintedGroupRow> rowsToPrint)
        {
            dynamic table = null;

            try
            {
                table = document.Tables.Item(1);

                for (var index = 0; index < rowsToPrint.Count; index++)
                {
                    var rowData = rowsToPrint[index];
                    dynamic row = null;

                    try
                    {
                        row = table.Rows.Add();
                        SetCellText(row, 1, (index + 1).ToString(CultureInfo.InvariantCulture), true);
                        SetCellText(row, 2, rowData.InstrumentName, false);
                        SetCellText(row, 3, rowData.InstrumentType, false);
                        SetCellText(row, 4, rowData.RangeText, false);
                        SetCellText(row, 5, rowData.SerialNumberText, false);
                        SetCellText(row, 6, rowData.GroupCount.ToString(CultureInfo.InvariantCulture), true);
                        SetCellText(row, 7, rowData.PassedCount > 0 ? rowData.PassedCount.ToString(CultureInfo.InvariantCulture) : string.Empty, true);
                        SetCellText(row, 8, rowData.FailedCount > 0 ? rowData.FailedCount.ToString(CultureInfo.InvariantCulture) : string.Empty, true);
                        SetCellText(row, 9, rowData.Notes, false);
                    }
                    finally
                    {
                        ReleaseComObject(row);
                    }
                }
            }
            finally
            {
                ReleaseComObject(table);
            }
        }

        private static void SetCellText(dynamic row, int columnIndex, string value, bool centerAlign)
        {
            dynamic cell = null;

            try
            {
                cell = row.Cells.Item(columnIndex);
                cell.Range.Text = NormalizeText(value);
                cell.Range.Bold = 0;
                cell.Range.Font.Underline = 0;
                cell.Range.ParagraphFormat.Alignment = centerAlign ? 1 : 0;
            }
            finally
            {
                ReleaseComObject(cell);
            }
        }

        private static void ReplacePlaceholder(dynamic document, string placeholder, string replacement)
        {
            dynamic range = null;
            dynamic find = null;

            try
            {
                range = document.Content;
                find = range.Find;
                find.ClearFormatting();
                find.Replacement.ClearFormatting();
                find.Execute(
                    FindText: placeholder,
                    MatchCase: false,
                    MatchWholeWord: false,
                    MatchWildcards: false,
                    MatchSoundsLike: false,
                    MatchAllWordForms: false,
                    Forward: true,
                    Wrap: 1,
                    Format: false,
                    ReplaceWith: NormalizeText(replacement),
                    Replace: 2);
            }
            finally
            {
                ReleaseComObject(find);
                ReleaseComObject(range);
            }
        }

        private static IReadOnlyList<PrintedGroupRow> BuildPrintedGroups(IEnumerable<PsvDocumentLine> lines, bool includeClosedNotes)
        {
            return (lines ?? Enumerable.Empty<PsvDocumentLine>())
                .GroupBy(delegate(PsvDocumentLine line)
                {
                    return new PrintGroupKey
                    {
                        InstrumentType = NormalizeText(line == null ? null : line.InstrumentType),
                        RangeText = NormalizeText(line == null ? null : line.RangeText),
                        RegistryNumber = NormalizeText(line == null ? null : line.RegistryNumber)
                    };
                })
                .OrderBy(delegate(IGrouping<PrintGroupKey, PsvDocumentLine> group) { return group.Key.InstrumentType; }, StringComparer.OrdinalIgnoreCase)
                .ThenBy(delegate(IGrouping<PrintGroupKey, PsvDocumentLine> group) { return group.Key.RegistryNumber; }, StringComparer.OrdinalIgnoreCase)
                .ThenBy(delegate(IGrouping<PrintGroupKey, PsvDocumentLine> group) { return group.Key.RangeText; }, StringComparer.OrdinalIgnoreCase)
                .Select(delegate(IGrouping<PrintGroupKey, PsvDocumentLine> group)
                {
                    var materializedLines = group.Where(delegate(PsvDocumentLine line) { return line != null; }).ToList();

                    return new PrintedGroupRow
                    {
                        InstrumentName = BuildInstrumentNameText(materializedLines),
                        InstrumentType = group.Key.InstrumentType,
                        RangeText = group.Key.RangeText,
                        SerialNumberText = BuildSerialNumberText(materializedLines),
                        GroupCount = materializedLines.Count,
                        PassedCount = materializedLines.Count(delegate(PsvDocumentLine line) { return line.IsPassed == true; }),
                        FailedCount = materializedLines.Count(delegate(PsvDocumentLine line) { return line.IsPassed == false; }),
                        Notes = includeClosedNotes ? BuildClosedNotesText(materializedLines) : string.Empty
                    };
                })
                .ToList();
        }

        private static string BuildInstrumentNameText(IReadOnlyList<PsvDocumentLine> lines)
        {
            return string.Join("; ", lines
                .Select(delegate(PsvDocumentLine line) { return NormalizeText(line.InstrumentName); })
                .Where(delegate(string value) { return !string.IsNullOrWhiteSpace(value); })
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .OrderBy(delegate(string value) { return value; }, StringComparer.OrdinalIgnoreCase)
                .ToArray());
        }

        private static string BuildSerialNumberText(IReadOnlyList<PsvDocumentLine> lines)
        {
            var serialNumbers = lines
                .Select(delegate(PsvDocumentLine line) { return NormalizeText(line.SerialNumber); })
                .Where(delegate(string value) { return !string.IsNullOrWhiteSpace(value); })
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .OrderBy(delegate(string value) { return value; }, StringComparer.OrdinalIgnoreCase)
                .ToList();

            if (serialNumbers.Count == 0)
            {
                return string.Empty;
            }

            if (serialNumbers.Count > 3)
            {
                return string.Format(CultureInfo.InvariantCulture, "{0} зав. номеров", serialNumbers.Count);
            }

            return string.Join(", ", serialNumbers.ToArray());
        }

        private static string BuildClosedNotesText(IReadOnlyList<PsvDocumentLine> lines)
        {
            var parts = new List<string>();

            var verificationDocuments = lines
                .Select(FormatVerificationDocument)
                .Where(delegate(string value) { return !string.IsNullOrWhiteSpace(value); })
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .OrderBy(delegate(string value) { return value; }, StringComparer.OrdinalIgnoreCase)
                .ToList();

            if (verificationDocuments.Count > 0)
            {
                parts.Add("Документы: " + string.Join("; ", verificationDocuments.ToArray()));
            }

            var stickerNumbers = lines
                .Select(delegate(PsvDocumentLine line) { return NormalizeText(line.StickerNumber); })
                .Where(delegate(string value) { return !string.IsNullOrWhiteSpace(value); })
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .OrderBy(delegate(string value) { return value; }, StringComparer.OrdinalIgnoreCase)
                .ToList();

            if (stickerNumbers.Count > 0)
            {
                parts.Add("Наклейки: " + string.Join(", ", stickerNumbers.ToArray()));
            }

            return string.Join(". ", parts.ToArray());
        }

        private static string FormatVerificationDocument(PsvDocumentLine line)
        {
            if (line == null)
            {
                return string.Empty;
            }

            var number = NormalizeText(line.VerificationDocumentNumber);
            var date = FormatDate(line.VerificationDocumentDate);

            if (string.IsNullOrWhiteSpace(number))
            {
                return date;
            }

            if (string.IsNullOrWhiteSpace(date))
            {
                return number;
            }

            return string.Format(CultureInfo.InvariantCulture, "{0} от {1}", number, date);
        }

        private static string BuildVerificationTypeText(PsvDocumentLine line)
        {
            if (line == null)
            {
                return string.Empty;
            }

            var type = NormalizeText(line.InstrumentType);
            var registryNumber = NormalizeText(line.RegistryNumber);

            if (string.IsNullOrWhiteSpace(type))
            {
                return string.Empty;
            }

            return string.IsNullOrWhiteSpace(registryNumber)
                ? type
                : string.Format(CultureInfo.InvariantCulture, "{0} (рег. № {1})", type, registryNumber);
        }

        private static DateTime? ResolveVerificationDate(PsvDocumentLine line)
        {
            if (line == null)
            {
                return null;
            }

            return line.VerificationPerformedOn ?? line.VerificationDocumentDate;
        }

        private static DateTime? ResolveNextVerificationDate(PsvDocumentLine line, DateTime? verificationDate)
        {
            if (line == null || !line.IsPassed.HasValue || !line.IsPassed.Value)
            {
                return null;
            }

            if (!verificationDate.HasValue || line.PeriodMonths <= 0)
            {
                return null;
            }

            return verificationDate.Value.AddMonths(line.PeriodMonths);
        }

        private static string ResolveTemplatePath(string fileName)
        {
            var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
            var candidates = new[]
            {
                Path.Combine(baseDirectory, fileName),
                Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", fileName)),
                Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", fileName))
            };

            var templatePath = candidates.FirstOrDefault(File.Exists);
            if (templatePath == null)
            {
                throw new FileNotFoundException(string.Format("Не найден шаблон печати ПСВ: {0}.", fileName), fileName);
            }

            return templatePath;
        }

        private static string CreateWorkingCopy(string templatePath, string documentNumber)
        {
            var tempDirectory = Path.Combine(Path.GetTempPath(), "XLAB", "Print");
            Directory.CreateDirectory(tempDirectory);

            var safeDocumentNumber = string.IsNullOrWhiteSpace(documentNumber)
                ? "PSV"
                : string.Concat(documentNumber.Where(delegate(char ch)
                {
                    return !Path.GetInvalidFileNameChars().Contains(ch);
                }));

            if (string.IsNullOrWhiteSpace(safeDocumentNumber))
            {
                safeDocumentNumber = "PSV";
            }

            var tempFileName = string.Format(
                CultureInfo.InvariantCulture,
                "{0}_{1:yyyyMMdd_HHmmss_fff}_{2}",
                safeDocumentNumber,
                DateTime.Now,
                Path.GetFileName(templatePath));

            var workingCopyPath = Path.Combine(tempDirectory, tempFileName);
            File.Copy(templatePath, workingCopyPath, true);
            return workingCopyPath;
        }

        private static string FormatDate(DateTime? value)
        {
            return value.HasValue
                ? value.Value.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture)
                : string.Empty;
        }

        private static string NormalizeText(string value)
        {
            return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
        }

        private static void TryDeleteFile(string path)
        {
            if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
            {
                return;
            }

            try
            {
                File.Delete(path);
            }
            catch (IOException)
            {
            }
            catch (UnauthorizedAccessException)
            {
            }
        }

        private static void ReleaseComObject(object value)
        {
            if (value == null || !Marshal.IsComObject(value))
            {
                return;
            }

            try
            {
                Marshal.FinalReleaseComObject(value);
            }
            catch (ArgumentException)
            {
            }
        }

        private sealed class PrintedGroupRow
        {
            public int FailedCount { get; set; }

            public int GroupCount { get; set; }

            public string InstrumentName { get; set; }

            public string InstrumentType { get; set; }

            public string Notes { get; set; }

            public int PassedCount { get; set; }

            public string RangeText { get; set; }

            public string SerialNumberText { get; set; }
        }

        private sealed class PrintGroupKey : IEquatable<PrintGroupKey>
        {
            public string InstrumentType { get; set; }

            public string RangeText { get; set; }

            public string RegistryNumber { get; set; }

            public bool Equals(PrintGroupKey other)
            {
                return other != null
                       && string.Equals(InstrumentType ?? string.Empty, other.InstrumentType ?? string.Empty, StringComparison.OrdinalIgnoreCase)
                       && string.Equals(RangeText ?? string.Empty, other.RangeText ?? string.Empty, StringComparison.OrdinalIgnoreCase)
                       && string.Equals(RegistryNumber ?? string.Empty, other.RegistryNumber ?? string.Empty, StringComparison.OrdinalIgnoreCase);
            }

            public override bool Equals(object obj)
            {
                return Equals(obj as PrintGroupKey);
            }

            public override int GetHashCode()
            {
                return string.Concat(
                        (InstrumentType ?? string.Empty).ToUpperInvariant(), "|",
                        (RangeText ?? string.Empty).ToUpperInvariant(), "|",
                        (RegistryNumber ?? string.Empty).ToUpperInvariant())
                    .GetHashCode();
            }
        }
    }
}
