import atexit
import itertools
import logging
import os
from os.path import dirname, join, basename
import random
from pymclevel import PocketLeveldbWorld
import re
import shutil
import subprocess
import sys
import tempfile
import time
import urllib
import json
import urllib2

import infiniteworld
from directories import getCacheDir
from mclevelbase import exhaust, ChunkNotPresent

log = logging.getLogger(__name__)

__author__ = 'Rio'

# Thank you, Stackoverflow
# http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python


def which(program):
    def is_exe(f):
        return os.path.exists(f) and os.access(f, os.X_OK)

    fpath, _fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        if sys.platform == "win32":
            if "SYSTEMROOT" in os.environ:
                root = os.environ["SYSTEMROOT"]
                exe_file = os.path.join(root, program)
                if is_exe(exe_file):
                    return exe_file
        if "PATH" in os.environ:
            for path in os.environ["PATH"].split(os.pathsep):
                exe_file = os.path.join(path, program)
                if is_exe(exe_file):
                    return exe_file

    return None


convert = lambda text: int(text) if text.isdigit() else text
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]


def getVersions(doSnapshot):
    JAR_VERSION_URL_TEMPLATE = "https://s3.amazonaws.com/Minecraft.Download/versions/{}/minecraft_server.{}.jar"
    versionSite = urllib2.urlopen("http://s3.amazonaws.com/Minecraft.Download/versions/versions.json")
    versionSiteResponse = versionSite.read()
    versionJSON = json.loads(versionSiteResponse)
    if doSnapshot:
        version = versionJSON["latest"]["snapshot"]
    else:
        version = versionJSON["latest"]["release"]
    print "Version: " + version
    URL = JAR_VERSION_URL_TEMPLATE.format(version, version)
    return URL


def sort_nicely(l):
    """ Sort the given list in the way that humans expect.
    """
    l.sort(key=alphanum_key)


class ServerJarStorage(object):
    _cacheDir = os.path.join(getCacheDir(), u"ServerJarStorage")

    def __init__(self, cacheDir=None):
        if not os.path.exists(self._cacheDir):
            os.makedirs(self._cacheDir)
        readme = os.path.join(self._cacheDir, "README.TXT")
        if not os.path.exists(readme):
            with open(readme, "w") as f:
                f.write("""
About this folder:

This folder is used by MCEdit and pymclevel to store different versions of the
Minecraft Server to use for terrain generation. It should have one or more
subfolders, one for each version of the server. Each subfolder must hold at
least one file named minecraft_server.jar, and the subfolder's name should
have the server's version plus the names of any installed mods.

There may already be a subfolder here (for example, "Release 1.7.10") if you have
used the Chunk Create feature in MCEdit to create chunks using the server.

Version numbers can be automatically detected. If you place one or more
minecraft_server.jar files in this folder, they will be placed automatically
into well-named subfolders the next time you run MCEdit. If a file's name
begins with "minecraft_server" and ends with ".jar", it will be detected in
this way.
""")

        self.reloadVersions()

    def reloadVersions(self):
        cacheDirList = os.listdir(self._cacheDir)
        self.versions = list(
            reversed(sorted([v for v in cacheDirList if os.path.exists(self.jarfileForVersion(v))], key=alphanum_key)))

        if MCServerChunkGenerator.java_exe:
            for f in cacheDirList:
                p = os.path.join(self._cacheDir, f)
                if f.startswith("minecraft_server") and f.endswith(".jar") and os.path.isfile(p):
                    print "Unclassified minecraft_server.jar found in cache dir. Discovering version number..."
                    self.cacheNewVersion(p)
                    os.remove(p)

        print "Minecraft_Server.jar storage initialized."
        print u"Each server is stored in a subdirectory of {0} named with the server's version number".format(
            self._cacheDir)

        print "Cached servers: ", self.versions

    def downloadCurrentServer(self, getSnapshot):
        self.snapshot = getSnapshot
        print "Downloading the latest Minecraft Server..."
        try:
            (filename, headers) = urllib.urlretrieve(getVersions(getSnapshot))
        except Exception as e:
            print "Error downloading server: {0!r}".format(e)
            return

        self.cacheNewVersion(filename, allowDuplicate=False)

    def cacheNewVersion(self, filename, allowDuplicate=True):
        """ Finds the version number from the server jar at filename and copies
        it into the proper subfolder of the server jar cache folder"""

        version = MCServerChunkGenerator._serverVersionFromJarFile(filename)
        print "Found version ", version
        versionDir = os.path.join(self._cacheDir, version)

        i = 1
        newVersionDir = versionDir
        while os.path.exists(newVersionDir):
            if not allowDuplicate:
                return

            newVersionDir = versionDir + " (" + str(i) + ")"
            i += 1

        os.mkdir(newVersionDir)

        shutil.copy2(filename, os.path.join(newVersionDir, "minecraft_server.jar"))

        if version not in self.versions:
            self.versions.append(version)

    def jarfileForVersion(self, v):
        return os.path.join(self._cacheDir, v, "minecraft_server.jar").encode(sys.getfilesystemencoding())

    def checksumForVersion(self, v):
        jf = self.jarfileForVersion(v)
        with open(jf, "rb") as f:
            import hashlib

            return hashlib.md5(f.read()).hexdigest()

    broken_versions = ["Beta 1.9 Prerelease {0}".format(i) for i in (1, 2, 3)]

    @property
    def latestVersion(self):
        if len(self.versions) == 0:
            return None
        return max((v for v in self.versions if v not in self.broken_versions), key=alphanum_key)

    def getJarfile(self, version=None):
        if len(self.versions) == 0:
            print "No servers found in cache."
            self.downloadCurrentServer(False)

        version = version or self.latestVersion
        if version not in self.versions:
            return None
        return self.jarfileForVersion(version)


class JavaNotFound(RuntimeError):
    pass


class VersionNotFound(RuntimeError):
    pass


def readProperties(filename):
    if not os.path.exists(filename):
        return {}

    with open(filename) as f:
        properties = dict((line.split("=", 2) for line in (l.strip() for l in f) if not line.startswith("#")))

    return properties


def saveProperties(filename, properties):
    with open(filename, "w") as f:
        for k, v in properties.iteritems():
            f.write("{0}={1}\n".format(k, v))


def findJava():
    # Here we implement the Java executable on the CLI.
    # This is because several version of Java may be installed on the system, and the one
    # needed fir the server may not be in the path...
    log.info("Looking for Java")
    if "--java" in sys.argv:
        idx = sys.argv.index("--java")
        java_exe_path = sys.argv.pop(idx + 1)
        sys.argv.pop(idx)
        log.info("CLI switch '--java' found: %s" % java_exe_path)
    else:
        if sys.platform == "win32":
            java_exe_path = "java.exe"
        else:
            java_exe_path = "java"
    if sys.platform == "win32":
        java_exe = which(java_exe_path)
        if java_exe is None:
            KEY_NAME = "HKLM\SOFTWARE\JavaSoft\Java Runtime Environment"
            try:
                p = subprocess.Popen(["REG", "QUERY", KEY_NAME, "/v", "CurrentVersion"], stdout=subprocess.PIPE,
                                     universal_newlines=True)
                o, e = p.communicate()
                lines = o.split("\n")
                for l in lines:
                    l = l.strip()
                    if l.startswith("CurrentVersion"):
                        words = l.split(None, 2)
                        version = words[-1]
                        p = subprocess.Popen(["REG", "QUERY", KEY_NAME + "\\" + version, "/v", "JavaHome"],
                                             stdout=subprocess.PIPE, universal_newlines=True)
                        o, e = p.communicate()
                        lines = o.split("\n")
                        for l in lines:
                            l = l.strip()
                            if l.startswith("JavaHome"):
                                w = l.split(None, 2)
                                javaHome = w[-1]
                                java_exe = os.path.join(javaHome, "bin", "java.exe")
                                print "RegQuery: java.exe found at ", java_exe
                                break

            except Exception as e:
                print "Error while locating java.exe using the Registry: ", repr(e)
    else:
        java_exe = which(java_exe_path)

    return java_exe


class MCServerChunkGenerator(object):
    """Generates chunks using minecraft_server.jar. Uses a ServerJarStorage to
    store different versions of minecraft_server.jar in an application support
    folder.

        from pymclevel import *

    Example usage:

        gen = MCServerChunkGenerator()  # with no arguments, use the newest
                                        # server version in the cache, or download
                                        # the newest one automatically
        level = loadWorldNamed("MyWorld")

        gen.generateChunkInLevel(level, 12, 24)


    Using an older version:

        gen = MCServerChunkGenerator("Beta 1.6.5")

    """
    defaultJarStorage = None

    java_exe = findJava()
    jarStorage = None
    _tempWorldCache = {}
    processes = []

    def __init__(self, version=None, jarfile=None, jarStorage=None):

        self.jarStorage = jarStorage or self.getDefaultJarStorage()

        if self.java_exe is None:
            raise JavaNotFound(
                "Could not find java. Please check that java is installed correctly. (Could not find java in your PATH environment variable.)")
        if jarfile is None:
            jarfile = self.jarStorage.getJarfile(version)
        if jarfile is None:
            raise VersionNotFound(
                "Could not find minecraft_server.jar for version {0}. Please make sure that a minecraft_server.jar is placed under {1} in a subfolder named after the server's version number.".format(
                    version or "(latest)", self.jarStorage._cacheDir))
        self.serverJarFile = jarfile
        self.serverVersion = version or self._serverVersion()
        atexit.register(MCServerChunkGenerator.terminateProcesses)

    @classmethod
    def getDefaultJarStorage(cls):
        if cls.defaultJarStorage is None:
            cls.defaultJarStorage = ServerJarStorage()
        return cls.defaultJarStorage

    @classmethod
    def clearWorldCache(cls):
        cls._tempWorldCache = {}

        for tempDir in os.listdir(cls.worldCacheDir):
            t = os.path.join(cls.worldCacheDir, tempDir)
            if os.path.isdir(t):
                shutil.rmtree(t)

    def createReadme(self):
        readme = os.path.join(self.worldCacheDir, "README.TXT")

        if not os.path.exists(readme):
            with open(readme, "w") as f:
                f.write("""
    About this folder:

    This folder is used by MCEdit and pymclevel to cache levels during terrain
    generation. Feel free to delete it for any reason.
    """)

    worldCacheDir = os.path.join(tempfile.gettempdir(), "pymclevel_MCServerChunkGenerator")

    def tempWorldForLevel(self, level):

        # tempDir = tempfile.mkdtemp("mclevel_servergen")
        tempDir = os.path.join(self.worldCacheDir, self.jarStorage.checksumForVersion(self.serverVersion),
                               str(level.RandomSeed))
        propsFile = os.path.join(tempDir, "server.properties")
        properties = readProperties(propsFile)

        tempWorld = self._tempWorldCache.get((self.serverVersion, level.RandomSeed))

        if tempWorld is None:
            if not os.path.exists(tempDir):
                os.makedirs(tempDir)
                self.createReadme()

            worldName = "world"
            worldName = properties.setdefault("level-name", worldName)

            tempWorldDir = os.path.join(tempDir, worldName)
            tempWorld = infiniteworld.MCInfdevOldLevel(tempWorldDir, create=True, random_seed=level.RandomSeed)
            tempWorld.close()

            tempWorldRO = infiniteworld.MCInfdevOldLevel(tempWorldDir, readonly=True)

            self._tempWorldCache[self.serverVersion, level.RandomSeed] = tempWorldRO

        if level.dimNo == 0:
            properties["allow-nether"] = "false"
        else:
            tempWorld = tempWorld.getDimension(level.dimNo)

            properties["allow-nether"] = "true"

        properties["server-port"] = int(32767 + random.random() * 32700)
        saveProperties(propsFile, properties)

        return tempWorld, tempDir

    def generateAtPosition(self, tempWorld, tempDir, cx, cz):
        return exhaust(self.generateAtPositionIter(tempWorld, tempDir, cx, cz))

    @staticmethod
    def addEULA(tempDir):
        eulaLines = [
            "#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\n",
            "#Wed Jul 23 21:10:11 EDT 2014\n", "eula=true\n"]
        with open(tempDir + "/" + "eula.txt", "w") as f:
            f.writelines(eulaLines)

    def generateAtPositionIter(self, tempWorld, tempDir, cx, cz, simulate=False):
        tempWorldRW = infiniteworld.MCInfdevOldLevel(tempWorld.filename)
        tempWorldRW.setPlayerSpawnPosition((cx * 16, 64, cz * 16))
        tempWorldRW.saveInPlace()
        tempWorldRW.close()
        del tempWorldRW

        tempWorld.unload()
        self.addEULA(tempDir)

        startTime = time.time()
        proc = self.runServer(tempDir)
        while proc.poll() is None:
            line = proc.stdout.readline().strip()
            yield line

            # Forge and FML change stderr output, causing MCServerChunkGenerator to wait endlessly.
            #
            #            Vanilla:
            #              2012-11-13 11:29:19 [INFO] Done (9.962s)!
            #
            #            Forge/FML:
            #              2012-11-13 11:47:13 [INFO] [Minecraft] Done (8.020s)!

            if "INFO" in line and "Done" in line:
                if simulate:
                    duration = time.time() - startTime

                    simSeconds = max(8, int(duration) + 1)

                    for i in xrange(simSeconds):
                        # process tile ticks
                        yield "%2d/%2d: Simulating the world for a little bit..." % (i, simSeconds)
                        time.sleep(1)

                proc.stdin.write("stop\n")
                proc.wait()
                break
            if "FAILED TO BIND" in line:
                proc.kill()
                proc.wait()
                raise RuntimeError("Server failed to bind to port!")

        stdout, _ = proc.communicate()

        if "Could not reserve enough space" in stdout and not MCServerChunkGenerator.lowMemory:
            MCServerChunkGenerator.lowMemory = True
            for i in self.generateAtPositionIter(tempWorld, tempDir, cx, cz):
                yield i

        (tempWorld.parentWorld or tempWorld).loadLevelDat()  # reload version number

    def copyChunkAtPosition(self, tempWorld, level, cx, cz):

        if level.containsChunk(cx, cz):
            return
        try:
            tempChunkBytes = tempWorld._getChunkBytes(cx, cz)
        except ChunkNotPresent as e:
            raise ChunkNotPresent("While generating a world in {0} using server {1} ({2!r})".format(tempWorld,
                                                                                                     self.serverJarFile,
                                                                                                     e), sys.exc_info()[
                2])
        if isinstance(level, PocketLeveldbWorld):
            level.saveGeneratedChunk(cx, cz, tempChunkBytes)
        else:
            level.worldFolder.saveChunk(cx, cz, tempChunkBytes)
        level._allChunks = None

    def generateChunkInLevel(self, level, cx, cz):
        assert isinstance(level, infiniteworld.MCInfdevOldLevel)

        tempWorld, tempDir = self.tempWorldForLevel(level)
        self.generateAtPosition(tempWorld, tempDir, cx, cz)
        self.copyChunkAtPosition(tempWorld, level, cx, cz)

    minRadius = 12
    maxRadius = 20

    def createLevel(self, level, box, simulate=False, **kw):
        return exhaust(self.createLevelIter(level, box, simulate, **kw))

    def createLevelIter(self, level, box, simulate=False, worldType="DEFAULT", **kw):
        if isinstance(level, basestring):
            filename = level
            level = infiniteworld.MCInfdevOldLevel(filename, create=True, **kw)

        assert isinstance(level, infiniteworld.MCInfdevOldLevel)
        minRadius = self.minRadius

        genPositions = list(itertools.product(
            xrange(box.mincx, box.maxcx, minRadius * 2),
            xrange(box.mincz, box.maxcz, minRadius * 2)))

        for i, (cx, cz) in enumerate(genPositions):
            log.info("Generating at %s" % ((cx, cz),))
            parentDir = dirname(os.path.abspath(level.worldFolder.filename))
            propsFile = join(parentDir, "server.properties")
            props = readProperties(join(dirname(self.serverJarFile), "server.properties"))
            props["level-name"] = basename(level.worldFolder.filename)
            props["server-port"] = int(32767 + random.random() * 32700)
            props["level-type"] = worldType
            saveProperties(propsFile, props)

            for p in self.generateAtPositionIter(level, parentDir, cx, cz, simulate):
                yield i, len(genPositions), p

        level.close()

    def generateChunksInLevel(self, level, chunks):
        return exhaust(self.generateChunksInLevelIter(level, chunks))

    def generateChunksInLevelIter_old(self, level, chunks, simulate=False):
        tempWorld, tempDir = self.tempWorldForLevel(level)

        startLength = len(chunks)
        minRadius = self.minRadius
        maxRadius = self.maxRadius
        chunks = set(chunks)

        while len(chunks):
            length = len(chunks)
            centercx, centercz = chunks.pop()
            chunks.add((centercx, centercz))
            # assume the generator always generates at least an 11x11 chunk square.
            centercx += minRadius
            centercz += minRadius

            # boxedChunks = [cPos for cPos in chunks if inBox(cPos)]

            print "Generating {0} chunks out of {1} starting from {2}".format("XXX", len(chunks), (centercx, centercz))
            yield startLength - len(chunks), startLength

            # chunks = [c for c in chunks if not inBox(c)]

            for p in self.generateAtPositionIter(tempWorld, tempDir, centercx, centercz, simulate):
                yield startLength - len(chunks), startLength, p

            i = 0
            for cx, cz in itertools.product(
                    xrange(centercx - maxRadius, centercx + maxRadius),
                    xrange(centercz - maxRadius, centercz + maxRadius)):
                if level.containsChunk(cx, cz):
                    chunks.discard((cx, cz))
                elif ((cx, cz) in chunks
                      and all(tempWorld.containsChunk(ncx, ncz) for ncx, ncz in
                              itertools.product(xrange(cx - 1, cx + 2), xrange(cz - 1, cz + 2)))
                ):
                    self.copyChunkAtPosition(tempWorld, level, cx, cz)
                    i += 1
                    chunks.discard((cx, cz))
                    yield startLength - len(chunks), startLength

            if length == len(chunks):
                print "No chunks were generated. Aborting."
                break

        level.saveInPlace()

    def generateChunksInLevelIter_new(self, level, chunks, simulate=False):
        # This chunk generator version can create an arbitrary amount of chunks.
        # It has been remarked that 'holes' appear in large zones (saw 'holes' when generated 250 and 350 chunks).
        # This version may also be faster than the 'old' one.
        tempWorld, tempDir = self.tempWorldForLevel(level)

        startLength = len(chunks)
        minRadius = self.minRadius
        maxRadius = self.maxRadius
        chunks = set(chunks)

        uncreated_chunks = []

        i = 0

        boxedChunks = [cPos for cPos in chunks if chunks]

        while len(chunks):
            length = len(chunks)
            centercx, centercz = chunks.pop()
            chunks.add((centercx, centercz))

            print "Generated {0} chunks out of {1} starting from {2}".format(startLength - len(chunks), startLength, (centercx, centercz))
            yield startLength - len(chunks), startLength, "Generating..."

            for p in self.generateAtPositionIter(tempWorld, tempDir, centercx, centercz, simulate):
                yield startLength - len(chunks), startLength, p

            yield i, startLength, "Adding chunks to world..."
            for cx, cz in itertools.product(
                    xrange(centercx - maxRadius, centercx + maxRadius),
                    xrange(centercz - maxRadius, centercz + maxRadius)):
                if level.containsChunk(cx, cz):
                    chunks.discard((cx, cz))
                    if (cx, cz) in uncreated_chunks:
                        uncreated_chunks.remove((cx, cz))
                elif ((cx, cz) in chunks
                      and tempWorld.containsChunk(cx, cz)):
                    try:
                        self.copyChunkAtPosition(tempWorld, level, cx, cz)
                        i += 1
                        chunks.discard((cx, cz))
                        if (cx, cz) in uncreated_chunks:
                            uncreated_chunks.remove((cx, cz))
                    except:
                        uncreated_chunks.append((cx, cz))
                else:
                    if (cx, cz) in boxedChunks and (cx, cz) not in uncreated_chunks:
                        log.info("adding (%s, %s) to uncreated_chunks"%(cx, cz))
                        uncreated_chunks.append((cx, cz))
            yield i, startLength, "Generating..."

        msg = ""
        if len(chunks) == startLength:
            msg = "No chunks were generated. Aborting."
        elif uncreated_chunks:
            msg = "Some chunks were not generated. Reselect them and retry."
        if msg:
            log.warning(msg)

#         print "len(uncreated_chunks)", len(uncreated_chunks)

        level.saveInPlace()

#     if __builtins__.get('mcenf_generateChunksInLevelIter', False):
#         log.info("Using new MCServerChunkGenerator.generateChunksInLevelIter")
#         generateChunksInLevelIter = generateChunksInLevelIter_new
#     else:
#         generateChunksInLevelIter = generateChunksInLevelIter_old
    generateChunksInLevelIter = generateChunksInLevelIter_new

    def runServer(self, startingDir):
        if isinstance(startingDir, unicode):
            startingDir = startingDir.encode(sys.getfilesystemencoding())

        return self._runServer(startingDir, self.serverJarFile)

    lowMemory = False

    @classmethod
    def _runServer(cls, startingDir, jarfile):
        log.info("Starting server %s in %s", jarfile, startingDir)
        if cls.lowMemory:
            memflags = []
        else:
            memflags = ["-Xmx1024M", "-Xms1024M", ]

        proc = subprocess.Popen([cls.java_exe, "-Djava.awt.headless=true"] + memflags + ["-jar", jarfile],
                                executable=cls.java_exe,
                                cwd=startingDir,
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT,
                                universal_newlines=True,
        )
        cls.processes.append(proc)
        return proc
    
    @classmethod
    def terminateProcesses(cls):
        for process in cls.processes:
            if process.poll() is None:
                try:
                    process.terminate()
                except:
                    pass

    def _serverVersion(self):
        return self._serverVersionFromJarFile(self.serverJarFile)

    @classmethod
    def _serverVersionFromJarFile(cls, jarfile):
        tempdir = tempfile.mkdtemp("mclevel_servergen")
        proc = cls._runServer(tempdir, jarfile)

        version = "Unknown"
        # out, err = proc.communicate()
        # for line in err.split("\n"):

        while proc.poll() is None:
            line = proc.stdout.readline()
            if "Preparing start region" in line:
                break
            if "Starting minecraft server version" in line:
                version = line.split("Starting minecraft server version")[1].strip()
                break

        if proc.returncode is None:
            try:
                proc.kill()
            except WindowsError:
                pass  # access denied, process already terminated

        proc.wait()
        shutil.rmtree(tempdir)
        if ";)" in version:
            version = version.replace(";)", "")  # Damnit, Jeb!
        # Versions like "0.2.1" are alphas, and versions like "1.0.0" without "Beta" are releases
        if version[0] == "0":
            version = "Alpha " + version
        elif 'w' in version or 'pre' in version:
            version = "Snapshot " + version
        else:
            try:
                if int(version[0]) > 0:
                    version = "Release " + version
            except ValueError:
                pass
        return version
