"""Create a tmux workspace from a configuration :py:obj:`dict`.

tmuxp.workspacebuilder
~~~~~~~~~~~~~~~~~~~~~~

"""
import logging
import time

from libtmux.exc import TmuxSessionExists
from libtmux.pane import Pane
from libtmux.server import Server
from libtmux.session import Session
from libtmux.window import Window

from . import exc
from .util import get_current_pane, run_before_script

logger = logging.getLogger(__name__)

DEFAULT_WIDTH = "800"
DEFAULT_HEIGHT = "600"
DEFAULT_SIZE = f"{DEFAULT_WIDTH}x{DEFAULT_HEIGHT}"


class WorkspaceBuilder:

    """
    Load workspace from session :py:obj:`dict`.

    Build tmux workspace from a configuration. Creates and names windows, sets
    options, splits windows into panes.

    Examples
    --------

    >>> import yaml

    >>> session_config = yaml.load('''
    ...     session_name: sampleconfig
    ...     start_directory: '~'
    ...     windows:
    ...     - window_name: editor
    ...       layout: main-vertical
    ...       panes:
    ...       - shell_command:
    ...         - cmd: vim
    ...       - shell_command:
    ...         - cmd: echo "hey"
    ...
    ...     - window_name: logging
    ...       panes:
    ...       - shell_command:
    ...         - cmd: tail | echo 'hi'
    ...
    ...     - window_name: test
    ...       panes:
    ...       - shell_command:
    ...         - cmd: htop
    ... ''', Loader=yaml.Loader)

    >>> builder = WorkspaceBuilder(sconf=session_config, server=server)

    **New session:**

    >>> builder.build()

    >>> new_session = builder.session

    >>> new_session.name == 'sampleconfig'
    True

    >>> len(new_session._windows)
    3

    >>> sorted([window.name for window in new_session.windows])
    ['editor', 'logging', 'test']

    **Existing session:**

    >>> len(session._windows)
    1

    >>> builder.build(session=session)

    _Caveat:_ Preserves old session name:

    >>> session.name == 'sampleconfig'
    False

    >>> len(session._windows)
    3

    >>> sorted([window.name for window in session.windows])
    ['editor', 'logging', 'test']

    The normal phase of loading is:

    1. :term:`kaptan` imports json/yaml/ini. ``.get()`` returns python
       :class:`dict`::

           import kaptan
           sconf = kaptan.Kaptan(handler='yaml')
           sconf = sconfig.import_config(self.yaml_config).get()

       or from config file with extension::

           import kaptan
           sconf = kaptan.Kaptan()
           sconf = sconfig.import_config('path/to/config.yaml').get()

       kaptan automatically detects the handler from filenames.

    2. :meth:`config.expand` sconf inline shorthand::

           from tmuxp import config
           sconf = config.expand(sconf)

    3. :meth:`config.trickle` passes down default values from session
       -> window -> pane if applicable::

           sconf = config.trickle(sconf)

    4. (You are here) We will create a :class:`libtmux.Session` (a real
       ``tmux(1)`` session) and iterate through the list of windows, and
       their panes, returning full :class:`libtmux.Window` and
       :class:`libtmux.Pane` objects each step of the way::

           workspace = WorkspaceBuilder(sconf=sconf)

    It handles the magic of cases where the user may want to start
    a session inside tmux (when `$TMUX` is in the env variables).
    """

    def __init__(self, sconf, plugins=[], server=None):
        """
        Initialize workspace loading.

        Parameters
        ----------
        sconf : dict
            session config, includes a :py:obj:`list` of ``windows``.

        plugins : list
            plugins to be used for this session

        server : :class:`libtmux.Server`
            tmux server to build session in

        Notes
        -----
        TODO: Initialize :class:`libtmux.Session` from here, in
        ``self.session``.
        """

        if not sconf:
            raise exc.EmptyConfigException("session configuration is empty.")

        # config.validate_schema(sconf)

        if isinstance(server, Server):
            self.server = server
        else:
            self.server = None

        self.sconf = sconf

        self.plugins = plugins

    def session_exists(self, session_name=None):
        exists = self.server.has_session(session_name)
        if not exists:
            return exists

        self.session = self.server.find_where({"session_name": session_name})
        return True

    def build(self, session=None, append=False):
        """
        Build tmux workspace in session.

        Optionally accepts ``session`` to build with only session object.

        Without ``session``, it will use :class:`libmtux.Server` at
        ``self.server`` passed in on initialization to create a new Session
        object.

        Parameters
        ----------
        session : :class:`libtmux.Session`
            session to build workspace in
        append : bool
            append windows in current active session
        """

        if not session:
            if not self.server:
                raise exc.TmuxpException(
                    "WorkspaceBuilder.build requires server to be passed "
                    + "on initialization, or pass in session object to here."
                )

            if self.server.has_session(self.sconf["session_name"]):
                self.session = self.server.find_where(
                    {"session_name": self.sconf["session_name"]}
                )
                raise TmuxSessionExists(
                    "Session name %s is already running." % self.sconf["session_name"]
                )
            else:
                new_session_kwargs = {}
                if "start_directory" in self.sconf:
                    new_session_kwargs["start_directory"] = self.sconf[
                        "start_directory"
                    ]
                session = self.server.new_session(
                    session_name=self.sconf["session_name"]
                )

            assert self.sconf["session_name"] == session.name
            assert len(self.sconf["session_name"]) > 0

        self.session = session
        self.server = session.server

        self.server._list_sessions()
        assert self.server.has_session(session.name)
        assert session.id

        assert isinstance(session, Session)

        for plugin in self.plugins:
            plugin.before_workspace_builder(self.session)

        focus = None

        if "before_script" in self.sconf:
            try:
                cwd = None

                # we want to run the before_script file cwd'd from the
                # session start directory, if it exists.
                if "start_directory" in self.sconf:
                    cwd = self.sconf["start_directory"]
                run_before_script(self.sconf["before_script"], cwd=cwd)
            except Exception as e:
                self.session.kill_session()
                raise e
        if "options" in self.sconf:
            for option, value in self.sconf["options"].items():
                self.session.set_option(option, value)
        if "global_options" in self.sconf:
            for option, value in self.sconf["global_options"].items():
                self.session.set_option(option, value, _global=True)
        if "environment" in self.sconf:
            for option, value in self.sconf["environment"].items():
                self.session.set_environment(option, value)

        for w, wconf in self.iter_create_windows(session, append):
            assert isinstance(w, Window)

            for plugin in self.plugins:
                plugin.on_window_create(w)

            focus_pane = None
            for p, pconf in self.iter_create_panes(w, wconf):
                assert isinstance(p, Pane)
                p = p

                if "layout" in wconf:
                    w.select_layout(wconf["layout"])

                if "focus" in pconf and pconf["focus"]:
                    focus_pane = p

            if "focus" in wconf and wconf["focus"]:
                focus = w

            self.config_after_window(w, wconf)

            for plugin in self.plugins:
                plugin.after_window_finished(w)

            if focus_pane:
                focus_pane.select_pane()

        if focus:
            focus.select_window()

    def iter_create_windows(self, session, append=False):
        """
        Return :class:`libtmux.Window` iterating through session config dict.

        Generator yielding :class:`libtmux.Window` by iterating through
        ``sconf['windows']``.

        Applies ``window_options`` to window.

        Parameters
        ----------
        session : :class:`libtmux.Session`
            session to create windows in
        append : bool
            append windows in current active session

        Returns
        -------
        tuple of (:class:`libtmux.Window`, ``wconf``)
            Newly created window, and the section from the tmuxp configuration
            that was used to create the window.
        """
        for i, wconf in enumerate(self.sconf["windows"], start=1):
            if "window_name" not in wconf:
                window_name = None
            else:
                window_name = wconf["window_name"]

            is_first_window_pass = self.first_window_pass(i, session, append)

            w1 = None
            if is_first_window_pass:  # if first window, use window 1
                w1 = session.attached_window
                w1.move_window(99)

            if "start_directory" in wconf:
                sd = wconf["start_directory"]
            else:
                sd = None

            # If the first pane specifies a start_directory, use that instead.
            panes = wconf["panes"]
            if panes and "start_directory" in panes[0]:
                sd = panes[0]["start_directory"]

            if "window_shell" in wconf:
                ws = wconf["window_shell"]
            else:
                ws = None

            # If the first pane specifies a shell, use that instead.
            try:
                if wconf["panes"][0]["shell"] != "":
                    ws = wconf["panes"][0]["shell"]
            except (KeyError, IndexError):
                pass

            w = session.new_window(
                window_name=window_name,
                start_directory=sd,
                attach=False,  # do not move to the new window
                window_index=wconf.get("window_index", ""),
                window_shell=ws,
            )

            if is_first_window_pass:  # if first window, use window 1
                session.attached_window.kill_window()

            assert isinstance(w, Window)
            session.server._update_windows()
            if "options" in wconf and isinstance(wconf["options"], dict):
                for key, val in wconf["options"].items():
                    w.set_window_option(key, val)

            if "focus" in wconf and wconf["focus"]:
                w.select_window()

            session.server._update_windows()

            yield w, wconf

    def iter_create_panes(self, w, wconf):
        """
        Return :class:`libtmux.Pane` iterating through window config dict.

        Run ``shell_command`` with ``$ tmux send-keys``.

        Parameters
        ----------
        w : :class:`libtmux.Window`
            window to create panes for
        wconf : dict
            config section for window

        Returns
        -------
        tuple of (:class:`libtmux.Pane`, ``pconf``)
            Newly created pane, and the section from the tmuxp configuration
            that was used to create the pane.
        """
        assert isinstance(w, Window)

        pane_base_index = int(w.show_window_option("pane-base-index", g=True))

        p = None

        for pindex, pconf in enumerate(wconf["panes"], start=pane_base_index):
            if pindex == int(pane_base_index):
                p = w.attached_pane
            else:

                def get_pane_start_directory():

                    if "start_directory" in pconf:
                        return pconf["start_directory"]
                    elif "start_directory" in wconf:
                        return wconf["start_directory"]
                    else:
                        return None

                def get_pane_shell():

                    if "shell" in pconf:
                        return pconf["shell"]
                    elif "window_shell" in wconf:
                        return wconf["window_shell"]
                    else:
                        return None

                p = w.split_window(
                    attach=True,
                    start_directory=get_pane_start_directory(),
                    shell=get_pane_shell(),
                    target=p.id,
                )

            assert isinstance(p, Pane)
            if "layout" in wconf:
                w.select_layout(wconf["layout"])

            if "suppress_history" in pconf:
                suppress = pconf["suppress_history"]
            elif "suppress_history" in wconf:
                suppress = wconf["suppress_history"]
            else:
                suppress = True

            enter = pconf.get("enter", True)
            sleep_before = pconf.get("sleep_before", None)
            sleep_after = pconf.get("sleep_after", None)
            for cmd in pconf["shell_command"]:
                enter = cmd.get("enter", enter)
                sleep_before = cmd.get("sleep_before", sleep_before)
                sleep_after = cmd.get("sleep_after", sleep_after)

                if sleep_before is not None:
                    time.sleep(sleep_before)

                p.send_keys(cmd["cmd"], suppress_history=suppress, enter=enter)

                if sleep_after is not None:
                    time.sleep(sleep_after)

            if "focus" in pconf and pconf["focus"]:
                w.select_pane(p["pane_id"])

            w.server._update_panes()

            yield p, pconf

    def config_after_window(self, w, wconf):
        """Actions to apply to window after window and pane finished.

        When building a tmux session, sometimes its easier to postpone things
        like setting options until after things are already structurally
        prepared.

        Parameters
        ----------
        w : :class:`libtmux.Window`
            window to create panes for
        wconf : dict
            config section for window
        """
        if "options_after" in wconf and isinstance(wconf["options_after"], dict):
            for key, val in wconf["options_after"].items():
                w.set_window_option(key, val)

    def find_current_attached_session(self):
        current_active_pane = get_current_pane(self.server)

        if not current_active_pane:
            raise exc.TmuxpException("No session active.")

        return next(
            (
                s
                for s in self.server.list_sessions()
                if s["session_id"] == current_active_pane["session_id"]
            ),
            None,
        )

    def first_window_pass(self, i, session, append):
        return len(session.windows) == 1 and i == 1 and not append


def freeze(session):
    """
    Freeze live tmux session and Return session config :py:obj:`dict`.

    Parameters
    ----------
    session : :class:`libtmux.Session`
        session object

    Returns
    -------
    dict
        tmuxp compatible workspace config
    """
    sconf = {"session_name": session["session_name"], "windows": []}

    for w in session.windows:
        wconf = {
            "options": w.show_window_options(),
            "window_name": w.name,
            "layout": w.layout,
            "panes": [],
        }
        if w.get("window_active", "0") == "1":
            wconf["focus"] = "true"

        # If all panes have same path, set 'start_directory' instead
        # of using 'cd' shell commands.
        def pane_has_same_path(p):
            return w.panes[0].current_path == p.current_path

        if all(pane_has_same_path(p) for p in w.panes):
            wconf["start_directory"] = w.panes[0].current_path

        for p in w.panes:
            pconf = {"shell_command": []}

            if "start_directory" not in wconf:
                pconf["shell_command"].append("cd " + p.current_path)

            if p.get("pane_active", "0") == "1":
                pconf["focus"] = "true"

            current_cmd = p.current_command

            def filter_interpretters_and_shells():
                return current_cmd.startswith("-") or any(
                    current_cmd.endswith(cmd) for cmd in ["python", "ruby", "node"]
                )

            if filter_interpretters_and_shells():
                current_cmd = None

            if current_cmd:
                pconf["shell_command"].append(current_cmd)
            else:
                if not len(pconf["shell_command"]):
                    pconf = "pane"

            wconf["panes"].append(pconf)

        sconf["windows"].append(wconf)

    return sconf
