#!/usr/bin/env python3
"""LingoScholar reference compliance runner, method version 1.0.0.

Run: python3 reference_runner.py model-input.json --output model-curve.csv
Or:  python3 reference_runner.py < model-input.json > model-curve.csv

Input is the same JSON object accepted by the browser reference model:
  wall: kind, heightMM, EIKNMM2, GAKN, slidingStiffnessKNPerMM,
        rockingStiffnessKNMMPerRad, displacementsMM, optional capacityKN
  beam: kind, spanMM, EIKNMM2, GAKN, displacementsMM, optional capacityKN

Units: mm, kN, EI kN*mm^2, effective GA kN (includes shear correction),
Ks kN/mm, Ktheta kN*mm/rad. All stiffness parameters must be positive.
Displacements start at zero and strictly increase. No dependencies beyond
Python's standard library. No material, opening or lamination parameters
are invented. The optional plateau is user-assumed, not predicted strength.
This linear reference model does not simulate damage or unloading and is
not validated for CLT walls with openings, VOL beams or structural design.
"""
import argparse
import csv
import json
import math
import sys

METHOD_VERSION = "1.0.0"


def finite_number(value):
    return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)


def positive(value, name):
    if not finite_number(value) or value <= 0:
        raise ValueError(name + " must be a finite positive number")
    return value


def run_reference_model(data):
    if not isinstance(data, dict) or data.get("kind") not in ("wall", "beam"):
        raise ValueError("Choose the wall or beam reference model")
    ei = positive(data.get("EIKNMM2"), "Effective EI (kN*mm^2)")
    ga = positive(data.get("GAKN"), "Effective GA (kN)")
    cap = None
    if "capacityKN" in data:
        cap = positive(data["capacityKN"], "Assumed force plateau (kN)")
    displacements = data.get("displacementsMM")
    if not isinstance(displacements, list) or not 2 <= len(displacements) <= 10000:
        raise ValueError("Provide 2-10000 displacements")
    if not finite_number(displacements[0]) or displacements[0] != 0:
        raise ValueError("The monotonic displacement sequence must start at zero")
    for i, displacement in enumerate(displacements):
        if not finite_number(displacement) or displacement < 0 or (i and displacement <= displacements[i - 1]):
            raise ValueError("Displacements must be finite, non-negative and strictly increasing after zero")
    if data["kind"] == "wall":
        height = positive(data.get("heightMM"), "Wall height H (mm)")
        sliding = positive(data.get("slidingStiffnessKNPerMM"), "Sliding stiffness Ks (kN/mm)")
        rocking = positive(data.get("rockingStiffnessKNMMPerRad"), "Rocking stiffness Ktheta (kN*mm/rad)")
        terms = [height ** 3 / (3 * ei), height / ga, 1 / sliding, height ** 2 / rocking]
    else:
        span = positive(data.get("spanMM"), "Beam span L (mm)")
        terms = [span ** 3 / (48 * ei), span / (4 * ga)]
    for term in terms:
        positive(term, "Compliance term (mm/kN)")
    compliance = positive(sum(terms), "Total compliance (mm/kN)")
    positive(1 / compliance, "Effective stiffness (kN/mm)")
    points = []
    for displacement in displacements:
        force = displacement / compliance
        if cap is not None:
            force = min(force, cap)
        if not finite_number(force):
            raise ValueError("Predicted force exceeded the finite numeric range")
        points.append((displacement, force))
    return points


def reject_nonfinite(token):
    raise ValueError("JSON non-finite number is not permitted: " + token)


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("input", nargs="?", help="Model input JSON; omit to read stdin")
    parser.add_argument("--output", help="Curve CSV output; omit to write stdout")
    args = parser.parse_args()
    try:
        if args.input:
            with open(args.input, encoding="utf-8-sig") as source:
                data = json.load(source, parse_constant=reject_nonfinite)
        else:
            data = json.load(sys.stdin, parse_constant=reject_nonfinite)
        points = run_reference_model(data)
        if args.output:
            with open(args.output, "w", encoding="utf-8", newline="") as target:
                writer = csv.writer(target)
                writer.writerow(["displacement_mm", "force_kN"])
                writer.writerows(points)
        else:
            writer = csv.writer(sys.stdout)
            writer.writerow(["displacement_mm", "force_kN"])
            writer.writerows(points)
    except (ValueError, TypeError, OverflowError, OSError, ZeroDivisionError) as error:
        print("Reference model input error: " + str(error), file=sys.stderr)
        return 2
    return 0


if __name__ == "__main__":
    sys.exit(main())
