
import typing as t
from pathlib import Path

from nr.caching.api import NamespaceStore
from nr.preconditions import check_not_none

from craftr.core.executor import IExecutor, ExecutionGraph
from craftr.core.plugin import IPluginLoader
from craftr.core.project import IProjectLoader, Project
from craftr.core.settings import Settings
from craftr.core.task import Task, ITaskSelector
from craftr.core.util.caching import JsonDirectoryStore


class Context:
  """
  The context carries globally accessible data for a craftr build. If not *settings* are specified,
  the `craftr.properties` file is read from the current working directory (if it exists).

  # Supported Settings

  * `core.build_directory` (no default)
  * `core.executor` (defaults to `craftr.core.executor.simple.SimpleExecutor`)
  * `core.task_selector` (defaults to `craftr.core.task.selector.default.DefaultTaskSelector`)
  """

  DEFAULT_EXECUTOR = 'craftr.core.executor.default.DefaultExecutor'
  DEFAULT_PLUGIN_LOADER = 'craftr.core.plugin.default.DefaultPluginLoader'
  DEFAULT_SELECTOR = 'craftr.core.task.selector.default.DefaultTaskSelector'
  DEFAULT_PROJECT_LOADER = 'craftr.core.project.loader.delegate.DelegateProjectLoader'
  CRAFTR_SETTINGS_FILE = Path('build.settings')

  def __init__(
    self, *,
    settings: t.Optional[Settings] = None,
    executor: t.Optional[IExecutor] = None,
    plugin_loader: t.Optional[IPluginLoader] = None,
    project_loader: t.Optional[IProjectLoader] = None,
  ) -> None:

    if settings is None and self.CRAFTR_SETTINGS_FILE.exists():
      settings = Settings.parse(self.CRAFTR_SETTINGS_FILE.read_text().splitlines())
    elif settings is None:
      settings = Settings.of({})

    self._root_project: t.Optional[Project] = None
    self.settings = settings
    self.executor = executor or settings.get_instance(
        IExecutor, 'core.executor', self.DEFAULT_EXECUTOR)  # type: ignore
    self.plugin_loader = plugin_loader or settings.get_instance(
        IPluginLoader, 'core.plugin.loader', self.DEFAULT_PLUGIN_LOADER)  # type: ignore
    self.project_loader = project_loader or settings.get_instance(
        IProjectLoader, 'core.project.loader', self.DEFAULT_PROJECT_LOADER)  # type: ignore
    self.task_selector = self.settings.get_instance(
        ITaskSelector, 'core.task_selector', self.DEFAULT_SELECTOR)  # type: ignore
    self.graph = ExecutionGraph()
    self._metadata_store: t.Optional[NamespaceStore] = None

  @property
  def metadata_store(self) -> NamespaceStore:
    if self._metadata_store is None:
      assert self.root_project, 'Context.root_project is not set'
      self._metadata_store = JsonDirectoryStore(
        str(self.get_default_build_directory(self.root_project) / '.craftr-metadata'), create_dir=True)
    return self._metadata_store

  @property
  def root_project(self) -> t.Optional[Project]:
    return self._root_project

  def load_project(self, path: Path) -> Project:
    """
    Initialize the root project and return it.
    """

    project = self.project_loader.load_project(self, None, path)
    self._root_project = project
    return project

  def initialize_project(self, project: Project) -> None:
    """
    Called when a project is created. Can be overwritten by subclasses to customize what happens
    when a project is created. The default implementationa applies the "default" plugin.
    """

    pass

  def get_default_build_directory(self, project: Project) -> Path:
    """
    Returns the default build directory for a project, used if no explicit build directory is
    set. The default implementation returns the `.build/` directory in the project's directory,
    unless `core.build_directory` is set.
    """

    build_directory = self.settings.get('core.build_directory', None)
    if build_directory is None:
      return project.directory.joinpath('.build')
    else:
      return Path(build_directory)

  def execute(self, selection: t.Union[None, str, t.List[str], Task, t.List[Task]] = None) -> None:
    root_project = check_not_none(self.root_project, 'no root project initialized')
    root_project.finalize()
    selected_tasks: t.Set[Task] = set()

    if selection is None:
      selected_tasks.update(self.task_selector.select_default(root_project))
    else:
      if isinstance(selection, (str, Task)):
        selection = t.cast(t.Union[t.List[str], t.List[Task]], [selection])
      for item in selection:
        if isinstance(item, Task):
          selected_tasks.add(item)
        elif isinstance(item, str):
          result_set = self.task_selector.select_tasks(item, root_project)
          if not result_set:
            raise ValueError(f'selector matched no tasks: {item!r}')
          selected_tasks.update(result_set)
        else:
          raise TypeError(f'expected str|Task, got {type(item).__name__}')

    self.graph.add_tasks(selected_tasks)
    self.graph.ready()
    self.executor.execute(self.graph)
