using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Microsoft.Win32;
using ACadSharp.IO;
using Rozpisky.Cad;
using Rozpisky.Data;
using Rozpisky.Dxf;
using Rozpisky.Models;
using Rozpisky.Rendering;
using Rozpisky.Views;
using Rozpisky.Xlsx;
namespace Rozpisky.ViewModels;
///
/// Hlavní VM aplikace (ROZHRANI.md §4). Drží datový model v paměti (projekt, seznam
/// příloh, revize, číselníky, nastavení) a akce. Náhled a export do PDF/PNG jedou přes
/// renderovací engine ().
///
public sealed class MainViewModel : ObservableObject
{
public Projekt Projekt { get; } = new();
public Nastaveni Nastaveni { get; } = new();
public Ciselniky Ciselniky { get; } = new();
public ObservableCollection Prilohy { get; } = new();
public ObservableCollection Revize { get; } = new();
// ---- CAD – připojení (PODKLAD_CAD_Layout_Tisk.md §1) ----
/// Zástupná položka „Manuálně" v comboboxu CADu – po výběru se zpřístupní textové pole
/// na vlastní ProgID (verzově konkrétní, např. „ZWCAD.Application.2023").
/// Je to sentinel (referenční porovnání), do cad.json se neukládá.
private static readonly CadEngine RucniPolozka = new() { ProgId = "", Nazev = "Manuálně (zadat ProgID)" };
/// Nabídka CADů v comboboxu = předpřipravené enginy z cad.json (databáze od
/// programátora, včetně verzově konkrétních jako „ZWCAD 2023") + zástupná položka „Manuálně".
/// CAD je vlastnost stroje, ne zakázky, takže výběr žije mimo projekt.
public ObservableCollection PodporovaneCady { get; }
private CadEngine? _vybranyCad;
/// Aktuálně vybraný CAD engine v Nastavení. Změna se rovnou uloží do cad.json,
/// aby se výběr pamatoval napříč projekty i restarty aplikace.
public CadEngine? VybranyCad
{
get => _vybranyCad;
set
{
// WPF ComboBox při inicializaci / přegenerování ItemsSource krátce nastaví SelectedItem = null
// a přes TwoWay binding to zapíše sem – to by přepsalo uložený výběr nullem (výběr se pak
// po restartu „ztratí"). Uživatel nemá jak vybrat „nic", takže null ignorujeme a vrátíme
// původní hodnotu zpět do UI (OnPropertyChanged bez zápisu do cad.json).
if (value is null)
{
if (_vybranyCad is not null) OnPropertyChanged();
return;
}
if (!SetField(ref _vybranyCad, value)) return;
OnPropertyChanged(nameof(JeRucniCad));
UlozCadEngines();
CommandManager.InvalidateRequerySuggested(); // CanExecute akcí závisí na výběru CADu
}
}
/// True, když je v comboboxu vybrané „Manuálně" – řídí zpřístupnění textového pole ProgID.
public bool JeRucniCad => ReferenceEquals(VybranyCad, RucniPolozka);
private string _rucniProgId = "";
/// Ručně zadaný ProgID (aktivní jen v režimu „Manuálně"). Ukládá se do cad.json
/// jako vybraný ProgID, takže se po restartu obnoví i s režimem „Manuálně".
public string RucniProgId
{
get => _rucniProgId;
set { if (SetField(ref _rucniProgId, value)) { UlozCadEngines(); CommandManager.InvalidateRequerySuggested(); } }
}
/// ProgID, se kterým se skutečně připojuje: ručně zadaný v režimu „Manuálně", jinak
/// z vybraného enginu. Null/prázdné = není s čím se připojit (blokuje akce).
private string? EfektivniProgId => JeRucniCad
? (string.IsNullOrWhiteSpace(RucniProgId) ? null : RucniProgId.Trim())
: VybranyCad?.ProgId;
/// Popisek CADu do hlášek/stavu – v režimu „Manuálně" samotný ProgID, jinak název enginu.
private string EfektivniNazev => JeRucniCad ? EfektivniProgId ?? "Manuálně" : VybranyCad?.Nazev ?? "CAD";
/// Je vybraný použitelný CAD (běžný engine, nebo „Manuálně" s vyplněným ProgID)?
/// Podmínka pro tlačítko „Připojit" i CAD akce.
public bool MaVybranyCad => !string.IsNullOrEmpty(EfektivniProgId);
/// Uloží seznam enginů (bez zástupné položky „Manuálně") + výběr do cad.json:
/// režim (combobox vs. Manuálně) i ruční text zvlášť, aby se po startu obnovil přesně stav UI.
/// Chyby zápisu (read-only složka) jen ohlásí.
private void UlozCadEngines()
{
OnPropertyChanged(nameof(MaVybranyCad));
try
{
var enginy = PodporovaneCady.Where(e => !ReferenceEquals(e, RucniPolozka)).ToList();
// Ruční text ukládáme vždy (i mimo režim „Manuálně"), ať zůstane poslední zadaná hodnota.
var vyber = new CadEnginesStore.Vyber(
JeRucniCad ? null : VybranyCad?.ProgId, JeRucniCad, RucniProgId);
CadEnginesStore.Save(enginy, vyber);
}
catch { StavovaZprava = "Výběr CADu se nepodařilo uložit do cad.json (složka jen pro čtení?)."; }
}
private string _cadStav = "Nepřipojeno";
/// Stav posledního pokusu o připojení na CAD (do UI pod tlačítkem „Připojit").
public string CadStav { get => _cadStav; private set => SetField(ref _cadStav, value); }
private Priloha? _selectedPriloha;
public Priloha? SelectedPriloha { get => _selectedPriloha; set => SetField(ref _selectedPriloha, value); }
/// Vícenásobný výběr řádků seznamu (pro „Exportuj výběr do PDF", hromadné smazání,
/// aktualizaci/tisk rozvržení). Plní ho View z řádků dotčených DataGrid.SelectedCells přes
/// (SelectionUnit=CellOrRowHeader → tah/Ctrl+klik vybírá buňky).
public ObservableCollection VybranePrilohy { get; } = new();
/// Převezme aktuální výběr řádků z View. Kromě naplnění
/// přepíná i na dotčených řádcích – z toho grid jemně podbarví
/// celé vybrané řádky (i při vícenásobném výběru tahem).
public void NastavVyber(IEnumerable vyber)
{
var novy = vyber.ToList();
// Zhasni příznak na řádcích, které už ve výběru nejsou (jinak by zvýraznění „zůstalo viset").
foreach (var p in VybranePrilohy) if (!novy.Contains(p)) p.JeVybrana = false;
VybranePrilohy.Clear();
foreach (var p in novy) { p.JeVybrana = true; VybranePrilohy.Add(p); }
}
/// Je co zpracovat na základě výběru? Vícenásobný výběr řádků, nebo aspoň aktivní řádek.
/// Sdílená podmínka pro CanExecute akcí nad přílohami (mazání, export, rozvržení).
private bool MaVyberPriloh => SelectedPriloha is not null || VybranePrilohy.Count > 0;
private string _stavovaZprava = "Připraveno";
/// Krátká informace ve stavovém řádku (např. průběh exportu „Exportuji rozpisku 1/41").
public string StavovaZprava { get => _stavovaZprava; private set => SetField(ref _stavovaZprava, value); }
private Revize? _selectedRevize;
public Revize? SelectedRevize { get => _selectedRevize; set => SetField(ref _selectedRevize, value); }
// ---- Výběry pro auto-fill (náhrada VLOOKUP, §4.2) ----
private Organizace? _selectedInvestor;
public Organizace? SelectedInvestor
{
get => _selectedInvestor;
set
{
if (!SetField(ref _selectedInvestor, value) || value is null) return;
Projekt.Investor = value.Nazev;
Projekt.InvestorAdresa = value.Adresa;
}
}
private Organizace? _selectedSlozkaSZ;
public Organizace? SelectedSlozkaSZ
{
get => _selectedSlozkaSZ;
set
{
if (!SetField(ref _selectedSlozkaSZ, value) || value is null) return;
Projekt.SlozkaSZ = value.Nazev;
Projekt.SlozkaSZAdresa = value.Adresa;
}
}
private Firma? _selectedZhotovitelSOD;
public Firma? SelectedZhotovitelSOD
{
get => _selectedZhotovitelSOD;
set
{
if (!SetField(ref _selectedZhotovitelSOD, value) || value is null) return;
Projekt.ZhotovitelSOD = value.Nazev;
Projekt.AdresaSOD = value.Adresa;
Projekt.TelSOD = value.Telefon;
Projekt.EmailSOD = value.Email;
}
}
private VerzeDokumentace? _selectedStupen;
public VerzeDokumentace? SelectedStupen
{
get => _selectedStupen;
set
{
if (!SetField(ref _selectedStupen, value) || value is null) return;
Projekt.StupenDokumentace = value.Hodnota;
}
}
// ---- Stav projektu / souboru ----
public int PocetPriloh => Prilohy.Count;
private string? _aktualniSoubor;
/// Cesta k aktuálně otevřenému souboru projektu (null = neuložený).
public string? AktualniSoubor
{
get => _aktualniSoubor;
set { if (SetField(ref _aktualniSoubor, value)) { OnPropertyChanged(nameof(NazevSouboru)); OnPropertyChanged(nameof(Titulek)); } }
}
/// Název souboru pro stavový řádek/titulek.
public string NazevSouboru => _aktualniSoubor is null ? "(neuložený projekt)" : Path.GetFileName(_aktualniSoubor);
/// Složka aktuálního .rzp (pro relativní cestu k orientačnímu schématu); null = neuložený projekt.
public string? RzpSlozka => string.IsNullOrEmpty(_aktualniSoubor) ? null : Path.GetDirectoryName(_aktualniSoubor);
/// Sestaví mapu „hladina placeholderu → cesta k DXF" (loga + schéma) pro danou přílohu.
public IReadOnlyDictionary ResolveLoga(Priloha r)
=> LogaResolver.Resolve(Projekt, r, Ciselniky, RzpSlozka);
/// Jména DXF log z obou složek loga\ – uživatelské i dodávané (prázdná položka
/// = „žádné") – nabídka pro výběr log financujících institucí v Údajích projektu.
public IReadOnlyList DostupnaLoga
{
get
{
var list = new List { "" };
list.AddRange(LogaResolver.DostupnaLoga());
return list;
}
}
private bool _jeZmeneno;
/// Příznak neuložených změn (dirty flag). Nastavují ho automaticky změny modelu;
/// shazuje se po uložení, načtení a založení nového projektu.
public bool JeZmeneno
{
get => _jeZmeneno;
private set { if (SetField(ref _jeZmeneno, value)) OnPropertyChanged(nameof(Titulek)); }
}
private bool _jenProCteni;
/// Projekt je otevřený jen pro čtení (soubor drží jiný proces). „Uložit" se chová
/// jako „Uložit jako" – nelze přepsat cizí zamčený soubor.
public bool JenProCteni
{
get => _jenProCteni;
private set { if (SetField(ref _jenProCteni, value)) OnPropertyChanged(nameof(Titulek)); }
}
/// Titulek hlavního okna: hvězdička = neuložené změny, pak název souboru a případně příznak režimu.
public string Titulek =>
$"{(JeZmeneno ? "*" : "")}{NazevSouboru}{(JenProCteni ? " [jen pro čtení]" : "")} – Rozpisky – generátor rozpisek";
/// Exkluzivní zámek aktuálně otevřeného souboru (drží se po celou dobu otevření,
/// aby ho jiný proces nemohl smazat/přepsat). Null = neuložený projekt bez souboru.
private ProjektFileHandle? _souborHandle;
// ---- Příkazy ----
public ICommand NovyProjektCmd { get; }
public ICommand OtevritProjektCmd { get; }
public ICommand UlozitProjektCmd { get; }
public ICommand UlozitJakoProjektCmd { get; }
public ICommand PridejPrilohuCmd { get; }
public ICommand DuplikujPrilohuCmd { get; }
public ICommand SmazPrilohuCmd { get; }
public ICommand PosunPrilohuNahoruCmd { get; }
public ICommand PosunPrilohuDoluCmd { get; }
public ICommand NahledPrilohyCmd { get; }
public ICommand VyberSchemaCmd { get; }
public ICommand PridejReviziCmd { get; }
public ICommand SmazReviziCmd { get; }
/// Žádost o otevření náhledu vybrané přílohy. Okno otvírá View (zná Window/Owner),
/// VM jen drží enable/disable a SelectedPriloha.
public event Action? NahledPozadovan;
/// Šířky sloupců gridu „Seznam příloh" (px) ve fixním pořadí sloupců. UI stav ukládaný
/// do .rzp; plní ho při ručním roztažení sloupce a čte při
/// obnově. Prázdný = výchozí šířky z XAML.
public List SirkySloupcuSeznamu { get; private set; } = new();
/// Žádost, aby View promítl do sloupců gridu (po načtení
/// projektu / novém projektu).
public event Action? SirkySloupcuObnoveny;
/// View sem hlásí aktuální šířky po ručním roztažení sloupce. Změna = neuložená změna
/// projektu (uloží se pak spolu s daty). Shoda s uloženým stavem (i po obnově) je no-op, takže
/// promítnutí načtených šířek do gridu projekt zpět „nešpiní".
public void NastavSirkySloupcuSeznamu(IEnumerable sirky)
{
var nove = sirky.ToList();
if (nove.SequenceEqual(SirkySloupcuSeznamu)) return;
SirkySloupcuSeznamu = nove;
JeZmeneno = true;
}
public ICommand ExportVyberPdfCmd { get; }
public ICommand ExportVsePdfCmd { get; }
public ICommand ExportVyberDxfCmd { get; }
public ICommand ExportVseDxfCmd { get; }
public ICommand ZrusitExportCmd { get; }
public ICommand PripojitCadCmd { get; }
public ICommand AktualizujRozvrzeniCmd { get; }
public ICommand VytiskniRozvrzeniCmd { get; }
public ICommand TiskSeznamuCmd { get; }
public ICommand ExportSeznamuCmd { get; }
public ICommand OtevritNapoveduCmd { get; }
public ICommand OtevritManualCmd { get; }
/// Otevře v Průzkumníku složku, kam si uživatel dává vlastní loga.
public ICommand OtevritSlozkuLogCmd { get; }
public ICommand OProgramuCmd { get; }
public MainViewModel()
{
var (cadEngines, vyber) = CadEnginesStore.Load();
// Combobox = předpřipravené enginy + zástupná položka „Manuálně" na konci.
PodporovaneCady = new ObservableCollection(cadEngines) { RucniPolozka };
// Ruční text se drží vždy (i mimo režim „Manuálně"), aby po přepnutí zpět na „Manuálně"
// – i po restartu – zůstala poslední ručně zadaná hodnota.
_rucniProgId = vyber.RucniProgId ?? "";
_vybranyCad = vyber.RucniRezim
? RucniPolozka
: cadEngines.FirstOrDefault(e => e.ProgId == vyber.VybranyProgId);
Prilohy.CollectionChanged += (_, _) => OnPropertyChanged(nameof(PocetPriloh));
NovyProjektCmd = new RelayCommand(NovyProjekt);
OtevritProjektCmd = new RelayCommand(OtevritProjekt);
UlozitProjektCmd = new RelayCommand(() => Uloz(AktualniSoubor));
UlozitJakoProjektCmd = new RelayCommand(() => Uloz(null));
PridejPrilohuCmd = new RelayCommand(() =>
{
var p = new Priloha();
Prilohy.Add(p);
SelectedPriloha = p;
});
DuplikujPrilohuCmd = new RelayCommand(
() =>
{
if (SelectedPriloha is null) return;
var kopie = SelectedPriloha.Klon();
Prilohy.Insert(Prilohy.IndexOf(SelectedPriloha) + 1, kopie);
SelectedPriloha = kopie;
},
() => SelectedPriloha is not null);
SmazPrilohuCmd = new RelayCommand(
() =>
{
// Snapshot: Prilohy.Remove spustí SelectedCellsChanged → NastavVyber přepíše VybranePrilohy,
// přes kterou bychom jinak iterovali.
var kSmazani = (VybranePrilohy.Count > 0
? VybranePrilohy.AsEnumerable()
: SelectedPriloha is not null ? new[] { SelectedPriloha } : Enumerable.Empty())
.ToList();
foreach (var p in kSmazani) Prilohy.Remove(p);
SelectedPriloha = null;
},
() => MaVyberPriloh);
PosunPrilohuNahoruCmd = new RelayCommand(
() => PosunPrilohu(-1),
() => SelectedPriloha is not null && Prilohy.IndexOf(SelectedPriloha) > 0);
PosunPrilohuDoluCmd = new RelayCommand(
() => PosunPrilohu(+1),
() => SelectedPriloha is not null && Prilohy.IndexOf(SelectedPriloha) is var i && i >= 0 && i < Prilohy.Count - 1);
NahledPrilohyCmd = new RelayCommand(
() => NahledPozadovan?.Invoke(),
() => SelectedPriloha is not null);
VyberSchemaCmd = new RelayCommand(VyberSchema, () => SelectedPriloha is not null);
PridejReviziCmd = new RelayCommand(() =>
{
var r = new Revize();
Revize.Add(r);
SelectedRevize = r;
});
SmazReviziCmd = new RelayCommand(
() => { if (SelectedRevize is not null) Revize.Remove(SelectedRevize); },
() => SelectedRevize is not null);
ExportVyberPdfCmd = new RelayCommand(
() => _ = ExportujDoPdf(VyberKExportu()),
() => MaVyberPriloh);
ExportVsePdfCmd = new RelayCommand(
() => _ = ExportujDoPdf(Prilohy.ToList()),
() => Prilohy.Count > 0);
ExportVyberDxfCmd = new RelayCommand(
() => _ = ExportujDoDxf(VyberKExportu()),
() => MaVyberPriloh);
ExportVseDxfCmd = new RelayCommand(
() => _ = ExportujDoDxf(Prilohy.ToList()),
() => Prilohy.Count > 0);
ZrusitExportCmd = new RelayCommand(
() => { _exportZrusen = true; StavovaZprava = "Ruším export…"; },
() => ExportBezi);
PripojitCadCmd = new RelayCommand(PripojNaCad, () => MaVybranyCad);
AktualizujRozvrzeniCmd = new RelayCommand(
() => _ = AktualizovatRozvrzeni(),
() => MaVybranyCad && MaVyberPriloh);
VytiskniRozvrzeniCmd = new RelayCommand(
() => _ = VytisknoutRozvrzeni(),
() => MaVybranyCad && MaVyberPriloh);
TiskSeznamuCmd = new RelayCommand(TisknoutSeznamPdf, () => Prilohy.Count > 0);
ExportSeznamuCmd = new RelayCommand(ExportujSeznamDoXlsx, () => Prilohy.Count > 0);
OtevritNapoveduCmd = new RelayCommand(OtevritNapovedu);
OtevritManualCmd = new RelayCommand(OtevritManual);
OtevritSlozkuLogCmd = new RelayCommand(OtevritSlozkuLog);
OProgramuCmd = new RelayCommand(OProgramu);
SledujZmeny();
NactiCiselniky();
DemoData.SeedProjekt(this);
JeZmeneno = false; // úvodní data nejsou „neuložená změna"
}
// ---- Sledování neuložených změn (dirty flag) ----
/// Naváže se na změny modelu (Projekt, Nastavení, položky příloh/revizí i jejich kolekce),
/// aby se při jakékoli editaci nastavil . Číselníky se nesledují – ukládají
/// se zvlášť vedle .exe.
private void SledujZmeny()
{
Projekt.PropertyChanged += (_, _) => JeZmeneno = true;
Nastaveni.PropertyChanged += (_, _) => JeZmeneno = true;
Prilohy.CollectionChanged += KolekceZmenena;
Revize.CollectionChanged += KolekceZmenena;
}
/// Položky přihlášené k podle kolekce. Clear() vyvolá
/// Reset **bez** OldItems, takže odhlašované položky nejde vzít z eventu – bez této evidence by
/// řádky starého projektu zůstaly přihlášené a (držené třeba náhledem) dál špinily dirty flag.
private readonly Dictionary