import os
import hashlib
import pathlib
import shlex
import subprocess

import SCons.Action
from platformio import fs

Import("env")

# We don't use `env.Execute` because it does not handle spaces in path
# See https://github.com/nanopb/nanopb/pull/834
# So, we resolve the path to the executable and then use `subprocess.run`
python_exe = env.subst("$PYTHONEXE")

try:
    import google.protobuf
except ImportError:
    print("[nanopb] Installing Protocol Buffers dependencies");

    # We need to specify protobuf version. In other case got next (on Ubuntu 20.04):
    # Requirement already satisfied: protobuf in /usr/lib/python3/dist-packages (3.6.1)
    subprocess.run([python_exe, '-m', 'pip', 'install', "protobuf>=3.19.1"])

try:
    import grpc_tools.protoc
except ImportError:
    print("[nanopb] Installing gRPC dependencies");
    subprocess.run([python_exe, '-m', 'pip', 'install', "grpcio-tools>=1.43.0"])


nanopb_root = os.path.join(os.getcwd(), '..')

project_dir = env.subst("$PROJECT_DIR")
build_dir = env.subst("$BUILD_DIR")

generated_src_dir = os.path.join(build_dir, 'nanopb', 'generated-src')
generated_build_dir = os.path.join(build_dir, 'nanopb', 'generated-build')
md5_dir = os.path.join(build_dir, 'nanopb', 'md5')

nanopb_protos =  env.subst(env.GetProjectOption("custom_nanopb_protos", ""))
nanopb_plugin_options = env.GetProjectOption("custom_nanopb_options", "")
nanopb_preserve_hierarchy = bool(env.GetProjectOption("custom_nanopb_preserve_directory_hierarchy", default=False))

if not nanopb_protos:
    print("[nanopb] No generation needed.")
else:
    if isinstance(nanopb_plugin_options, (list, tuple)):
        nanopb_plugin_options = " ".join(nanopb_plugin_options)

    nanopb_plugin_options = shlex.split(nanopb_plugin_options)

    protos_files = fs.match_src_files(project_dir, nanopb_protos)
    if not len(protos_files):
        print("[nanopb] ERROR: No files matched pattern:")
        print(f"custom_nanopb_protos: {nanopb_protos}")
        exit(1)

    nanopb_generator = os.path.join(nanopb_root, 'generator', 'nanopb_generator.py')

    nanopb_options = []
    nanopb_options.extend(["--output-dir", generated_src_dir])
    for opt in nanopb_plugin_options:
        nanopb_options.append(opt)

    try:
        os.makedirs(generated_src_dir)
    except FileExistsError:
        pass

    try:
        os.makedirs(md5_dir)
    except FileExistsError:
        pass

    if not nanopb_preserve_hierarchy:
        # Collect include dirs based on
        proto_include_dirs = set()
        for proto_file in protos_files:
            proto_file_abs = os.path.join(project_dir, proto_file)
            proto_dir = os.path.dirname(proto_file_abs)
            proto_include_dirs.add(proto_dir)

        for proto_include_dir in proto_include_dirs:
            nanopb_options.extend(["--proto-path", proto_include_dir])

    for proto_file in protos_files:
        proto_file_abs = os.path.join(project_dir, proto_file)

        proto_file_path_abs = os.path.dirname(proto_file_abs)
        proto_file_basename = os.path.basename(proto_file_abs)
        proto_file_without_ext = os.path.splitext(proto_file_basename)[0]

        proto_file_md5_abs = os.path.join(md5_dir, proto_file_basename + '.md5')
        proto_file_current_md5 = hashlib.md5(pathlib.Path(proto_file_abs).read_bytes()).hexdigest()

        options_file = proto_file_without_ext + ".options"
        options_file_abs = os.path.join(proto_file_path_abs, options_file)
        options_file_md5_abs = os.path.join(md5_dir, options_file + '.md5')
        options_file_current_md5 = None
        if pathlib.Path(options_file_abs).exists():
            options_file_current_md5 = hashlib.md5(pathlib.Path(options_file_abs).read_bytes()).hexdigest()
        else:
            options_file = None

        header_file = proto_file_without_ext + ".pb.h"
        source_file = proto_file_without_ext + ".pb.c"

        header_file_abs = os.path.join(generated_src_dir, source_file)
        source_file_abs = os.path.join(generated_src_dir, header_file)

        need_generate = False

        # Check proto file md5
        try:
            last_md5 = pathlib.Path(proto_file_md5_abs).read_text()
            if last_md5 != proto_file_current_md5:
                need_generate = True
        except FileNotFoundError:
            need_generate = True

        if options_file:
            # Check options file md5
            try:
                last_md5 = pathlib.Path(options_file_md5_abs).read_text()
                if last_md5 != options_file_current_md5:
                    need_generate = True
            except FileNotFoundError:
                need_generate = True
        elif pathlib.Path(options_file_md5_abs).exists():
            need_generate = True

        options_info = f"{options_file}" if options_file else "no options"

        if not need_generate:
            print(f"[nanopb] Skipping '{proto_file}' ({options_info})")
        else:
            print(f"[nanopb] Processing '{proto_file}' ({options_info})")
            if nanopb_preserve_hierarchy:
                nanopb_options.append(proto_file)
            else:
                nanopb_options.append(proto_file_basename)
            cmd = [python_exe, nanopb_generator] + nanopb_options
            action = SCons.Action.CommandAction(cmd, chdir=project_dir)
            result = env.Execute(action)
            if result != 0:
                print(f"[nanopb] ERROR: ({result}) processing cmd: '{cmd}'")
                exit(1)
            pathlib.Path(proto_file_md5_abs).write_text(proto_file_current_md5)
            if options_file:
                pathlib.Path(options_file_md5_abs).write_text(options_file_current_md5)
            else:
                try:
                    pathlib.Path(options_file_md5_abs).unlink()
                except FileNotFoundError:
                    pass

    # 1. Add path to the generated headers, so main.cpp can find them.
    env.Append(CPPPATH=[generated_src_dir])

    # 2. Add path to the Nanopb library itself, so the generated
    #    .c file can find the core headers like pb.h https://github.com/nanopb/nanopb/issues/1117
    env.Append(CPPPATH=[nanopb_root])

    # 3. Compile the generated sources
    # Fix for ESP32 ESP-IDF https://github.com/nanopb/nanopb/issues/734#issuecomment-1001544447
    global_env = DefaultEnvironment()
    already_called_env_name = "_PROTOBUF_GENERATOR_ALREADY_CALLED_" + env['PIOENV'].replace("-", "_")
    if not global_env.get(already_called_env_name, False):
        env.BuildSources(generated_build_dir, generated_src_dir)
    global_env[already_called_env_name] = True

