# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *   Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net>              *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
# *   as published by the Free Software Foundation; either version 2 of     *
# *   the License, or (at your option) any later version.                   *
# *   for detail see the LICENCE text file.                                 *
# *                                                                         *
# *   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 Library General Public License for more details.                  *
# *                                                                         *
# *   You should have received a copy of the GNU Library 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                                                                   *
# *                                                                         *
# ***************************************************************************

import FreeCAD
import Path
import glob
import os
import pathlib
from collections import defaultdict
from typing import Optional


if False:
    Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
    Path.Log.trackModule(Path.Log.thisModule())
else:
    Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())


translate = FreeCAD.Qt.translate

PreferencesGroup = "User parameter:BaseApp/Preferences/Mod/CAM"

DefaultFilePath = "DefaultFilePath"
DefaultJobTemplate = "DefaultJobTemplate"
DefaultStockTemplate = "DefaultStockTemplate"
DefaultTaskPanelLayout = "DefaultTaskPanelLayout"

PostProcessorDefault = "PostProcessorDefault"
PostProcessorDefaultArgs = "PostProcessorDefaultArgs"
PostProcessorBlacklist = "PostProcessorBlacklist"
PostProcessorOutputFile = "PostProcessorOutputFile"
PostProcessorOutputPolicy = "PostProcessorOutputPolicy"

ToolGroup = PreferencesGroup + "/Tools"
ToolPath = "ToolPath"
LastToolLibrary = "LastToolLibrary"
LastToolLibrarySortKey = "LastToolLibrarySortKey"

# Linear tolerance to use when generating Paths, eg when tessellating geometry
GeometryTolerance = "GeometryTolerance"
LibAreaCurveAccuracy = "LibAreaCurveAccuracy"

WarningSuppressRapidSpeeds = "WarningSuppressRapidSpeeds"
WarningSuppressAllSpeeds = "WarningSuppressAllSpeeds"
WarningSuppressSelectionMode = "WarningSuppressSelectionMode"
WarningSuppressOpenCamLib = "WarningSuppressOpenCamLib"
WarningSuppressVelocity = "WarningSuppressVelocity"
EnableExperimentalFeatures = "EnableExperimentalFeatures"
EnableAdvancedOCLFeatures = "EnableAdvancedOCLFeatures"


_observers = defaultdict(list)  # maps group name to callback functions


def _add_group_observer(group, callback):
    """Add an observer for any changes on the given parameter group"""
    _observers[group].append(callback)


def _emit_change(group, *args):
    for cb in _observers[group]:
        cb(group, *args)


def preferences():
    return FreeCAD.ParamGet(PreferencesGroup)


def tool_preferences():
    return FreeCAD.ParamGet(ToolGroup)


def addToolPreferenceObserver(callback):
    _add_group_observer(ToolGroup, callback)


def pathPostSourcePath():
    return os.path.join(FreeCAD.getHomePath(), "Mod/CAM/Path/Post/")


def getBuiltinAssetPath() -> pathlib.Path:
    home = pathlib.Path(FreeCAD.getHomePath())
    return home / "Mod" / "CAM" / "Tools"


def getBuiltinLibraryPath() -> pathlib.Path:
    return getBuiltinAssetPath() / "Library"


def getBuiltinShapePath() -> pathlib.Path:
    return getBuiltinAssetPath() / "Shape"


def getBuiltinToolBitPath() -> pathlib.Path:
    return getBuiltinAssetPath() / "Bit"


def getDefaultAssetPath() -> Path:
    data_dir = pathlib.Path(FreeCAD.getUserAppDataDir())
    asset_path = data_dir / "CamAssets"
    asset_path.mkdir(parents=True, exist_ok=True)
    return asset_path


def getAssetPath() -> pathlib.Path:
    pref = tool_preferences()

    # Check if we have a CamAssets path already set
    cam_assets_path = pref.GetString(ToolPath, "")
    if cam_assets_path:
        # Use mostRecentConfigFromBase to get the most recent versioned path
        most_recent_path = FreeCAD.ApplicationDirectories.mostRecentConfigFromBase(cam_assets_path)
        return pathlib.Path(most_recent_path)

    # Migration: Check for legacy DefaultFilePath and use it for CamAssets
    legacy_path = defaultFilePath()
    if legacy_path:
        legacy_path_obj = pathlib.Path(legacy_path)
        if legacy_path_obj.exists() and legacy_path_obj.is_dir():
            # Migrate: Set the legacy path as the new CamAssets path
            setAssetPath(legacy_path_obj)
            # Return the most recent version of the legacy path
            most_recent_legacy = FreeCAD.ApplicationDirectories.mostRecentConfigFromBase(
                str(legacy_path_obj)
            )
            return pathlib.Path(most_recent_legacy)

    # Fallback to default if no legacy path found
    default = getDefaultAssetPath()
    # Return the most recent version of the default path
    most_recent_default = FreeCAD.ApplicationDirectories.mostRecentConfigFromBase(str(default))
    return pathlib.Path(most_recent_default)


def setAssetPath(path: pathlib.Path):
    Path.Log.debug(f"Setting asset path to {path}")
    assert path.is_dir(), f"Cannot put a non-initialized asset directory into preferences: {path}"
    pref = tool_preferences()
    current_path = pref.GetString(ToolPath, "")
    if str(path) == current_path:
        return
    pref.SetString(ToolPath, str(path))
    _emit_change(ToolGroup, ToolPath, path)


def getToolBitPath() -> pathlib.Path:
    return getAssetPath() / "Tools" / "Bit"


def getTemplateDirectory() -> pathlib.Path:
    """Returns the directory where job templates should be saved."""
    template_path = getAssetPath() / "Templates"
    template_path.mkdir(parents=True, exist_ok=True)
    return template_path


def getLastToolLibrary() -> Optional[str]:
    pref = tool_preferences()
    return pref.GetString(LastToolLibrary) or None


def setLastToolLibrary(name: str):
    assert isinstance(name, str), f"Library name '{name}' is not a string"
    pref = tool_preferences()
    pref.SetString(LastToolLibrary, name)


def getLastToolLibrarySortKey() -> Optional[str]:
    pref = tool_preferences()
    return pref.GetString(LastToolLibrarySortKey) or None


def setLastToolLibrarySortKey(name: str):
    pref = tool_preferences()
    pref.SetString(LastToolLibrarySortKey, name)


def allAvailablePostProcessors():
    allposts = []
    for path in searchPathsPost():
        posts = [
            str(os.path.split(os.path.splitext(p)[0])[1][:-5])
            for p in glob.glob(path + "/*_post.py")
        ]
        allposts.extend(posts)
    allposts.sort()
    return allposts


def allEnabledPostProcessors(include=None):
    blacklist = postProcessorBlacklist()
    enabled = [
        processor for processor in allAvailablePostProcessors() if processor not in blacklist
    ]
    if include:
        postlist = list(set(include + enabled))
        postlist.sort()
        return postlist
    return enabled


def defaultPostProcessor():
    pref = preferences()
    return pref.GetString(PostProcessorDefault, "")


def defaultPostProcessorArgs():
    pref = preferences()
    return pref.GetString(PostProcessorDefaultArgs, "")


def defaultGeometryTolerance():
    return preferences().GetFloat(GeometryTolerance, 0.01)


def defaultLibAreaCurveAccuracy():
    return preferences().GetFloat(LibAreaCurveAccuracy, 0.01)


def defaultFilePath():
    return preferences().GetString(DefaultFilePath)


def filePath():
    path = defaultFilePath()
    if not path:
        path = str(getAssetPath())
    return path


def macroFilePath():
    grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro")
    return grp.GetString("MacroPath", FreeCAD.getUserMacroDir())


def searchPaths():
    paths = []
    # Add new CamAssets/Templates directory first (highest priority)
    paths.append(str(getTemplateDirectory()))
    # Add legacy locations for backward compatibility
    p = defaultFilePath()
    if p:
        paths.append(p)
    paths.append(macroFilePath())
    return paths


def searchPathsPost():
    paths = []
    p = defaultFilePath()
    if p:
        paths.append(p)
    paths.append(macroFilePath())
    paths.append(os.path.join(pathPostSourcePath(), "scripts/"))
    paths.append(pathPostSourcePath())
    return paths


def defaultJobTemplate():
    template = preferences().GetString(DefaultJobTemplate)
    if "xml" not in template:
        return template
    return ""


def setJobDefaults(jobTemplate, geometryTolerance, curveAccuracy):
    Path.Log.track("(%s, %s, %s)" % (jobTemplate, geometryTolerance, curveAccuracy))
    pref = preferences()
    pref.SetString(DefaultJobTemplate, jobTemplate)
    pref.SetFloat(GeometryTolerance, geometryTolerance)
    pref.SetFloat(LibAreaCurveAccuracy, curveAccuracy)


def postProcessorBlacklist():
    pref = preferences()
    blacklist = pref.GetString(PostProcessorBlacklist, "")
    if not blacklist:
        return []
    return eval(blacklist)


def setPostProcessorDefaults(processor, args, blacklist):
    pref = preferences()
    pref.SetString(PostProcessorDefault, processor)
    pref.SetString(PostProcessorDefaultArgs, args)
    pref.SetString(PostProcessorBlacklist, "%s" % (blacklist))


def setOutputFileDefaults(fileName, policy):
    pref = preferences()
    pref.SetString(PostProcessorOutputFile, fileName)
    pref.SetString(PostProcessorOutputPolicy, policy)


def defaultOutputFile():
    pref = preferences()
    return pref.GetString(PostProcessorOutputFile, "")


def defaultOutputPolicy():
    pref = preferences()
    return pref.GetString(PostProcessorOutputPolicy, "")


def defaultStockTemplate():
    return preferences().GetString(DefaultStockTemplate, "")


def setDefaultStockTemplate(template):
    preferences().SetString(DefaultStockTemplate, template)


def defaultTaskPanelLayout():
    return preferences().GetInt(DefaultTaskPanelLayout, 0)


def setDefaultTaskPanelLayout(style):
    preferences().SetInt(DefaultTaskPanelLayout, style)


def advancedOCLFeaturesEnabled():
    return preferences().GetBool(EnableAdvancedOCLFeatures, False)


def experimentalFeaturesEnabled():
    return preferences().GetBool(EnableExperimentalFeatures, False)


def suppressAllSpeedsWarning():
    return preferences().GetBool(WarningSuppressAllSpeeds, True)


def suppressRapidSpeedsWarning(user=True):
    return (user and suppressAllSpeedsWarning()) or preferences().GetBool(
        WarningSuppressRapidSpeeds, True
    )


def suppressSelectionModeWarning():
    return preferences().GetBool(WarningSuppressSelectionMode, True)


def suppressOpenCamLibWarning():
    return preferences().GetBool(WarningSuppressOpenCamLib, True)


def suppressVelocity():
    return preferences().GetBool(WarningSuppressVelocity, False)


def setPreferencesAdvanced(ocl, warnSpeeds, warnRapids, warnModes, warnOCL, warnVelocity):
    preferences().SetBool(EnableAdvancedOCLFeatures, ocl)
    preferences().SetBool(WarningSuppressAllSpeeds, warnSpeeds)
    preferences().SetBool(WarningSuppressRapidSpeeds, warnRapids)
    preferences().SetBool(WarningSuppressSelectionMode, warnModes)
    preferences().SetBool(WarningSuppressOpenCamLib, warnOCL)
    preferences().SetBool(WarningSuppressVelocity, warnVelocity)
