# Analyses library for visual circuit analysis setup.
# Copyright (C) 2025 Arpad Buermen
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

namespace eval ::analyses {

# Parenthesize string if not empty
proc parenthesize {str} {
    if {[string length $str] > 0} {
        return "( $str )"
    } else {
        return "$str"
    }
}

# Indent a multiline string
proc indent {str indent} {
    set lines [split $str \n]
    set indented_lines [lmap line $lines {string cat $indent $line}]
    return [join $indented_lines \n]
}

# Helper function for display
# props: flat list of name-type pairs
# type can be
#   N .. normal, print value
#   G .. given, print if parameter is given (not empty string)
#   SG .. string given, if quoted, print it out, otherwise just print given
#   NV .. normal, value only
proc format_props {symname props} {
    set args {}
    foreach {propname type} $props {
        set val [ xschem getprop instance $symname $propname ]
        set len [string len $val]
        set quoted [expr {[string index $val 0] eq "\""}]
        if {$len > 0} {
            if { $type eq "N" } {
                lappend args "$propname=$val"
            } elseif { $type eq "NV" } {
                lappend args "$val"
            } elseif { $type eq "G" } {
                lappend args "$propname given"
            } elseif { $type eq "SG" } {
                if {$quoted} {
                    lappend args "$propname=$val"
                } else {
                    lappend args "$propname given"
                }
            }
        }
    }
    return [join $args "\n"]
}

# Helper function for spectre/spice netlisting
# N .. normal
# NV .. normal, value only
# UNV .. unquote, normal, value only
# S .. string (dump quoted)
proc format_args {symname props} {
    set str ""
    set args {}
    foreach {propname type} $props {
        set val [ xschem getprop instance $symname $propname ]
        set len [string len $val]
        # Unquote value if requested
        if { $type eq "UNV" } {
            if {[string match "\"*\"" $val]} {
                set val [string range $val 1 end-1]
            }
        }
        # Add to formatted arguments list
        if {$len > 0} {
            set first false
            if { $type eq "N" } {
                lappend args "$propname=$val"
            } elseif { $type eq "NV" } {
                lappend args "$val"
            } elseif { $type eq "UNV" } {
                lappend args "$val"
            } elseif { $type eq "S" } {
                lappend args "$propname=\"$val\""
            }
        }
    }
    return [join $args " "]
}

# Display output order
proc display_order {symname} {
    set names [list order NV]
    set txt [format_props $symname $names]
    if {[string length $txt] > 0} {
        return "#$txt"
    } else {
        return "unordered (#0)"
    }
}

# Display simulator name
proc display_simulator {symname} {
    set names [list simulator NV]
    return [format_props $symname $names]
}

# Display sweep
proc display_sweep {symname} {
    set names [list sweep N instance N model N parameter N "option" N "variable" N from N to N step N mode N points N "values" N continuation N]
    return [format_props $symname $names]
}

# Display verbatim block
proc display_verbatim {symname} {
    return [format_props $symname [list verbatim NV]]
}

# Display OP analysis
proc display_op {symname} {
    set names [list sweep N nodeset SG store N write N]
    return [format_props $symname $names]
}

# Display 1D DC sweep analysis
proc display_dc1d {symname} {
    set names [list sweep N instance N model N parameter N "option" N "variable" N from N to N step N mode N points N "values" N continuation N nodeset SG store N write N]
    return [format_props $symname $names]
}

# Display DCINC analysis
proc display_dcinc {symname} {
    set names [list sweep N nodeset SG store N write N writeop N]
    return [format_props $symname $names]
}

# Display DCXF analysis
proc display_dcxf {symname} {
    set names [list sweep N outp N outn N in N nodeset SG store N write N writeop N]
    return [format_props $symname $names]
}

# Display AC analysis
proc display_ac {symname} {
    set names [list sweep N from N to N step N mode N points N values N nodeset SG store N write N writeop N]
    return [format_props $symname $names]
}

# Display ACXF analysis
proc display_acxf {symname} {
    set names [list sweep N outp N outn N from N to N step N mode N points N values N nodeset SG store N write N writeop N]
    return [format_props $symname $names]
}

# Display ACSTB analysis
proc display_acstb {symname} {
    set names [list sweep N probe N localgnd N from N to N step N mode N points N values N nodeset SG store N write N writeop N]
    return [format_props $symname $names]
}

# Display ACSP analysis
proc display_acsp {symname} {
    set names [list sweep N ports N from N to N step N mode N points N values N nodeset SG store N write N writeop N]
    return [format_props $symname $names]
}

# Display NOISE analysis
proc display_noise {symname} {
    set names [list sweep N outp N outn N in N from N to N step N mode N points N values N ptssum N nodeset SG store N write N writeop N]
    return [format_props $symname $names]
}

# Display TRAN analysis
proc display_tran {symname} {
    set names [list sweep N step N stop N start N maxstep N icmode N nodeset SG ic SG store N write N]
    return [format_props $symname $names]
}

# Display HB analysis
proc display_hb {symname} {
    set names [list sweep N freq N nharm N immax N truncate N samplefac N nper N sample N harmonic N imorder N nodeset N store N write N]
    return [format_props $symname $names]
}

# Display postprocessing
proc display_postprocess {symname} {
    set names [list file N]
    return [format_props $symname $names]
}

#
# Netlister
#

proc netlister {netlist_type} {
    set cmds {}
    set types [dict create]
    set prefix "netlist_command_"
    foreach {name symfile type} [xschem instance_list] {
        # Do not netlist entry point symbol
        if {[string match netlist_command_header $type]} {
            continue
        }
        # Collect only symbols of type netlist_*
        if {[string match netlist_* $type]} {
            dict set types $name $type
        }
        if {[string match $prefix* $type]} {
            # Get order
            set order [xschem getprop instance $name order]
            if {[string len $order] == 0}  {
                set order 0
            }
            # Append as sublist
            lappend cmds [list $order $name $type]
        }
    }
    # Sort
    set cmds [lsort -integer -index 0 $cmds]
    # Loop and format
    set blocks {}
    foreach tuple $cmds {
        lassign $tuple order name type
        set suffix [string range $type [string length $prefix] end]
        # Construct formatter function name
        set func [join [list "format_" "$suffix" "_" "$netlist_type"] ""]
        try {
            # Format analysis and post analysis script
            set retval [$func $name]
            # retval has 2 members:
            # - command and
            # - post-command (for writing results in ngpice, empty string for spectre)
            lassign $retval cmd postcmd
            # Format sweep and add it to analysis
            set swcmd [format_sweep_chain_$netlist_type $name $cmd types]
            # Wrap (swept) analysis and post analysis script in a block
            if {[string length $swcmd] > 0} {
                lappend blocks [wrap_analysis_$netlist_type $swcmd $postcmd]
            }
        } on error msg {
            puts "Error formatting $name: $msg"
            continue
        }
    }
    return [wrap_control_$netlist_type $blocks]
}


#
# Netlister for VACASK
#

# Wrap analysis
proc wrap_analysis_spectre {cmds postcmd} {
    return "$cmds"
}

# Wrap in control block
proc wrap_control_spectre {cmds} {
    set control [indent [join $cmds "\n\n"] "  "]
    return [join [list "//// begin user architecture code" "control" "$control" "endc" "//// end user architecture code"] "\n"]
}

# Add sweep chain to analysis
proc format_sweep_chain_spectre {name anstr types} {
    upvar 1 $types typesdict
    set sweep [ xschem getprop instance $name sweep ]
    set sweeplist {}
    format_sweep_spectre $name sweeplist typesdict
    # Do we have any sweeps
    if {[llength $sweeplist] > 0} {
        # Yes, reverse sweep chain
        set sweeplist [lreverse $sweeplist]
        # Join sweeps
        set sweeps [join "$sweeplist" "\n"]
        return "$sweeps\n  $anstr"
    } else {
        return "$anstr"
    }
}

# Sweep formatter, construct sweep chain, innermost first
proc format_sweep_spectre {parent sweeplist types} {
    upvar 1 $sweeplist swl
    upvar 1 $types typesdict
    set sweep [ xschem getprop instance $parent sweep ]
    if {[string length $sweep] > 0} {
        # Parent has sweep property
        set tag [ xschem getprop instance $sweep tag ]
        set type [dict get $typesdict $sweep]
        if {[string length $tag] > 0 && ($type eq "netlist_modifier_sweep")} {
            # Sweep has tag property
            try {
                lappend swl [format_single_sweep_spectre $sweep]
            } on error msg {
                # Stop traversing chain on error
                error "$sweep: $msg"
            }
            # Recursion into parent sweep
            format_sweep_spectre $sweep swl typesdict
        }
    }
}

# Fomat a single sweep
proc format_single_sweep_spectre {sweep} {
    set tag [ xschem getprop instance $sweep tag ]
    return "sweep $tag ( [format_sweep_spectre_params $sweep] [format_sweep_spectre_range $sweep] )"
}

# Sweep formatter, what to sweep
proc format_sweep_spectre_params {name} {
    return [format_args $name [list instance N model N parameter N "option" N "variable" N]]
}

# Sweep formatter, how to sweep
proc format_sweep_spectre_range {name} {
    return [format_args $name [list from N to N step N mode N points N "values" N continuation N]]
}

# Analysis formatters
proc format_verbatim_spectre {name} {
    set sim [ xschem getprop instance $name simulator ]
    set dump false
    if {[string length $sim]==0} {
        set dump true
    } else {
        try {
            if {[sim_is_$sim]} {
                set dump true
            }
        } on error msg {}
    }
    if { !$dump } {
        return ""
    }
    return [list [format_args $name [list verbatim NV]] ""]
}

proc format_analysis_op_spectre {name} {
    set args [format_args $name [list nodeset N store N write N]]
    return [list "analysis $name op [parenthesize $args]" ""]
}

proc format_analysis_dc1d_spectre {name} {
    # OP formatting
    set args [format_args $name [list nodeset N store N write N]]
    set anstr "analysis $name op [parenthesize $args]"
    # 1D sweep formatting
    set swp [format_single_sweep_spectre $name]
    return [list "$swp\n  $anstr" ""]
}

proc format_analysis_dcinc_spectre {name} {
    set args [format_args $name [list nodeset N store N write N writeop N]]
    return [list "analysis $name dcinc [parenthesize $args]" ""]
}

proc format_signal_output_spectre {name} {
    set outp [ xschem getprop instance $name outp ]
    set outn [ xschem getprop instance $name outn ]
    set vecstr "\[ $outp"
    if {[string length $outn] > 0} {
        append vecstr ", $outn"
    }
    append vecstr " \]"
    return $vecstr
}

proc format_analysis_dcxf_spectre {name} {
    set args "out=[format_signal_output_spectre $name] "
    append args [format_args $name [list nodeset N store N write N writeop N]]
    return [list "analysis $name dcxf [parenthesize $args]" ""]
}

proc format_analysis_ac_spectre {name} {
    set args "[format_sweep_spectre_range $name] "
    append args [format_args $name [list nodeset N store N write N writeop N]]
    return [list "analysis $name ac [parenthesize $args]" ""]
}

proc format_analysis_acxf_spectre {name} {
    set args "out=[format_signal_output_spectre $name] "
    append args "[format_sweep_spectre_range $name] "
    append args [format_args $name [list nodeset N store N write N writeop N]]
    return [list "analysis $name acxf [parenthesize $args]" ""]
}

proc format_analysis_acstb_spectre {name} {
    set args "[format_args $name [list probe N localgnd N]] "
    append args "[format_sweep_spectre_range $name] "
    append args [format_args $name [list nodeset N store N write N writeop N]]
    return [list "analysis $name acstb [parenthesize $args]" ""]
}

proc format_analysis_acsp_spectre {name} {
    set args "[format_args $name [list ports N]] "
    append args "[format_sweep_spectre_range $name] "
    append args [format_args $name [list nodeset N store N write N writeop N]]
    return [list "analysis $name acsp [parenthesize $args]" ""]
}

proc format_analysis_noise_spectre {name} {
    set args "out=[format_signal_output_spectre $name] "
    append args "[format_args $name [list in N]] "
    append args "[format_sweep_spectre_range $name] "
    append args [format_args $name [list nodeset N store N write N writeop N]]
    return [list "analysis $name noise [parenthesize $args]" ""]
}

proc format_analysis_tran_spectre {name} {
    set args [format_args $name [list step N stop N start N maxstep N icmode N nodeset N ic N store N write N]]
    return [list "analysis $name tran [parenthesize $args]" ""]
}

proc format_analysis_hb_spectre {name} {
    set args [format_args $name [list freq N nharm N immax N truncate N samplefac N nper N sample N harmonic N imorder N nodeset N store N write N]]
    return [list "analysis $name hb [parenthesize $args]" ""]
}

proc format_postprocess_spectre {name} {
    set tool [format_args $name [list tool NV]]
    set file [format_args $name [list file NV]]
    return [list "postprocess($tool, $file)" ""]
}


#
# Netlister for Ngspice
#

# Wrap analysis
proc wrap_analysis_spice {cmds postcmd} {
    set txt ""
    if {[string length $postcmd] > 0} {
        append txt "destroy all\n"
    }
    append txt $cmds
    if {[string length $postcmd] > 0} {
        # Write only if analysis succeeded
        # append txt "\nif $#plots gt 1\n$postcmd\nend"
        append txt "\n$postcmd"
    }
    return $txt
}

# Wrap in control block
proc wrap_control_spice {cmds} {
    set control [join $cmds "\n\n"]
    return [join [list "**** begin user architecture code" ".control" "set filetype=binary" "" "$control" ".endc" "**** end user architecture code"] "\n"]
}

# Add sweep chain to analysis
proc format_sweep_chain_spice {name anstr types} {
    upvar 1 $types typesdict
    # Get analysis type (op/dc1d or ac/noise)
    set antype [dict get $typesdict $name]
    # Is it a dec/oct/lin sweep
    if {$antype eq "netlist_command_analysis_ac"} {
        set decoctlin true
    } elseif {$antype eq "netlist_command_analysis_noise"} {
        set decoctlin true
    } elseif {$antype eq "netlist_command_analysis_op"} {
        # For swept op rename analysis to dc
        set decoctlin false
        set anstr "dc"
    } else {
        set decoctlin false
    }
    set sweep [ xschem getprop instance $name sweep ]
    set sweeplist {}
    format_sweep_spice $decoctlin $name sweeplist typesdict
    # Limit sweep dimension for analyses
    if {[string match "netlist_command_analysis_*" $antype]} {
        if {$antype eq "netlist_command_analysis_op"} {
            if {[llength $sweeplist] > 2} {
                error "sweep dimension can be at most 2"
            }
        } elseif {$antype eq "netlist_command_analysis_dc1d"} {
            if {[llength $sweeplist] > 1} {
                error "sweep dimension can be at most 2"
            }
        } else {
            if {[llength $sweeplist] > 0} {
                error "sweep is not suported"
            }
        }
    }
    # Do we have any sweeps
    if {[llength $sweeplist] > 0} {
        # Yes, join sweeps
        set sweeps [join $sweeplist " "]
        return "$anstr $sweeps"
    } else {
        return "$anstr"
    }
}

# Sweep formatter, construct sweep chain, innermost first
proc format_sweep_spice {decoctlin parent sweeplist types} {
    upvar 1 $sweeplist swl
    upvar 1 $types typesdict
    set sweep [ xschem getprop instance $parent sweep ]
    if {[string length $sweep] > 0} {
        # Parent has sweep property
        set tag [ xschem getprop instance $sweep tag ]
        set type [dict get $typesdict $sweep]
        if {[string length $tag] > 0 && ($type eq "netlist_modifier_sweep")} {
            # Sweep has tag property
            try {
                lappend swl [format_single_sweep_spice $decoctlin $sweep]
            } on error msg {
                # Stop traversing chain on error
                error "$sweep: $msg"
            }
            # Recursion into parent sweep
            format_sweep_spice $decoctlin $sweep swl typesdict
        }
    }
}

# Fomat a single sweep
proc format_single_sweep_spice {decoctlin sweep} {
    return "[format_sweep_spice_params $sweep] [format_sweep_spice_range $decoctlin $sweep]"
}

# Sweep formatter, what to sweep
proc format_sweep_spice_params {name} {
    return [format_args $name [list instance UNV]]
}

# Sweep formatter, how to sweep
proc format_sweep_spice_range {decoctlin name} {
    if {$decoctlin} {
        return [format_args $name [list mode UNV points NV from NV to NV]]
    } else {
        return [format_args $name [list from NV to NV step NV]]
    }
}

# Each formatter returns a list with two elements
# - analysis
# - rawfile write script / post-command script

proc format_verbatim_spice {name} {
    return [list [format_verbatim_spectre $name] ""]
}

proc format_analysis_op_spice {name} {
    return [list "op" "write $name.raw"]
}

proc format_analysis_dc1d_spice {name} {
    # 1D sweep formatting
    set swp [format_single_sweep_spice false $name]
    return [list "dc $swp" "write $name.raw"]
}

proc format_analysis_dcinc_spice {name} {
    error "dcinc is not supported by Ngspice"
}

proc format_signal_output_spice {name} {
    set outp [ xschem getprop instance $name outp ]
    set outn [ xschem getprop instance $name outn ]
    # If outp starts with v(, use it literally, ignore outn
    if {[string match "v(*" $outp]} {
        return $outp
    }
    # If outp starts with i(, use it literally, ignore outn
    if {[string match "i(*" $outp]} {
        return $outp
    }
    # Unquote outp, outn
    if {[string match "\"*\"" $outp]} {
        set outp [string range $outp 1 end-1]
    }
    if {[string match "\"*\"" $outn]} {
        set outn [string range $outn 1 end-1]
    }
    # Is outn empty
    if {[string length $outn] == 0} {
        return "v($outp)"
    } else {
        return "v($outp,$outn)"
    }
}

proc format_analysis_dcxf_spice {name} {
    set output "[format_signal_output_spice $name]"
    return [list "tf $output [format_args $name [list in UNV]]" "write $name.raw"]
}

proc format_analysis_ac_spice {name} {
    set swp "[format_sweep_spice_range true $name] "
    return [list "ac $swp" "write $name.raw"]
}

proc format_analysis_acxf_spice {name} {
    error "acxf is not supported by Ngspice"
}

proc format_analysis_acstb_spice {name} {
    error "acstb is not supported by Ngspice"
}

proc format_analysis_acsp_spice {name} {
    error "acsp is not supported by Ngspice"
}

proc format_analysis_noise_spice {name} {
    set output "[format_signal_output_spice $name]"
    set swp "[format_sweep_spice_range true $name]"
    set writer "write $name-integrated.raw\nsetplot previous\nwrite $name.raw"
    return [list "noise $output [format_args $name [list in UNV]] $swp [format_args $name [list ptssum NV]]" "$writer"]
}

proc format_analysis_tran_spice {name} {
    set args [format_args $name [list step NV stop NV start NV maxstep NV]]
    set icmode [format_args $name [list icmode UNV]]
    if {$icmode eq "uic"} {
        append args " uic"
    }
    return [list "tran $args" "write $name.raw"]
}

proc format_analysis_hb_spice {name} {
    error "hb is not supported by Ngspice"
}

proc format_postprocess_spice {name} {
    global tcl_platform
    set tool [format_args $name [list tool NV]]
    if {![string match "\"*\"" $tool]} {
        # Not quoted, check if it is PYTHON (VACASK variable for autodetected Python)
        if {$tool eq "PYTHON"} {
            if {$tcl_platform(platform) eq "windows"} {
                set tool "python.exe"
            } else {
                set tool "python3"
            }
        }
    }
    # Keep quotes around tool and file
    set file [format_args $name [list file NV]]
    return [list "shell $tool $file" ""]
}

}

