#!/usr/bin/env python3

import sys as _sys
import os as _os
import json as _json
import argparse as _argparse


def file_read(path):
    handle = open(path, "r")
    content = handle.read()
    handle.close()
    return content


def string_coin(pattern, arguments):
    result = pattern
    for (key, value, ) in arguments.items():
        result = result.replace("{{%s}}" % key, value)
    return result


def command_log(content):
    return string_coin(
        "echo '-- {{content}}'",
        {
            "content": content,
        }
    )


def command_mkdir(path):
    return string_coin(
        "mkdir --parents {{path}}",
        {
            "path": path,
        }
    )


def command_cat(source_paths, target_path, options = None):
    options = (
        {
            "append": False,
        }
        |
        (options or {})
    )
    return string_coin(
        "cat {{source_paths}} {{operator}} {{target_path}}",
        {
            "source_paths": " ".join(source_paths),
            "operator": (">>" if options["append"] else ">"),
            "target_path": target_path,
        }
    )


def command_copy(source_path, target_path, options = None):
    options = (
        {
        }
        |
        (options or {})
    )
    return string_coin(
        "cp -r -u {{source_path}} {{target_path}}",
        {
            "source_path": source_path,
            "target_path": target_path,
        }
    )


def command_tsc(source_paths, target_path, options = None):
    options = (
        {
            "declaration": False,
            "libs": [],
        }
        |
        (options or {})
    )
    return string_coin(
        "tsc{{libs}} {{source_paths}} --outFile {{target_path}}{{declaration}}",
        {
            "libs": "".join(
                map(
                    lambda name: string_coin(
                        " --lib {{name}}",
                        {"name": name}
                    ),
                    options["libs"]
                ),
            ),
            "source_paths": " ".join(source_paths),
            "target_path": target_path,
            "declaration": (" --declaration" if options["declaration"] else ""),
        }
    )


def gnumake_rule(data):
    return string_coin(
        "{{phony}}{{targets}}: {{dependencies}}\n{{actions}}\n",
        {
            "phony": (
                string_coin(".PHONY: {{name}}\n", {"name": data["targets"][0]})
                if (data["phony"] and (len(data["targets"]) == 1)) else
                ""
            ),
            "targets": " ".join(data["targets"]),
            "dependencies": " ".join(data["dependencies"]),
            "actions": "".join(
                map(
                    lambda data_action: string_coin(
                        "\t{{prefix}}{{name}}\n",
                        {
                            "prefix": ("@ " if data_action["silent"] else ""),
                            "name": data_action["command"],
                        }
                    ),
                    data["actions"]
                )
            ),
        }
    )


def gnumake_sheet(data):
    return string_coin(
        "{{rules}}",
        {
            "rules": "".join(
                map(
                    lambda data_rule: gnumake_rule(data_rule),
                    data["rules"]
                )
            ),
        }
    )


def action_update_plankton(conf, definition):
    directory = _os.path.join(definition["directories"]["lib"], "plankton")
    commands = [
        command_mkdir(directory),
        string_coin("cd {{directory}}", {"directory": directory}),
        string_coin(
            "ptk bundle {{target}} {{modules}}",
            {
                "target": definition["kind"],
                "modules": " ".join(definition["dependencies"]["plankton"]),
            }
        ),
        "cd - > /dev/null",
    ]
    _sys.stdout.write(
        "".join(
            map(
                lambda command: (command + "\n"),
                commands
            )
        )
    )


def refine_definition(definition_raw):
    return {
        "name": definition_raw["name"],
        "kind": definition_raw.get("kind", "node"),
        "directories": {
            "lib": definition_raw.get("directories", {}).get("lib", "lib"),
            "misc": definition_raw.get("directories", {}).get("misc", "misc"),
            "source": definition_raw.get("directories", {}).get("source", "source"),
        },
        "dependencies": {
            "plankton": definition_raw.get("dependencies", {}).get("plankton", []),
            "node": definition_raw.get("dependencies", {}).get("node", []),
        },
        "core": definition_raw["core"],
        "apps": list(
            map(
                lambda app_raw: {
                    "name": app_raw["name"],
                    "sources": app_raw["sources"],
                    "additional_dependencies": app_raw.get("additional_dependencies", []),
                    "destination": app_raw.get("destination", "{{project}}-{{app}}"),
                    "shebang": app_raw.get("shebang", ""),
                    "executable": app_raw.get("executable", True),
                },
                definition_raw["apps"]
            )
        ),
        "custom_rules": definition_raw.get("custom_rules", []),
    }


def action_update_node_modules(conf, definition):
    directory = _os.path.join(definition["directories"]["lib"], "node")
    commands = [
        command_mkdir(directory),
        string_coin("cd {{directory}}", {"directory": directory}),
        string_coin(
            "npm install {{modules}}",
            {
                "modules": " ".join(definition["dependencies"]["node"]),
            }
        ),
        "cd - > /dev/null",
    ]
    _sys.stdout.write(
        "".join(
            map(
                lambda command: (command + "\n"),
                commands
            )
        )
    )


def action_build(conf, definition):
    data = {
        "rules": (
            [
                {
                    "phony": True,
                    "targets": ["_default"],
                    "dependencies": list(
                        map(
                            lambda data_app: string_coin("app-{{name}}", {"name": data_app["name"]}),
                            definition["apps"]
                        )
                    ),
                    "actions": [],
                },
                {
                    "phony": True,
                    "targets": [
                        "node_modules"
                    ],
                    "dependencies": [],
                    "actions": [
                        {
                            "silent": True,
                            "command": command_log("placing node modules …"),
                        },
                        {
                            "silent": True,
                            "command": command_mkdir(_os.path.join(conf["directories"]["build"], "node_modules")),
                        },
                        {
                            "silent": True,
                            "command": command_copy(
                                _os.path.join(definition["directories"]["lib"], "node", "node_modules", "*"),
                                _os.path.join(conf["directories"]["build"], "node_modules", "")
                            ),
                        },
                    ]
                },
                {
                    "phony": False,
                    "targets": [
                        _os.path.join(conf["directories"]["temp"], "core-unlinked.js"),
                        _os.path.join(conf["directories"]["temp"], "core-unlinked.d.ts"),
                    ],
                    "dependencies": (
                        [
                            _os.path.join(definition["directories"]["lib"], "plankton", "plankton.d.ts"),
                        ]
                        +
                        list(
                            map(
                                lambda path: _os.path.join(definition["directories"]["source"], path),
                                definition["core"]
                            )
                        )
                    ),
                    "actions": [
                        {
                            "silent": True,
                            "command": command_log("core | compiling …")
                        },
                        {
                            "silent": True,
                            "command": command_mkdir("$(dir $@)")
                        },
                        {
                            "silent": True,
                            "command": command_tsc(
                                ["$^"],
                                _os.path.join(conf["directories"]["temp"], "core-unlinked.js"),
                                {
                                    "libs": ["es2020"],
                                    "declaration": True,
                                }
                            )
                        }
                    ]
                },
            ]
            +
            list(
                map(
                    lambda data_custom_rule: {
                        "phony": False,
                        "targets": list(
                            map(
                                lambda target: string_coin(
                                    target,
                                    {
                                        "dir_build": conf["directories"]["build"],
                                    }
                                ),
                                data_custom_rule["targets"]
                            )
                        ),
                        "dependencies": list(
                            map(
                                lambda dependency: string_coin(
                                    dependency,
                                    {
                                        "dir_source": definition["directories"]["source"],
                                        "dir_misc": definition["directories"]["misc"],
                                    }
                                ),
                                data_custom_rule["dependencies"]
                            )
                        ),
                        "actions": (
                            [
                                {
                                    "silent": True,
                                    "command": command_log(
                                        string_coin(
                                            "{{info}} …",
                                            {
                                                "info": data_custom_rule["info"],
                                            }
                                        ),
                                    ),
                                }
                            ]
                            +
                            list(
                                map(
                                    lambda action: {
                                        "silent": True,
                                        "command": action,
                                    },
                                    data_custom_rule["actions"]
                                )
                            )
                        ),
                    },
                    definition["custom_rules"]
                )
            )
            +
            list(
                map(
                    lambda data_app: {
                        "phony": False,
                        "targets": [
                            _os.path.join(conf["directories"]["temp"], string_coin("app-{{name}}-unlinked.js", {"name": data_app["name"]})),
                        ],
                        "dependencies": (
                            [
                                _os.path.join(definition["directories"]["lib"], "plankton", "plankton.d.ts"),
                                _os.path.join(conf["directories"]["temp"], "core-unlinked.d.ts"),
                            ]
                            +
                            list(
                                map(
                                    lambda path: _os.path.join(definition["directories"]["source"], path),
                                    data_app["sources"]
                                )
                            )
                        ),
                        "actions": [
                            {
                                "silent": True,
                                "command": command_log(string_coin("app:{{name}} | compiling …", {"name": data_app["name"]})),
                            },
                            {
                                "silent": True,
                                "command": command_mkdir("$(dir $@)"),
                            },
                            {
                                "silent": True,
                                "command": command_tsc(["$^"], "$@", {"libs": ["es2020"]}),
                            },
                        ],
                    },
                    definition["apps"]
                )
            )
            +
            list(
                map(
                    lambda data_app: {
                        "phony": False,
                        "targets": [
                            _os.path.join(
                                conf["directories"]["build"],
                                string_coin(
                                    data_app["destination"],
                                    {
                                        "project": definition["name"],
                                        "app": data_app["name"],
                                    }
                                )
                            ),
                        ],
                        "dependencies": (
                            [
                                _os.path.join(definition["directories"]["lib"], "plankton", "plankton.js"),
                            ]
                            +
                            [
                                _os.path.join(conf["directories"]["temp"], "core-unlinked.js"),
                                _os.path.join(conf["directories"]["temp"], string_coin("app-{{name}}-unlinked.js", {"name": data_app["name"]})),
                            ]
                        ),
                        "actions": (
                            [
                                {
                                    "silent": True,
                                    "command": command_log(string_coin("app:{{name}} | linking …", {"name": data_app["name"]}))
                                },
                                {
                                    "silent": True,
                                    "command": command_mkdir("$(dir $@)")
                                },
                                {
                                    "silent": True,
                                    "command": string_coin(
                                        "echo '{{shebang}}' > $@",
                                        {
                                            "shebang": data_app["shebang"],
                                        }
                                    )
                                },
                                {
                                    "silent": True,
                                    "command": command_cat(["$^"], "$@", {"append": True})
                                },
                            ]
                            +
                            (
                                [
                                    {
                                        "silent": True,
                                        "command": "chmod +x $@",
                                    },
                                ]
                                if data_app["executable"] else
                                [
                                ]
                            )
                        )
                    },
                    definition["apps"]
                )
            )
            +
            list(
                map(
                    lambda data_app: {
                        "phony": True,
                        "targets": [
                            string_coin(
                                "app-{{name}}",
                                {
                                    "name": data_app["name"],
                                }
                            ),
                        ],
                        "dependencies": (
                            [
                                "node_modules",
                                _os.path.join(
                                    conf["directories"]["build"],
                                    string_coin(
                                        data_app["destination"],
                                        {
                                            "project": definition["name"],
                                            "name": data_app["name"],
                                        }
                                    )
                                ),
                            ]
                            +
                            list(
                                map(
                                    lambda dependency: string_coin(
                                        dependency,
                                        {
                                            "dir_source": definition["directories"]["source"],
                                            "dir_misc": definition["directories"]["misc"],
                                            "dir_temp": conf["directories"]["temp"],
                                            "dir_build": conf["directories"]["build"],
                                        }
                                    ),
                                    data_app["additional_dependencies"]
                                )
                            )
                        ),
                        "actions": []
                    },
                    definition["apps"]
                )
            )
        )
    }
    _sys.stdout.write(gnumake_sheet(data))


def main():
    ## args
    argument_parser = _argparse.ArgumentParser()
    argument_parser.add_argument(
        "action",
        type = str,
        metavar = "<action>",
    )
    argument_parser.add_argument(
        "-d",
        "--definition-path",
        type = str,
        default = "prj.json",
        metavar = "<definition-path>",
    )
    argument_parser.add_argument(
        "-b",
        "--build-directory-pattern",
        type = str,
        default = "/tmp/{{project}}",
        metavar = "<build-directory-pattern>",
    )
    args = argument_parser.parse_args()

    ## vars
    definition = refine_definition(_json.loads(file_read(args.definition_path)))
    conf = {
        "directories": {
            "temp": string_coin("/tmp/{{project}}-temp", {"project": definition["name"]}),
            "build": string_coin(args.build_directory_pattern, {"project": definition["name"]}),
        }
    }

    ## exec
    ### todo: refine
    if (args.action == "update-plankton"):
        action_update_plankton(conf, definition)
    elif (args.action == "update-node"):
        action_update_node_modules(conf, definition)
    elif (args.action == "build"):
        action_build(conf, definition)
    else:
        _sys.stderr.write("unhandled action")
    

main()
