import tempfile
import os
import sys
import subprocess
import mimetypes
import collections
import io
import difflib

from typing import Mapping


class EditProcessException(RuntimeError):
    pass


class EditableFile(object):
    """
    EditableFile saves some data to a temporary file, launches a
    platform editor for interactive editing, and then reloads the data,
    setting .changed to True if the data was edited.

    e.g.::

            x = EditableFile('foo')
            x.edit()

            if x.changed:
                    print(x.data)

    The EDITOR environment variable can define which executable to use
    (also XML_EDITOR if the content-type to edit includes 'xml'). If no
    EDITOR is defined, defaults to 'notepad' on Windows and 'edit' on
    other platforms.
    """

    platform_default_editors: Mapping[str, str] = collections.defaultdict(
        lambda: 'edit',
        win32='notepad',
        linux2='vi',
    )
    encoding = 'utf-8'

    def __init__(self, data='', content_type='text/plain'):
        self.data = str(data)
        self.content_type = content_type

    def __enter__(self):
        extension = mimetypes.guess_extension(self.content_type) or ''
        fobj, self.name = tempfile.mkstemp(extension)
        os.write(fobj, self.data.encode(self.encoding))
        os.close(fobj)
        return self

    def read(self):
        with open(self.name, 'rb') as f:
            return f.read().decode(self.encoding)

    def __exit__(self, *tb_info):
        os.remove(self.name)

    def edit(self):
        """
        Edit the file
        """
        self.changed = False
        with self:
            editor = self.get_editor()
            cmd = [editor, self.name]
            try:
                res = subprocess.call(cmd)
            except Exception as e:
                print("Error launching editor %(editor)s" % locals())
                print(e)
                return
            if res != 0:
                msg = '%(editor)s returned error status %(res)d' % locals()
                raise EditProcessException(msg)
            new_data = self.read()
            if new_data != self.data:
                self.changed = self._save_diff(self.data, new_data)
                self.data = new_data

    @staticmethod
    def _search_env(keys):
        """
        Search the environment for the supplied keys, returning the first
        one found or None if none was found.
        """
        matches = (os.environ[key] for key in keys if key in os.environ)
        return next(matches, None)

    def get_editor(self):
        """
        Give preference to an XML_EDITOR or EDITOR defined in the
        environment. Otherwise use a default editor based on platform.
        """
        env_search = ['EDITOR']
        if 'xml' in self.content_type:
            env_search.insert(0, 'XML_EDITOR')
        default_editor = self.platform_default_editors[sys.platform]
        return self._search_env(env_search) or default_editor

    @staticmethod
    def _save_diff(*versions):
        def get_lines(content):
            return list(io.StringIO(content))

        lines = map(get_lines, versions)
        diff = difflib.context_diff(*lines)
        return tuple(diff)
