# Copyright (C) 2025 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

import os
import shutil
import sys
from pathlib import Path
from argparse import ArgumentParser, RawDescriptionHelpFormatter

USAGE = """
Updates example images, shaders, *.qml, *.ui, *.qrc and qmldir files from
a Qt source tree.

Check the diffs produced with care ("prefer" in qmldir, QML module
definitions).
"""

BINARY_SUFFIXES = ["jpg", "png", "svgz", "webp"]
TEXT_SUFFIXES = ["frag", "qrc", "qml", "svg", "ui", "vert"]
SUFFIXES = BINARY_SUFFIXES + TEXT_SUFFIXES


QML_SIMPLE_TUTORIAL_NAMES = ["chapter1-basics", "chapter2-methods",
                             "chapter3-bindings", "chapter4-customPropertyTypes",
                             "chapter5-listproperties", "chapter6-plugins"]
QML_SIMPLE_TUTORIALS = ["qml/tutorials/extending-qml/" + n for n in QML_SIMPLE_TUTORIAL_NAMES]

QML_ADVANCED_TUTORIAL_NAMES = ["advanced1-Base-project", "advanced2-Inheritance-and-coercion",
                               "advanced3-Default-properties", "advanced4-Grouped-properties",
                               "advanced5-Attached-properties", "advanced6-Property-value-source"]
QML_ADVANCED_TUTORIALS = ["qml/tutorials/extending-qml-advanced/" + n
                          for n in QML_ADVANCED_TUTORIAL_NAMES]


# Qt Module -> List of PySide 6 examples
EXAMPLE_MAPPING = {
    "qtbase": ["corelib/ipc/sharedmemory", "gui/rhiwindow", "sql/books",
               "widgets/animation/easing", "widgets/rhi/simplerhiwidget"],
    "qtconnectivity": ["bluetooth/heartrate_game", "bluetooth/lowenergyscanner"],
    "qtdeclarative": (QML_SIMPLE_TUTORIALS + QML_ADVANCED_TUTORIALS
                      + ["quick/models/stringlistmodel", "quick/models/objectlistmodel",
                         "quick/window",
                         "quick/rendercontrol/rendercontrol_opengl",
                         "quick/scenegraph/openglunderqml",
                         "quick/scenegraph/scenegraph_customgeometry",
                         "quick/customitems/painteditem",
                         "quickcontrols/filesystemexplorer", "quickcontrols/gallery"]),
    "qtgraphs": ["graphs/2d/hellographs", "graphs/3d/bars", "graphs/3d/widgetgraphgallery"],
    "qtlocation": ["location/mapviewer"],
    "qtmultimedia": ["multimedia/camera"],
    "qtquick3d": ["quick3d/customgeometry", "quick3d/intro", "quick3d/proceduraltexture"],
    "qtserialbus": ["serialbus/can", "serialbus/modbus/modbusclient"],
    "qtserialport": ["serialport/terminal"],
    "qtspeech": ["speech/hello_speak"],
    "qtwebchannel": ["webchannel/standalone"],
    "qtwebengine": ["pdfwidgets/pdfviewer", "webenginequick/nanobrowser",
                    "webenginewidgets/notifications", "webenginewidgets/simplebrowser"],
    "qtwebview": ["webview/minibrowser"],
    "qtdoc": ["demos/colorpaletteclient", "demos/documentviewer"]
}


file_count = 0
updated_file_count = 0
new_file_count = 0
warnings_count = 0


def pyside_2_qt_example(e):
    """Fix some example names differing in PySide."""
    if "heartrate" in e:
        return e.replace("heartrate_", "heartrate-")
    if e == "webenginequick/nanobrowser":
        return "webenginequick/quicknanobrowser"
    if e.endswith("scenegraph_customgeometry"):
        return e.replace("scenegraph_customgeometry", "customgeometry")
    if e.endswith("modbusclient"):
        return e.replace("modbusclient", "client")
    if e.endswith("demos/documentviewer"):
        return e + "/app"
    return e


def files_differ(p1, p2):
    return (p1.stat().st_size != p2.stat().st_size
            or p1.read_bytes() != p2.read_bytes())


def use_file(path):
    """Exclude C++ docs and Qt Creator builds."""
    path_str = os.fspath(path)
    return "/doc/" not in path_str and "_install_" not in path_str


def example_sources(qt_example):
    """Retrieve all update-able files of a Qt C++ example."""
    result = []
    for suffix in SUFFIXES:
        for file in qt_example.glob(f"**/*.{suffix}"):
            if use_file(file):
                result.append(file)
    for file in qt_example.glob("**/qmldir*"):
        if use_file(file):
            result.append(file)
    return result


def detect_qml_module(pyside_example, sources):
    """Detect the directory of a QML module of a PySide example.
       While in Qt C++, the QML module's .qml files are typically
       located in the example root, PySide has an additional directory
       since it loads the QML files from the file system.
       Read the qmldir file and check whether a module directory exists."""
    qml_dir_file = None
    for source in sources:
        if source.name.startswith("qmldir"):  # "qmldir"/"qmldir.in"
            qml_dir_file = source
            break
    if not qml_dir_file:
        return None
    for line in qml_dir_file.read_text(encoding="utf-8").split("\n"):
        if line.startswith("module "):
            module = line[7:].strip()
            if (pyside_example / module).is_dir():
                return module
            break
    return None


def sync_example(pyside_example, qt_example, dry_run):
    """Update files of a PySide example."""
    global file_count, updated_file_count, new_file_count, warnings_count
    sources = example_sources(qt_example)
    source_count = len(sources)
    if source_count == 0:
        print(f"No sources found in {qt_example}", file=sys.stderr)
        return
    count = 0
    qml_module = detect_qml_module(pyside_example, sources)
    for source in sources:
        rel_source = source.relative_to(qt_example)
        target = pyside_example / rel_source
        if qml_module and not target.is_file():
            target = pyside_example / qml_module / rel_source
        if target.is_file():
            if files_differ(source, target):
                if not dry_run:
                    shutil.copy(source, target)
                count += 1
        else:
            print(f"{qt_example.name}: {rel_source} does not have an equivalent "
                  "PySide file, skipping", file=sys.stderr)
            warnings_count += 1
            new_file_count += 1
    if count > 0:
        print(f"  {qt_example.name:<30}: Updated {count}/{source_count} files(s)")
    else:
        print(f"  {qt_example.name:<30}: Unchanged, {source_count} files(s)")
    file_count += source_count
    updated_file_count += count


def main():
    global warnings_count
    parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter,
                            description=USAGE)
    parser.add_argument("--dry-run", action="store_true", help="show the files to be updated")
    parser.add_argument('qtsource', nargs=1)
    args = parser.parse_args()
    dry_run = args.dry_run
    qt_source = Path(args.qtsource[0])
    if not qt_source.is_dir():
        raise Exception(f"{qt_source} is not a directory")

    pyside_examples = Path(__file__).parents[1].resolve() / "examples"
    print(qt_source, '->', pyside_examples)

    for qt_module, example_list in EXAMPLE_MAPPING.items():
        for example in example_list:
            pyside_example = pyside_examples / example
            qt_example = (qt_source / qt_module / "examples"
                          / pyside_2_qt_example(example))
            if pyside_example.is_dir() and qt_example.is_dir():
                sync_example(pyside_example, qt_example, dry_run)
            else:
                print(f"Invalid mapping {qt_example} -> {pyside_example}",
                      file=sys.stderr)
                warnings_count += 1
    msg = f"Updated {updated_file_count}/{file_count} file(s)"
    if new_file_count:
        msg += f", {new_file_count} new files(s)"
    if warnings_count:
        msg += f", {warnings_count} warning(s)"
    print(f"\n{msg}.\n")
    return 0


if __name__ == "__main__":
    r = -1
    try:
        r = main()
    except Exception as e:
        print(str(e), file=sys.stderr)
    sys.exit(r)
