import os
import logging
import pprint
from typing import List
from collections import namedtuple
from pathlib import Path
from contextlib import contextmanager

from yaml import safe_load
from jinja2 import Environment, FileSystemLoader


def isnamedtupleinstance(x):
    t = type(x)
    b = t.__bases__
    if len(b) != 1 or b[0] != tuple:
        return False
    f = getattr(t, "_fields", None)
    if not isinstance(f, tuple):
        return False
    return all(type(n) == str for n in f)


def freeze(data):
    """
    Used to turn a dict into a named tuple so that key words do not clash. For
    example `items` cannot be used in a dict since it will clash with the
    `items()` function name.
    """
    if isinstance(data, dict):
        Data = namedtuple("Data", " ".join(data.keys()))
        return Data(**{k: freeze(v) for k, v in data.items()})
    if isinstance(data, list):
        return tuple(data)
    return data


def load_yaml(src_path: Path, template_path: Path):
    """
    Given a path to a template,
    Loads all YAML files by name in all directories up to that template.

    - A file located in the root source folder will always be loaded for all templates.
    - A file called `_data.yml` placed in `/some/folder` will be overridden by
      a file called `_data.yml` placed in `/some/folder/view`.

    """

    def merge(d1, d2):
        if isnamedtupleinstance(d1):
            d1 = d1._asdict()
        if isnamedtupleinstance(d2):
            d2 = d2._asdict()
        # ---
        assert type(d1) == type(
            d2
        ), f"Cannot merge different types: {type(d1)} {type(d2)}"
        # ---
        if isinstance(d1, list):
            return d1 + d2
        if isinstance(d1, tuple):
            return tuple(list(d1) + list(d2))
        if isinstance(d1, dict):
            result = {}
            keys = list(d1.keys()) + list(d2.keys())
            for key in keys:
                if key in d1 and key not in d2:
                    result[key] = d1[key]
                    continue
                if key not in d1 and key in d2:
                    result[key] = d2[key]
                    continue
                assert key in d1 and key in d2
                result[key] = freeze(merge(d1[key], d2[key]))
            return result
        if d1 == d2:
            return d1
        return d2

    def load_from(path):
        path_yaml = {}
        for ext in ["yaml", "yml"]:
            for datafile in path.glob(f"*.{ext}"):
                print(f"\t{path} -> {datafile.name}")
                with open(datafile, "r", encoding="utf-8") as fl:
                    data = safe_load(fl.read())
                    path_yaml[datafile.name[: -len(datafile.suffix)]] = freeze(data)
        return path_yaml

    yaml = load_from(src_path)
    for part in template_path.parent.relative_to(src_path).parts:
        src_path = src_path / part
        yaml = merge(yaml, load_from(src_path))
    return yaml


def get_build_paths(src_path, ext):
    """
    Walks the source folder and generates paths for all the files that will be
    generated by the build process.
    """
    for path in src_path.glob("**/*"):
        if any(part.startswith("_") for part in path.relative_to(src_path).parts):
            continue
        if path.name.startswith("_") or path.is_dir() or path.suffix not in ext:
            continue
        yield path


def set_new_filename(outpath):
    def fn(new_name):
        outpath[0] = outpath[0].parent / new_name

    return fn


def build(*, src: str, dest: str, ext: List[str]):
    """
    - Walk a given `src` folder and generate output inside a `dest` folder.
    - If will generate files using the Jinja2 template engine.
        - You can use any imports etc
    - Any file name starting with `_` will be ignored.
    - Only files with extensions that are provided in `ext` will be rendered.
    - Variables that are available:
        - All environment variables can be used via `{{ env.MY_VARIABLE }}`.
        - `{{ relative_path }}` refers to the path of the current file being rendered.
        - `yaml` can be used to access data inside yaml files.
    """
    src_path = Path(src).resolve()
    dest_path = Path(dest).resolve()
    env = Environment(loader=FileSystemLoader(str(src)))
    env.globals["env"] = os.environ
    if not dest_path.exists():
        os.makedirs(dest_path)
    build_paths = list(get_build_paths(src_path, ext))
    relative_paths = [path.relative_to(src_path) for path in build_paths]
    for path in build_paths:
        relative_path = path.relative_to(src_path)
        out_path = dest_path / relative_path
        if not out_path.parent.exists():
            os.makedirs(out_path.parent)
        template_path = str(path.relative_to(src_path))
        print("-" * 20)
        print(f"{template_path} -> {out_path}")
        template = env.get_template(template_path)
        yaml = load_yaml(src_path, path)
        pprint.pprint(yaml)
        out_path = [out_path]
        html = template.render(
            relative_path=relative_path,
            yaml=yaml,
            build_paths=relative_paths,
            set_filename=set_new_filename(out_path),
        )
        print(out_path)
        with open(out_path[0], "w", encoding="utf-8") as output:
            output.write(html)
