'''
Created on Mar-07-2024

@author: bthakur

pip install roman

https://openstax.org/books/calculus-volume-3/pages/2-5-equations-of-lines-and-planes-in-space


'''

import random

import inspect
import traceback

from sympy import (latex,
                   Matrix,
                   nsimplify, Rational,
                   Symbol, symbols,
                   Add, Mul, Eq,
                   UnevaluatedExpr as ueval,
                   cos, sin,
                   )
import sympy as sp
import numpy as np

from xv.util import (UtilClass, list_to_str, toIntOrFloat,
                     get_arrow_symbols,
                     get_arrow_symbol,
                     )

from sympy.vector import (CoordSys3D, BaseVector, BaseScalar,
                          VectorAdd, VectorMul,
                          )

from ._3d_helpers import (Geometry3dBaseHelper,
                          Geometry3dLineHelper,
                          Geometry3dPlainHelper,
                          )


# _geometry_3d_helper
# Geometry3dHelper

class Geometry3dHelper(Geometry3dBaseHelper):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        pass


    def get_problem_3d_line(self, **kwargs):

        line_helper = Geometry3dLineHelper(verbose = self.verbose)

        sub_problem_idx = 0

        v1 = self.get_random_vector()
        v2 = self.get_random_vector()
        v3 = self.get_random_vector()
        N = v1.get_coord_sys()

        prob_stmts = []
        soln_stmts = []
        answer_stmts = []
        prob_soln_answer_tuples = [[], [], []]

        prob_stmt = f"""
        A line passes through points with position vectors $\\displaystyle
        {v1.get_latex()}\\text {{ and }} {v2.get_latex()}
        $.
        """
        prob_stmts.append(prob_stmt.strip())

        #equation of line
        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_line_eq_form1 = line_helper.get_problem_equation_line_using_vector_form(
            v1 = v1, v2 = v2, N = N, **kwargs)

        result_line_eq_form1['prob_s'] =  "Find equation of the line using vector form."
        self._update_stmts(prob_soln_answer_tuples, result_line_eq_form1)

        line_eq = result_line_eq_form1['line_eq']


        # equation of the line using cosines
        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_line_eq_form2 = line_helper.get_problem_equation_line_using_cosines(
            v1 = v1, v2 = v2, N = N, **kwargs)
        result_line_eq_form2['prob_s'] =  "Find equation of the line using cosines."
        self._update_stmts(prob_soln_answer_tuples, result_line_eq_form2)


        # find a point on the line
        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_point = line_helper.get_problem_find_point_on_the_line(
                                        line_eq, N, r_val = 0, **kwargs)
        result_point['prob_s'] =  "Find a point on the line."
        self._update_stmts(prob_soln_answer_tuples, result_point)




        #equation of line
        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_direction_vector = line_helper.get_problem_direction_unit_vector_of_line(
                                    line_eq, N, **kwargs)

        result_direction_vector['prob_s'] =  "Find direction unit vector of the line."
        self._update_stmts(prob_soln_answer_tuples, result_direction_vector)


        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_distance = line_helper.get_problem_distance_of_point_from_line(
                                    v3, line_eq, N, **kwargs)

        result_distance['prob_s'] = f"Find the distance of the line from ${v3.get_latex()}$."
        self._update_stmts(prob_soln_answer_tuples, result_distance)



        prob_stmt = self._get_numbered_divs(prob_soln_answer_tuples[0])
        prob_stmts.append(prob_stmt.strip())

        soln_stmt = self._get_numbered_divs(prob_soln_answer_tuples[1], sep = "<br/><hr/><br/>")
        soln_stmts.append(soln_stmt.strip())

        answer_stmt = self._get_numbered_divs(prob_soln_answer_tuples[2], sep = "<br/>")
        answer_stmts.append(answer_stmt.strip())


        prob_s = "\n\n".join(prob_stmts)
        soln_s = "\n\n".join(soln_stmts)
        answer_s = "\n\n".join(answer_stmts)


        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        return result
        pass


    def get_problem_multiple_questions_about_planes(self, **kwargs):
        sub_problem_idx = 0


        line_helper = Geometry3dLineHelper(verbose = self.verbose)
        plane_helper = Geometry3dPlainHelper(verbose = self.verbose)

        v1 = self.get_random_vector()
        v2 = self.get_random_vector()
        v3 = self.get_random_vector()
        v4 = self.get_random_vector()
        N = v1.get_coord_sys()

        prob_stmts = []
        soln_stmts = []
        answer_stmts = []
        prob_soln_answer_tuples = [[], [], []]

        prob_stmt = f"""
        If the two points in a plane are are given by position vectors $\\displaystyle
        {v1.get_latex()}, {v2.get_latex()} \\text {{ and }} {v3.get_latex()} $.
        """
        prob_stmts.append(prob_stmt.strip())


        #equation of plane
        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_plane_eq = plane_helper.get_problem_equation_of_plane(
                                        v1 = v1, v2 = v2, v3 = v3, **kwargs)
        result_plane_eq['prob_s'] =  "Find equation of the plane."
        self._update_stmts(prob_soln_answer_tuples, result_plane_eq)

        plane_eq = result_plane_eq['plane_eq']

        #equation of normal
        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_normal_eq = plane_helper.get_problem_normal_of_plane_through_3_points(
                                        v1 = v1, v2 = v2, v3 = v3, **kwargs)
        result_normal_eq['prob_s'] =  "Find equation of normal to the plane."
        self._update_stmts(prob_soln_answer_tuples, result_normal_eq)

        unit_n_vector = result_normal_eq['unit_n_vector']
        n_vector = result_normal_eq['n_vector']
        vP = result_normal_eq['vP']


        #equation of plane from a point on it and its normal
        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_normal_eq_form2 = plane_helper.get_problem_plane_eq_from_a_point_and_a_normal(
                                        v1, n_vector, **kwargs)
        result_normal_eq_form2['prob_s'] = f"""
        Verify the equation of a plane by re-deriving from the point $\\displaystyle
        {v1.get_latex()}$ on it and the normal derived in the earlier question.
        """.strip()

        self._update_stmts(prob_soln_answer_tuples, result_normal_eq_form2)


        #equation of line
        result_line_eq_form1 = line_helper.get_problem_equation_line_using_vector_form(
            v1 = v1, v2 = v2, N = N, **kwargs)
        line_eq = result_line_eq_form1['line_eq']


        #equation of plane from a point and a line on it
        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_normal_eq_form3 = plane_helper.get_problem_plane_eq_from_a_point_and_a_line_on_it(
                                         v3, line_eq, **kwargs)
        result_normal_eq_form3['prob_s'] = f"""
        Verify the equation of a plane by re-deriving from the point $\\displaystyle
        {v3.get_latex()}$ on it and containing the line

        $\\displaystyle
        {self.get_line_eq_latex(line_eq)}$.
        """.strip()

        self._update_stmts(prob_soln_answer_tuples, result_normal_eq_form3)


        #equation of line
        result_line_eq_form2 = line_helper.get_problem_equation_line_using_vector_form(
            v1 = v2, v2 = v3, N = N, **kwargs)
        line_eq2 = result_line_eq_form2['line_eq']

        #equation of plane with two lines passing through it
        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_normal_eq_form3 = plane_helper.get_problem_plane_eq_from_two_lines_on_it(
                                         line_eq, line_eq2, **kwargs)
        result_normal_eq_form3['prob_s'] = f"""
        Verify the equation of a plane by re-deriving it if it passes through line

        $\\displaystyle {self.get_line_eq_latex(line_eq)}$, and

        $\\displaystyle {self.get_line_eq_latex(line_eq2)}$

        """.strip()

        self._update_stmts(prob_soln_answer_tuples, result_normal_eq_form3)


        #equation of plane with two lines passing through it
        sub_problem_idx = sub_problem_idx + 1
        kwargs['sub_problem_idx'] = sub_problem_idx

        result_distance_of_a_point = plane_helper.get_problem_plane_distance_of_a_point(
                                         plane_eq, v4, N = N, **kwargs)
        result_distance_of_a_point['prob_s'] = f"""
        Find the distance of this plane from the point  $\\displaystyle {v4.get_latex()}$.
        """.strip()

        self._update_stmts(prob_soln_answer_tuples, result_distance_of_a_point)





        prob_stmt = self._get_numbered_divs(prob_soln_answer_tuples[0], sep = "<br/>")
        prob_stmts.append(prob_stmt.strip())

        soln_stmt = self._get_numbered_divs(prob_soln_answer_tuples[1], sep = "<br/><hr/><br/>")
        soln_stmts.append(soln_stmt.strip())

        answer_stmt = self._get_numbered_divs(prob_soln_answer_tuples[2], sep = "<br/>")
        answer_stmts.append(answer_stmt.strip())


        prob_s = "\n\n".join(prob_stmts)
        soln_s = "\n\n".join(soln_stmts)
        answer_s = "\n\n".join(answer_stmts)


        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        return result
        pass

    def get_problem_angle_bisector_and_projection_in_a_triangle(self, *args, **kwargs):
        """
        TODO: modify the question to a condition for non isosceles triangle
        doc_style: xv_doc
        """

        A, B, C, D, theta = symbols('A, B, C, D, theta')
        tr_ABC = Symbol(f'\\triangle {A}{B}{C}')


        pA_arr = [random.randint(-9, 9), random.randint(-9, 9), random.randint(-9, 9)]
        side_delta = [random.randint(-9, 9), random.randint(-9, 9), random.randint(-9, 9)]

        pB_arr = [pA_arr[idx] + x * random.choice([1, -1])
                  for idx, x in enumerate(random.sample(side_delta, len(side_delta)))]

        pC_arr = [pA_arr[idx] + x * random.choice([1, -1])
                  for idx, x in enumerate(random.sample(side_delta, len(side_delta)))]

        pA = sp.Point(pA_arr)
        pB = sp.Point(pB_arr)
        pC = sp.Point(pC_arr)



        pD = (pB + pC)/2
        ag_BAC = Symbol(f'\\angle {B}{A}{C}')
        vec_AD = Symbol(f'\\overrightarrow{{{latex(A)} {latex(D)}}}')
        vec_AC = Symbol(f'\\overrightarrow{{{latex(A)} {latex(C)}}}')
        vec_AB = Symbol(f'\\overrightarrow{{{latex(A)} {latex(B)}}}')
        vec_AC_calc = sp.Segment(pA, pC)
        vec_AB_calc = sp.Segment(pA, pB)
        vec_AD_calc = sp.Segment(pA, pD)

        AC_coord = (pC - pA).coordinates
        AB_coord = (pB - pA).coordinates
        AD_coord = (pD - pA).coordinates

        AB_dot_AC = np.array(AB_coord).dot(AC_coord)
        cos_theta_calc = sp.sqrt((AB_dot_AC/(vec_AB_calc.length * vec_AC_calc.length) + 1) / 2)

        prob_s = f"""
        Consider a ${latex(tr_ABC)}$
        where
        ${latex(A)}{latex(pA.coordinates)}$,
        ${latex(B)}{latex(pB.coordinates)}$
        and
        ${latex(C)}{latex(pC.coordinates)}$.
        If the angle bisector of ${latex(ag_BAC)}$
        meets the line ${latex(B)} {latex(C)}$ at ${latex(D)}$,
        then what is the length of the projection of
        the vector ${latex(vec_AD)}$ on
        the vector ${latex(vec_AC)}$?

        """.replace('\n', ' ')

        soln_stmts = []

        soln_stmt = f"""
        $\\displaystyle
        \\begin{{align}}
        {latex(vec_AC)}
        &= {latex(C)} - {latex(A)}
        \\\\ \\\\

        &= {latex(pC.coordinates)} - {latex(pA.coordinates)}
        \\\\ \\\\

        &= {latex(AC_coord)}

        \\end{{align}}
        $

        $\\displaystyle
        \\begin{{align}}
        \\left| {latex(vec_AC)} \\right|
        &= \\sqrt{{
            {latex(ueval(AC_coord[0])**2)} +
            {latex(ueval(AC_coord[1])**2)} +
            {latex(ueval(AC_coord[2])**2)}
        }}
        \\\\ \\\\

        &= {latex(vec_AC_calc.length)}
        \\qquad \\cdots \\text{{ (i) }}

        \\end{{align}}
        $
        """
        soln_stmts.append(soln_stmt.strip())

        soln_stmt = f"""
        $\\displaystyle
        \\begin{{align}}
        {latex(vec_AB)}
        &= {latex(B)} - {latex(A)}
        \\\\ \\\\

        &= {latex(pB.coordinates)} - {latex(pA.coordinates)}
        \\\\ \\\\

        &= {latex(AB_coord)}

        \\end{{align}}
        $

        $\\displaystyle
        \\begin{{align}}
        \\left| {latex(vec_AB)} \\right|
        &= \\sqrt{{
            {latex(ueval(AB_coord[0])**2)} +
            {latex(ueval(AB_coord[1])**2)} +
            {latex(ueval(AB_coord[2])**2)}
        }}
        \\\\ \\\\

        &= {latex(vec_AB_calc.length)}
        \\qquad \\cdots \\text{{ (ii) }}

        \\end{{align}}
        $
        """
        soln_stmts.append(soln_stmt.strip())

        soln_stmt = f"""
        As $\\left| {latex(vec_AB)} \\right| = \\left| {latex(vec_AC)} \\right|
        \\qquad \\text{{ from (i) and (ii) }}$

        $\\implies $ ${latex(tr_ABC)}$ is Isosceles triangle.

        $\\implies $ the angle bisector of ${latex(ag_BAC)}
        $ will pass through the midpoint(${D}$) of ${B}{C}$.

        """
        soln_stmts.append(soln_stmt.strip())

        soln_stmt = f"""
        $\\displaystyle
        \\begin{{align}}
        \\implies {latex(D)}
        &= \\frac{{ {latex(B + C)} }}{{2}}
        \\\\ \\\\

        &= \\frac{{
            {latex(pB.coordinates)} +
            {latex(pC.coordinates)}
        }}{{2}}
        \\\\ \\\\

        &= \\frac{{
            {latex((pB + pC).coordinates)}
        }}{{2}}
        \\\\ \\\\

        &= {latex(((pB + pC)/2).coordinates)}

        \\end{{align}}
        $
        """
        soln_stmts.append(soln_stmt.strip())

        soln_stmt = f"""
        $\\displaystyle
        \\begin{{align}}
        {latex(vec_AD)}
        &= {latex(D)} - {latex(A)}
        \\\\ \\\\

        &= {latex(pD.coordinates)} - {latex(pA.coordinates)}
        \\\\ \\\\

        &= {latex(AD_coord)}

        \\end{{align}}
        $

        $\\displaystyle
        \\begin{{align}}
        \\left| {latex(vec_AD)} \\right|
        &= \\sqrt{{
            {latex(ueval(AD_coord[0])**2)} +
            {latex(ueval(AD_coord[1])**2)} +
            {latex(ueval(AD_coord[2])**2)}
        }}
        \\\\ \\\\

        &= {latex(vec_AD_calc.length)}
        \\qquad \\cdots \\text{{ (iii) }}

        \\end{{align}}
        $
        """
        soln_stmts.append(soln_stmt.strip())

        soln_stmt = f"""
        Let ${ag_BAC} = {latex(2*theta)}$. Then
        $\\displaystyle
        \\begin{{align}}
        {latex(vec_AB)} \\cdot {latex(vec_AC)}
        &= {latex(AB_coord)} \\cdot {latex(AC_coord)}
        \\\\ \\\\

        &= {latex(AB_dot_AC)}

        \\end{{align}}
        $

        """
        soln_stmts.append(soln_stmt.strip())

        soln_stmt = f"""
        $\\displaystyle
        \\begin{{align}}
        \\implies
        \\left| {latex(vec_AB)} \\right| \\left| {latex(vec_AC)} \\right|
        {latex(sp.cos(2*theta))}
        &= {latex(AB_dot_AC)}
        \\\\ \\\\

        {latex(sp.cos(2*theta))}
        &= \\frac{{{latex(AB_dot_AC)}}}{{
        \\left| {latex(vec_AB)} \\right| \\left| {latex(vec_AC)} \\right|
        }}
        \\\\ \\\\

        {latex(sp.cos(2*theta))}
        &= \\frac{{{latex(AB_dot_AC)}}}{{
        {latex(vec_AB_calc.length)} \\cdot {latex(vec_AC_calc.length)}
        }}
        \\\\ \\\\

        {latex(sp.cos(2*theta))}
        &= {latex(AB_dot_AC/(vec_AB_calc.length * vec_AC_calc.length))}
        \\\\ \\\\

        {latex(2*sp.cos(theta)**2 - 1)}
        &= {latex(AB_dot_AC/(vec_AB_calc.length * vec_AC_calc.length))}
        \\qquad \\text{{ As }}
        {latex(sp.cos(2*theta))} = {latex(sp.cos(theta)**2)} - {latex(sp.sin(theta)**2)}
        \\\\ \\\\

        {latex(sp.cos(theta))}
        &={latex(cos_theta_calc)}
        \\qquad \\cdots \\text{{ (iv) }}


        \\end{{align}}
        $

        """
        soln_stmts.append(soln_stmt.strip())


        soln_stmt = f"""
        Projection of ${latex(vec_AD)}$ on ${latex(vec_AC)}$
        $\\displaystyle
        \\begin{{align}}
        &= \\left|{latex(vec_AD)}\\right| {latex(sp.cos(theta))}
        \\\\ \\\\

        &= \\left|{latex(vec_AD)}\\right| {latex(cos_theta_calc)}
        \\qquad \\cdots \\text{{ From (iv) }}
        \\\\ \\\\

        &= {latex(vec_AD_calc.length)} {latex(cos_theta_calc)}
        \\\\ \\\\

        &= {latex(sp.simplify(vec_AD_calc.length * cos_theta_calc, force = True))}
        \\\\ \\\\


        \\end{{align}}
        $

        """
        soln_stmts.append(soln_stmt.strip())

        soln_stmt = f"""

        """
        soln_stmts.append(soln_stmt.strip())

        soln_stmt = f"""

        """
        soln_stmts.append(soln_stmt.strip())

        soln_stmt = f"""

        """
        soln_stmts.append(soln_stmt.strip())


        soln_s = '\n\n'.join(soln_stmts)

        answer_s = f"""

        """

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result
        pass


    def get_problem_plane_equation_from_three_points(self,
                                                     v1 = None,
                                                     v2 = None,
                                                     v3 = None,
                                                     N = None,
                                                     **kwargs):
        """
        doc_style: xv_doc
        """

        N = self._parse_N(v1, v2, v3, N = N)

        if v1 is None or v2 is None or v3 is None:
            v1, v2, v3 = self._get_random_non_collinear_vectors(N = N)
            pass

        x, y, z = symbols("x, y, z")
        a, b, c, d = symbols("a, b, c, d")
        A, B, C = symbols("A, B, C")
        AB, AC, n = get_arrow_symbols("AB AC n".split(" "))

        vP = self.get_vector(x, y, z, N)
        A_coords = list(v1.get_coords())
        B_coords = list(v2.get_coords())
        C_coords = list(v3.get_coords())
        AB_coords = list((v2 - v1).get_coords())
        AC_coords = list((v3 - v1).get_coords())
        n_vector = (v2 - v1).cross(v3 - v1)
        n_coords = list(n_vector.get_coords())
        base_vectors = get_arrow_symbols(['i', 'j', 'k'])
        cross_product_matrix = Matrix([base_vectors, AB_coords, AC_coords])

        prob_s = f"""
        Find the equation of the plane passing through the three points
        $\\displaystyle {latex(A)}{latex(tuple(A_coords))}$,
        $\\displaystyle {latex(B)}{latex(tuple(B_coords))}$,
        $\\displaystyle {latex(C)}{latex(tuple(C_coords))}$.
        Use the vector method, determinant method and general form method.
        """.strip()

        if all([sp.simplify(coord) == 0 for coord in n_coords]):
            soln_s = f"""
            <h3>Given points</h3>
            $\\displaystyle
            {latex(A)}{latex(tuple(A_coords))},\\quad
            {latex(B)}{latex(tuple(B_coords))},\\quad
            {latex(C)}{latex(tuple(C_coords))}
            $

            <h3>1. Check collinearity</h3>
            If $\\displaystyle {latex(AB)}$ and $\\displaystyle {latex(AC)}$
            are parallel, then $\\displaystyle {latex(A)}, {latex(B)}, {latex(C)}$
            are collinear and no unique plane exists.

            $\\displaystyle
            \\begin{{aligned}}
            {latex(AB)}
            &= {latex(B)} - {latex(A)}
            = {latex(tuple(B_coords))} - {latex(tuple(A_coords))}
            = {latex(tuple(AB_coords))}
            \\\\
            {latex(AC)}
            &= {latex(C)} - {latex(A)}
            = {latex(tuple(C_coords))} - {latex(tuple(A_coords))}
            = {latex(tuple(AC_coords))}
            \\\\
            {latex(AB)} \\times {latex(AC)}
            &= {latex(cross_product_matrix.as_symb_det())}
            \\\\
            &= {n_vector.get_latex()}
            \\end{{aligned}}
            $

            Since $\\displaystyle {latex(AB)} \\times {latex(AC)} = 0$,
            the three points are collinear. Therefore, no unique plane exists.
            """.strip()

            answer_s = "No unique plane exists."
            result = {
                'prob_s': prob_s,
                'soln_s': soln_s,
                'answer_s': answer_s,
                'plane_eq': None,
                }
            return result
            pass

        plane_expr = sp.expand((vP - v1).dot(n_vector))
        plane_eq = Eq(plane_expr, 0)

        d_value = sp.expand(-sum([n_coords[idx] * A_coords[idx] for idx in range(3)]))

        det_matrix = Matrix([
            [x - A_coords[0], y - A_coords[1], z - A_coords[2]],
            AB_coords,
            AC_coords,
            ])
        det_expr = sp.expand(det_matrix.det())
        det_eq = Eq(det_expr, 0)

        coeff_eqs = [
            Eq(a * A_coords[0] + b * A_coords[1] + c * A_coords[2] + d, 0),
            Eq(a * B_coords[0] + b * B_coords[1] + c * B_coords[2] + d, 0),
            Eq(a * C_coords[0] + b * C_coords[1] + c * C_coords[2] + d, 0),
            ]
        coeff_matrix = Matrix([
            [A_coords[0], A_coords[1], A_coords[2], 1],
            [B_coords[0], B_coords[1], B_coords[2], 1],
            [C_coords[0], C_coords[1], C_coords[2], 1],
            ])
        coeff_solution = Matrix([n_coords[0], n_coords[1], n_coords[2], d_value])

        soln_s = f"""
        <h3>Given points</h3>
        $\\displaystyle
        {latex(A)}{latex(tuple(A_coords))},\\quad
        {latex(B)}{latex(tuple(B_coords))},\\quad
        {latex(C)}{latex(tuple(C_coords))}
        $

        <h3>1. Check non-collinearity</h3>
        The three points must not be collinear for a unique plane to exist.
        First, form two direction vectors in the plane:

        $\\displaystyle
        \\begin{{aligned}}
        {latex(AB)}
        &= {latex(B)} - {latex(A)}
        = {latex(tuple(B_coords))} - {latex(tuple(A_coords))}
        = {latex(tuple(AB_coords))}
        \\\\
        {latex(AC)}
        &= {latex(C)} - {latex(A)}
        = {latex(tuple(C_coords))} - {latex(tuple(A_coords))}
        = {latex(tuple(AC_coords))}
        \\\\
        {latex(AB)} \\times {latex(AC)}
        &= {latex(cross_product_matrix.as_symb_det())}
        \\\\
        &= {n_vector.get_latex()}
        \\end{{aligned}}
        $

        Since $\\displaystyle {latex(AB)} \\times {latex(AC)} \\ne 0$,
        the vectors $\\displaystyle {latex(AB)}$ and $\\displaystyle {latex(AC)}$
        are not parallel. Therefore, the three points are not collinear and
        a unique plane exists.

        <h3>2. Vector method</h3>
        From the non-collinearity check, $\\displaystyle {latex(n)} = {latex(AB)} \\times {latex(AC)} = {n_vector.get_latex()}$ is a normal vector to the plane.

        Let $\\displaystyle P(x,y,z)$ be any point on the plane.

        Since $\\displaystyle {latex(n)}$ is normal to the plane,
        $\\displaystyle
        \\begin{{aligned}}
        &{latex(n)} \\cdot \\left(P - {latex(A)}\\right) = 0 \\\\
        &{n_vector.get_latex()} \\cdot \\left({latex(tuple([x, y, z]))} - {latex(tuple(A_coords))}\\right) = 0 \\\\
        &{latex(plane_eq)}
        \\end{{aligned}}
        $

        <h3>3. Determinant method</h3>
        $\\displaystyle
        \\begin{{aligned}}
        &{latex(det_matrix.as_symb_det())} = 0
        \\\\
        &{latex(det_matrix.det())} = 0
        \\\\
        &{latex(det_eq)}
        \\end{{aligned}}
        $

        <h3>4. General form method</h3>
        Assume the plane is $\\displaystyle ax + by + cz + d = 0$.
        Substituting the three points gives

        $\\displaystyle
        \\begin{{aligned}}
        {latex(coeff_eqs[0])}
        \\\\
        {latex(coeff_eqs[1])}
        \\\\
        {latex(coeff_eqs[2])}
        \\end{{aligned}}
        $

        In matrix form,
        $\\displaystyle
        {latex(coeff_matrix)}
        {latex(Matrix([a, b, c, d]))}
        =
        {latex(Matrix([0, 0, 0]))}
        $.
        The coefficients are determined up to a non-zero scale.
        One non-zero solution is

        $\\displaystyle
        {latex(Matrix([a, b, c, d]))}
        =
        {latex(coeff_solution)}
        $

        Therefore, the plane is
        $\\displaystyle {latex(plane_eq)}$.

        """.strip()

        answer_s = f"""
        $\\displaystyle {latex(plane_eq)}$
        """.strip()

        result = {
            'v1': v1,
            'v2': v2,
            'v3': v3,
            'n_vector': n_vector,
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            'plane_eq': plane_eq,
            }
        return result
        pass


    def _get_random_non_collinear_vectors(self, N = None):
        if N is None:
            N = self.N

        for _ in range(100):
            v1 = self.get_random_vector(N = N)
            v2 = self.get_random_vector(N = N)
            v3 = self.get_random_vector(N = N)
            n_vector = (v2 - v1).cross(v3 - v1)
            n_coords = list(n_vector.get_coords())
            if not all([sp.simplify(coord) == 0 for coord in n_coords]):
                return v1, v2, v3
                pass
            pass

        raise ValueError("Failed to generate three non-collinear points.")
        pass


    pass
#class Geometry3dHelper:
