%% ========================================================================
%  SIMULAZIONI DINAMICHE per il modello ODE (viroterapia oncolitica)
%  - Traiettorie temporali u(t), i(t), z(t) con vincolo di NON-NEGATIVITA'
%  - Considera TUTTI e 3 gli equilibri:
%       E1: assenza di tumore            
%       E2: solo tumore non infetto      
%       E3: infezione attiva (coesistenza) 
%
%  - Classificazione stabilita'  (nodo/fuoco/sella) e diagnostica Hopf (RH)
%  - Scelta automatica ODE45 (non-stiff) / ODE15s (stiff) con Jacobiano
%% ========================================================================

clear; close all; clc;

%% -------------------- PARAMETRI DI BASE (personalizzare per la tesi) ----
par.p     = 1.87e-2;    % proliferazione tumore (1/h)
par.q     = 8.34e-3;    % clearance cellule infette (1/h)
par.q_z   = 7.50e-3;    % decadimento cellule immunitarie (1/h)
par.S_z   = 5.00e-2;    % sorgente baseline immunitaria (cell/h)
par.K     = 1.00e4;     % capacita'  portante (cell)
par.zeta  = 5.00e-1;    % forza citotossica immunitaria (1/h)
par.alpha = 5.00e-2;    % reclutamento immunitario indotto da i (1/h)
par.beta  = 3.00e-2;    % efficienza di infezione virale (1/h)

% Tolleranze e opzioni generali
tol.zero    = 1e-12;    % per test di "zero numerico"
tol.exists  = 1e-12;    % per test di esistenza biologica (>0)
tol.hopf    = 1e-7;     % soglia diagnostica Hopf (gap RH ~ 0)
MAXPOP      = 1e2*par.K; % soglia di sicurezza per evento di arresto

% Verifica parametri di base
check_param_consistency(par, tol);

%% -------------------- Calcolo degli equilibri (E1, E2, E3) ---------------
[E1, ok1, info1] = equilibrium_E1(par, tol);
[E2, ok2, info2] = equilibrium_E2(par, tol);
[E3, ok3, info3] = equilibrium_E3(par, tol); 

% Classificazione stabilita'  (autovalori della Jacobiana in ciascun equilibrio)
if ok1
    J1 = jacobian_state([E1.u; E1.i; E1.z], par);
    [stab1, txt1] = classify_by_eigs(eig(J1));
else
    stab1 = "non definito"; txt1 = info1;
end

if ok2
    J2 = jacobian_state([E2.u; E2.i; E2.z], par);
    [stab2, txt2] = classify_by_eigs(eig(J2));
else
    stab2 = "non definito"; txt2 = info2;
end

if ok3
    J3 = jacobian_state([E3.u; E3.i; E3.z], par);
    [stab3, txt3] = classify_by_eigs(eig(J3));
    % Diagnostica Routh-Hurwitz per Hopf
    [T3,M23,D3] = invariants_TMD(J3);
    a2 = -T3; a1 = M23; a0 = -D3;
    RHgap = a2*a1 - a0;
else
    stab3 = "non definito"; txt3 = info3; RHgap = NaN;
end

% Stampa riepilogo in console
fprintf('\n================== RIEPILOGO EQUILIBRI ==================\n');
disp_equilibrium("E1 (assenza di tumore)", E1, ok1, txt1);
disp_equilibrium("E2 (solo tumore non infetto)", E2, ok2, txt2);
disp_equilibrium("E3 (infezione attiva)", E3, ok3, txt3);
if ok3
    fprintf('  [E3] Routh-Hurwitz: a2=%.3e, a1=%.3e, a0=%.3e, gap=a2*a1-a0=%.3e\n', a2, a1, a0, RHgap);
    if a2>0 && a1>0 && a0>0 && abs(RHgap) < tol.hopf
        fprintf('  [E3] *** Vicinanza a biforcazione di Hopf (gap ~ 0) ***\n');
    end
end
fprintf('=========================================================\n\n');

%% -------------------- Configurazione simulazioni -------------------------
% Tre simulazioni "di base": intorno a ciascun equilibrio disponibile.
% Ogni simulazione applica una piccola perturbazione percentuale (delta)
% e integra su un orizzonte temporale adattivo in base agli autovalori locali.

sc = []; ksc = 0;

% --- Simulazione intorno a E1
if ok1
    ksc = ksc+1;
    sc(ksc).nome   = 'Sim-1: intorno a E1 (assenza di tumore)';
    sc(ksc).x0     = max([E1.u; E1.i; E1.z] .* (1+[0.05; 0.05; 0.00]) + [1e-6; 1e-6; 0], 0); % piccola semina
    sc(ksc).eq     = E1; sc(ksc).ok = ok1; sc(ksc).txt = txt1;
    sc(ksc).descr  = 'Piccola semina di tumore e infetto su sfondo immune basale.';
end

% --- Simulazione intorno a E2
if ok2
    ksc = ksc+1;
    sc(ksc).nome   = 'Sim-2: intorno a E2 (solo tumore non infetto)';
    seed_i = 1e-6*max(1,E2.u);  % piccolo seme virale per test di invasione
    sc(ksc).x0     = max([E2.u; 0; E2.z] .* (1+[0.02; 0; 0]) + [0; seed_i; 0], 0);
    sc(ksc).eq     = E2; sc(ksc).ok = ok2; sc(ksc).txt = txt2;
    sc(ksc).descr  = 'Seme virale minimo per testare la soglia di invasione vicino a E2.';
end

% --- Simulazione intorno a E3
if ok3
    ksc = ksc+1;
    sc(ksc).nome   = 'Sim-3: intorno a E3 (infezione attiva)';
    sc(ksc).x0     = max([E3.u; E3.i; E3.z] .* (1+[0.03; -0.03; 0.02]), 0);
    sc(ksc).eq     = E3; sc(ksc).ok = ok3; sc(ksc).txt = txt3;
    sc(ksc).descr  = 'Perturbazione asimmetrica per indagare smorzamento/oscillazioni.';
end

if isempty(sc)
    error('Nessun equilibrio fisico disponibile con i parametri attuali. Modificare "par".');
end

%% -------------------- Loop sulle simulazioni -----------------------------
for j = 1:numel(sc)
    fprintf('==> %s\n', sc(j).nome);

    % Scelta risolutore: stima stiffness dal Jacobiano in (x0)
    J0 = jacobian_state(sc(j).x0, par);
    ev0 = eig(J0);
    stiff = is_stiff(ev0);
    solver = ternary(stiff, @ode15s, @ode45);

    % Orizzonte temporale suggerito dai tassi locali (periodi/tempi di rilassamento)
    tf = suggest_tfinal(ev0);
    tspan = [0, tf];

    % Opzioni ODE: non-negativita' , Jacobiano, Eventi
    opts = odeset('RelTol',1e-8, 'AbsTol',1e-10, ...
                  'NonNegative',[1 2 3], ...
                  'Jacobian', @(t,y) jacobian_state(y,par), ...
                  'Events',   @(t,y) blowup_event(t,y,MAXPOP));

    % Integrazione
    try
        [t, X] = solver(@(tt,yy) rhs(tt,yy,par), tspan, sc(j).x0, opts);
    catch ME
        warning(['[', sc(j).nome, '] Integrazione fallita: ', ME.message]);
        continue;
    end

    % Post-process: estrai componenti
    u = X(:,1); i = X(:,2); z = X(:,3);

    % Figura: traiettorie temporali
    fig = figure('Color','w','Name',sc(j).nome); tl = tiledlayout(3,1,'TileSpacing','compact','Padding','compact');

    nexttile; hold on; grid on;
    plot(t,u,'LineWidth',1.6); ylabel('u(t)  [cell]');
    plot_eq_refline(t, E1.u, ok1, '--', [0.6 0.6 0.6], 'E1');
    if ok2, plot_eq_refline(t, E2.u, ok2, ':',  [0.4 0.4 0.4], 'E2'); end
    if ok3, plot_eq_refline(t, E3.u, ok3, '-.', [0.2 0.2 0.2], 'E3'); end
    title(sprintf('%s - componente tumorale non infetta u(t)', sc(j).nome),'FontWeight','bold');

    nexttile; hold on; grid on;
    plot(t,i,'LineWidth',1.6); ylabel('i(t)  [cell]');
    plot_eq_refline(t, E1.i, ok1, '--', [0.6 0.6 0.6], 'E1');
    if ok2, plot_eq_refline(t, E2.i, ok2, ':',  [0.4 0.4 0.4], 'E2'); end
    if ok3, plot_eq_refline(t, E3.i, ok3, '-.', [0.2 0.2 0.2], 'E3'); end
    title(sprintf('%s - componente infetta i(t)', sc(j).nome),'FontWeight','bold');

    nexttile; hold on; grid on;
    plot(t,z,'LineWidth',1.6); ylabel('z(t)  [cell]'); xlabel('tempo t  [h]');
    plot_eq_refline(t, E1.z, ok1, '--', [0.6 0.6 0.6], 'E1');
    if ok2, plot_eq_refline(t, E2.z, ok2, ':',  [0.4 0.4 0.4], 'E2'); end
    if ok3, plot_eq_refline(t, E3.z, ok3, '-.', [0.2 0.2 0.2], 'E3'); end
    title(sprintf('%s - componente immunitaria z(t)', sc(j).nome),'FontWeight','bold');

    title(tl, 'Traiettorie temporali (con linee di riferimento degli equilibri)','FontSize',12,'FontWeight','bold');

    % Annotazioni diagnostiche
    dim = [0.62 0.10 0.35 0.20];  % posizione normalizzata
    ann_txt = compose_annotation_text(par, sc(j), E1, ok1, txt1, E2, ok2, txt2, E3, ok3, txt3, RHgap);
    annotation(fig,'textbox',dim,'String',ann_txt,'FitBoxToText','on','BackgroundColor',[0.97 0.97 0.99]);

    % Figura di fase (proiezione u-i)
    figure('Color','w','Name',[sc(j).nome ' - fase (u,i)']);
    hold on; grid on; box on;
    plot(u,i,'LineWidth',1.5);
    if ok1, plot(E1.u,E1.i,'ko','MarkerFaceColor',[.7 .7 .7],'DisplayName','E1'); end
    if ok2, plot(E2.u,E2.i,'ks','MarkerFaceColor',[.5 .5 .5],'DisplayName','E2'); end
    if ok3, plot(E3.u,E3.i,'k^','MarkerFaceColor',[.3 .3 .3],'DisplayName','E3'); end
    legend('Traiettoria','E1','E2','E3','Location','best');
    xlabel('u  [cell]'); ylabel('i  [cell]');
    title([sc(j).nome ' - proiezione di fase (u,i)'],'FontWeight','bold');

    drawnow;
end

%% ============================ FUNZIONI LOCALI ============================

function check_param_consistency(par, tol)
% Controlli di sanita'  sui parametri (tutti > 0, tranne dove fisicamente ammessi)
    fields = {'p','q','q_z','S_z','K','zeta','alpha','beta'};
    for k=1:numel(fields)
        v = par.(fields{k});
        if ~(isnumeric(v) && isscalar(v) && isfinite(v))
            error('Parametro "%s" non valido (non scalare finito).', fields{k});
        end
        if v < -tol.zero
            error('Parametro "%s" negativo. Controllare i dati.', fields{k});
        end
    end
    if par.K <= tol.zero
        error('La capacita portante K deve essere positiva.');
    end
    if par.q_z <= tol.zero
        warning('q_z risulta nullo o molto piccolo: z* potrebbe diventare non fisico (divisioni).');
    end
end

function dx = rhs(~, x, par)
% Dinamica ODE:

    u = x(1); i = x(2); z = x(3);
    f1 = par.p*u*(1 - (u+i)/par.K) - (par.beta/par.K)*u*i - (par.zeta/par.K)*u*z;
    f2 = (par.beta/par.K)*u*i - par.q*i - (par.zeta/par.K)*i*z;
    f3 = par.alpha*i - par.q_z*z + par.S_z;
    dx = [f1; f2; f3];
end

function J = jacobian_state(x, par)
% Jacobiano (generale, valido per ogni stato x)
    u = x(1); i = x(2); z = x(3);
    J11 = par.p - (2*par.p*u)/par.K - (par.p*i)/par.K - (par.beta*i)/par.K - (par.zeta*z)/par.K;
    J12 = -(par.p/par.K)*u - (par.beta/par.K)*u;
    J13 = -(par.zeta/par.K)*u;
    J21 =  (par.beta/par.K)*i;
    J22 =  (par.beta/par.K)*u - par.q - (par.zeta/par.K)*z;
    J23 = -(par.zeta/par.K)*i;
    J31 = 0;
    J32 = par.alpha;
    J33 = -par.q_z;
    J = [J11 J12 J13; J21 J22 J23; J31 J32 J33];
end

function [T, M2, D] = invariants_TMD(J)
% Invarianti del caratteristico: lambda^3 - (tr J) lambda^2 + (somma minori 2x2) lambda - det J
    T  = trace(J);
    M2 = 0.5*((trace(J))^2 - trace(J*J));
    D  = det(J);
end

function [isStable, label] = classify_by_eigs(ev)
% Classificazione qualitativa in base ai segni delle parti reali
    re = real(ev(:)); imv = imag(ev(:));
    isStable = all(re < -1e-12);
    pos = sum(re > +1e-12); neg = sum(re < -1e-12);
    if isStable
        if any(abs(imv) > 1e-9), label = "fuoco stabile"; else, label = "nodo stabile"; end
        return;
    end
    if any(abs(re) <= 1e-12)
        label = "critico (non iperbolico)"; return;
    end
    if pos==1 && neg==2
        if any(abs(imv) > 1e-9), label = "sella (indice 1, oscillatoria)"; else, label = "sella (indice 1)"; end
        return;
    end
    if pos==2 && neg==1, label = "sella (indice 2)"; return; end
    if pos==3
        if any(abs(imv) > 1e-9), label = "fuoco instabile"; else, label = "nodo instabile"; end
        return;
    end
    label = "indeterminato";
end

function stiff = is_stiff(ev)
% Euristica di stiffness: rapporto di scale >> 100 oppure autovalori molto negativi
    re = abs(real(ev));
    re = re(re>1e-12);
    if isempty(re), stiff = false; return; end
    cond = max(re)/min(re);
    stiff = (cond > 200) || (max(re) > 1);
end

function tf = suggest_tfinal(ev)
% Suggerisce un orizzonte temporale in base alle scale dinamiche locali
    re = -real(ev(:));
    re = re(re>1e-6);           % frequenze di rilassamento rilevanti
    if isempty(re), base = 500; else, base = 30/min(re); end   % ~30 tempi caratteristici
    tf = min(max(base, 300), 2.0e4); % clamp (per sicurezza)
end

function [E1, ok, txt] = equilibrium_E1(par, tol)
% E1: 
    if par.q_z <= tol.zero
        ok = false; txt = "q_z nullo: z* non definito"; E1 = struct('u',NaN,'i',NaN,'z',NaN); return;
    end
    E1.u = 0; E1.i = 0; E1.z = par.S_z/par.q_z;
    ok = (E1.z > tol.exists); txt = ternary(ok,"esiste (biologico)","z*<=0: non fisico");
end

function [E2, ok, txt] = equilibrium_E2(par, tol)
% E2: 
    if par.q_z <= tol.zero || par.p <= tol.zero
        E2 = struct('u',NaN,'i',NaN,'z',NaN); ok=false; txt="p o q_z non validi"; return;
    end
    E2.u = par.K - (par.zeta*par.S_z)/(par.p*par.q_z);
    E2.i = 0;
    E2.z = par.S_z/par.q_z;
    ok = (E2.u > tol.exists) && (E2.z > tol.exists);
    if ok, txt="esiste (biologico)";
    else, txt="u*<=0 oppure z*<=0: E2 non fisico"; end
end

function [E3, ok, txt] = equilibrium_E3(par, tol)
% E3: 
    E3 = struct('u',NaN,'i',NaN,'z',NaN);
    denom = (par.beta + par.p) * (par.alpha*par.zeta + par.beta*par.q_z);
    if denom <= tol.zero
        ok=false; txt="Denominatore nullo o non positivo in i*"; return;
    end
    num = par.K*par.p*par.q_z*(par.beta - par.q) - par.zeta*par.S_z*(par.beta + par.p);
    i3 = num / denom;
    if par.q_z <= tol.zero
        ok=false; txt="q_z nullo: z* non definito"; return;
    end
    z3 = par.alpha*i3/par.q_z + par.S_z/par.q_z;
    if par.beta <= tol.zero
        ok=false; txt="beta nullo o troppo piccolo: u* non definito"; return;
    end
    u3 = par.K*par.q/par.beta + (par.zeta/par.beta)*z3;

    ok = (i3 > tol.exists) && (u3 > tol.exists) && (z3 > tol.exists);
    if ok
        E3.u = u3; E3.i = i3; E3.z = z3; txt="esiste (biologico)";
    else
        txt="almeno una componente a<=0: E3 non biologico";
    end
end

function val = ternary(cond, a, b)
% Operatore ternario
    if cond, val=a; else, val=b; end
end

function [value,isterminal,direction] = blowup_event(~,y,MAXPOP)
% Evento: arresta integrazione se popolazioni enormi (protezione numerica)
    value = double( all(y < MAXPOP) ); % diventa 0 quando una componente supera MAXPOP
    isterminal = 1;  % stop integrazione
    direction  = -1; % attraversamento decrescente
end

function plot_eq_refline(t, val, ok, ls, col, lab)
% Linea orizzontale di riferimento per la soluzione di equilibrio
    if ok && isfinite(val) && ~isnan(val)
        y = val*ones(size(t));
        plot(t,y,ls,'Color',col,'LineWidth',1.0,'DisplayName',['riferimento ' lab]);
    end
end

function disp_equilibrium(name, E, ok, txt)
% Stampa a video lo stato di un equilibrio
    if ok
        fprintf('%s: u*=%.4g, i*=%.4g, z*=%.4g  => %s\n', name, E.u, E.i, E.z, txt);
    else
        fprintf('%s: NON disponibile (%s)\n', name, txt);
    end
end

function s = compose_annotation_text(par, sc, E1, ok1, txt1, E2, ok2, txt2, E3, ok3, txt3, RHgap)
% Testo diagnostico per riquadro annotazione
    lines = {};
    lines{end+1} = sprintf('Descrizione: %s', sc.descr);
    lines{end+1} = sprintf('Parametri: p=%.3g, q=%.3g, q_z=%.3g, S_z=%.3g, K=%.3g, zeta=%.3g, alpha=%.3g, beta=%.3g', ...
                           par.p, par.q, par.q_z, par.S_z, par.K, par.zeta, par.alpha, par.beta);
    if ok1, lines{end+1} = sprintf('E1: u*=%.3g, i*=%.3g, z*=%.3g (%s)', E1.u,E1.i,E1.z, txt1); end
    if ok2, lines{end+1} = sprintf('E2: u*=%.3g, i*=%.3g, z*=%.3g (%s)', E2.u,E2.i,E2.z, txt2); end
    if ok3
        lines{end+1} = sprintf('E3: u*=%.3g, i*=%.3g, z*=%.3g (%s)', E3.u,E3.i,E3.z, txt3);
        if ~isnan(RHgap), lines{end+1} = sprintf('Diagnostica Hopf (E3): gap RH = %.3e', RHgap); end
    end
    s = strjoin(lines, '\n');
end
