-- tkz_elements_line.lua
-- date 2025/05/25
-- version 4.00c
-- Copyright 2025  Alain Matthes
-- This work may be distributed and/or modified under the
-- conditions of the LaTeX Project Public License, either version 1.3
-- of this license or (at your option) any later version.
-- The latest version of this license is in
-- http://www.latex-project.org/lppl.txt
-- and version 1.3 or later is part of all distributions of LaTeX
-- version 2005/12/01 or later.
-- This work has the LPPL maintenance status “maintained”.
-- The Current Maintainer of this work is Alain Matthes.

-- -------------------------------------------------------------------------
--                           Lines
-- -------------------------------------------------------------------------
line = {}
line.__index = line
-- Function to create a new line object with two points za and zb.
function line:new(za, zb)
    local type = 'line'
    local mid = (za + zb) / 2
    local north_pa = rotation_(za, math.pi / 2, zb)
    local south_pa = rotation_(za, -math.pi / 2, zb)
    local north_pb = rotation_(zb, -math.pi / 2, za)
    local south_pb = rotation_(zb, math.pi / 2, za)
    local west = rotation_(za, math.pi / 2, north_pa)
    local east = rotation_(zb, math.pi / 2, south_pb)
    local slope = angle_normalize_(point.arg(zb - za))
    local length = point.mod(zb - za)
    local vec = vector:new(za, zb)
    local li = {
        pa = za,
        pb = zb,
        north_pa = north_pa,
        south_pa = south_pa,
        west = west,
        east = east,
        north_pb = north_pb,
        south_pb = south_pb,
        slope = slope,
        mid = mid,
        type = type,
        vec = vec,
        length = length
    }
    setmetatable(li, self)
    return li
end

setmetatable(line, {
  __call = function(cls, ...)
    return cls:new(...)
  end
})

function line:get(i)
  if i == 1 then
    return self.pa
  elseif i == 2 then
    return self.pb
  else
    return self.pa, self.pb
  end
end

-------------------
-- Résultats -> réel
-------------------

-- Calculate the distance between a point and a line
function line:distance(pt)   
    return point.mod(projection(self, pt) - pt)
end

-------------------
-- Résultats -> booléen
-------------------

-- Checks whether a point is on the line (outside the segment)
function line:in_out(pt)
    return math.abs((pt - self.pa) ^ (pt - self.pb)) <= tkz_epsilon
end

-- Checks if a point is on the line segment
function line:in_out_segment(pt)
    return point.mod(pt - self.pa) + point.mod(pt - self.pb) - point.mod(self.pb - self.pa) <= tkz_epsilon
end

function line:is_parallel(L)
    return math.abs(self.slope - L.slope) < tkz_epsilon
end

function line:is_orthogonal(L)
    local alpha = angle_between_vectors(self.pa, self.pb, L.pa, L.pb)
    return math.abs(alpha - math.pi / 2) < tkz_epsilon
end

function line:is_equidistant(p)
    return math.abs(point.mod(self.pa - p) - point.mod(self.pb - p)) < tkz_epsilon
end

-------------------
-- Résultats -> point
-------------------

-- Returns a point on the line to a distance d from pa (optionally modified by pt).
function line:report(d, pt)
    if not self.length or math.abs(self.length) < tkz_epsilon  
    then
        tex.error("self.length must be non-zero")
    end
    local t = d / self.length
    local result = barycenter_({self.pa, 1 - t}, {self.pb, t})
    if pt then
        return result + pt - self.pa
    else
        return result
    end
end
line.point_at_distance = line.report

-- Calculates the barycenter of two points on the line with weights ka and kb
function line:barycenter(ka, kb)
    return barycenter_({self.pa, ka}, {self.pb, kb})
end

function line:point(t)
    return barycenter_({self.pa, 1 - t}, {self.pb, t})
end

function line:midpoint()
    return (self.pa + self.pb) / 2
end

function line:harmonic_int(pt)
    return div_harmonic_int_(self.pa, self.pb, pt)
end

function line:harmonic_ext(pt)
    return div_harmonic_ext_(self.pa, self.pb, pt)
end

function line:harmonic_both(k)
    return div_harmonic_both_(self.pa, self.pb, k)
end

-- Returns the point corresponding to the gold ratio on the line
function line:gold_ratio()
    return self.pa + (self.pb - self.pa) * tkzinvphi
end

-- Normalize line (point at one unit distance)
function line:normalize()
    return self.pa + (self.pb - self.pa) / point.mod(self.pb - self.pa)
end

-- Reverse normalizes the line
function line:normalize_inv()
    return normalize_(self.pb, self.pa)
end

function line:colinear_at(pt, k)
    if k == nil then
        return colinear_at_(self.pa, self.pb, pt, 1)
    else
        return colinear_at_(self.pa, self.pb, pt, k)
    end
end

function line:orthogonal_at(pt, k)
    if k == nil then
        return orthogonal_at_(self.pa, self.pb, pt, 1)
    else
        return orthogonal_at_(self.pa, self.pb, pt, k)
    end
end

function line:random()
  math.randomseed(os.time())
  return self:point(math.random())
end

-------------------
-- Result -> line
-------------------

function line:ll_from( pt )
	return line:new(pt, pt + self.pb - self.pa) 
end

function line:ortho_from( pt )
	return line:new(pt + (self.pb - self.pa) * point(0, -1),
                  pt + (self.pb - self.pa) * point(0, 1))
end

function line:mediator() 
   local m = midpoint_(self.pa, self.pb)
  return line:new(rotation_(m, -math.pi / 2, self.pb), rotation_(m, math.pi / 2, self.pb)) 
end
line.perpendicular_bisector = line.mediator

function line:swap_line()
  self.pa, self.pb = self.pb, self.pa  -- Inverse les points pa et pb
  return line:new(self.pa, self.pb)  -- Crée et retourne la ligne avec les points permutés
end

-------------------
-- Result -> circle
-------------------

function line:circle(swap)
  swap = (swap == "swap")
  if swap then
    return circle:new(self.pb, self.pa)
  else
    return circle:new(self.pa, self.pb)
  end
end

function line:circle_swap()   
    return circle:new(self.pb,self.pa)
end

function line:diameter()
   local c = midpoint_(self.pa, self.pb)
  return circle:new(c, self.pb)
end

function line:apollonius(k)
   local z1,z2,c
    z1 = barycenter_({self.pa, 1}, {self.pb, k})
    z2 = barycenter_({self.pa, 1}, {self.pb, -k})
    c = midpoint_(z1, z2)
  return circle:new(c, z2)
end

function line:test(x,y) 
  
end

-- Circle tangent to a line passing through two points
-- In general, there are two solutions
function line:c_l_pp(a, b) -- a and b on the same side
  -- Initialisation
  local lab = line:new(a, b) -- Line through a and b
  local Cab = circle:diameter(a, b) -- Circle with a and b diameters
  local i = intersection(lab, self) -- Intersection with current line
 
  -- One point on the line  (a)
   if self:in_out(a) and not self:in_out(b) 
   then
    local lmed = lab : mediator()
    local laperp = self:ortho_from(a)
    local o = intersection(lmed,laperp)
    return circle:new(o,a), circle:new(o,a)
    end
      -- One point on the line  (b)
    if self:in_out(b) and not self:in_out(a) then
     local lmed  = lab:mediator()
     local laperp = self:ortho_from(b)
     local o = intersection(lmed, laperp)
     return circle:new(o, b), circle:new(o, b)
    end    
  -- Check: if the intersection exists and lies on the segment [a, b].
   if i and lab:in_out_segment(i) then
    return nil, nil -- No circle is possible
   end

  -- If the current line is orthogonal to lab
  if self:is_orthogonal(lab) then
    local lmed = lab:mediator() 
    local m = midpoint(a, b) 
    local r = length(m, i) 
    local pt1 = lab:isosceles_s(r)
    local pt2 = lab:isosceles_s(r, true)
    return circle:new(pt1, a), circle:new(pt2, a)
  end

  -- If the two lines are parallel
  if lab:is_parallel(self) then
    local mid = midpoint(a, b) -- Midpoint of segment [a, b]
    local proj = self:projection(mid) -- Mid projection on the running line

    return circle:new(circum_center_(a, b, proj), proj),
           circle:new(circum_center_(a, b, proj), proj)
  end

  -- General case
  local t = Cab:tangent_from(i).pb
  local x, y = intersection(self, circle:new(i, t))
  return circle:new(intersection(self:ortho_from(x), lab:mediator()), x),
         circle:new(intersection(self:ortho_from(y), lab:mediator()), y)
end

-- Circle tangent to two straight lines passing through a given point
function line:c_ll_p(a, p)
  
  -- Compute the bisector of the triangle formed by self.pa, self.pb, and a
  local lbi = bisector(self.pa, self.pb, a)

  if lbi:in_out(p) then
    -- Orthogonal projection of p onto the bisector
    local lp = lbi:ortho_from(p)

    -- Intersection of line from p to its projection with self.pa and self.pb
    local i = intersection_ll_(p, lp.pb, self.pa, self.pb)

    -- Intersection points of the line with the circle defined by (i, p)
    local t1, t2 = intersection_lc_(self.pa, self.pb, i, p)

    -- Create the main line and find orthogonal projections from t1 and t2
    local lab = line:new(self.pa, self.pb)
    local x = lab:ortho_from(t1).pb
    local y = lab:ortho_from(t2).pb

    -- Return two circles based on the orthogonal projections and points t1, t2
    return circle:new(intersection_ll_(x, t1, self.pa, p), t1),
           circle:new(intersection_ll_(y, t2, self.pa, p), t2)
  else
    -- Reflection of p across the bisector
    local q = lbi : reflection (p)

    -- Compute circles from the Wallis construction
    local c1, c2 = self:c_l_pp(p, q)

    -- Return two circles with centers and points on their circumference
    return  c1,c2
  end
end

----------------------
-- Result -> triangle
----------------------
function line:equilateral(swap)
   swap = (swap == "swap")
    if swap then
        return triangle:new(self.pa, self.pb, rotation_(self.pa, -math.pi / 3, self.pb))
    else
        return triangle:new(self.pa, self.pb, rotation_(self.pa, math.pi / 3, self.pb))
    end
end

function line:isosceles(a, swap)
  swap = (swap == "swap")
  local c1, c2, pta, ptb, pt1, pt2
  -- Create circles with radius 'a' centered on self.pa and self.pb
  c1 = circle : radius (self.pa, a)
  c2 = circle : radius (self.pb, a)
  
  -- Calculating the points of intersection of the two circles
  pta, ptb = intersection_cc(c1, c2)
  
  if get_angle(self.pa, self.pb, pta) < get_angle(self.pa, self.pb, ptb) then
    pt1 = pta
    pt2 = ptb
  else
    pt1 = ptb
    pt2 = pta
  end 
  if swap then
    return triangle : new (self.pa, self.pb, pt2)
  else
    return triangle : new (self.pa, self.pb, pt1)
  end
end

function line:school(swap)
   local pta, ptb, pt
   swap = (swap == "swap")

   if swap then
       pta = rotation_(self.pa, math.pi / 3, self.pb)
       ptb = rotation_(self.pb, -math.pi / 6, self.pa)
   else
       pta = rotation_(self.pa, math.pi / 6, self.pb)
       ptb = rotation_(self.pb, -math.pi / 3, self.pa)
   end

   pt = intersection_ll_(self.pa, pta, self.pb, ptb)
   return triangle:new(self.pa, self.pb, pt)
end


function line:half(swap)
   local x, pt
   x = midpoint_(self.pa, self.pb)
   swap = (swap == "swap")

   if swap then
       pt = rotation_(self.pa, math.pi / 2, x)
   else
       pt = rotation_(self.pb, -math.pi / 2, x)
   end

   return triangle:new(self.pa, self.pb, pt)
end

function line:two_angles(alpha, beta, swap)
    local pta, ptb, pt
    swap = (swap == "swap")
    
    if swap then
        pta = rotation_(self.pa, -alpha, self.pb)
        ptb = rotation_(self.pb, beta, self.pa)
    else
        pta = rotation_(self.pa, alpha, self.pb)
        ptb = rotation_(self.pb, -beta, self.pa)
    end
    
    pt = intersection_ll_(self.pa, pta, self.pb, ptb)
    return triangle:new(self.pa, self.pb, pt)
end

line.asa = line.two_angles
line.a_a = line.two_angles

function line:s_s(a, b, swap)
   local pta, ptb, i, j
   swap = (swap == "swap")

   pta = self.pa + point(a, 0)
   ptb = self.pb + point(-b, 0)

   local i, j = intersection_cc_(self.pa, pta, self.pb, ptb)
   local a = get_angle(self.pa,self.pb,i)
   if a < 0  then i, j = j, i end
   if swap then
       return triangle:new(self.pa, self.pb, j)
   else
       return triangle:new(self.pa, self.pb, i)
   end
end
line.sss = line.s_s

function line:s_a(a, phi, swap)
    local x = rotation_(self.pb, -phi, self.pa)
    local y = report_(self.pa, self.pb, a)
    local u, v = intersection_lc_(self.pb, x, self.pa, y)

    if not u then -- Pas d'intersection
        tex.error("Invalid configuration. No solution found.")
        return nil
    end

    if point.mod(u - v) < tkz_epsilon then -- Solution unique
        return triangle:new(self.pa, self.pb, u)
    end

    -- Deux solutions
    if length(self.pb, u) < length(self.pb, v) then
        u, v = v, u
    end

    if swap == "swap" then
        if point.mod(v - self.pb) < tkz_epsilon then
            tex.error("Degenerated triangle.")
            return nil
        else
            return triangle:new(self.pa, self.pb, v)
        end
    else
        return triangle:new(self.pa, self.pb, u)
    end
end
line.ssa = line.s_a

function line:a_s(a, phi, swap)
    local x = rotation_(self.pa, phi, self.pb)
    local y = report_(self.pb, self.pa, a)
    local u, v = intersection_lc_(self.pa, x, self.pb, y)

    if not u then -- No intersection
        tex.error("Invalid configuration. No solution found.")
        return nil
    end

    if point.mod(u - v) < tkz_epsilon then -- One-stop solution
        return triangle:new(self.pa, self.pb, u)
    end

    -- Two solutions
    if length(self.pa, u) < length(self.pa, v) then
        u, v = v, u
    end

    if swap == "swap" then
        if point.mod(v - self.pa) < tkz_epsilon then
            tex.error("Degenerated triangle.")
            return nil
        else
            return triangle:new(self.pa, self.pb, v)
        end
    else
        return triangle:new(self.pa, self.pb, u)
    end
end



function line:sa_(a, phi, swap)
    local x, pt
    swap = (swap == "swap")
    x =report_(self.pa,self.pb,a)
    if swap then
        pt = rotation_(self.pa, -phi, x)
    else
        pt = rotation_(self.pa, phi, x)
    end
    return triangle:new(self.pa, self.pb, pt)
end

function line:_as(a, phi, swap)
    local x, pt
    swap = (swap == "swap")
    x =report_(self.pb,self.pa,a)
    if swap then
        pt = rotation_(self.pb, phi, x)
    else
        pt = rotation_(self.pb, -phi, x)
    end
    return triangle:new(self.pa, self.pb, pt)
end

--------------------------
---- sacred triangles ----
--------------------------

function line:gold(swap)
    local pt
    swap = (swap == "swap")

    if swap then  
        pt = rotation_(self.pa, -math.pi / 2, self.pb)
    else
        pt = rotation_(self.pa, math.pi / 2, self.pb)
    end
    
    return triangle:new(self.pa, self.pb, self.pa + (pt - self.pa) * tkzinvphi)
end

function line:golden(swap)
    local pta, ptb, pt
    swap = (swap == "swap")
    local angle = 2 * math.pi / 5

    if swap then  
        pta = rotation_(self.pa, -angle, self.pb)
        ptb = rotation_(self.pb, angle, self.pa)
    else
        pta = rotation_(self.pa, angle, self.pb)
        ptb = rotation_(self.pb, -angle, self.pa)
    end

    pt = intersection_ll_(self.pa, pta, self.pb, ptb)
    return triangle:new(self.pa, self.pb, pt)
end
line.sublime = line.golden 
line.euclid = line.golden

function line:divine(swap)
    swap = (swap == "swap")
    local angle = math.pi / 5
    local pta = rotation_(self.pa, swap and -angle or angle, self.pb)
    local ptb = rotation_(self.pb, swap and angle or -angle, self.pa)
    local pt = intersection_ll_(self.pa, pta, self.pb, ptb)
    return triangle:new(self.pa, self.pb, pt)
end

function line:egyptian(swap)
    swap = (swap == "swap")
    local n = rotation_(self.pb, swap and math.pi / 2 or -math.pi / 2, self.pa)
    local pt = self.pb + (n - self.pb) / point.mod(n - self.pb) * self.length * 0.75
    return triangle:new(self.pa, self.pb, pt)
end

line.pythagoras = line.egyptian
line.isis = line.egyptian

line.golden_gnomon = line.divine

------------------------------
-- Résultat -> carré
------------------------------
function line:square(swap)
   swap = (swap == "swap")
    if swap then 
    return square:side(self.pa, self.pb, "swap")
  else
    return square:side(self.pa, self.pb)
  end
end

-------------------
-- Transformations
-------------------

-- Translation of a given point in the direction of the line (pb - pa)
function line:translation_pt(pt)
    return translation_(self.pb - self.pa, pt)
end

-- Translation of a circle object in line direction
function line:translation_C(obj)
    local pa = obj.center
    local pb = obj.through
    local x, y = set_translation_(self.pb - self.pa, pa, pb)
    return circle:new(x, y)
end

--Translation of a triangle object in line direction
function line:translation_T(obj)
    local pa = obj.pa
    local pb = obj.pb
    local pc = obj.pc
    local x, y, z = set_translation_(self.pb - self.pa, pa, pb, pc)
    return triangle:new(x, y, z)
end

-- Translation of a line object along the line direction
function line:translation_L(obj)
    local pa = obj.pa
    local pb = obj.pb
    local x, y = set_translation_(self.pb - self.pa, pa, pb)
    return line:new(x, y)
end

-- General function for translating one or more objects
function line:translation(...)
    local tp = table.pack(...)  -- Groups all arguments in a table
    local obj = tp[1]  -- The first object is retrieved
    local nb = tp.n  -- Number of objects to be processed

    --If only one object is passed
    if nb == 1 then
        if obj.type == "point" then
            return translation_(self.pb - self.pa, obj) 
        elseif obj.type == "line" then
            return self:translation_L(obj)
        elseif obj.type == "triangle" then
            return self:translation_T(obj)
        elseif obj.type == "circle" then
            return self:translation_C(obj)
        else
            tex.error("Unsupported object type for translation")
        end
    else
        -- If several objects are passed, they are translated one by one
        local t = {}
        for i = 1, nb do
            -- Translation of each object with the translation vector
            table.insert(t, translation_(self.pb - self.pa, tp[i]))
        end
        return table.unpack(t)  --Returns translated objects
    end
end

-- Function for translating an object using line direction
function line:set_translation(...)
    return set_translation_(self.pb - self.pa, ...)
end

function line:projection_ll(...)
  local tp = table.pack(...)
  local c, d = tp[1].pa, tp[1].pb  -- c and d are the two points that define the projection line
  local obj = tp[2]  -- The object to be projected
  local nb = tp.n  -- Number of objects passed as parameters
  
  if nb == 2 then
    --Projection of line onto object
    return projection_ll_(self.pa, self.pb, c, d, obj)
  else
    local t = {}
    for i = 2, tp.n do
      -- Projection of each object on the line
      table.insert(t, projection_ll_(self.pa, self.pb, c, d, tp[i]))
    end
    return table.unpack(t)  -- Retourne les résultats de la projection pour tous les objets
  end
end


function line:set_projection_ll(...)
  local tp = table.pack(...)
  local c, d = tp[1].pa, tp[1].pb  -- c and d are the two points defining the projection line
  local t = {}
  
  for i = 2, tp.n do
    -- Projection de chaque objet sur la ligne
    table.insert(t, projection_ll_(self.pa, self.pb, c, d, tp[i]))
  end
  return table.unpack(t)  -- Returns projection results for all objects
end



function line:projection(...)
    local tp = table.pack(...)  -- Groups arguments in a table
    local obj = tp[1]           -- Retrieves the first object
    local nb = tp.n             -- Number of objects to be processed
    
    -- If only one object is passed
    if nb == 1 then
        return projection_(self.pa, self.pb, obj)  -- Projects object onto line
    else
        local t = {}
        -- If several objects are passed, each is projected onto the line
        for i = 1, tp.n do
            table.insert(t, projection_(self.pa, self.pb, tp[i]))  -- Projection of each object
        end
        return table.unpack(t)  --Return projections
    end
end

function line:set_projection(...)
    local tp = table.pack(...)  -- Groups arguments in a table
    local t = {}
    -- Projects each object onto the line
    for i = 1, tp.n do
        table.insert(t, projection_(self.pa, self.pb, tp[i]))  --Projection of each object
    end
    return table.unpack(t)  -- Return projections
end


function line:symmetry_axial_L( obj )
  local pa = obj.pa
  local pb = obj.pb
  local x, y = self:set_reflection(pa, pb)
  return line:new(x, y)
end

function line:symmetry_axial_T( obj )
  local pa = obj.pa
  local pb = obj.pb
  local pc = obj.pc
  local x, y, z = self:set_reflection(pa, pb, pc)
  return triangle:new(x, y, z)
end


function line:symmetry_axial_C( obj )
  local pa = obj.center
  local pb = obj.through
  local x, y = self:set_reflection(pa, pb)
  return circle:new(x, y)
end


function line:reflection(...)
  local tp = table.pack(...)
  local obj = tp[1]
  local nb = tp.n
  
  if nb == 1 then
    if obj.type == "point" then
      return symmetry_axial_(self.pa, self.pb, obj)
    elseif obj.type == "line" then
      return self:symmetry_axial_L(obj)
    elseif obj.type == "triangle" then
      return self:symmetry_axial_T(obj)
    elseif obj.type == "circle" then
        return self:symmetry_axial_C(obj)
    else
    tex.error("Unsupported object type for reflection")
    end
  else
    local t = {}
    for i = 1, tp.n do
      table.insert(t, symmetry_axial_(self.pa, self.pb, tp[i]))
    end
    return table.unpack(t)
  end
end

function line:set_reflection(...)
  return set_symmetry_axial_(self.pb, self.pa, ...)
end

function line:affinity(...)
  local tp = table.pack(...)
  local c,d = tp[1].pa,tp[1].pb
  local k = tp[2]
  local obj = tp[3]
  local nb = tp.n
  if nb == 3 then
       return  affinity_(self.pa,self.pb,c,d,k,obj)
       else
         local  t = {}
        for i=3,tp.n do
            table.insert( t , affinity_ (self.pa, self.pb,c,d, k,tp[i])  )
         end
       return table.unpack ( t )
     end
end

function line:set_affinity(...)
  local tp = table.pack(...)  -- Gather all arguments passed to table tp
  local c, d = tp[1].pa, tp[1].pb  --Extract the two points of the reference line (tp[1])
  local k = tp[2]  -- The affinity coefficient
  local t = {}

  -- Apply affinity for each object passed in parameter (from 3rd object upwards)
  for i = 3, tp.n do
    table.insert(t, affinity_(self.pa, self.pb, c, d, k, tp[i]))  --Apply affinity to each object
  end
  
  return table.unpack(t)  --Returns transformed objects
end

function line:path(nb)
  local list = {}
  for i = 0, nb do
    local t = i / nb
    local P = self:point(t)
     table.insert(list, "(" .. checknumber(P.re) .. "," .. checknumber(P.im) .. ")")
   end
	return path:new(list)
end
  

return line