##############################################################################
# ThanCad 0.9.2 "Tartu": n-dimensional CAD with raster support for engineers
#
# Copyright (C) 2001-2026 Thanasis Stamos, January 20, 2026
# Athens, Greece, Europe
# URL: http://thancad.sourceforge.net
# e-mail: cyberthanasis@gmx.net
#
# 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 (www.gnu.org/licenses/gpl.html).
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
##############################################################################
"""\
ThanCad 0.9.2 "Tartu": n-dimensional CAD with raster support for engineers

This module defines the object snap functionality.
"""

from thanvar import thanLogTk
from thanopt import thancadconf
from thandefs.thanatt import ThanAttCol

_AVOIDTAG = frozenset(("e0", "edrag", "enull"))  #e0 is the element being created, edrag is the ones dragged in command move
                                                 #and enull is obscure (searcjh all .py files for it)

class ThanOsnap:
    "Object snap functionality class."

    def __init__(self, proj):
        "Initialize the object."
        self.thanProj = proj
        self.BSEL = thancadconf.thanBSEL
        self.size = thancadconf.thanBOSN      # size of osnap signs
        self.types = thancadconf.thanOsnapModes

        self.active = "ena" in self.types
        self.preempt = False
        self.items = ()
        self.tcol = ThanAttCol(thancadconf.thanColOsn).thanTk
        self.selems = proj[2].thanSelems # Elements which may be snapped to.
        self.selem = None                # Snapable element which is near cursor.
        self.cc1 = None   #This is the first point of a line, when we prompt the user to enter the other point (and dragging the line)


    def thanClear(self):
        "Clear icons, if any."
        dc = self.thanProj[2].thanCanvas
        for item in self.items:
            dc.delete(item)
        self.items = ()


    def thanFind(self):
        "Finds end, mid, center etc near the mouse cursor."
        if self.preempt:
            thanLogTk.error("tklowget: osnapFind: preemptive call. It shouldn't happen.")
            return
        self.preempt = True
        dc = self.thanProj[2].thanCanvas
        tagel = self.thanProj[1].thanTagel
        ct = self.thanProj[2].thanCt
        ddu, _ = ct.local2GlobalRel(10*self.BSEL, 10*self.BSEL)  #Thanasis2025_10_05: max distance of osnap points in world coordinates
        bpix, hpix = self.BSEL//2, self.BSEL//2
        x1, y1 = dc.thanXcu-bpix, dc.thanYcu-hpix
        x2, y2 = dc.thanXcu+bpix, dc.thanYcu+hpix
        items = dc.find_overlapping(x1, y1, x2, y2)
        otypes = self.types

        ccu = list(self.thanProj[1].thanVar["elevation"])
        ccu[:2] = ct.local2Global(dc.thanXcu, dc.thanYcu)
        if "ele" in otypes:
            selems = self.selems
            for item in items:
                tags = dc.gettags(item)
                                                # Tkinter may automatically add the tag 'current' in any element
                                                # so the next test (which should succeed) does not succeed.
                                                # Thus we have to check for "e0", "edrag" and "enull"
                if len(tags) < 2: continue      # We avoid current (rubber line)
                if tags[0] in _AVOIDTAG: continue    # We avoid current compound element (we shouldn't really)
                e = tagel[tags[0]]
                if e in selems:
                    dc.delete("e0")
                    self.selem = e
                    e1 = e.thanClone()
                    e1.thanUntag()             #Make thanTags and handle invalid
                    e1.thanTags = ("e0",)
                    than = self.thanProj[2].than
                    col1 = than.outline
                    than.outline = self.tcol
                    e1.thanTkDraw(than)
                    dc.itemconfig("e0", width=5)
                    than.outline = col1
                    self.preempt = False
                    return
            self.selem = None
            dc.delete("e0")
            self.preempt = False
            return
        elif "int" in otypes:
            ps = self.__int(dc, tagel, otypes, items, ccu, ddu, self.cc1)
        else:
            ps = []
            for item in items:
#               tags = dc.itemcget(item, "tags").split()
                if dc.type(item) == "image": continue # Ignore Tk images; only the bounding rectangle counts
                tags = dc.gettags(item)
                if len(tags) < 2: continue      # We avoid current (rubber line)
                if tags[0] in _AVOIDTAG: continue    # We avoid current compound element (we shouldn't really)
                e = tagel[tags[0]]
                p = e.thanOsnap(self.thanProj, otypes, ccu, ddu, None, self.cc1)
                if p is not None:
                    ps.append(p)
                    if len(ps) > 2: break
        for item1 in self.items: dc.delete(item1)
        if len(ps) < 1:
            self.items = ()
            self.preempt = False
            return
        try:
            p = min(ps)
        except:
            print("ps=\n", ps)
            raise
        #b, h = ct.global2LocalRel(p[0], p[0])
        #if b > 10*self.BSEL:
        if p[0] > ddu:
            self.items = ()
            self.preempt = False
            return
        t = self.type = p[1]
        cc = self.cc = p[2]
        x, y = ct.global2Local(cc[0], cc[1])
        self.x = x
        self.y = y
        b = h = self.size//2
        tcol = self.tcol
        if t == "end":
            self.items = \
            ( dc.create_rectangle(x-b, y-h, x+b, y+h, width=3, outline=tcol, fill=""),
            )
        elif t == "mid":
            self.items = \
            ( dc.create_polygon(x-b, y-b, x+b, y-b, x, y+b, width=3, outline=tcol, fill=""),
            )
        elif t == "cen":
            self.items = \
            ( dc.create_oval(x-b, y-b, x+b, y+b, width=3, outline=tcol, fill=""),
            )
        elif t == "nod":
            self.items = \
            ( dc.create_oval(x-b, y-b, x+b, y+b, width=3, outline=tcol, fill=""),
              dc.create_line(x-b, y-b, x+b, y+b, width=2, fill=tcol),
              dc.create_line(x-b, y+b, x+b, y-b, width=2, fill=tcol),
            )
        elif t == "qua":
            self.items = \
            ( dc.create_polygon(x-b,y, x,y+b, x+b,y, x,y-b, width=2, outline=tcol, fill=""),
            )
        elif t == "int":
            self.items = \
            ( dc.create_line(x-b, y-b, x+b, y+b, width=2, fill=tcol),
              dc.create_line(x-b, y+b, x+b, y-b, width=2, fill=tcol),
            )
        elif t == "tan":
            bb = 0.8*b
            self.items = \
            ( dc.create_oval(x-bb, y-bb, x+bb, y+bb, width=3, outline=tcol, fill=""),
              dc.create_line(x-b, y-b, x+b, y-b, width=2, fill=tcol),
            )
        elif t == "nea":
            self.items = \
            ( dc.create_polygon(x-b, y-b, x+b, y+b, x-b, y+b, x+b, y-b, width=2, outline=tcol, fill=""),
            )
        elif t == "per":
            self.items = \
            ( dc.create_line(x-b, y-b, x-b, y+b, x+b, y+b, width=3, fill=tcol),
              dc.create_line(x-b, y, x, y, x, y+b, width=2, fill=tcol),
            )
        self.preempt = False


    def __int(self, dc, tagel, otypes, items, ccu, ddu, cc1):    #Thanasis2025_10_04: Rewrite
        """Finds all types of osnap including 'int' near the mouse cursor.

        For each element find the osnap points without a second elementm and at the same time
        it tries pairs of elements sequentially (not all posiible combination) to save time.
        It also includes pairs of an element with itself."""

        def iteritems(items):
            "Iterate over items."
            seen = set()
            for item in items:                     # Search for first element of intersection
                if item in seen: continue
                seen.add(item)
                if dc.type(item) == "image": continue # Ignore Tk images; only the bounding rectangle counts
                tags = dc.gettags(item)
                if len(tags) < 2:   continue    # We avoid current (rubber line)
                if tags[0] in _AVOIDTAG: continue    # We avoid current compound element (we shouldn't really)
                yield tagel[tags[0]]

        dc = self.thanProj[2].thanCanvas
        ps = []
        it = iteritems(items)
        for e in it:                     # Search for first element of intersection
            p = e.thanOsnap(self.thanProj, otypes, ccu, ddu, e, cc1)     #Check element e for all types of osnap, and 'int' with itself
            if p is not None: ps.append(p)
            break

        inttypes = {"int": True, "ena": True}
        for e2 in it:                     # Search for second element of intersection (after the first one)
            p = e2.thanOsnap(self.thanProj, otypes, ccu, ddu, e2, cc1)  #Check element e2 for all types of osnap, and 'int' with itself
            if p is not None: ps.append(p)
            p = e.thanOsnap(self.thanProj, inttypes, ccu, ddu, e2, cc1) #Check element e for 'int' with next element e2
            if p is not None: ps.append(p)
            if len(ps) > 2: break   # 3 or more osnap points are enough; skip other osnap points to save time
            e = e2
        return ps



    def thanCleanup(self):
        "Do cleanup after error/command-line action."
        dc = self.thanProj[2].thanCanvas
        for item1 in self.items:
            dc.delete(item1)
        self.items = ()


class ThanOrtho:
    "Ortho mode functionality class."

    def __init__(self):
        "Initialize the object."
        self.thanIdirs = [(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0)]
        self.on = False      # If this is false then ortho is off
        self.active = False  # This is false if ortho has no meaning for current action
        self.xa = None       # The reference point for ortho mode..
        self.ya = None       # ..in canvas coordinates


    def enable(self, xa, ya):
        "Enable ortho mode, if it is on."
        self.xa = xa
        self.ya = ya
        self.active = True


    def disable(self):
        "Disable ortho mode; ortho has no meaning for current action."
        self.active = False
        self.xa = self.ya = None


    def toggle(self):
        "Toggle on/off and return the current state."
        self.on = not self.on
        return self.on


    def orthoxy(self, xb, yb):
        "Force the coordinates of the mouse parallel to a direction; usually ortho mode."
        if not self.active or not self.on: return xb, yb
        dx = xb - self.xa
        dy = yb - self.ya
        dismax = 0.0
        for idir1 in self.thanIdirs:
            disproj = dx*idir1[0] - dy*idir1[1]   # The negative sign in y is due to local coordinate systems
            if disproj > dismax:
                dismax = disproj
                idirmax = idir1
        return self.xa + dismax*idirmax[0], self.ya - dismax*idirmax[1]
