import os
import re
import contextlib
from copy import copy

from homely._errors import HelperError
from homely._engine2 import getengine, Helper, Engine, getrepoinfo
from homely._cleaners import CleanLineInFile, CleanBlockInFile
from homely._utils import filereplacer, _resolve
from homely._ui import warning


def run(updatehelper):
    getengine().run(updatehelper)


def section(func):
    name = func.__name__
    engine = getengine()
    try:
        if engine.pushsection(name):
            func()
    finally:
        engine.popsection(name)


def mkdir(path):
    path = _resolve(path)
    getengine().run(MakeDir(path))


def lineinfile(filename, contents, where=None):
    filename = _resolve(filename)
    obj = LineInFile(filename, contents, where)
    getengine().run(obj)


def symlink(target, linkname=None):
    target = _resolve(target)
    linkname = _resolve(linkname)
    # if [target] doesn't start with '/', assume it is relative to the repo
    if not target.startswith('/'):
        info = getrepoinfo()
        target = os.path.join(info.localrepo.repo_path, target)
    # if [linkname] is omited, assume the symlink goes into $HOME/ at the top
    # level
    if linkname is None:
        linkname = os.path.join(os.environ.get('HOME'),
                                os.path.basename(target))
    getengine().run(MakeSymlink(target, linkname))


# TODO: phase out all use of this old thing
class UpdateHelper(object):
    _section = None

    @property
    def identifiers(self):
        raise NotImplementedError(
            "%s needs to implement @property identifiers()" %
            self.__class__.__name__)

    @classmethod
    def fromidentifiers(class_, identifiers):
        prototype = "@classmethod fromidentifiers(class_, identifiers)"
        raise NotImplementedError("%s needs to implement %s" %
                                  (class_.__name__, prototype))

    @property
    def uniqueid(self):
        identifiers = self.identifiers
        items = [self.__class__.__name__]
        for key in sorted(identifiers):
            items.extend([key, identifiers[key]])
        return repr(items)

    def setsection(self, name):
        self._section = name

    def getsection(self):
        return self._section

    def isdone(self):
        raise NotImplementedError("%s needs to implement isdone()" %
                                  (self.__class__.__name__))

    def makechanges(self):
        raise NotImplementedError("%s needs to implement .makechanges()" %
                                  self.__class__.__name__)

    # TODO: get rid of this permanently
    def undochanges(self, prevchanges):
        prototype = "undochanges(self, prevchanges)"
        raise NotImplementedError("%s needs to implement %s" %
                                  (self.__class__.__name__, prototype))


class MakeDir(Helper):
    _path = None

    def __init__(self, path):
        super(MakeDir, self).__init__()
        self._path = path

    @property
    def description(self):
        return "Create dir %s" % self._path

    def getcleaner(self):
        return

    def isdone(self):
        return os.path.exists(self._path) and os.path.isdir(self._path)

    def makechanges(self):
        os.mkdir(self._path)

    def affectspath(self, path):
        return path == self._path

    def pathsownable(self):
        return {self._path: Engine.TYPE_FOLDER}


class MakeSymlink(Helper):
    def __init__(self, target, linkname):
        assert target.startswith('/')
        assert linkname.startswith('/')
        self._target = target
        self._linkname = linkname

    def getcleaner(self):
        return

    def isdone(self):
        return (os.path.islink(self._linkname) and
                os.readlink(self._linkname) == self._target)

    @property
    def description(self):
        return "Create symlink %s -> %s" % (self._linkname, self._target)

    def makechanges(self):
        assert not os.path.exists(self._linkname)
        os.symlink(self._target, self._linkname)

    # TODO: get rid of all of these functions ...
    def undochanges(self, prevchanges):
        raise Exception("TODO: move this to a cleaner")  # noqa
        if not os.path.islink(self._dest):
            return
        if os.readlink(self._dest) == self._source:
            os.unlink(self._dest)
        else:
            warning("Refusing to clean up symlink %s which now points to"
                    " %s" % (self._dest, os.readlink(self._dest)))

    def affectspath(self, path):
        return path == self._linkname

    def pathsownable(self):
        return {self._linkname: Engine.TYPE_LINK}


WHERE_TOP = "__TOP__"
WHERE_END = "__END__"
WHERE_ANY = "__ANY__"


class LineInFile(Helper):
    def __init__(self, filename, contents, where=None):
        super(LineInFile, self).__init__()
        self._filename = filename
        self._contents = contents
        if not len(contents):
            raise Exception("LineInFile cannot work with empty contents")
        if "\n" in contents or "\r" in contents:
            raise Exception("LineInFile contents cannot include linebreaks")
        if contents.strip() != contents:
            raise Exception(
                "LineInFile contents cannot start or end with whitespace")
        if where is None:
            where = WHERE_ANY
        self._where = where

    @property
    def description(self):
        pos = ""
        if self._where == WHERE_TOP:
            pos = " (at top)"
        elif self._where == WHERE_END:
            pos = " (at end)"
        return "Add line to %s: %r%s" % (self._filename, self._contents, pos)

    def getcleaner(self):
        return CleanLineInFile(self._filename, self._contents)

    def isdone(self):
        if not os.path.exists(self._filename):
            return False
        foundat = []
        linecount = 0
        with open(self._filename) as f:
            for line in f.readlines():
                linecount += 1
                if line.rstrip('\r\n') == self._contents:
                    foundat.append(linecount)
        # if the line appears multiple times, we count it as NOT done
        if len(foundat) != 1:
            return False
        if self._where == WHERE_TOP:
            return foundat[0] == 1
        if self._where == WHERE_END:
            return foundat[0] == linecount
        return True

    def makechanges(self):
        # the content wasn't found in the file, so we'll have to add it
        with filereplacer(self._filename) as (tmp, origlines, NL):
            seen = False
            # if the new line goes at the top, write it out first
            if self._where == WHERE_TOP:
                tmp.write(self._contents)
                tmp.write(NL)
                seen = True
            # read through the original file and look for a line to replace
            if origlines is not None:
                for line in origlines:
                    if line == self._contents:
                        # skip the line if it's already been seen before, or it
                        # needs to move to the end of the file
                        if seen or self._where == WHERE_END:
                            continue
                        seen = True
                    tmp.write(line)
                    tmp.write(NL)
            if not seen:
                assert self._where in (WHERE_END, WHERE_ANY)
                tmp.write(self._contents)
                tmp.write(NL)

    def pathsownable(self):
        return {self._filename: Engine.TYPE_FILE}

    def affectspath(self, path):
        return path == self._filename


class BlockInFile(Helper):
    def __init__(self, filename, lines, prefix, suffix, where=None):
        self._filename = filename
        self._lines = lines
        self._prefix = prefix
        self._suffix = suffix
        self._where = WHERE_ANY if where is None else where

    @property
    def description(self):
        pos = {WHERE_TOP: "top of ", WHERE_END: "end of ", WHERE_ANY: ""}
        return "Add %d lines to %s%s: %r...%r" % (
            2 + len(self._lines),
            pos[self._where],
            self._filename,
            self._prefix,
            self._suffix,
        )

    def getcleaner(self):
        return CleanBlockInFile(self._filename,
                                self._prefix,
                                self._suffix,
                                )

    def isdone(self):
        if not os.path.exists(self._filename):
            return False

        # look to see if our contents appear in the file
        with open(self._filename) as f:
            firstline = True
            count = 0
            expect = None
            prev = None
            for line in f:
                stripped = line.rstrip('\r\n')
                if firstline and self._where == WHERE_TOP:
                    if stripped != self._prefix:
                        return False
                if expect is not None:
                    if stripped != expect.pop(0):
                        return False
                    if len(expect):
                        prev = "INNER"
                    else:
                        prev = "SUFFIX"
                        count += 1
                elif stripped == self._prefix:
                    expect = copy(self._lines)
                    expect.extend(self._suffix)
                    prev = "PREFIX"
                else:
                    prev = "OTHER"
        if self._where == WHERE_END and prev != "SUFFIX":
            return False
        return count == 1

    def makechanges(self):
        with filereplacer(self._filename) as (tmp, origlines, NL):
            findsuffix = False
            found = False

            def _writeall():
                nonlocal found
                found = True
                tmp.write(self._prefix)
                tmp.write(NL)
                for line in self._lines:
                    tmp.write(line)
                    tmp.write(NL)
                tmp.write(self._suffix)
                tmp.write(NL)

            if self._where == WHERE_TOP:
                _writeall()
            if origlines is not None:
                for line in origlines:
                    if findsuffix:
                        # while we're looking for the suffix, discard any other
                        # lines we encounter
                        # encountered into a temporary holding area
                        if line != self._suffix:
                            continue

                        # We've found the suffix! If we're allowed to write the
                        # block out anywhere, and we haven't written it out
                        # yet write it out here, now
                        if self._where == WHERE_ANY and not found:
                            _writeall()
                        findsuffix = False
                        continue

                    if line == self._prefix:
                        findsuffix = True
                        continue

                    tmp.write(line)
                    tmp.write(NL)

            if findsuffix:
                # FIXME: some sort of exception that we can handle nicely
                raise Exception(
                    "Error in %s: Found prefix %r but not suffix %r" % (
                        self._filename,
                        self._prefix,
                        self._suffix,
                    ))

            # if we haven't found the block yet, write it out now
            if not found:
                _writeall()

    def affectspath(self, path):
        return path == self._filename

    def pathsownable(self):
        return {self._filename: Engine.TYPE_FILE}
