//(P)erhaps (C)lown (N)onsense (I)s (C)onstructive
//(O)nly (T)oward (G)etting (S)ettlers (U)nderwear

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.math.RoundingMode;

//because ALL classes inherit from super parent Object
//we can but are not required to 'extend' Object
public class Quadratic extends Object {        // P  C  N

    // Ivars ─────────────────────────────────── I
    private double a, b, c;
    private double discriminant;
    private OrderedPair vertex;      // composition: Quadratic HAS-A OrderedPair
    private OrderedPair yIntercept;
    private OrderedPair[] roots;     // roots pre-computed as OrderedPair array
    private String axisOfSymmetry;   // NEW: computed String ivar
    private String memoryAddress;    // NEW: JVM address via super.toString()

    // Constructors ───────────────────────────── C
    public Quadratic(){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...default Quadratic constructor...");
        a = 1; b = 0; c = 0;
        computeYIntercept();
        computeDiscriminant();
        computeVertex();
        computeRoots();
        memoryAddress = obtainMemoryAddress();
    }//end default constructor

    public Quadratic(double a, double b, double c){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...3 parameter Quadratic constructor...");
        if (Math.abs(a-0.0) < QuadraticDriver.EPSILON){
            try{
                throw new IllegalArgumentException("a cannot be zero");
            }catch(Exception e){
                System.out.println("Exception: " + e);
                System.out.println("Set 'a' to a non-zero number.");
                System.out.println("Thanks for using our program...program terminated.");
                System.exit(0);
            }
        }
        this.a = a; this.b = b; this.c = c;
        computeYIntercept();
        computeDiscriminant();
        computeVertex();
        computeRoots();
        memoryAddress = obtainMemoryAddress();
    }//end 3 param constructor

    //copy constructor
    public Quadratic(Quadratic orig){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...Quadratic copy constructor...");
        a = orig.a; b = orig.b; c = orig.c;
        computeYIntercept();
        computeDiscriminant();
        computeVertex();
        computeRoots();
        memoryAddress = obtainMemoryAddress();   // NEW object gets its own address
    }//end copy constructor

    // toString ───────────────────────────────── O/T
    @Override public String toString(){
        return "y = f(x) = " + a + "x\u00B2 + " + b + "x + " + c;
    }//end toString

    public String summary(){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...summary...");
        String temp = this.toString();
        temp += "\nVertex:           " + this.getVertex();
        temp += "\nDiscriminant:     " + this.getDiscriminant();
        temp += "\ny-Intercept:      " + this.getYIntercept();
        temp += "\nRoots:            " + this.getRootsDescription();
        temp += "\nAxis of Symmetry: " + this.getAxisOfSymmetry();
        temp += "\nMemory Address:   " + this.getMemoryAddress();
        return temp;
    }//end summary

    //utility calling a utility
    public void printSummary(){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...printSummary...");
        System.out.println(this.summary());
    }//end printSummary

    // Getters ────────────────────────────────── G
    public double getA()              { return a;             }
    public double getDiscriminant()   { return discriminant;  }
    public OrderedPair getVertex()    { return vertex;        }
    public OrderedPair getYIntercept(){ return yIntercept;    }
    public String getAxisOfSymmetry() { return axisOfSymmetry;}
    public String getMemoryAddress()  { return memoryAddress; }

    // Setters — validate + side effects ──────── S
    public void setA(double a){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...setA...");
        if (Math.abs(a-0.0) < QuadraticDriver.EPSILON){
            try{
                throw new IllegalArgumentException("a cannot be zero");
            }catch(Exception e){
                System.out.println("Exception: " + e);
                System.exit(0);
            }
        }
        this.a = a;
        computeYIntercept();
        computeDiscriminant();
        computeVertex();
        computeRoots();
    }//end setA

    public void setB(double b){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...setB...");
        this.b = b;
        computeYIntercept();
        computeDiscriminant();
        computeVertex();
        computeRoots();
    }//end setB

    public void setC(double c){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...setC...");
        this.c = c;
        computeYIntercept();   // tied to c
        computeDiscriminant();
        computeVertex();
        computeRoots();
    }//end setC

    // Utility methods ────────────────────────── U
    public double f(double x){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...f...");
        return a*Math.pow(x, 2) + b*x + c;
    }//end f

    private void computeYIntercept(){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...computeYIntercept...");
        yIntercept = new OrderedPair(0, c);
    }//end computeYIntercept

    private void computeDiscriminant(){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...computeDiscriminant...");
        discriminant = Math.pow(b, 2) - 4*a*c;
    }//end computeDiscriminant

    private void computeVertex(){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...computeVertex...");
        double h = -b/(2*a) + 0.0; //hack to prevent -0.0
        vertex = new OrderedPair(h, f(h));
        vertex.setLabel("V");
        axisOfSymmetry = "x = " + QuadraticDriver.DF.format(h);
    }//end computeVertex

    public String getRootsDescription(){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...getRootsDescription...");
        if (discriminant > 0){
            return "Two real: x = " + roots[0] + " and x = " + roots[1];
        } else if (Math.abs(discriminant - 0) < QuadraticDriver.EPSILON){
            return "One root: x = " + roots[0];
        } else {
            return "Complex: " + roots[0] + " and " + roots[1];
        }
    }//end getRootsDescription

    public void computeRoots(){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...computeRoots...");
        roots = new OrderedPair[2];
        OrderedPair root1, root2;

        if (discriminant > 0){
            double r1=(-b+Math.sqrt(discriminant))/(2*a);
            double r2=(-b-Math.sqrt(discriminant))/(2*a);
            root1 = new OrderedPair(r1, 0);
            root2 = new OrderedPair(r2, 0);
            root1.setLabel("R1");
            root2.setLabel("R2");
            roots[0] = root1;
            roots[1] = root2;
        } else if (Math.abs(discriminant-0) < QuadraticDriver.EPSILON){
            root1 = new OrderedPair(vertex.getX(), 0);
            root1.setLabel("R");
            roots[0] = root1;
            roots[1] = null;
        } else {
            // discriminant < 0: ComplexOrderedPair enters the chat!
            double rp = vertex.getX();
            double ip = Math.sqrt(-discriminant)/(2*a);
            roots[0] = new ComplexOrderedPair(rp,  ip);
            roots[1] = new ComplexOrderedPair(rp, -ip);
            roots[0].setLabel("CR1");
            roots[1].setLabel("CR2");
        }
    }//end computeRoots

    //use a parent property to obtain a child's address
    //this is an example of the idea of 'inheritance'
    public String obtainMemoryAddress(){
        if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
            System.out.println("...obtainMemoryAddress...");
        //we can call a method from the parent by using the prefix 'super'
        String simulatedAddress = super.toString();
        return simulatedAddress;
    }//end obtainMemoryAddress
    
    //test #1
    public static void tryToInflate(int n) {
        n = n + 100;
        System.out.println("  inside tryToInflate:  n = " + n);
    }//end tryToInflate
    
    //test #2
    public static void zapC(Quadratic q) {
        //q is an alias for qa, which was sent as an argument
        q.setC(0);                       // mutates through the alias
        System.out.println("  inside zapC: " + q.getMemoryAddress()); //same address as qa
        System.out.println("  inside zapC:          q = " + q);
    }//end zapC
    
    // ── Test 3: object ── copy of the ADDRESS is passed ────────────
    //    The method CANNOT redirect the caller's variable.
    public static void tryToReplace(Quadratic q) {
        //qb was the argument passed to this method
        q = new Quadratic(9, 9, 9);     // only re-points the LOCAL copy
        //different address!
        System.out.println("  inside tryToReplace: " + q.getMemoryAddress());
        System.out.println("  inside tryToReplace:  q = " + q);
        //qb remains unchanged, once we leave here; local copy, q, 'dies' when we leave
    }//end tryToReplace

}//end class Quadratic