using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows.Input;

namespace XLAB2
{
    internal sealed class MainWindowViewModel : ObservableObject
    {
        private readonly List<PsvDocumentSummary> _draftDocuments;
        private readonly IDialogService _dialogService;
        private readonly Dictionary<string, List<PsvDocumentLine>> _pendingLinesByDocumentKey;
        private readonly PsvPrintService _printService;
        private readonly PsvDataService _service;
        private string _documentFilterText;
        private string _documentNumberEditor;
        private string _documentStatusText;
        private string _detailTableCountText;
        private string _groupFilterText;
        private string _groupDetailFilterText;
        private string _headerDepartmentName;
        private int _headerInstrumentCount;
        private DateTime? _headerIssuedOn;
        private DateTime? _headerReceivedOn;
        private bool _isBusy;
        private string _lineStatusText;
        private PsvDocumentLine _lastCloneSourceLine;
        private int? _selectedCustomerId;
        private PsvDocumentSummary _selectedDocument;
        private PsvDocumentGroupSummary _selectedDocumentGroup;
        private PsvDocumentLine _selectedDocumentLine;
        private bool _showClosedDocuments;

        public MainWindowViewModel(PsvDataService service, IDialogService dialogService)
        {
            _service = service;
            _dialogService = dialogService;
            _printService = new PsvPrintService();
            _draftDocuments = new List<PsvDocumentSummary>();
            _pendingLinesByDocumentKey = new Dictionary<string, List<PsvDocumentLine>>(StringComparer.OrdinalIgnoreCase);

            Customers = new ObservableCollection<CustomerReference>();
            Documents = new ObservableCollection<PsvDocumentSummary>();
            DocumentLines = new ObservableCollection<PsvDocumentLine>();
            DocumentGroupSummaries = new ObservableCollection<PsvDocumentGroupSummary>();

            DocumentsView = CollectionViewSource.GetDefaultView(Documents);
            DocumentsView.Filter = FilterDocuments;

            DocumentGroupsView = CollectionViewSource.GetDefaultView(DocumentGroupSummaries);
            DocumentGroupsView.Filter = FilterDocumentGroups;

            DocumentLinesView = CollectionViewSource.GetDefaultView(DocumentLines);
            DocumentLinesView.Filter = FilterDocumentLines;

            AddDocumentCommand = new RelayCommand(delegate { AddDocument(); }, delegate { return !IsBusy && !ShowClosedDocuments; });
            CloneLineVerificationCommand = new RelayCommand(delegate { CloneSelectedLineVerificationAsync(); }, delegate { return CanCloneSelectedLineVerification(); });
            DeleteDocumentCommand = new RelayCommand(delegate { DeleteDocumentAsync(); }, delegate { return !IsBusy && SelectedDocument != null; });
            DeleteSelectedLinesCommand = new RelayCommand(delegate { DeleteSelectedLinesAsync(); }, delegate { return CanDeleteSelectedLines(); });
            DeleteSelectedGroupsCommand = new RelayCommand(delegate { DeleteSelectedGroupsAsync(); }, delegate { return CanDeleteSelectedGroups(); });
            MarkLinePassedCommand = new RelayCommand(delegate { EditLineVerificationAsync(true); }, delegate { return CanEditSelectedLineVerification(); });
            MarkLineRejectedCommand = new RelayCommand(delegate { EditLineVerificationAsync(false); }, delegate { return CanEditSelectedLineVerification(); });
            OpenInstrumentPickerCommand = new RelayCommand(delegate { OpenInstrumentPickerAsync(); }, delegate { return CanAddInstrumentsToSelectedDocument(); });
            OpenInstrumentTypePickerCommand = new RelayCommand(delegate { OpenInstrumentTypePickerAsync(); }, delegate { return CanAddInstrumentsToSelectedDocument(); });
            PrintDocumentCommand = new RelayCommand(delegate { PrintSelectedDocumentAsync(); }, delegate { return CanPrintSelectedDocument(); });
            PrintVerificationDocumentCommand = new RelayCommand(delegate { PrintSelectedVerificationDocumentAsync(); }, delegate { return CanPrintSelectedVerificationDocument(); });
            RefreshDocumentsCommand = new RelayCommand(delegate { RefreshDocumentsAsync(null, null); }, delegate { return !IsBusy; });
            ResetLineVerificationCommand = new RelayCommand(delegate { ResetSelectedLineVerificationAsync(); }, delegate { return CanResetSelectedLineVerification(); });
            SaveDocumentHeaderCommand = new RelayCommand(delegate { SaveDocumentAsync(); }, delegate { return CanSaveDocument(); });

            DocumentStatusText = "Готово.";
            DetailTableCountText = "Приборов в таблице: 0.";
            LineStatusText = "Документ не выбран.";
        }

        public ICommand AddDocumentCommand { get; private set; }

        public ICommand CloneLineVerificationCommand { get; private set; }

        public ObservableCollection<CustomerReference> Customers { get; private set; }

        public string DocumentFilterText
        {
            get { return _documentFilterText; }
            set
            {
                if (SetProperty(ref _documentFilterText, value))
                {
                    DocumentsView.Refresh();
                }
            }
        }

        public bool ShowClosedDocuments
        {
            get { return _showClosedDocuments; }
            set
            {
                if (!SetProperty(ref _showClosedDocuments, value))
                {
                    return;
                }

                OnPropertyChanged("ShowOpenDocuments");
                OnPropertyChanged("IsDocumentHeaderEditable");
                RaiseCommandStates();
                RefreshDocumentsAsync(null, null);
            }
        }

        public bool ShowOpenDocuments
        {
            get { return !ShowClosedDocuments; }
            set
            {
                if (value)
                {
                    ShowClosedDocuments = false;
                }
            }
        }

        public ObservableCollection<PsvDocumentLine> DocumentLines { get; private set; }

        public ICollectionView DocumentLinesView { get; private set; }

        public string DocumentNumberEditor
        {
            get { return _documentNumberEditor; }
            set { SetProperty(ref _documentNumberEditor, value); }
        }

        public ObservableCollection<PsvDocumentGroupSummary> DocumentGroupSummaries { get; private set; }

        public ICollectionView DocumentGroupsView { get; private set; }

        public string DocumentStatusText
        {
            get { return _documentStatusText; }
            private set { SetProperty(ref _documentStatusText, value); }
        }

        public string DetailTableCountText
        {
            get { return _detailTableCountText; }
            private set { SetProperty(ref _detailTableCountText, value); }
        }

        public ObservableCollection<PsvDocumentSummary> Documents { get; private set; }

        public ICollectionView DocumentsView { get; private set; }

        public ICommand DeleteDocumentCommand { get; private set; }

        public ICommand DeleteSelectedLinesCommand { get; private set; }

        public ICommand DeleteSelectedGroupsCommand { get; private set; }

        public string GroupFilterText
        {
            get { return _groupFilterText; }
            set
            {
                if (SetProperty(ref _groupFilterText, value))
                {
                    RefreshDocumentGroupsView();
                }
            }
        }

        public string GroupDetailFilterText
        {
            get { return _groupDetailFilterText; }
            set
            {
                if (SetProperty(ref _groupDetailFilterText, value))
                {
                    RefreshDocumentLinesView();
                }
            }
        }

        public string HeaderDepartmentName
        {
            get { return _headerDepartmentName; }
            private set { SetProperty(ref _headerDepartmentName, value); }
        }

        public int HeaderInstrumentCount
        {
            get { return _headerInstrumentCount; }
            private set { SetProperty(ref _headerInstrumentCount, value); }
        }

        public bool IsDocumentHeaderEditable
        {
            get
            {
                return !IsBusy
                       && SelectedDocument != null
                       && !IsDocumentClosed(SelectedDocument);
            }
        }

        public DateTime? HeaderIssuedOn
        {
            get { return _headerIssuedOn; }
            set { SetProperty(ref _headerIssuedOn, value); }
        }

        public DateTime? HeaderReceivedOn
        {
            get { return _headerReceivedOn; }
            set { SetProperty(ref _headerReceivedOn, value); }
        }

        public bool IsBusy
        {
            get { return _isBusy; }
            private set
            {
                if (SetProperty(ref _isBusy, value))
                {
                    RaiseCommandStates();
                    OnPropertyChanged("IsCustomerEditable");
                    OnPropertyChanged("IsDocumentHeaderEditable");
                }
            }
        }

        public bool IsCustomerEditable
        {
            get
            {
                return !IsBusy
                       && SelectedDocument != null
                       && SelectedDocument.IsDraft
                       && GetPendingLines(SelectedDocument).Count == 0;
            }
        }

        public string LineStatusText
        {
            get { return _lineStatusText; }
            private set { SetProperty(ref _lineStatusText, value); }
        }

        public ICommand MarkLinePassedCommand { get; private set; }

        public ICommand MarkLineRejectedCommand { get; private set; }

        public ICommand OpenInstrumentPickerCommand { get; private set; }

        public ICommand OpenInstrumentTypePickerCommand { get; private set; }

        public ICommand PrintDocumentCommand { get; private set; }

        public ICommand PrintVerificationDocumentCommand { get; private set; }

        public ICommand RefreshDocumentsCommand { get; private set; }

        public ICommand ResetLineVerificationCommand { get; private set; }

        public ICommand SaveDocumentHeaderCommand { get; private set; }

        public int? SelectedCustomerId
        {
            get { return _selectedCustomerId; }
            set
            {
                if (!SetProperty(ref _selectedCustomerId, value))
                {
                    return;
                }

                ApplySelectedCustomer();
            }
        }

        public PsvDocumentSummary SelectedDocument
        {
            get { return _selectedDocument; }
            set
            {
                if (SetProperty(ref _selectedDocument, value))
                {
                    _lastCloneSourceLine = null;
                    FillHeaderFromSelection();
                    RaiseCommandStates();
                    OnPropertyChanged("IsCustomerEditable");
                    OnPropertyChanged("IsDocumentHeaderEditable");
                    LoadSelectedDocumentAsync();
                }
            }
        }

        public PsvDocumentGroupSummary SelectedDocumentGroup
        {
            get { return _selectedDocumentGroup; }
            set
            {
                if (SetProperty(ref _selectedDocumentGroup, value))
                {
                    RefreshDocumentLinesView();
                }
            }
        }

        public PsvDocumentLine SelectedDocumentLine
        {
            get { return _selectedDocumentLine; }
            set
            {
                if (SetProperty(ref _selectedDocumentLine, value))
                {
                    if (CanUseLineAsCloneSource(value))
                    {
                        _lastCloneSourceLine = value;
                    }

                    RaiseCommandStates();
                }
            }
        }

        public async Task InitializeAsync()
        {
            await ExecuteBusyOperationAsync(async delegate
            {
                await LoadCustomersCoreAsync();
                await RefreshDocumentsCoreAsync(null, null);
            });
        }

        private void AddDocument()
        {
            var request = _dialogService.ShowCreateDocumentDialog(new DocumentEditorResult
            {
                AcceptedOn = DateTime.Today,
                IssuedOn = null,
                DocumentNumber = string.Empty
            });

            if (request == null)
            {
                return;
            }

            if (string.IsNullOrWhiteSpace(request.DocumentNumber))
            {
                _dialogService.ShowWarning("Введите номер ПСВ.");
                return;
            }

            if (DocumentExistsInCollections(request.DocumentNumber, null))
            {
                _dialogService.ShowWarning("ПСВ с таким номером уже есть в списке.");
                return;
            }

            var draft = new PsvDocumentSummary
            {
                DocumentKey = Guid.NewGuid().ToString("N"),
                DocumentNumber = request.DocumentNumber.Trim(),
                AcceptedOn = request.AcceptedOn,
                IssuedOn = null,
                CustomerName = string.Empty,
                CustomerId = null,
                DepartmentName = string.Empty,
                ItemCount = 0,
                IssuedCount = 0,
                PassedCount = 0,
                FailedCount = 0,
                IsDraft = true
            };

            _draftDocuments.Add(draft);
            InsertDraftIntoCollection(draft);
            DocumentsView.Refresh();
            SelectedDocument = draft;
            DocumentStatusText = BuildDocumentStatusText(Documents.Count);
        }

        private void ApplySelectedCustomer()
        {
            if (SelectedDocument == null || !SelectedDocument.IsDraft)
            {
                return;
            }

            SelectedDocument.CustomerId = SelectedCustomerId;
            var customer = Customers.FirstOrDefault(delegate(CustomerReference item) { return item.CustomerId == SelectedCustomerId; });
            SelectedDocument.CustomerName = customer == null ? string.Empty : customer.CustomerName;
            DocumentsView.Refresh();
            RaiseCommandStates();
        }

        private bool CanSaveDocument()
        {
            if (IsBusy || SelectedDocument == null)
            {
                return false;
            }

            if (IsDocumentClosed(SelectedDocument))
            {
                return false;
            }

            if (!SelectedDocument.IsDraft)
            {
                return true;
            }

            return GetPendingLines(SelectedDocument).Count > 0;
        }

        private bool CanDeleteSelectedGroups()
        {
            return CanModifySelectedDocument()
                   && GetDeleteTargetGroups().Count > 0;
        }

        private bool CanDeleteSelectedLines()
        {
            return CanModifySelectedDocument()
                   && GetDeleteTargetLines().Count > 0;
        }

        private bool CanPrintSelectedDocument()
        {
            return !IsBusy
                   && SelectedDocument != null
                   && !SelectedDocument.IsDraft;
        }

        private bool CanEditSelectedLineVerification()
        {
            var targetLines = GetVerificationTargetLines();
            return CanModifySelectedDocument()
                   && targetLines.Count > 0
                   && targetLines.All(delegate(PsvDocumentLine line) { return !HasVerificationData(line); });
        }

        private bool CanCloneSelectedLineVerification()
        {
            var sourceLine = ResolveCloneSourceLine();
            return CanModifySelectedDocument()
                   && CanUseLineAsCloneSource(sourceLine)
                   && GetCheckedCloneTargetLines(sourceLine).Count > 0;
        }

        private bool CanPrintSelectedVerificationDocument()
        {
            return !IsBusy && HasPrintableVerificationDocument(SelectedDocumentLine);
        }

        private bool CanResetSelectedLineVerification()
        {
            var targetLines = GetVerificationTargetLines();
            return CanModifySelectedDocument()
                   && targetLines.Count > 0
                   && targetLines.All(HasVerificationData);
        }

        private bool CanModifySelectedDocument()
        {
            return !IsBusy
                   && SelectedDocument != null
                   && !IsDocumentClosed(SelectedDocument);
        }

        private bool CanAddInstrumentsToSelectedDocument()
        {
            return CanModifySelectedDocument() && SelectedDocument.CustomerId.HasValue;
        }

        private static bool IsDocumentClosed(PsvDocumentSummary document)
        {
            return document != null && document.IssuedOn.HasValue;
        }

        private static string BuildSerialNumbersText(IEnumerable<PsvDocumentLine> lines)
        {
            var serialNumbers = (lines ?? Enumerable.Empty<PsvDocumentLine>())
                .Select(delegate(PsvDocumentLine line)
                {
                    return line == null || string.IsNullOrWhiteSpace(line.SerialNumber)
                        ? null
                        : line.SerialNumber.Trim();
                })
                .Where(delegate(string serialNumber) { return !string.IsNullOrWhiteSpace(serialNumber); })
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .OrderBy(delegate(string serialNumber) { return serialNumber; }, StringComparer.OrdinalIgnoreCase)
                .ToList();

            return serialNumbers.Count == 0 ? string.Empty : string.Join(", ", serialNumbers.ToArray());
        }

        private static bool HasVerificationData(PsvDocumentLine line)
        {
            return line != null
                   && (line.IsPassed.HasValue
                       || line.VerificationPerformedOn.HasValue
                       || line.VerifierId.HasValue
                       || !string.IsNullOrWhiteSpace(line.StickerNumber)
                       || line.VerificationDocumentFormId.HasValue
                       || line.VerificationDocumentLinkTypeId.HasValue
                       || !string.IsNullOrWhiteSpace(line.VerificationDocumentNumber)
                       || line.VerificationDocumentDate.HasValue
                       || !string.IsNullOrWhiteSpace(line.RejectionReason));
        }

        private static bool CanUseLineAsCloneSource(PsvDocumentLine line)
        {
            if (line == null || !line.IsPassed.HasValue || !line.VerifierId.HasValue)
            {
                return false;
            }

            if (!line.VerificationPerformedOn.HasValue && !line.VerificationDocumentDate.HasValue)
            {
                return false;
            }

            if (!line.IsPassed.Value
                && (string.IsNullOrWhiteSpace(line.RejectionReason)
                    || string.IsNullOrWhiteSpace(line.VerificationDocumentNumber)))
            {
                return false;
            }

            if (!string.IsNullOrWhiteSpace(line.VerificationDocumentNumber)
                && (!line.VerificationDocumentFormId.HasValue || !line.VerificationDocumentLinkTypeId.HasValue))
            {
                return false;
            }

            return true;
        }

        private static bool HasPrintableVerificationDocument(PsvDocumentLine line)
        {
            if (line == null || !line.IsPassed.HasValue)
            {
                return false;
            }

            if (string.IsNullOrWhiteSpace(line.VerificationDocumentNumber))
            {
                return false;
            }

            if (!line.VerificationPerformedOn.HasValue && !line.VerificationDocumentDate.HasValue)
            {
                return false;
            }

            if (!line.IsPassed.Value && string.IsNullOrWhiteSpace(line.RejectionReason))
            {
                return false;
            }

            return true;
        }

        private bool CanEditVerificationFromDoubleClick(PsvDocumentLine line)
        {
            return CanModifySelectedDocument()
                   && line != null
                   && line.IsPassed.HasValue
                   && HasVerificationData(line);
        }

        private List<PsvDocumentLine> GetCheckedDocumentLines()
        {
            return DocumentLinesView.Cast<object>()
                .OfType<PsvDocumentLine>()
                .Where(delegate(PsvDocumentLine line) { return line.IsBatchSelected; })
                .ToList();
        }

        private List<PsvDocumentGroupSummary> GetCheckedDocumentGroups()
        {
            return DocumentGroupSummaries
                .Where(delegate(PsvDocumentGroupSummary group) { return group.IsBatchSelected; })
                .ToList();
        }

        private List<PsvDocumentGroupSummary> GetDeleteTargetGroups()
        {
            var checkedGroups = GetCheckedDocumentGroups();
            if (checkedGroups.Count > 0)
            {
                return checkedGroups;
            }

            return SelectedDocumentGroup == null
                ? new List<PsvDocumentGroupSummary>()
                : new List<PsvDocumentGroupSummary> { SelectedDocumentGroup };
        }

        private List<PsvDocumentLine> GetVerificationTargetLines()
        {
            var checkedLines = GetCheckedDocumentLines();
            if (checkedLines.Count > 0)
            {
                return checkedLines;
            }

            return SelectedDocumentLine == null
                ? new List<PsvDocumentLine>()
                : new List<PsvDocumentLine> { SelectedDocumentLine };
        }

        private List<PsvDocumentLine> GetDeleteTargetLines()
        {
            var checkedLines = GetCheckedDocumentLines();
            if (checkedLines.Count > 0)
            {
                return checkedLines;
            }

            return SelectedDocumentLine == null
                ? new List<PsvDocumentLine>()
                : new List<PsvDocumentLine> { SelectedDocumentLine };
        }

        private List<PsvDocumentLine> GetCheckedCloneTargetLines(PsvDocumentLine sourceLine)
        {
            if (sourceLine == null)
            {
                return new List<PsvDocumentLine>();
            }

            return GetCheckedDocumentLines()
                .Where(delegate(PsvDocumentLine line)
                {
                    return line != null
                           && !ReferenceEquals(line, sourceLine)
                           && BelongsToSameGroup(line, sourceLine);
                })
                .ToList();
        }

        private PsvDocumentLine ResolveCloneSourceLine()
        {
            if (CanUseLineAsCloneSource(SelectedDocumentLine))
            {
                return SelectedDocumentLine;
            }

            if (_lastCloneSourceLine != null
                && DocumentLines.Contains(_lastCloneSourceLine)
                && CanUseLineAsCloneSource(_lastCloneSourceLine))
            {
                return _lastCloneSourceLine;
            }

            var checkedSourceLines = GetCheckedDocumentLines()
                .Where(CanUseLineAsCloneSource)
                .ToList();

            return checkedSourceLines.Count == 1 ? checkedSourceLines[0] : null;
        }

        private void ClearCollections<T>(ObservableCollection<T> collection)
        {
            collection.Clear();
        }

        private void ClearDocumentLines()
        {
            foreach (var line in DocumentLines.ToList())
            {
                UnsubscribeFromDocumentLine(line);
            }

            ClearCollections(DocumentLines);
            HeaderInstrumentCount = 0;
        }

        private void ClearDocumentGroups()
        {
            foreach (var group in DocumentGroupSummaries.ToList())
            {
                UnsubscribeFromDocumentGroup(group);
            }

            ClearCollections(DocumentGroupSummaries);
        }

        private void SubscribeToDocumentLine(PsvDocumentLine line)
        {
            if (line == null)
            {
                return;
            }

            line.PropertyChanged -= DocumentLine_PropertyChanged;
            line.PropertyChanged += DocumentLine_PropertyChanged;
        }

        private void SubscribeToDocumentGroup(PsvDocumentGroupSummary group)
        {
            if (group == null)
            {
                return;
            }

            group.PropertyChanged -= DocumentGroup_PropertyChanged;
            group.PropertyChanged += DocumentGroup_PropertyChanged;
        }

        private void UnsubscribeFromDocumentLine(PsvDocumentLine line)
        {
            if (line == null)
            {
                return;
            }

            line.PropertyChanged -= DocumentLine_PropertyChanged;
        }

        private void UnsubscribeFromDocumentGroup(PsvDocumentGroupSummary group)
        {
            if (group == null)
            {
                return;
            }

            group.PropertyChanged -= DocumentGroup_PropertyChanged;
        }

        private void DocumentLine_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (string.Equals(e.PropertyName, "IsBatchSelected", StringComparison.Ordinal))
            {
                UpdateLineStatus();
                RaiseCommandStates();
            }
        }

        private void DocumentGroup_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (string.Equals(e.PropertyName, "IsBatchSelected", StringComparison.Ordinal))
            {
                RaiseCommandStates();
            }
        }

        private void ClearHeader()
        {
            DocumentNumberEditor = string.Empty;
            HeaderReceivedOn = null;
            HeaderIssuedOn = null;
            HeaderDepartmentName = string.Empty;
            SelectedCustomerId = null;
        }

        private VerificationEditSeed CreateVerificationSeed(PsvDocumentLine line, bool isPassed, IReadOnlyList<DocumentFormReference> documentForms)
        {
            var selectedForm = documentForms == null
                ? null
                : documentForms.FirstOrDefault(delegate(DocumentFormReference item)
                {
                    return item.DocumentFormId == line.VerificationDocumentFormId;
                });

            return new VerificationEditSeed
            {
                DocumentForm = selectedForm,
                IsPassed = isPassed,
                RejectionReason = isPassed ? string.Empty : line.RejectionReason,
                StickerNumber = isPassed ? line.StickerNumber : string.Empty,
                VerificationDate = line.VerificationPerformedOn ?? line.VerificationDocumentDate ?? DateTime.Today,
                VerificationDocumentNumber = line.VerificationDocumentNumber,
                VerifierId = line.VerifierId
            };
        }

        private CloneVerificationSeed CreateCloneVerificationSeed(PsvDocumentLine sourceLine)
        {
            var documentDisplay = string.IsNullOrWhiteSpace(sourceLine.VerificationDocumentDisplay)
                ? "не указан"
                : sourceLine.VerificationDocumentDisplay;
            var rejectionPart = sourceLine.IsPassed == false && !string.IsNullOrWhiteSpace(sourceLine.RejectionReason)
                ? string.Format(" Причина: {0}.", sourceLine.RejectionReason.Trim())
                : string.Empty;

            return new CloneVerificationSeed
            {
                SourceSerialNumber = sourceLine.SerialNumber,
                VerificationSummary = string.Format(
                    "Результат: {0}. Дата поверки: {1:d}. Поверитель: {2}. Документ: {3}.{4}",
                    sourceLine.ResultText,
                    sourceLine.VerificationPerformedOn ?? sourceLine.VerificationDocumentDate ?? DateTime.Today,
                    string.IsNullOrWhiteSpace(sourceLine.VerifierName) ? "не указан" : sourceLine.VerifierName,
                    documentDisplay,
                    rejectionPart)
            };
        }

        private VerificationEditResult CreateVerificationResultFromLine(PsvDocumentLine sourceLine)
        {
            return new VerificationEditResult
            {
                DocumentFormId = string.IsNullOrWhiteSpace(sourceLine.VerificationDocumentNumber)
                    ? 0
                    : sourceLine.VerificationDocumentFormId.GetValueOrDefault(),
                DocumentLinkTypeId = string.IsNullOrWhiteSpace(sourceLine.VerificationDocumentNumber)
                    ? 0
                    : sourceLine.VerificationDocumentLinkTypeId.GetValueOrDefault(),
                IsPassed = sourceLine.IsPassed.GetValueOrDefault(),
                RejectionReason = string.IsNullOrWhiteSpace(sourceLine.RejectionReason) ? string.Empty : sourceLine.RejectionReason.Trim(),
                StickerNumber = string.IsNullOrWhiteSpace(sourceLine.StickerNumber) ? string.Empty : sourceLine.StickerNumber.Trim(),
                VerificationDate = sourceLine.VerificationPerformedOn ?? sourceLine.VerificationDocumentDate ?? DateTime.Today,
                VerificationDocumentNumber = string.IsNullOrWhiteSpace(sourceLine.VerificationDocumentNumber) ? string.Empty : sourceLine.VerificationDocumentNumber.Trim(),
                VerifierId = sourceLine.VerifierId.GetValueOrDefault(),
                VerifierName = string.IsNullOrWhiteSpace(sourceLine.VerifierName) ? string.Empty : sourceLine.VerifierName
            };
        }

        private bool Contains(string source, string filter)
        {
            return !string.IsNullOrWhiteSpace(source)
                   && source.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
        }

        private static bool BelongsToSameGroup(PsvDocumentLine candidate, PsvDocumentLine sourceLine)
        {
            return candidate != null
                   && sourceLine != null
                   && string.Equals(candidate.InstrumentType ?? string.Empty, sourceLine.InstrumentType ?? string.Empty, StringComparison.OrdinalIgnoreCase)
                   && string.Equals(candidate.RangeText ?? string.Empty, sourceLine.RangeText ?? string.Empty, StringComparison.OrdinalIgnoreCase)
                   && string.Equals(candidate.RegistryNumber ?? string.Empty, sourceLine.RegistryNumber ?? string.Empty, StringComparison.OrdinalIgnoreCase);
        }

        private static string NormalizeSerialNumber(string serialNumber)
        {
            return string.IsNullOrWhiteSpace(serialNumber) ? string.Empty : serialNumber.Trim();
        }

        private static string BuildSerialNumberPreview(IEnumerable<string> serialNumbers)
        {
            var materialized = serialNumbers == null
                ? new List<string>()
                : serialNumbers
                    .Where(delegate(string serialNumber) { return !string.IsNullOrWhiteSpace(serialNumber); })
                    .ToList();
            var preview = materialized.Take(5).ToList();
            if (preview.Count == 0)
            {
                return string.Empty;
            }

            var text = string.Join(", ", preview);
            if (materialized.Count > preview.Count)
            {
                text += ", ...";
            }

            return text;
        }

        private string BuildOpenDocumentConflictMessage(IEnumerable<OpenDocumentConflictInfo> conflicts)
        {
            var materializedConflicts = NormalizeOpenDocumentConflicts(conflicts);
            if (materializedConflicts.Count == 0)
            {
                return "Прибор уже находится в другой открытой ПСВ этого заказчика.";
            }

            var preview = materializedConflicts
                .Take(5)
                .Select(delegate(OpenDocumentConflictInfo conflict)
                {
                    return string.Format("зав. № {0} -> {1}", conflict.SerialNumber, conflict.DocumentNumber);
                })
                .ToList();

            var suffix = materializedConflicts.Count > preview.Count
                ? string.Format(" Еще конфликтов: {0}.", materializedConflicts.Count - preview.Count)
                : string.Empty;

            return string.Format(
                "Приборы уже находятся в других открытых ПСВ этого заказчика: {0}.{1}",
                string.Join("; ", preview.ToArray()),
                suffix);
        }

        private List<OpenDocumentConflictInfo> FindOpenDocumentConflicts(IEnumerable<PsvDocumentLine> candidateLines)
        {
            if (SelectedDocument == null || !SelectedDocument.CustomerId.HasValue || candidateLines == null)
            {
                return new List<OpenDocumentConflictInfo>();
            }

            var conflicts = FindPendingOpenDocumentConflicts(SelectedDocument, candidateLines);
            conflicts.AddRange(_service.FindOpenDocumentConflicts(
                SelectedDocument.CustomerId.Value,
                SelectedDocument.IsDraft ? null : SelectedDocument.DocumentNumber,
                candidateLines));

            return NormalizeOpenDocumentConflicts(conflicts);
        }

        private List<OpenDocumentConflictInfo> FindPendingOpenDocumentConflicts(PsvDocumentSummary currentDocument, IEnumerable<PsvDocumentLine> candidateLines)
        {
            if (currentDocument == null || !currentDocument.CustomerId.HasValue || candidateLines == null)
            {
                return new List<OpenDocumentConflictInfo>();
            }

            var candidateKeys = new HashSet<string>(
                candidateLines
                    .Where(delegate(PsvDocumentLine line)
                    {
                        return line != null
                               && line.TypeSizeId > 0
                               && !string.IsNullOrWhiteSpace(line.SerialNumber);
                    })
                    .Select(delegate(PsvDocumentLine line) { return line.OpenDocumentConflictKey; }),
                StringComparer.OrdinalIgnoreCase);

            if (candidateKeys.Count == 0)
            {
                return new List<OpenDocumentConflictInfo>();
            }

            var conflicts = new List<OpenDocumentConflictInfo>();

            foreach (var pendingLinesByDocument in _pendingLinesByDocumentKey)
            {
                if (string.Equals(pendingLinesByDocument.Key, currentDocument.DocumentKey, StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                var otherDocument = Documents.FirstOrDefault(delegate(PsvDocumentSummary document)
                {
                    return string.Equals(document.DocumentKey, pendingLinesByDocument.Key, StringComparison.OrdinalIgnoreCase);
                });

                if (otherDocument == null
                    || !otherDocument.CustomerId.HasValue
                    || otherDocument.CustomerId.Value != currentDocument.CustomerId.Value)
                {
                    continue;
                }

                foreach (var otherLine in pendingLinesByDocument.Value.Where(delegate(PsvDocumentLine line)
                {
                    return line != null
                           && line.TypeSizeId > 0
                           && !string.IsNullOrWhiteSpace(line.SerialNumber);
                }))
                {
                    if (!candidateKeys.Contains(otherLine.OpenDocumentConflictKey))
                    {
                        continue;
                    }

                    conflicts.Add(new OpenDocumentConflictInfo
                    {
                        DocumentNumber = otherDocument.DocumentNumber,
                        TypeSizeId = otherLine.TypeSizeId,
                        SerialNumber = otherLine.SerialNumber
                    });
                }
            }

            return NormalizeOpenDocumentConflicts(conflicts);
        }

        private static List<OpenDocumentConflictInfo> NormalizeOpenDocumentConflicts(IEnumerable<OpenDocumentConflictInfo> conflicts)
        {
            return (conflicts ?? Enumerable.Empty<OpenDocumentConflictInfo>())
                .Where(delegate(OpenDocumentConflictInfo conflict) { return conflict != null; })
                .GroupBy(delegate(OpenDocumentConflictInfo conflict)
                {
                    return string.Format(
                        "{0}|{1}|{2}",
                        conflict.DocumentNumber ?? string.Empty,
                        conflict.TypeSizeId,
                        conflict.SerialNumber ?? string.Empty);
                }, StringComparer.OrdinalIgnoreCase)
                .Select(delegate(IGrouping<string, OpenDocumentConflictInfo> group) { return group.First(); })
                .OrderBy(delegate(OpenDocumentConflictInfo conflict) { return conflict.SerialNumber ?? string.Empty; })
                .ThenBy(delegate(OpenDocumentConflictInfo conflict) { return conflict.DocumentNumber ?? string.Empty; })
                .ToList();
        }

        private PsvDocumentLine CreatePendingLine(AvailableInstrumentItem item)
        {
            return new PsvDocumentLine
            {
                CardId = 0,
                InstrumentId = item.InstrumentId,
                TypeSizeId = item.TypeSizeId,
                SerialNumber = item.SerialNumber,
                InventoryNumber = item.InventoryNumber,
                CustomerName = item.CustomerName,
                InstrumentType = item.InstrumentType,
                InstrumentName = item.InstrumentName,
                MeasurementArea = item.MeasurementArea,
                RangeText = item.RangeText,
                RegistryNumber = item.RegistryNumber,
                AccuracyText = item.AccuracyText,
                VerificationType = string.Empty,
                PeriodMonths = 0,
                AcceptedOn = HeaderReceivedOn,
                IssuedOn = null,
                IsPassed = null,
                VerificationPerformedOn = null,
                VerifierId = null,
                VerifierName = string.Empty,
                StickerNumber = string.Empty,
                VerificationDocumentFormId = null,
                VerificationDocumentLinkTypeId = null,
                VerificationDocumentNumber = string.Empty,
                VerificationDocumentDate = null,
                RejectionReason = string.Empty,
                Notes = string.Empty,
                IsPendingInsert = true
            };
        }

        private PsvDocumentLine CreatePendingTypeLine(AvailableInstrumentItem item, string serialNumber)
        {
            return new PsvDocumentLine
            {
                CardId = 0,
                InstrumentId = 0,
                TypeSizeId = item.TypeSizeId,
                SerialNumber = string.IsNullOrWhiteSpace(serialNumber) ? string.Empty : serialNumber.Trim(),
                InventoryNumber = string.Empty,
                CustomerName = SelectedDocument == null ? string.Empty : SelectedDocument.CustomerName,
                InstrumentType = item.InstrumentType,
                InstrumentName = item.InstrumentName,
                MeasurementArea = item.MeasurementArea,
                RangeText = item.RangeText,
                RegistryNumber = item.RegistryNumber,
                AccuracyText = item.AccuracyText,
                VerificationType = string.Empty,
                PeriodMonths = 0,
                AcceptedOn = HeaderReceivedOn,
                IssuedOn = null,
                IsPassed = null,
                VerificationPerformedOn = null,
                VerifierId = null,
                VerifierName = string.Empty,
                StickerNumber = string.Empty,
                VerificationDocumentFormId = null,
                VerificationDocumentLinkTypeId = null,
                VerificationDocumentNumber = string.Empty,
                VerificationDocumentDate = null,
                RejectionReason = string.Empty,
                Notes = string.Empty,
                IsPendingInsert = true
            };
        }

        private static void ApplyVerificationResultCore(PsvDocumentLine line, VerificationEditResult result)
        {
            if (line == null || result == null)
            {
                return;
            }

            line.IsPassed = result.IsPassed;
            line.VerificationPerformedOn = result.VerificationDate;
            line.VerifierId = result.VerifierId;
            line.VerifierName = result.VerifierName;
            line.StickerNumber = result.IsPassed ? result.StickerNumber : string.Empty;
            line.VerificationDocumentFormId = string.IsNullOrWhiteSpace(result.VerificationDocumentNumber) ? (int?)null : result.DocumentFormId;
            line.VerificationDocumentLinkTypeId = string.IsNullOrWhiteSpace(result.VerificationDocumentNumber) ? (int?)null : result.DocumentLinkTypeId;
            line.VerificationDocumentNumber = result.VerificationDocumentNumber;
            line.VerificationDocumentDate = result.VerificationDate;
            line.RejectionReason = result.IsPassed ? string.Empty : result.RejectionReason;
        }

        private void ApplyVerificationResultToLine(PsvDocumentLine line, VerificationEditResult result)
        {
            ApplyVerificationResultCore(line, result);
            RefreshAfterLineVerificationChanged(line);
        }

        private static void ClearVerificationFromLineCore(PsvDocumentLine line)
        {
            if (line == null)
            {
                return;
            }

            line.IsPassed = null;
            line.VerificationPerformedOn = null;
            line.VerifierId = null;
            line.VerifierName = string.Empty;
            line.StickerNumber = string.Empty;
            line.VerificationDocumentFormId = null;
            line.VerificationDocumentLinkTypeId = null;
            line.VerificationDocumentNumber = string.Empty;
            line.VerificationDocumentDate = null;
            line.RejectionReason = string.Empty;
        }

        private void ClearVerificationFromLine(PsvDocumentLine line)
        {
            ClearVerificationFromLineCore(line);
            RefreshAfterLineVerificationChanged(line);
        }

        private void CloneSelectedLineVerificationAsync()
        {
            var sourceLine = ResolveCloneSourceLine();
            if (!CanUseLineAsCloneSource(sourceLine))
            {
                _dialogService.ShowWarning("Выберите строку-источник с заполненной поверкой. Источник можно выделить строкой или отметить единственную подходящую строку чекбоксом.");
                return;
            }

            var checkedLines = GetCheckedDocumentLines();
            if (checkedLines.Count == 0)
            {
                _dialogService.ShowWarning("Отметьте чекбоксами строки, в которые нужно склонировать поверочные данные.");
                return;
            }

            var includedSourceSerialNumber = checkedLines.Any(delegate(PsvDocumentLine line) { return ReferenceEquals(line, sourceLine); });
            var matchedLines = GetCheckedCloneTargetLines(sourceLine);
            var targetLines = matchedLines.Where(delegate(PsvDocumentLine line) { return !HasVerificationData(line); }).ToList();
            var skippedWithExistingVerificationCount = matchedLines.Count - targetLines.Count;
            if (targetLines.Count == 0)
            {
                ShowCloneVerificationResult(0, skippedWithExistingVerificationCount, includedSourceSerialNumber, true);
                return;
            }

            var result = CreateVerificationResultFromLine(sourceLine);
            var pendingLines = targetLines.Where(delegate(PsvDocumentLine line) { return line.IsPendingInsert; }).ToList();
            var persistedCardIds = targetLines
                .Where(delegate(PsvDocumentLine line) { return !line.IsPendingInsert; })
                .Select(delegate(PsvDocumentLine line) { return line.CardId; })
                .Distinct()
                .ToList();

            if (persistedCardIds.Count == 0)
            {
                foreach (var pendingLine in pendingLines)
                {
                    ApplyVerificationResultCore(pendingLine, result);
                }

                RefreshAfterLineVerificationChanged(sourceLine);
                ShowCloneVerificationResult(targetLines.Count, skippedWithExistingVerificationCount, includedSourceSerialNumber, false);
                return;
            }

            RunBusyOperation(async delegate
            {
                await _service.SaveLineVerificationAsync(persistedCardIds, result);

                foreach (var pendingLine in pendingLines)
                {
                    ApplyVerificationResultCore(pendingLine, result);
                }

                await ReloadSelectedDocumentLinesAsync();
                ShowCloneVerificationResult(targetLines.Count, skippedWithExistingVerificationCount, includedSourceSerialNumber, false);
            });
        }

        private void ShowCloneVerificationResult(
            int clonedCount,
            int skippedWithExistingVerificationCount,
            bool includedSourceSerialNumber,
            bool showWarning)
        {
            var messages = new List<string>();
            if (clonedCount > 0)
            {
                messages.Add(string.Format("Клонировано поверок: {0}.", clonedCount));
            }

            if (skippedWithExistingVerificationCount > 0)
            {
                messages.Add(string.Format("Пропущено строк с уже заполненной поверкой: {0}.", skippedWithExistingVerificationCount));
            }

            if (includedSourceSerialNumber)
            {
                messages.Add("Номер строки-источника пропущен.");
            }

            if (messages.Count == 0)
            {
                messages.Add("Подходящих отмеченных строк для клонирования не найдено.");
            }

            var message = string.Join(" ", messages.ToArray());
            if (showWarning || clonedCount == 0)
            {
                _dialogService.ShowWarning(message);
                return;
            }

            _dialogService.ShowInfo(message);
        }

        private void EditLineVerificationAsync(bool isPassed)
        {
            var targetLines = GetVerificationTargetLines();
            if (targetLines.Count == 0)
            {
                return;
            }

            EditLineVerificationCoreAsync(targetLines, isPassed);
        }

        public void TryEditVerificationFromDoubleClick(PsvDocumentLine line)
        {
            if (line == null)
            {
                return;
            }

            SelectedDocumentLine = line;
            if (!CanEditVerificationFromDoubleClick(line))
            {
                return;
            }

            EditLineVerificationCoreAsync(new[] { line }, line.IsPassed.Value);
        }

        private async void EditLineVerificationCoreAsync(IReadOnlyList<PsvDocumentLine> targetLines, bool isPassed)
        {
            IReadOnlyList<DocumentFormReference> documentForms;
            IReadOnlyList<PersonReference> verifiers;

            try
            {
                IsBusy = true;
                verifiers = await _service.LoadVerifiersAsync();
                documentForms = await _service.LoadVerificationDocumentFormsAsync(isPassed);
            }
            catch (Exception ex)
            {
                _dialogService.ShowError(ex.Message);
                return;
            }
            finally
            {
                IsBusy = false;
            }

            var seed = CreateVerificationSeed(targetLines[0], isPassed, documentForms);
            var result = _dialogService.ShowVerificationDialog(seed, verifiers, documentForms);
            if (result == null)
            {
                return;
            }

            var pendingLines = targetLines.Where(delegate(PsvDocumentLine line) { return line.IsPendingInsert; }).ToList();
            var persistedCardIds = targetLines.Where(delegate(PsvDocumentLine line) { return !line.IsPendingInsert; })
                .Select(delegate(PsvDocumentLine line) { return line.CardId; })
                .Distinct()
                .ToList();

            if (persistedCardIds.Count == 0)
            {
                foreach (var pendingLine in pendingLines)
                {
                    ApplyVerificationResultCore(pendingLine, result);
                    pendingLine.IsBatchSelected = false;
                }

                RefreshAfterLineVerificationChanged(targetLines[0]);
                return;
            }

            RunBusyOperation(async delegate
            {
                await _service.SaveLineVerificationAsync(persistedCardIds, result);

                foreach (var pendingLine in pendingLines)
                {
                    ApplyVerificationResultCore(pendingLine, result);
                    pendingLine.IsBatchSelected = false;
                }

                await ReloadSelectedDocumentLinesAsync();
            });
        }

        private void RefreshAfterLineVerificationChanged(PsvDocumentLine line)
        {
            var previousGroup = SelectedDocumentGroup;
            RebuildDocumentGroupSummaries(DocumentLines);
            SelectedDocumentGroup = FindMatchingGroup(previousGroup) ?? DocumentGroupSummaries.FirstOrDefault();
            RefreshDocumentLinesView();
            SelectedDocumentLine = line;
            RaiseCommandStates();
        }

        private void ResetSelectedLineVerificationAsync()
        {
            var targetLines = GetVerificationTargetLines();
            if (targetLines.Count == 0)
            {
                return;
            }

            var pendingLines = targetLines.Where(delegate(PsvDocumentLine line) { return line.IsPendingInsert; }).ToList();
            var persistedCardIds = targetLines.Where(delegate(PsvDocumentLine line) { return !line.IsPendingInsert; })
                .Select(delegate(PsvDocumentLine line) { return line.CardId; })
                .Distinct()
                .ToList();

            if (persistedCardIds.Count == 0)
            {
                foreach (var pendingLine in pendingLines)
                {
                    ClearVerificationFromLineCore(pendingLine);
                    pendingLine.IsBatchSelected = false;
                }

                RefreshAfterLineVerificationChanged(targetLines[0]);
                return;
            }

            RunBusyOperation(async delegate
            {
                await _service.ResetLineVerificationAsync(persistedCardIds);

                foreach (var pendingLine in pendingLines)
                {
                    ClearVerificationFromLineCore(pendingLine);
                    pendingLine.IsBatchSelected = false;
                }

                await ReloadSelectedDocumentLinesAsync();
            });
        }

        private void DeleteDocumentAsync()
        {
            if (SelectedDocument == null)
            {
                return;
            }

            var selectedDocument = SelectedDocument;
            if (!_dialogService.Confirm(string.Format("Удалить ПСВ \"{0}\"?", selectedDocument.DocumentNumber)))
            {
                return;
            }

            if (selectedDocument.IsDraft)
            {
                DeleteDraftDocument(selectedDocument);
                return;
            }

            RunBusyOperation(async delegate
            {
                var result = await _service.DeleteDocumentAsync(selectedDocument.DocumentNumber);
                _pendingLinesByDocumentKey.Remove(selectedDocument.DocumentKey);
                await RefreshDocumentsCoreAsync(null, null);
                _dialogService.ShowInfo(
                    string.Format(
                        "Удалено строк EKZMKFCTVL: {0}. Удалено строк EKZMK: {1}. Удалено связанных DMS: {2}.",
                        result.DeletedEkzMkFctvlCount,
                        result.DeletedEkzMkCount,
                        result.DeletedDmsCount));
            });
        }

        private void DeleteDraftDocument(PsvDocumentSummary draft)
        {
            _draftDocuments.RemoveAll(delegate(PsvDocumentSummary item) { return item.DocumentKey == draft.DocumentKey; });
            _pendingLinesByDocumentKey.Remove(draft.DocumentKey);
            Documents.Remove(draft);
            DocumentsView.Refresh();
            SelectedDocument = Documents.Count > 0 ? Documents[0] : null;
            DocumentStatusText = BuildDocumentStatusText(Documents.Count);
        }

        private void PrintSelectedDocumentAsync()
        {
            if (SelectedDocument == null)
            {
                return;
            }

            var selectedDocument = SelectedDocument;
            if (selectedDocument.IsDraft)
            {
                _dialogService.ShowWarning("Черновик нельзя распечатать. Сначала сохраните ПСВ.");
                return;
            }

            if (string.IsNullOrWhiteSpace(selectedDocument.DocumentNumber))
            {
                _dialogService.ShowWarning("Для печати не указан номер ПСВ.");
                return;
            }

            RunBusyOperation(async delegate
            {
                var persistedLines = await _service.LoadDocumentLinesAsync(selectedDocument.DocumentNumber);
                var linesToPrint = MergeDocumentLinesForPrint(selectedDocument, persistedLines);
                if (linesToPrint.Count == 0)
                {
                    _dialogService.ShowWarning("В выбранном ПСВ нет строк для печати.");
                    return;
                }

                _printService.PrintDocument(selectedDocument, linesToPrint);
            });
        }

        private void DeleteSelectedGroupsAsync()
        {
            if (SelectedDocument == null)
            {
                return;
            }

            var targetGroups = GetDeleteTargetGroups();
            if (targetGroups.Count == 0)
            {
                return;
            }

            var targetLines = DocumentLines
                .Where(delegate(PsvDocumentLine line)
                {
                    return targetGroups.Any(delegate(PsvDocumentGroupSummary group) { return group.Matches(line); });
                })
                .ToList();

            if (targetLines.Count == 0)
            {
                _dialogService.ShowWarning("Для удаления не найдено строк выбранных групп.");
                return;
            }

            if (!_dialogService.Confirm(string.Format(
                "Удалить из ПСВ \"{0}\" групп: {1}, строк приборов: {2}?",
                SelectedDocument.DocumentNumber,
                targetGroups.Count,
                targetLines.Count)))
            {
                return;
            }

            var selectedDocument = SelectedDocument;
            var selectedDocumentKey = selectedDocument.DocumentKey;
            var selectedDocumentNumber = selectedDocument.DocumentNumber;

            RunBusyOperation(async delegate
            {
                var pendingLines = GetPendingLines(selectedDocument);
                var pendingLinesToRemove = pendingLines
                    .Where(delegate(PsvDocumentLine line)
                    {
                        return line.IsPendingInsert
                               && targetGroups.Any(delegate(PsvDocumentGroupSummary group) { return group.Matches(line); });
                    })
                    .ToList();

                var persistedCardIds = targetLines
                    .Where(delegate(PsvDocumentLine line) { return !line.IsPendingInsert; })
                    .Select(delegate(PsvDocumentLine line) { return line.CardId; })
                    .Distinct()
                    .ToList();

                foreach (var pendingLine in pendingLinesToRemove)
                {
                    pendingLines.Remove(pendingLine);
                }

                var deletedPendingCount = pendingLinesToRemove.Count;
                var deletedResult = new DocumentGroupDeleteResult();

                if (persistedCardIds.Count > 0)
                {
                    deletedResult = await _service.DeleteDocumentGroupsAsync(selectedDocumentNumber, persistedCardIds);
                }

                var remainingPendingCount = pendingLines.Count;
                var remainingPersistedCount = DocumentLines.Count(delegate(PsvDocumentLine line) { return !line.IsPendingInsert; }) - persistedCardIds.Count;

                if (selectedDocument.IsDraft)
                {
                    UpdateDocumentSummaryFromLines(selectedDocument, pendingLines);
                    ApplyDocumentLines(pendingLines, SelectedDocumentGroup);
                    DocumentsView.Refresh();
                    DocumentStatusText = BuildDocumentStatusText(Documents.Count);
                }
                else if (remainingPersistedCount == 0 && remainingPendingCount > 0)
                {
                    if (!_draftDocuments.Any(delegate(PsvDocumentSummary draft) { return draft.DocumentKey == selectedDocumentKey; }))
                    {
                        _draftDocuments.Add(selectedDocument);
                    }

                    selectedDocument.IsDraft = true;
                    UpdateDocumentSummaryFromLines(selectedDocument, pendingLines);
                    await RefreshDocumentsCoreAsync(selectedDocumentKey, selectedDocumentNumber);
                }
                else
                {
                    await RefreshDocumentsCoreAsync(selectedDocumentKey, selectedDocumentNumber);
                }

                var messages = new List<string>();
                if (deletedPendingCount > 0)
                {
                    messages.Add(string.Format("Удалено черновых строк: {0}.", deletedPendingCount));
                }

                if (deletedResult.DeletedEkzMkFctvlCount > 0)
                {
                    messages.Add(string.Format("Удалено строк EKZMKFCTVL: {0}.", deletedResult.DeletedEkzMkFctvlCount));
                }

                if (deletedResult.DeletedEkzMkCount > 0)
                {
                    messages.Add(string.Format("Удалено строк EKZMK: {0}.", deletedResult.DeletedEkzMkCount));
                }

                if (deletedResult.DeletedDmsCount > 0)
                {
                    messages.Add(string.Format("Удалено связанных DMS: {0}.", deletedResult.DeletedDmsCount));
                }

                if (messages.Count > 0)
                {
                    _dialogService.ShowInfo(string.Join(" ", messages.ToArray()));
                }
            });
        }

        private void DeleteSelectedLinesAsync()
        {
            if (SelectedDocument == null)
            {
                return;
            }

            var targetLines = GetDeleteTargetLines();
            if (targetLines.Count == 0)
            {
                return;
            }

            if (!_dialogService.Confirm(string.Format(
                "Удалить из ПСВ \"{0}\" строк приборов: {1}?",
                SelectedDocument.DocumentNumber,
                targetLines.Count)))
            {
                return;
            }

            var selectedDocument = SelectedDocument;
            var selectedDocumentKey = selectedDocument.DocumentKey;
            var selectedDocumentNumber = selectedDocument.DocumentNumber;

            RunBusyOperation(async delegate
            {
                var pendingLines = GetPendingLines(selectedDocument);
                var pendingLinesToRemove = pendingLines
                    .Where(delegate(PsvDocumentLine line) { return targetLines.Any(delegate(PsvDocumentLine targetLine) { return ReferenceEquals(line, targetLine); }); })
                    .ToList();

                var persistedCardIds = targetLines
                    .Where(delegate(PsvDocumentLine line) { return !line.IsPendingInsert; })
                    .Select(delegate(PsvDocumentLine line) { return line.CardId; })
                    .Distinct()
                    .ToList();

                foreach (var pendingLine in pendingLinesToRemove)
                {
                    pendingLines.Remove(pendingLine);
                }

                var deletedPendingCount = pendingLinesToRemove.Count;
                var deletedResult = new DocumentGroupDeleteResult();

                if (persistedCardIds.Count > 0)
                {
                    deletedResult = await _service.DeleteDocumentGroupsAsync(selectedDocumentNumber, persistedCardIds);
                }

                var remainingPendingCount = pendingLines.Count;
                var remainingPersistedCount = DocumentLines.Count(delegate(PsvDocumentLine line) { return !line.IsPendingInsert; }) - persistedCardIds.Count;

                if (selectedDocument.IsDraft)
                {
                    UpdateDocumentSummaryFromLines(selectedDocument, pendingLines);
                    ApplyDocumentLines(pendingLines, SelectedDocumentGroup);
                    DocumentsView.Refresh();
                    DocumentStatusText = BuildDocumentStatusText(Documents.Count);
                }
                else if (remainingPersistedCount == 0 && remainingPendingCount > 0)
                {
                    if (!_draftDocuments.Any(delegate(PsvDocumentSummary draft) { return draft.DocumentKey == selectedDocumentKey; }))
                    {
                        _draftDocuments.Add(selectedDocument);
                    }

                    selectedDocument.IsDraft = true;
                    UpdateDocumentSummaryFromLines(selectedDocument, pendingLines);
                    await RefreshDocumentsCoreAsync(selectedDocumentKey, selectedDocumentNumber);
                }
                else
                {
                    await RefreshDocumentsCoreAsync(selectedDocumentKey, selectedDocumentNumber);
                }

                var messages = new List<string>();
                if (deletedPendingCount > 0)
                {
                    messages.Add(string.Format("Удалено черновых строк: {0}.", deletedPendingCount));
                }

                if (deletedResult.DeletedEkzMkFctvlCount > 0)
                {
                    messages.Add(string.Format("Удалено строк EKZMKFCTVL: {0}.", deletedResult.DeletedEkzMkFctvlCount));
                }

                if (deletedResult.DeletedEkzMkCount > 0)
                {
                    messages.Add(string.Format("Удалено строк EKZMK: {0}.", deletedResult.DeletedEkzMkCount));
                }

                if (deletedResult.DeletedDmsCount > 0)
                {
                    messages.Add(string.Format("Удалено связанных DMS: {0}.", deletedResult.DeletedDmsCount));
                }

                if (messages.Count > 0)
                {
                    _dialogService.ShowInfo(string.Join(" ", messages.ToArray()));
                }
            });
        }

        private bool DocumentExistsInCollections(string documentNumber, string excludeDocumentKey)
        {
            return Documents.Any(delegate(PsvDocumentSummary document)
            {
                return !string.Equals(document.DocumentKey, excludeDocumentKey, StringComparison.OrdinalIgnoreCase)
                       && string.Equals(document.DocumentNumber, documentNumber, StringComparison.OrdinalIgnoreCase);
            });
        }

        private void PrintSelectedVerificationDocumentAsync()
        {
            var selectedLine = SelectedDocumentLine;
            if (selectedLine == null)
            {
                _dialogService.ShowWarning("Сначала выберите строку прибора.");
                return;
            }

            if (!HasPrintableVerificationDocument(selectedLine))
            {
                _dialogService.ShowWarning("Для выбранной строки не указан печатаемый документ по поверке.");
                return;
            }

            RunBusyOperation(delegate
            {
                _printService.PrintVerificationDocument(selectedLine);
                return Task.FromResult(0);
            });
        }

        private static void UpdateDocumentSummaryFromLines(PsvDocumentSummary document, IEnumerable<PsvDocumentLine> lines)
        {
            if (document == null)
            {
                return;
            }

            var materializedLines = lines == null ? new List<PsvDocumentLine>() : lines.ToList();
            document.ItemCount = materializedLines.Count;
            document.PassedCount = materializedLines.Count(delegate(PsvDocumentLine line) { return line.IsPassed == true; });
            document.FailedCount = materializedLines.Count(delegate(PsvDocumentLine line) { return line.IsPassed == false; });
            document.IssuedCount = materializedLines.Count(delegate(PsvDocumentLine line) { return line.IssuedOn.HasValue; });
            document.SerialNumbersText = BuildSerialNumbersText(materializedLines);
        }

        private async Task ExecuteBusyOperationAsync(Func<Task> operation)
        {
            if (IsBusy)
            {
                return;
            }

            try
            {
                IsBusy = true;
                await operation();
            }
            catch (Exception ex)
            {
                _dialogService.ShowError(ex.Message);
            }
            finally
            {
                IsBusy = false;
            }
        }

        private async void RunBusyOperation(Func<Task> operation)
        {
            await ExecuteBusyOperationAsync(operation);
        }

        private bool FilterDocuments(object item)
        {
            var document = item as PsvDocumentSummary;
            if (document == null)
            {
                return false;
            }

            if (!string.IsNullOrWhiteSpace(DocumentFilterText)
                && !Contains(document.DocumentNumber, DocumentFilterText)
                && !Contains(document.CustomerName, DocumentFilterText)
                && !Contains(document.SerialNumbersText, DocumentFilterText))
            {
                return false;
            }

            return true;
        }

        private bool FilterDocumentGroups(object item)
        {
            var group = item as PsvDocumentGroupSummary;
            if (group == null)
            {
                return false;
            }

            if (string.IsNullOrWhiteSpace(GroupFilterText))
            {
                return true;
            }

            return Contains(group.InstrumentType, GroupFilterText)
                   || Contains(group.RangeText, GroupFilterText)
                   || Contains(group.RegistryNumber, GroupFilterText)
                   || Contains(group.SerialNumbersText, GroupFilterText);
        }

        private string BuildDocumentStatusText(int count)
        {
            if (ShowClosedDocuments)
            {
                return string.Format("Закрытых ПСВ за {0:yyyy}: {1}.", DateTime.Today, count);
            }

            return string.Format("Открытых ПСВ: {0}.", count);
        }

        private bool FilterDocumentLines(object item)
        {
            var line = item as PsvDocumentLine;
            if (line == null || SelectedDocumentGroup == null)
            {
                return false;
            }

            if (!SelectedDocumentGroup.Matches(line))
            {
                return false;
            }

            if (string.IsNullOrWhiteSpace(GroupDetailFilterText))
            {
                return true;
            }

            return Contains(line.SerialNumber, GroupDetailFilterText);
        }

        private PsvDocumentGroupSummary FindMatchingGroup(PsvDocumentGroupSummary group)
        {
            if (group == null)
            {
                return null;
            }

            return DocumentGroupSummaries.FirstOrDefault(delegate(PsvDocumentGroupSummary current)
            {
                return AreSameGroup(current, group);
            });
        }

        private PsvDocumentGroupSummary FindMatchingVisibleGroup(PsvDocumentGroupSummary group)
        {
            if (group == null)
            {
                return null;
            }

            return GetVisibleDocumentGroups().FirstOrDefault(delegate(PsvDocumentGroupSummary current)
            {
                return AreSameGroup(current, group);
            });
        }

        private List<PsvDocumentGroupSummary> GetVisibleDocumentGroups()
        {
            return DocumentGroupsView == null
                ? new List<PsvDocumentGroupSummary>()
                : DocumentGroupsView.Cast<object>().OfType<PsvDocumentGroupSummary>().ToList();
        }

        private static bool AreSameGroup(PsvDocumentGroupSummary left, PsvDocumentGroupSummary right)
        {
            return left != null
                   && right != null
                   && string.Equals(left.InstrumentType ?? string.Empty, right.InstrumentType ?? string.Empty, StringComparison.OrdinalIgnoreCase)
                   && string.Equals(left.RangeText ?? string.Empty, right.RangeText ?? string.Empty, StringComparison.OrdinalIgnoreCase)
                   && string.Equals(left.RegistryNumber ?? string.Empty, right.RegistryNumber ?? string.Empty, StringComparison.OrdinalIgnoreCase);
        }

        private void FillHeaderFromSelection()
        {
            if (SelectedDocument == null)
            {
                ClearHeader();
                return;
            }

            DocumentNumberEditor = SelectedDocument.DocumentNumber;
            HeaderReceivedOn = SelectedDocument.AcceptedOn;
            HeaderIssuedOn = SelectedDocument.IssuedOn;
            HeaderDepartmentName = SelectedDocument.DepartmentName;
            SelectedCustomerId = SelectedDocument.CustomerId;
        }

        private List<PsvDocumentLine> GetPendingLines(PsvDocumentSummary document)
        {
            if (document == null)
            {
                return new List<PsvDocumentLine>();
            }

            List<PsvDocumentLine> lines;
            return _pendingLinesByDocumentKey.TryGetValue(document.DocumentKey, out lines)
                ? lines
                : new List<PsvDocumentLine>();
        }

        private List<PsvDocumentLine> MergeDocumentLinesForPrint(PsvDocumentSummary document, IEnumerable<PsvDocumentLine> persistedLines)
        {
            var mergedLines = new List<PsvDocumentLine>();
            if (persistedLines != null)
            {
                mergedLines.AddRange(persistedLines);
            }

            mergedLines.AddRange(GetPendingLines(document));
            return mergedLines;
        }

        private PsvDocumentSummary CreateSavedDocumentSummaryForPrint(PsvDocumentSummary source, DocumentEditorResult request)
        {
            return new PsvDocumentSummary
            {
                DocumentKey = source == null ? null : source.DocumentKey,
                DocumentNumber = request == null ? string.Empty : request.DocumentNumber,
                AcceptedOn = request == null ? (DateTime?)null : request.AcceptedOn,
                IssuedOn = request == null ? (DateTime?)null : request.IssuedOn,
                CustomerId = request == null ? null : request.CustomerId,
                CustomerName = ResolveCustomerNameForPrint(source, request),
                DepartmentName = source == null ? string.Empty : source.DepartmentName,
                IsDraft = false
            };
        }

        private string ResolveCustomerNameForPrint(PsvDocumentSummary source, DocumentEditorResult request)
        {
            if (source != null && !string.IsNullOrWhiteSpace(source.CustomerName))
            {
                return source.CustomerName;
            }

            if (request != null && request.CustomerId.HasValue)
            {
                var customer = Customers.FirstOrDefault(delegate(CustomerReference item) { return item.CustomerId == request.CustomerId.Value; });
                if (customer != null)
                {
                    return customer.CustomerName;
                }
            }

            return string.Empty;
        }

        private void InsertDraftIntoCollection(PsvDocumentSummary draft)
        {
            var insertIndex = 0;
            while (insertIndex < Documents.Count
                   && (Documents[insertIndex].AcceptedOn ?? DateTime.MinValue) >= (draft.AcceptedOn ?? DateTime.MinValue))
            {
                insertIndex++;
            }

            Documents.Insert(insertIndex, draft);
        }

        private async Task LoadCustomersCoreAsync()
        {
                var customers = await _service.LoadCustomersAsync();
            ClearCollections(Customers);
            foreach (var customer in customers)
            {
                Customers.Add(customer);
            }
        }

        private async void LoadSelectedDocumentAsync()
        {
            if (SelectedDocument == null)
            {
                ClearDocumentLines();
                ClearDocumentGroups();
                SelectedDocumentGroup = null;
                SelectedDocumentLine = null;
                LineStatusText = "Документ не выбран.";
                return;
            }

            try
            {
                await ReloadSelectedDocumentLinesAsync();
            }
            catch (Exception ex)
            {
                _dialogService.ShowError(ex.Message);
            }
        }

        private async Task ReloadSelectedDocumentLinesAsync()
        {
            if (SelectedDocument == null)
            {
                ClearDocumentLines();
                ClearDocumentGroups();
                SelectedDocumentGroup = null;
                SelectedDocumentLine = null;
                return;
            }

            if (SelectedDocument.IsDraft)
            {
                ApplyDocumentLines(GetPendingLines(SelectedDocument), SelectedDocumentGroup);
                return;
            }

            LineStatusText = "Загрузка строк документа...";

            var previousGroup = SelectedDocumentGroup;
            var documentNumber = SelectedDocument.DocumentNumber;
            var persistedLines = await _service.LoadDocumentLinesAsync(documentNumber);
            var mergedLines = persistedLines.Concat(GetPendingLines(SelectedDocument)).ToList();

            ApplyDocumentLines(mergedLines, previousGroup);
        }

        private void OpenInstrumentPickerAsync()
        {
            if (SelectedDocument == null)
            {
                return;
            }

            if (!SelectedDocument.CustomerId.HasValue)
            {
                _dialogService.ShowWarning("Сначала выберите заказчика в ПСВ.");
                return;
            }

            OpenInstrumentPickerCoreAsync(SelectedDocument.CustomerId.Value, SelectedDocument.CustomerName);
        }

        private async void OpenInstrumentPickerCoreAsync(int customerId, string customerName)
        {
            IReadOnlyList<AvailableInstrumentItem> instruments;

            try
            {
                IsBusy = true;
                instruments = await _service.LoadCustomerInstrumentsAsync(customerId);
            }
            catch (Exception ex)
            {
                _dialogService.ShowError(ex.Message);
                return;
            }
            finally
            {
                IsBusy = false;
            }

            var selectedItems = _dialogService.ShowInstrumentPickerDialog(customerName, instruments);
            if (selectedItems == null || selectedItems.Count == 0)
            {
                return;
            }

            AddSelectedInstruments(selectedItems);
        }

        private void OpenInstrumentTypePickerAsync()
        {
            if (SelectedDocument == null)
            {
                return;
            }

            if (!SelectedDocument.CustomerId.HasValue)
            {
                _dialogService.ShowWarning("Сначала выберите заказчика в ПСВ.");
                return;
            }

            OpenInstrumentTypePickerCoreAsync(SelectedDocument.CustomerName);
        }

        private async void OpenInstrumentTypePickerCoreAsync(string customerName)
        {
            IReadOnlyList<AvailableInstrumentItem> instrumentTypes;

            try
            {
                IsBusy = true;
                instrumentTypes = await _service.LoadInstrumentTypesAsync();
            }
            catch (Exception ex)
            {
                _dialogService.ShowError(ex.Message);
                return;
            }
            finally
            {
                IsBusy = false;
            }

            var result = _dialogService.ShowInstrumentTypeDialog(customerName, instrumentTypes);
            if (result == null || result.TypeItem == null)
            {
                return;
            }

            AddSelectedTypeInstrument(result);
        }

        private void AddSelectedInstruments(IReadOnlyList<AvailableInstrumentItem> selectedItems)
        {
            if (SelectedDocument == null)
            {
                return;
            }

            List<PsvDocumentLine> pendingLines;
            if (!_pendingLinesByDocumentKey.TryGetValue(SelectedDocument.DocumentKey, out pendingLines))
            {
                pendingLines = new List<PsvDocumentLine>();
                _pendingLinesByDocumentKey[SelectedDocument.DocumentKey] = pendingLines;
            }

            var candidateLines = selectedItems
                .Where(delegate(AvailableInstrumentItem item) { return item != null; })
                .Select(CreatePendingLine)
                .ToList();

            List<OpenDocumentConflictInfo> openDocumentConflicts;
            try
            {
                openDocumentConflicts = FindOpenDocumentConflicts(candidateLines);
            }
            catch (Exception ex)
            {
                _dialogService.ShowError(ex.Message);
                return;
            }

            var openConflictKeys = new HashSet<string>(
                openDocumentConflicts.Select(delegate(OpenDocumentConflictInfo conflict) { return conflict.OpenDocumentConflictKey; }),
                StringComparer.OrdinalIgnoreCase);

            var duplicateKeys = new HashSet<string>(DocumentLines.Select(delegate(PsvDocumentLine line) { return line.DuplicateKey; }), StringComparer.OrdinalIgnoreCase);
            var addedCount = 0;
            var skippedDuplicateCount = 0;
            var skippedOpenDocumentCount = 0;
            var skippedWithoutTemplateCount = 0;

            foreach (var item in selectedItems)
            {
                if (item == null)
                {
                    continue;
                }

                if (openConflictKeys.Contains(PsvDocumentLine.BuildOpenDocumentConflictKey(item.TypeSizeId, item.SerialNumber)))
                {
                    skippedOpenDocumentCount++;
                    continue;
                }

                if (!item.HasTemplate)
                {
                    skippedWithoutTemplateCount++;
                    continue;
                }

                var duplicateKey = PsvDocumentLine.BuildDuplicateKey(item.InstrumentType, item.RangeText, item.RegistryNumber, item.SerialNumber);
                if (duplicateKeys.Contains(duplicateKey))
                {
                    skippedDuplicateCount++;
                    continue;
                }

                pendingLines.Add(CreatePendingLine(item));
                duplicateKeys.Add(duplicateKey);
                addedCount++;
            }

            if (SelectedDocument.IsDraft)
            {
                UpdateDocumentSummaryFromLines(SelectedDocument, pendingLines);
            }

            LoadSelectedDocumentAsync();

            var messages = new List<string>();
            if (addedCount > 0)
            {
                messages.Add(string.Format("Добавлено приборов: {0}.", addedCount));
            }

            if (skippedDuplicateCount > 0)
            {
                messages.Add(string.Format("Исключено дублей: {0}.", skippedDuplicateCount));
            }

            if (skippedWithoutTemplateCount > 0)
            {
                messages.Add(string.Format("Пропущено без источника данных для EKZMK: {0}.", skippedWithoutTemplateCount));
            }

            if (skippedOpenDocumentCount > 0)
            {
                messages.Add(string.Format("Пропущено из-за других открытых ПСВ: {0}.", skippedOpenDocumentCount));
                messages.Add(BuildOpenDocumentConflictMessage(openDocumentConflicts));
            }

            if (messages.Count > 0)
            {
                var message = string.Join(" ", messages.ToArray());
                if (addedCount == 0 && skippedOpenDocumentCount > 0)
                {
                    _dialogService.ShowWarning(message);
                }
                else
                {
                    _dialogService.ShowInfo(message);
                }
            }

            RaiseCommandStates();
            OnPropertyChanged("IsCustomerEditable");
        }

        private void AddSelectedTypeInstrument(InstrumentTypeSelectionResult result)
        {
            if (SelectedDocument == null || result == null || result.TypeItem == null)
            {
                return;
            }

            List<PsvDocumentLine> pendingLines;
            if (!_pendingLinesByDocumentKey.TryGetValue(SelectedDocument.DocumentKey, out pendingLines))
            {
                pendingLines = new List<PsvDocumentLine>();
                _pendingLinesByDocumentKey[SelectedDocument.DocumentKey] = pendingLines;
            }

            var serialNumber = string.IsNullOrWhiteSpace(result.SerialNumber) ? string.Empty : result.SerialNumber.Trim();
            if (string.IsNullOrWhiteSpace(serialNumber))
            {
                _dialogService.ShowWarning("Введите заводской номер.");
                return;
            }

            if (!result.TypeItem.HasTemplate)
            {
                _dialogService.ShowWarning("Выбранный тип нельзя добавить: для него не найден шаблон EKZMK, период из TPRMCP или регистрационный период TIPS.");
                return;
            }

            var candidateLine = CreatePendingTypeLine(result.TypeItem, serialNumber);
            List<OpenDocumentConflictInfo> openDocumentConflicts;
            try
            {
                openDocumentConflicts = FindOpenDocumentConflicts(new[] { candidateLine });
            }
            catch (Exception ex)
            {
                _dialogService.ShowError(ex.Message);
                return;
            }

            if (openDocumentConflicts.Count > 0)
            {
                _dialogService.ShowWarning(BuildOpenDocumentConflictMessage(openDocumentConflicts));
                return;
            }

            var duplicateKey = PsvDocumentLine.BuildDuplicateKey(
                result.TypeItem.InstrumentType,
                result.TypeItem.RangeText,
                result.TypeItem.RegistryNumber,
                serialNumber);

            if (DocumentLines.Any(delegate(PsvDocumentLine line)
            {
                return string.Equals(line.DuplicateKey, duplicateKey, StringComparison.OrdinalIgnoreCase);
            }))
            {
                _dialogService.ShowWarning("Такой прибор уже есть в ПСВ.");
                return;
            }

            pendingLines.Add(candidateLine);

            if (SelectedDocument.IsDraft)
            {
                UpdateDocumentSummaryFromLines(SelectedDocument, pendingLines);
            }

            LoadSelectedDocumentAsync();
            _dialogService.ShowInfo("Прибор по типу добавлен в ПСВ.");
            RaiseCommandStates();
            OnPropertyChanged("IsCustomerEditable");
        }

        private void ApplyDocumentLines(IEnumerable<PsvDocumentLine> lines, PsvDocumentGroupSummary previousGroup)
        {
            var previousLine = SelectedDocumentLine;
            ClearDocumentLines();
            foreach (var line in lines)
            {
                SubscribeToDocumentLine(line);
                DocumentLines.Add(line);
            }

            RebuildDocumentGroupSummaries(DocumentLines);
            SelectedDocumentGroup = FindMatchingVisibleGroup(previousGroup)
                ?? GetVisibleDocumentGroups().FirstOrDefault();
            SelectedDocumentLine = previousLine == null
                ? null
                : DocumentLines.FirstOrDefault(delegate(PsvDocumentLine line)
                {
                    if (previousLine.CardId > 0 && line.CardId > 0)
                    {
                        return line.CardId == previousLine.CardId;
                    }

                    return line.IsPendingInsert
                           && previousLine.IsPendingInsert
                           && string.Equals(line.DuplicateKey, previousLine.DuplicateKey, StringComparison.OrdinalIgnoreCase);
                });
            HeaderInstrumentCount = DocumentLines.Count;
            RefreshDocumentGroupsView();
            RefreshDocumentLinesView();
            RaiseCommandStates();
        }

        private void RaiseCommandStates()
        {
            ((RelayCommand)AddDocumentCommand).RaiseCanExecuteChanged();
            ((RelayCommand)CloneLineVerificationCommand).RaiseCanExecuteChanged();
            ((RelayCommand)DeleteDocumentCommand).RaiseCanExecuteChanged();
            ((RelayCommand)DeleteSelectedLinesCommand).RaiseCanExecuteChanged();
            ((RelayCommand)DeleteSelectedGroupsCommand).RaiseCanExecuteChanged();
            ((RelayCommand)MarkLinePassedCommand).RaiseCanExecuteChanged();
            ((RelayCommand)MarkLineRejectedCommand).RaiseCanExecuteChanged();
            ((RelayCommand)OpenInstrumentPickerCommand).RaiseCanExecuteChanged();
            ((RelayCommand)OpenInstrumentTypePickerCommand).RaiseCanExecuteChanged();
            ((RelayCommand)PrintDocumentCommand).RaiseCanExecuteChanged();
            ((RelayCommand)PrintVerificationDocumentCommand).RaiseCanExecuteChanged();
            ((RelayCommand)RefreshDocumentsCommand).RaiseCanExecuteChanged();
            ((RelayCommand)ResetLineVerificationCommand).RaiseCanExecuteChanged();
            ((RelayCommand)SaveDocumentHeaderCommand).RaiseCanExecuteChanged();
        }

        private void RebuildDocumentGroupSummaries(IEnumerable<PsvDocumentLine> lines)
        {
            var checkedGroups = DocumentGroupSummaries
                .Where(delegate(PsvDocumentGroupSummary group) { return group.IsBatchSelected; })
                .ToList();

            ClearDocumentGroups();

            var groups = lines.GroupBy(line => new
                {
                    InstrumentType = line.InstrumentType ?? string.Empty,
                    RangeText = line.RangeText ?? string.Empty,
                    RegistryNumber = line.RegistryNumber ?? string.Empty
                })
                .OrderBy(group => group.Key.InstrumentType)
                .ThenBy(group => group.Key.RegistryNumber)
                .ThenBy(group => group.Key.RangeText)
                .Select(group => new PsvDocumentGroupSummary
                {
                    InstrumentType = group.Key.InstrumentType,
                    RangeText = group.Key.RangeText,
                    RegistryNumber = group.Key.RegistryNumber,
                    SerialNumbersText = BuildSerialNumbersText(group),
                    IsBatchSelected = checkedGroups.Any(delegate(PsvDocumentGroupSummary previous)
                    {
                        return string.Equals(previous.InstrumentType ?? string.Empty, group.Key.InstrumentType, StringComparison.OrdinalIgnoreCase)
                               && string.Equals(previous.RangeText ?? string.Empty, group.Key.RangeText, StringComparison.OrdinalIgnoreCase)
                               && string.Equals(previous.RegistryNumber ?? string.Empty, group.Key.RegistryNumber, StringComparison.OrdinalIgnoreCase);
                    }),
                    InVerificationCount = group.Count(line => !line.IsPassed.HasValue),
                    VerifiedCount = group.Count(line => line.IsPassed.HasValue),
                    GoodCount = group.Count(line => line.IsPassed == true),
                    RejectedCount = group.Count(line => line.IsPassed == false)
                });

            foreach (var group in groups)
            {
                SubscribeToDocumentGroup(group);
                DocumentGroupSummaries.Add(group);
            }
        }

        private void RefreshDocumentGroupsView()
        {
            if (DocumentGroupsView != null)
            {
                DocumentGroupsView.Refresh();
            }

            var selectedVisibleGroup = FindMatchingVisibleGroup(SelectedDocumentGroup);
            if (selectedVisibleGroup != null)
            {
                if (!ReferenceEquals(SelectedDocumentGroup, selectedVisibleGroup))
                {
                    SelectedDocumentGroup = selectedVisibleGroup;
                    return;
                }

                RefreshDocumentLinesView();
                return;
            }

            var firstVisibleGroup = GetVisibleDocumentGroups().FirstOrDefault();
            if (!ReferenceEquals(SelectedDocumentGroup, firstVisibleGroup))
            {
                SelectedDocumentGroup = firstVisibleGroup;
                return;
            }

            RefreshDocumentLinesView();
        }

        private void RefreshDocumentLinesView()
        {
            DocumentLinesView.Refresh();
            UpdateLineStatus();
            RaiseCommandStates();
        }

        private void RefreshDocumentsAsync(string documentKeyToSelect, string documentNumberToSelect)
        {
            RunBusyOperation(delegate { return RefreshDocumentsCoreAsync(documentKeyToSelect, documentNumberToSelect); });
        }

        private async Task RefreshDocumentsCoreAsync(string documentKeyToSelect, string documentNumberToSelect)
        {
            DocumentStatusText = "Загрузка списка ПСВ...";

            var databaseDocuments = await _service.LoadDocumentsAsync(ShowClosedDocuments);
            var currentDocumentKey = documentKeyToSelect ?? (SelectedDocument != null ? SelectedDocument.DocumentKey : null);
            var currentDocumentNumber = documentNumberToSelect ?? (SelectedDocument != null ? SelectedDocument.DocumentNumber : null);

            ClearCollections(Documents);

            foreach (var draft in _draftDocuments
                .Where(delegate(PsvDocumentSummary item) { return !ShowClosedDocuments; })
                .OrderByDescending(delegate(PsvDocumentSummary item) { return item.AcceptedOn ?? DateTime.MinValue; }))
            {
                Documents.Add(draft);
            }

            foreach (var document in databaseDocuments)
            {
                if (_draftDocuments.Any(delegate(PsvDocumentSummary draft)
                {
                    return string.Equals(draft.DocumentNumber, document.DocumentNumber, StringComparison.OrdinalIgnoreCase);
                }))
                {
                    continue;
                }

                Documents.Add(document);
            }

            DocumentsView.Refresh();

            SelectedDocument = Documents.FirstOrDefault(delegate(PsvDocumentSummary document)
            {
                if (!string.IsNullOrWhiteSpace(currentDocumentKey)
                    && string.Equals(document.DocumentKey, currentDocumentKey, StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }

                return !string.IsNullOrWhiteSpace(currentDocumentNumber)
                       && string.Equals(document.DocumentNumber, currentDocumentNumber, StringComparison.OrdinalIgnoreCase);
            });

            if (SelectedDocument == null && Documents.Count > 0)
            {
                SelectedDocument = Documents[0];
            }

            DocumentStatusText = BuildDocumentStatusText(Documents.Count);
        }

        private void SaveDocumentAsync()
        {
            RunBusyOperation(async delegate
            {
                if (SelectedDocument == null)
                {
                    _dialogService.ShowWarning("Сначала выберите документ.");
                    return;
                }

                var selectedDocument = SelectedDocument;

                if (!HeaderReceivedOn.HasValue)
                {
                    _dialogService.ShowWarning("Укажите дату приемки.");
                    return;
                }

                if (string.IsNullOrWhiteSpace(DocumentNumberEditor))
                {
                    _dialogService.ShowWarning("Введите номер ПСВ.");
                    return;
                }

                if (selectedDocument.IsDraft && !SelectedCustomerId.HasValue)
                {
                    _dialogService.ShowWarning("Для новой ПСВ сначала выберите заказчика.");
                    return;
                }

                if (DocumentExistsInCollections(DocumentNumberEditor.Trim(), selectedDocument.DocumentKey)
                    || await _service.DocumentNumberExistsAsync(DocumentNumberEditor.Trim(), selectedDocument.IsDraft ? null : selectedDocument.DocumentNumber))
                {
                    _dialogService.ShowWarning("ПСВ с таким номером уже существует.");
                    return;
                }

                var pendingLines = GetPendingLines(selectedDocument)
                    .Where(delegate(PsvDocumentLine line)
                    {
                        return line != null
                               && (line.InstrumentId > 0
                                    || (line.TypeSizeId > 0 && !string.IsNullOrWhiteSpace(line.SerialNumber)));
                    })
                    .ToList();
                if (selectedDocument.IsDraft && pendingLines.Count == 0)
                {
                    _dialogService.ShowWarning("Черновик нельзя сохранить без строк EKZMK.");
                    return;
                }

                var openDocumentConflicts = FindPendingOpenDocumentConflicts(selectedDocument, pendingLines);
                if (openDocumentConflicts.Count > 0)
                {
                    _dialogService.ShowWarning(BuildOpenDocumentConflictMessage(openDocumentConflicts));
                    return;
                }

                var request = new DocumentEditorResult
                {
                    DocumentNumber = DocumentNumberEditor.Trim(),
                    AcceptedOn = HeaderReceivedOn.Value,
                    IssuedOn = HeaderIssuedOn,
                    CustomerId = selectedDocument.CustomerId ?? SelectedCustomerId
                };

                var currentDocumentNumber = selectedDocument.IsDraft ? null : selectedDocument.DocumentNumber;
                var documentKey = selectedDocument.DocumentKey;
                var wasDraft = selectedDocument.IsDraft;
                var closingNow = !selectedDocument.IssuedOn.HasValue && request.IssuedOn.HasValue;
                var printDocument = closingNow ? CreateSavedDocumentSummaryForPrint(selectedDocument, request) : null;
                var result = await Task.Run(delegate { return _service.SaveDocument(currentDocumentNumber, request, pendingLines); });

                _pendingLinesByDocumentKey.Remove(documentKey);
                if (wasDraft)
                {
                    _draftDocuments.RemoveAll(delegate(PsvDocumentSummary draft) { return draft.DocumentKey == documentKey; });
                }

                var messages = new List<string>();
                messages.Add(string.Format("Обновлено строк EKZMK: {0}.", result.UpdatedEkzMkCount));
                messages.Add(string.Format("Добавлено строк EKZMK: {0}.", result.InsertedEkzMkCount));

                if (result.SkippedDuplicateCount > 0)
                {
                    messages.Add(string.Format("Исключено дублей: {0}.", result.SkippedDuplicateCount));
                }

                if (result.SkippedWithoutTemplateCount > 0)
                {
                    messages.Add(string.Format("Пропущено без источника данных для EKZMK: {0}.", result.SkippedWithoutTemplateCount));
                }

                var messageText = string.Join(" ", messages.ToArray());
                if (closingNow)
                {
                    var prompt = messageText + " ПСВ закрыта. Распечатать приемо-сдаточную ведомость?";
                    if (_dialogService.Confirm(prompt))
                    {
                        var printLines = await _service.LoadDocumentLinesAsync(result.DocumentNumber);
                        if (printDocument != null)
                        {
                            printDocument.DocumentNumber = result.DocumentNumber;
                            _printService.PrintDocument(printDocument, printLines.ToList());
                        }
                    }
                }
                else
                {
                    _dialogService.ShowInfo(messageText);
                }

                await RefreshDocumentsCoreAsync(null, closingNow ? null : result.DocumentNumber);
            });
        }

        private void UpdateLineStatus()
        {
            var visibleGroupCount = GetVisibleDocumentGroups().Count;

            if (SelectedDocument == null)
            {
                DetailTableCountText = "Приборов в таблице: 0.";
                LineStatusText = "Документ не выбран.";
                return;
            }

            if (DocumentGroupSummaries.Count == 0)
            {
                DetailTableCountText = "Приборов в таблице: 0.";
                LineStatusText = SelectedDocument.IsDraft
                    ? "Черновик пуст. Добавьте приборы через контекстное меню таблицы групп."
                    : "В документе нет групп приборов.";
                return;
            }

            if (visibleGroupCount == 0)
            {
                DetailTableCountText = "Приборов в таблице: 0.";
                LineStatusText = string.IsNullOrWhiteSpace(GroupFilterText)
                    ? "Выберите группу."
                    : string.Format("Группы по фильтру \"{0}\" не найдены.", GroupFilterText.Trim());
                return;
            }

            if (SelectedDocumentGroup == null)
            {
                DetailTableCountText = "Приборов в таблице: 0.";
                LineStatusText = string.Format("Групп: {0}/{1}. Выберите группу.", visibleGroupCount, DocumentGroupSummaries.Count);
                return;
            }

            var groupLineCount = DocumentLines.Count(delegate(PsvDocumentLine line) { return SelectedDocumentGroup.Matches(line); });
            var filteredCount = DocumentLines.Count(delegate(PsvDocumentLine line)
            {
                return SelectedDocumentGroup.Matches(line)
                       && (string.IsNullOrWhiteSpace(GroupDetailFilterText) || Contains(line.SerialNumber, GroupDetailFilterText));
            });
            var pendingCount = DocumentLines.Count(delegate(PsvDocumentLine line) { return line.IsPendingInsert; });
            DetailTableCountText = string.Format("Приборов в таблице: {0}.", filteredCount);

            LineStatusText = string.Format(
                "Групп: {0}/{1}. Приборов в выбранной группе: {2}. Отображено по фильтру: {3}. Не сохранено строк: {4}.",
                visibleGroupCount,
                DocumentGroupSummaries.Count,
                groupLineCount,
                filteredCount,
                pendingCount);
        }
    }
}
