package Siru::Din;

# El lector de las secciones comentadas de un .din, el unico.
#
# Todo lo que hace falta para usar un .din va en el mismo archivo, en
# secciones de texto llano:
#
#   # corrida       desde, hasta, paso, tiempo, pasos
#   # adaptacion    metodo, orden_error, absoluto, relativo, paso_min, paso_max
#   # parametros    nombre valor
#   # estados       actual siguiente inicial estimacion    (- si no hay)
#   # historia      actual nombre_historico     (solo BDF2)
#                   o, en el del instante,  nombre inicial
#   # llaves        rama variable          (el del instante)
#   # paradas       variable condicion     (el del instante)
#   # salidas       NOMBRE variable        (el del instante: OUTPUTS del archivo;
#                                           - si no se pudo traducir)
#   # equilibrio    estado pre_estado      (el del equilibrio)
#   # puntodc       estado derivada        (el del instante: RUN INITIAL
#                   solo 0|1                CONDITIONS de SCEPTRE, el punto de
#                                           operacion DC; con solo 1, nada mas)
#   # iniciales     estado voltaje valor   (el del instante: la carga inicial
#                                           de un capacitor cuya capacidad
#                                           depende del circuito; se resuelve
#                                           en t=0 con el voltaje como dato)
#   # convoluciones rama modo variable fuente resistencia respuesta
#                                          (los elementos K, la convolucion de
#                                           SCEPTRE; ver Siru::Convolucion)
#   # compresion    cuenta, criterio       (el del instante, con convoluciones:
#                                           COMPRESSION COUNT y CRITERION)
#   # etapas        nombre c               (el de integracion, con
#                                           convoluciones: la etapa i es en
#                                           t + c*dt)
#
# 'tiempo' y 'pasos' de la corrida nombran los datos del sistema que pone el
# simulador en cada paso: el tiempo (T) y la cantidad de pasos dados (TICK).
#
# Van COMENTADAS porque el .din lo lee Calcula, que compila cada linea con
# ExprTk: lo que no es comentario tiene que ser una ecuacion. Asi viajan en el
# mismo archivo sin que Calcula tenga que saber de ellas. Las escribe libdin
# (slex/din/din.c).
#
# Las lineas  # PENDIENTE ...  son lo que libdin no pudo traducir; con alguna,
# el .din no lleva corrida y no se puede simular.

use strict;
use File::Basename ();
use File::Spec;
use warnings;
use Carp qw(croak);
use JSON::PP qw(decode_json);
use Siru::CatalogoGLM;

my %DE_PARES = map { $_ => 1 } qw(corrida adaptacion parametros compresion glm pglm ros split);
my %DE_FILAS = map { $_ => 1 } qw(estados llaves equilibrio paradas salidas iniciales puntodc convoluciones etapas historia);

sub leer {
    my ($clase, $ruta) = @_;
    open my $fh, '<:raw', $ruta or croak "no se pudo abrir $ruta: $!";
    my %s = (ruta => $ruta, pendientes => [],
             (map { $_ => {} } keys %DE_PARES), (map { $_ => [] } keys %DE_FILAS));
    my $seccion = '';
    my $en_variables = 0;
    $s{declaradas} = {};
    while (my $linea = <$fh>) {
        $linea =~ s/\s+\z//;
        if ($linea =~ /^variables\s*$/) { $en_variables = 1; next }
        if ($linea =~ /^ecuaciones\s*$/) { $en_variables = 0; next }
        if ($en_variables && $linea =~ /^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=/) {
            croak "variable duplicada en .din: $1" if $s{declaradas}{$1}++;
        }
        if ($linea =~ /^#\s*PENDIENTE\s+(.*)/) { push @{$s{pendientes}}, $1; next }
        next unless $linea =~ s/^\s*#\s*//;          # solo los comentarios
        next if $linea !~ /\S/;
        if ($linea =~ /^([a-z]+)\s*$/ && ($DE_PARES{$1} || $DE_FILAS{$1})) { $seccion = $1; next }
        next unless $seccion;
        my @c = split ' ', $linea;
        if (@c < 2) { $seccion = ''; next }           # un comentario cualquiera corta la seccion
        if ($seccion eq 'glm' || $seccion eq 'pglm' || $seccion eq 'ros' || $seccion eq 'split') {
            croak "metadatos $seccion invalidos o duplicados" unless $linea =~ /^descriptor\s+(.+)$/ && !exists $s{$seccion}{descriptor};
            $s{$seccion}{descriptor} = decode_json($1);
        }
        elsif ($DE_PARES{$seccion}) { $s{$seccion}{$c[0]} = $c[1] }
        else                     { push @{$s{$seccion}}, \@c }
    }
    close $fh;
    _factor_de_paso(\%s);
    return \%s;
}

# SIRU_PASO_FACTOR=F (o siru --paso-factor F): la corrida con el paso
# multiplicado por F, para ver si el resultado cambia al achicarlo
# (siru/corpus/convergencia; pedido del autor, 04/10/2026). A paso fijo se
# multiplica el paso; con paso adaptivo, las tolerancias por F^orden, que
# achica el paso en F, y el paso maximo por F.
sub _factor_de_paso {
    my ($s) = @_;
    my $f = $ENV{SIRU_PASO_FACTOR};
    return unless defined $f && length $f;
    croak "SIRU_PASO_FACTOR debe ser un numero positivo: $f"
        unless $f =~ /^\s*(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\s*\z/ && $f > 0;
    my $c = $s->{corrida};
    $c->{paso} *= $f if defined $c->{paso} && length $c->{paso};
    my $a = $s->{adaptacion};
    if (%$a) {
        my $orden = $a->{orden_error} // 2;
        for my $k (qw(absoluto relativo)) {
            $a->{$k} *= $f ** $orden if defined $a->{$k};
        }
        $a->{paso_max} *= $f if defined $a->{paso_max};
        # ROS v4 resuelve las tolerancias por estado en el descriptor.
        # --paso-factor debe ajustar tambien esas tolerancias efectivas.
        my $desc=($s->{ros}//{})->{descriptor};
        my $por_estado=ref($desc) eq 'HASH' ? $desc->{tolerancias} : undef;
        if (ref($por_estado) eq 'HASH') {
            for my $tol (values %$por_estado) {
                next unless ref($tol) eq 'HASH';
                for my $k (qw(absoluto relativo)) {
                    $tol->{$k} *= $f ** $orden if defined($tol->{$k}) && !ref($tol->{$k})
                        && $tol->{$k} =~ /\A[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?\z/;
                }
            }
        }
    }
}

# Las tablas que viajan adentro del .din, en la seccion 'tablas' (no
# comentada: la lee Calcula): 'tabla NOMBRE', una linea '# X Y' y las filas;
# o, la de una TABLE de SCEPTRE, '#formato pares' y el bloque como estaba en
# el archivo, los valores de a pares en tantos renglones como haga falta.
# Devuelve { NOMBRE => [[x, y], ...] }. Las usa Siru::Convolucion para
# una respuesta al impulso dada por tabla.
sub tablas {
    my ($clase, $ruta) = @_;
    open my $fh, '<:raw', $ruta or croak "no se pudo abrir $ruta: $!";
    my (%t, %pares, %sueltos, $en, $actual);
    while (my $linea = <$fh>) {
        $linea =~ s/\s+\z//;
        if ($linea =~ /^tablas\s*$/) { $en = 1; next }
        next unless $en;
        if ($linea =~ /^tabla\s+(\S+)/) { $actual = $1; $t{$actual} = []; next }
        if (defined $actual && $linea =~ /^#formato\s+pares\b/) { $pares{$actual} = 1; next }
        next if $linea =~ /^\s*#/ || $linea !~ /\S/;
        last if $linea =~ /^[a-z]+\s*$/;            # otra seccion
        next unless defined $actual;
        my @c = grep { length } split /[\s,]+/, $linea;
        if ($pares{$actual}) { push @{ $sueltos{$actual} }, map { $_ + 0 } @c; next }
        push @{ $t{$actual} }, [$c[0] + 0, $c[1] + 0] if @c >= 2;
    }
    close $fh;
    for my $n (keys %pares) {
        my @v = @{ $sueltos{$n} // [] };
        push @{ $t{$n} }, [splice @v, 0, 2] while @v >= 2;
    }
    return \%t;
}

# Los saltos en el tiempo: los instantes donde una fuente salta. El
# integrador los usa de puntos de quiebre: un paso termina justo antes del
# salto y uno minimo lo cruza, para que ningun paso promedie el valor de
# antes con el de despues (pedido del autor, 04/10/2026; Siru::Integrador,
# siru/corpus/EXACTAS.md, ejemplos en siru/corpus/corpus/inventados/salto-*.ds).
#
# Las expresiones de las ecuaciones se analizan con un pequeno analizador
# que calcula cada subexpresion como funcion afin del tiempo, a t + b,
# cuando lo es (el tiempo, los numeros, los parametros del .din, sumas,
# restas, productos y cocientes por constantes; el tiempo de una etapa,
# t + c dt, cuenta como t). Se reconocen:
#   - una comparacion (<, <=, >, >=) entre dos lados afines: salta donde
#     se anulan: t < 1, 2*t < 1, t - 1 > 0, 2.5 >= t, t < TCORTE (un
#     parametro), en un if o donde sea;
#   - sgn(X) con X afin: donde X = 0;
#   - floor, ceil y trunc de X afin: donde X es entero; round: donde X es
#     entero y medio;
#   - mod(X, P) o fmod, y X % P, con X afin y P constante: donde X es
#     multiplo de P, y donde X = kP + C si se compara con C (la onda
#     cuadrada if(mod(t,2)<1,1,0));
#   - una tabla que repite la abscisa (TABLE 1: 0,1, 10800,1, 10800,0), en
#     la seccion tablas del .din o en un archivo aparte de tabular2
#     (Table('j.txt','TIEMPO','J',T,1)), evaluada en X afin: donde X llega
#     a la abscisa repetida;
#   - una funcion propia (FJ(T) con FJ(x) = if(x<1,1,0)): su cuerpo se
#     analiza con los parametros valiendo lo que valen los argumentos.
# Lo que no se reconoce (una variable del circuito que salta, el tiempo
# dentro de una funcion no lineal, sin(t) > 0.5) sigue perdiendo medio paso en
# cada salto.
# quiebres(RUTA, TIEMPO [, DESDE, HASTA]): el intervalo sale de la seccion
# corrida del archivo, y si no la tiene --el .mdin del instante, que lleva las
# condiciones de las llaves y no se integra-- lo da el llamador.
sub quiebres {
    my ($clase, $ruta, $tiempo, $desde_dado, $hasta_dado) = @_;
    return [] unless defined $tiempo && length $tiempo;
    my $s = $clase->leer($ruta);
    my $desde = $s->{corrida}{desde} // $desde_dado // 0;
    my $hasta = $s->{corrida}{hasta} // $hasta_dado;
    return [] unless defined $hasta;
    my $par = $s->{parametros};
    my $internas = $clase->tablas($ruta);
    my (%funciones, @ecuaciones, $seccion);
    open my $fh, '<:raw', $ruta or croak "no se pudo abrir $ruta: $!";
    while (my $linea = <$fh>) {
        $linea =~ s/\s+\z//;
        if ($linea =~ /^(funciones|variables|ecuaciones|tablas)\s*$/) { $seccion = $1; next }
        next if !$seccion || $linea =~ /^\s*#/ || $linea !~ /\S/;
        $linea =~ s/\s+#.*//;
        if ($seccion eq 'funciones' && $linea =~ /^\s*([A-Za-z_]\w*)\s*\(([^)]*)\)\s*=\s*(.+)$/) {
            $funciones{ lc $1 } = { params => [ map { s/^\s+|\s+$//gr } split /,/, $2 ], cuerpo => $3 };
        }
        push @ecuaciones, $linea if $seccion eq 'ecuaciones';
    }
    close $fh;
    my $num = qr/[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/;
    my $valor = sub {                      # un numero o un parametro del .din
        my ($x) = @_;
        $x =~ s/^\s*\(\s*(.*?)\s*\)\s*$/$1/;
        $x =~ s/^\s+|\s+$//g;
        return $x + 0 if $x =~ /^$num\z/;
        return $par->{$x} + 0 if exists $par->{$x} && $par->{$x} =~ /^\s*$num\s*\z/;
        return undef;
    };
    my %q;
    # un salto en el final tambien cuenta: el ultimo paso evaluaria la fuente
    # ya saltada (la onda cuadrada de salto-mod.ds en t = 5)
    my $poner = sub { my ($x) = @_; $q{$x + 0} = 1 if defined $x && $x > $desde && $x <= $hasta };
    my $periodicos = sub {             # k*P + c en el intervalo, a lo sumo 100000
        my ($p, $c) = @_;
        return unless defined $p && $p > 0;
        my $k = int(($desde - $c) / $p) - 1;
        for my $n (0 .. 100000) {
            my $x = ($k + $n) * $p + $c;
            last if $x > $hasta;
            $poner->($x);
        }
    };
    my $saltos_de_tabla = sub {        # las abscisas repetidas de una tabla
        my ($nombre, $columna, $af) = @_;  # $af: el argumento, a t + b
        my @x;
        if (my $filas = $internas->{$nombre}) { @x = map { $_->[0] } @$filas }
        else {
            my ($archivo) = grep { -f } map { File::Spec->rel2abs($nombre, $_) }
                (File::Basename::dirname($ruta), File::Spec->catdir(File::Basename::dirname($ruta), '..'), '.');
            return unless $archivo && open my $t, '<', $archivo;
            my (@coment, @datos);
            while (my $l = <$t>) {
                next if $l =~ /^\s*##/;
                if ($l =~ /^\s*#(.*)/) { push @coment, [ split ' ', $1 ] unless @datos; next }
                my @c = split /[\s,]+/, $l =~ s/^\s+|\s+$//gr;
                next unless @c && $c[0] =~ /^$num\z/;
                push @datos, \@c;
            }
            close $t;
            return unless @datos;
            my ($cab) = grep { @$_ == @{ $datos[0] } } reverse @coment;
            my $i = 0;
            if ($cab) { ($i) = grep { $cab->[$_] eq $columna } 0 .. $#$cab; $i //= 0 }
            @x = map { $_->[$i] } @datos;
        }
        for my $k (1 .. $#x) { $poner->(($x[$k] - $af->{b}) / $af->{a}) if $x[$k] == $x[$k - 1] }
    };
    # El analizador. Un valor es undef (no se sabe, o no es afin), un hash
    # { a, b } (a t + b) o { mod => X, p => P } (lo que da mod(X, P)).
    my $afin = sub { my ($v) = @_; ref($v) eq 'HASH' && exists $v->{a} };
    my $cte = sub { my ($v) = @_; $afin->($v) && $v->{a} == 0 };
    my $donde = sub {                  # X = c, con X afin: el instante
        my ($x, $c) = @_;
        $poner->(($c - $x->{b}) / $x->{a}) if $afin->($x) && $x->{a} != 0;
    };
    my $cada = sub {                   # X = k P + c para todo k entero
        my ($x, $p, $c) = @_;
        return unless $afin->($x) && $x->{a} != 0 && $p > 0;
        $periodicos->($p / abs($x->{a}), ($c - $x->{b}) / $x->{a});
    };
    my $analizar;
    $analizar = sub {
        my ($texto, $env, $hondo) = @_;
        return if $hondo > 5;
        my @f = $texto =~ /\G\s*(
              (?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?
            | '[^']*'
            | [A-Za-z_]\w*
            | <=|>=|==|!=|<>|:=|[-+*\/^%<>=(),&|!]
            )/gx;
        my $i = 0;
        my $ver = sub { $f[$i] // '' };
        my $tomar = sub { $f[$i++] };
        my ($expr, $logica, $comparacion, $suma, $producto, $unario, $potencia, $atomo);
        $expr = sub {
            my $v = $logica->();
            while ($ver->() eq '=' || $ver->() eq ':=') { $tomar->(); $logica->(); $v = undef }
            return $v;
        };
        $logica = sub {
            my $v = $comparacion->();
            while ($ver->() =~ /\A(?:and|or|xor|nand|nor|&|\|)\z/i) { $tomar->(); $comparacion->(); $v = undef }
            return $v;
        };
        $comparacion = sub {
            my $l = $suma->();
            while ($ver->() =~ /\A(?:<=|>=|<|>|==|!=|<>)\z/) {
                my $op = $tomar->();
                my $r = $suma->();
                if ($op =~ /[<>]/ && $op ne '<>') {
                    if ($afin->($l) && $afin->($r)) {
                        $donde->({ a => $l->{a} - $r->{a}, b => $l->{b} - $r->{b} }, 0);
                    }
                    elsif (ref($l) eq 'HASH' && $l->{mod} && $cte->($r)) { $cada->($l->{mod}, $l->{p}, $r->{b}) }
                    elsif (ref($r) eq 'HASH' && $r->{mod} && $cte->($l)) { $cada->($r->{mod}, $r->{p}, $l->{b}) }
                }
                $l = undef;
            }
            return $l;
        };
        $suma = sub {
            my $v = $producto->();
            while ($ver->() eq '+' || $ver->() eq '-') {
                my $op = $tomar->();
                my $w = $producto->();
                $v = $afin->($v) && $afin->($w)
                    ? { a => $v->{a} + ($op eq '+' ? 1 : -1) * $w->{a}, b => $v->{b} + ($op eq '+' ? 1 : -1) * $w->{b} }
                    : undef;
            }
            return $v;
        };
        $producto = sub {
            my $v = $unario->();
            while ($ver->() =~ /\A[*\/%]\z/) {
                my $op = $tomar->();
                my $w = $unario->();
                if ($op eq '*') {
                    $v = $afin->($v) && $cte->($w) ? { a => $v->{a} * $w->{b}, b => $v->{b} * $w->{b} }
                       : $cte->($v) && $afin->($w) ? { a => $w->{a} * $v->{b}, b => $w->{b} * $v->{b} } : undef;
                }
                elsif ($op eq '/') {
                    $v = $afin->($v) && $cte->($w) && $w->{b} != 0 ? { a => $v->{a} / $w->{b}, b => $v->{b} / $w->{b} } : undef;
                }
                else {                              # X % P
                    if ($afin->($v) && $cte->($w) && $w->{b} > 0) { $cada->($v, $w->{b}, 0); $v = { mod => $v, p => $w->{b} } }
                    else { $v = undef }
                }
            }
            return $v;
        };
        $unario = sub {
            if ($ver->() eq '-') { $tomar->(); my $v = $unario->(); return $afin->($v) ? { a => -$v->{a}, b => -$v->{b} } : undef }
            if ($ver->() eq '+') { $tomar->(); return $unario->() }
            if ($ver->() =~ /\A(?:not|!)\z/i) { $tomar->(); $unario->(); return undef }
            return $potencia->();
        };
        $potencia = sub {
            my $v = $atomo->();
            if ($ver->() eq '^') {
                $tomar->();
                my $w = $unario->();
                return $cte->($v) && $cte->($w) ? { a => 0, b => $v->{b} ** $w->{b} } : undef;
            }
            return $v;
        };
        my $argumentos = sub {
            my @a;
            croak 'falta (' unless $tomar->() eq '(';
            if ($ver->() eq ')') { $tomar->(); return @a }
            while (1) {
                push @a, $expr->();
                my $c = $tomar->() // croak 'falta )';
                last if $c eq ')';
                croak "se esperaba , y vino $c" unless $c eq ',';
            }
            return @a;
        };
        $atomo = sub {
            my $x = $tomar->() // croak 'expresion incompleta';
            if ($x eq '(') { my $v = $expr->(); croak 'falta )' unless ($tomar->() // '') eq ')'; return $v }
            return { a => 0, b => $x + 0 } if $x =~ /\A(?:\d|\.\d)/;
            return { texto => substr($x, 1, -1) } if $x =~ /\A'/;
            croak "no se esperaba $x" unless $x =~ /\A[A-Za-z_]/;
            my $l = lc $x;
            if ($ver->() eq '(') {
                my $ini = $i;
                my @a = $argumentos->();
                if ($l eq 'sgn' && @a) { $donde->($a[0], 0) }
                elsif ($l =~ /\A(?:floor|ceil|trunc)\z/ && @a) { $cada->($a[0], 1, 0) }
                elsif ($l eq 'round' && @a) { $cada->($a[0], 1, 0.5) }
                elsif (($l eq 'mod' || $l eq 'fmod') && @a == 2 && $cte->($a[1]) && $a[1]{b} > 0) {
                    $cada->($a[0], $a[1]{b}, 0);
                    return $afin->($a[0]) ? { mod => $a[0], p => $a[1]{b} } : undef;
                }
                elsif ($l eq 'tabla' && @a >= 4 && ref($a[0]) eq 'HASH' && defined $a[0]{texto}
                       && $afin->($a[3]) && $a[3]{a} != 0) {
                    $saltos_de_tabla->($a[0]{texto}, ref($a[1]) eq 'HASH' ? $a[1]{texto} // 'X' : 'X', $a[3]);
                }
                elsif (my $fn = $funciones{$l}) {
                    my %e;
                    for my $k (0 .. $#{ $fn->{params} }) { $e{ $fn->{params}[$k] } = $a[$k] }
                    eval { $analizar->($fn->{cuerpo}, \%e, $hondo + 1) };
                }
                return undef;
            }
            return $env->{$x} if exists $env->{$x};
            return { a => 1, b => 0 } if $x eq $tiempo;
            return { a => 0, b => 0 } if $x eq 'dt';        # t + c dt: el tiempo de la etapa
            return { a => 0, b => $par->{$x} + 0 } if exists $par->{$x} && defined $par->{$x} && $par->{$x} =~ /\A\s*$num\s*\z/;
            return undef;
        };
        my $v = $expr->();
        croak "sobra $f[$i]" if $i < @f;
        return $v;
    };
    # una ecuacion que no se puede analizar no aporta quiebres (y no frena
    # la corrida): el integrador sigue como antes
    eval { $analizar->($_, {}, 0) } for @ecuaciones;
    return [ sort { $a <=> $b } keys %q ];
}

# El metodo BDF2 de un .din debe declarar la historia de todos sus estados.
sub glm_de_din {
    my ($clase, $s) = @_;
    my $metodo = $s->{corrida}{metodo};
    if (($metodo//'') eq 'SPLIT' || keys %{$s->{split} || {}}) {
        require Siru::SPLIT;
        Siru::SPLIT->de_din($s);
        return undef if ($metodo//'') eq 'SPLIT';
    }
    if (($metodo//'') eq 'ROS' || keys %{$s->{ros} || {}}) {
        require Siru::ROS;
        Siru::ROS->de_din($s);
        return undef if ($metodo//'') eq 'ROS';
    }
    if (($metodo//'') eq 'PGLM' || keys %{$s->{pglm} || {}}) {
        require Siru::PGLM;
        Siru::PGLM->de_din($s);
        return undef if ($metodo//'') eq 'PGLM';
    }
    if (!defined $metodo) {
        croak 'metadatos GLM sin selector' if keys %{$s->{glm} || {}} || @{$s->{historia} || []};
        return undef;
    }
    if ($metodo eq 'GLM') {
        my $d = $s->{glm}{descriptor} // croak 'falta descriptor GLM';
        Siru::CatalogoGLM->validar($d);
        croak 'GLM requiere estados' unless @{$s->{estados}};
        Siru::CatalogoGLM->adaptacion($d, $s->{adaptacion},
            [map {{actual=>$_->[0]}} @{$s->{estados}}]);
        my %requeridas;
        for my $e (@{$s->{estados}}) {
            croak 'estado GLM invalido' unless @$e == 4 && $e->[0] =~ /\A[A-Za-z_][A-Za-z0-9_]*\z/ && $e->[1] eq "p_$e->[0]" && $e->[3] eq '-';
            croak 'estado GLM repetido' if $requeridas{$e->[0]}++;
            $requeridas{$e->[1]} = 1;
            if ($d->{representacion} eq 'bloques') {
                $requeridas{"glm_h${_}_$e->[0]"} = $requeridas{"glm_next${_}_$e->[0]"} = 1 for 1 .. $d->{historia}-1;
            }
        }
        if ($d->{representacion} eq 'bloques') {
            for my $k (qw(a u b v c)) {
                my $b = $d->{bloques}{$k};
                for my $i (0 .. $#$b) {
                    my $cols = $k eq 'c' ? 1 : scalar @{$b->[$i]};
                    $requeridas{"glm_${k}_${i}_$_"} = 1 for 0 .. $cols-1;
                }
            }
        }
        croak 'variable GLM no declarada: ' . join(', ',sort grep { !$s->{declaradas}{$_} } keys %requeridas)
            if grep { !$s->{declaradas}{$_} } keys %requeridas;
        $s->{descriptor_glm} = $d;
        return [$d->{id}];
    }
    croak "metodo de integracion desconocido: $metodo" unless $metodo eq 'BDF2';
    my @estados = @{$s->{estados}};
    my @historia = @{$s->{historia}};
    croak 'BDF2 requiere una fila de historia por estado' unless @estados && @historia == @estados;
    my %esperados = map { $_->[0] => 1 } @estados;
    my %vistos;
    for my $fila (@historia) {
        croak 'historia BDF2 invalida' unless @$fila == 2
            && $fila->[0] =~ /\A[A-Za-z_][A-Za-z0-9_]*\z/
            && $fila->[1] eq "prev_$fila->[0]"
            && $esperados{$fila->[0]} && !$vistos{$fila->[0]}++;
    }
    $s->{descriptor_glm} = Siru::CatalogoGLM->metodo(20);
    return [20];
}

# los estados del sistema de un metodo, como los quiere Siru::Integrador
sub estados_de_integracion {
    my ($clase, $s) = @_;
    return map {
        croak "estados: se esperaba 'actual siguiente inicial estimacion' y vino: @$_"
            unless @$_ == 4;
        my %e = (actual => $_->[0], siguiente => $_->[1], inicial => $_->[2]);
        $e{estimacion} = $_->[3] unless $_->[3] eq '-';
        \%e;
    } @{$s->{estados}};
}

1;
