"""
Handles a Minecraft world save using either the Anvil or McRegion format.

For more information about the world format:
https://minecraft.gamepedia.com/Level_format
"""

import os, glob, re
from . import region
from . import chunk
from .region import InconceivedChunk, Location

class UnknownWorldFormat(Exception):
    """Unknown or invalid world folder."""
    def __init__(self, msg=""):
        self.msg = msg


class _BaseWorldFolder(object):
    """
    Abstract class, representing either a McRegion or Anvil world folder.
    This class will use either Anvil or McRegion, with Anvil the preferred format.
    Simply calling WorldFolder() will do this automatically.
    """
    type = "Generic"
    extension = ''
    chunkclass = chunk.Chunk

    def __init__(self, world_folder):
        """Initialize a WorldFolder."""
        self.worldfolder = world_folder
        self.regionfiles = {}
        self.regions     = {}
        self.chunks  = None
        # os.listdir triggers an OSError for non-existant directories or permission errors.
        # This is needed, because glob.glob silently returns no files.
        os.listdir(world_folder)
        self.set_regionfiles(self.get_filenames())

    def get_filenames(self):
        """Find all matching file names in the world folder.
        
        This method is private, and it's use it deprecated. Use get_regionfiles() instead."""
        # Warning: glob returns a empty list if the directory is unreadable, without raising an Exception
        return list(glob.glob(os.path.join(self.worldfolder,'region','r.*.*.'+self.extension)))

    def set_regionfiles(self, filenames):
        """
        This method directly sets the region files for this instance to use.
        It assumes the filenames are in the form r.<x-digit>.<z-digit>.<extension>
        """
        for filename in filenames:
            # Assume that filenames have the name r.<x-digit>.<z-digit>.<extension>
            m = re.match(r"r.(\-?\d+).(\-?\d+)."+self.extension, os.path.basename(filename))
            if m:
                x = int(m.group(1))
                z = int(m.group(2))
            else:
                # Only raised if a .mca of .mcr file exists which does not comply to the
                #  r.<x-digit>.<z-digit>.<extension> filename format. This may raise false
                # errors if a copy is made, e.g. "r.0.-1 copy.mca". If this is an issue, override
                # get_filenames(). In most cases, it is an error, and we like to raise that.
                # Changed, no longer raise error, because we want to continue the loop.
                # raise UnknownWorldFormat("Unrecognized filename format %s" % os.path.basename(filename))
                # TODO: log to stderr using logging facility.
                pass
            self.regionfiles[(x,z)] = filename

    def get_regionfiles(self):
        """Return a list of full path of all region files."""
        return list(self.regionfiles.values())

    def nonempty(self):
        """Return True is the world is non-empty."""
        return len(self.regionfiles) > 0

    def get_region(self, x,z):
        """Get a region using x,z coordinates of a region. Cache results."""
        if (x,z) not in self.regions or self.regions[x,z].closed:
            if (x,z) in self.regionfiles:
                self.regions[(x,z)] = region.RegionFile(self.regionfiles[(x,z)])
            else:
                # Return an empty RegionFile object
                # TODO: this does not yet allow for saving of the region file
                # TODO: this currently fails with a ValueError!
                # TODO: generate the correct name, and create the file
                # and add the fie to self.regionfiles
                self.regions[(x,z)] = region.RegionFile()
            self.regions[(x,z)].loc = Location(x=x,z=z)
        return self.regions[(x,z)]

    def iter_regions(self):
        """
        Return an iterable list of all region files. Use this function if you only
        want to loop through each region files once, and do not want to cache the results.
        """
        # TODO: Implement BoundingBox
        # TODO: Implement sort order
        for x,z in self.regionfiles.keys():
            close_after_use = False
            if (x,z) in self.regions:
                regionfile = self.regions[(x,z)]
            else:
                # It is not yet cached.
                # Get file, but do not cache later.
                regionfile = region.RegionFile(self.regionfiles[(x,z)], chunkclass = self.chunkclass)
                regionfile.loc = Location(x=x,z=z)
                close_after_use = True
            try:
                yield regionfile
            finally:
                if close_after_use:
                    regionfile.close()

    def call_for_each_region(self, callback_function, boundingbox=None):
        """
        Return an iterable that calls callback_function for each region file 
        in the world. This is equivalent to:
        ```
        for the_region in iter_regions():
                yield callback_function(the_region)
        ````
        
        This function is threaded. It uses pickle to pass values between threads.
        See [What can be pickled and unpickled?](https://docs.python.org/library/pickle.html#what-can-be-pickled-and-unpickled) in the Python documentation
        for limitation on the output of `callback_function()`.
        """
        raise NotImplementedError()

    def get_nbt(self,x,z):
        """
        Return a NBT specified by the chunk coordinates x,z. Raise InconceivedChunk
        if the NBT file is not yet generated. To get a Chunk object, use get_chunk.
        """
        rx,cx = divmod(x,32)
        rz,cz = divmod(z,32)
        if (rx,rz) not in self.regions and (rx,rz) not in self.regionfiles:
            raise InconceivedChunk("Chunk %s,%s is not present in world" % (x,z))
        nbt = self.get_region(rx,rz).get_nbt(cx,cz)
        assert nbt != None
        return nbt

    def set_nbt(self,x,z,nbt):
        """
        Set a chunk. Overrides the NBT if it already existed. If the NBT did not exists,
        adds it to the Regionfile. May create a new Regionfile if that did not exist yet.
        nbt must be a nbt.NBTFile instance, not a Chunk or regular TAG_Compound object.
        """
        raise NotImplementedError()
        # TODO: implement

    def iter_nbt(self):
        """
        Return an iterable list of all NBT. Use this function if you only
        want to loop through the chunks once, and don't need the block or data arrays.
        """
        # TODO: Implement BoundingBox
        # TODO: Implement sort order
        for region in self.iter_regions():
            for c in region.iter_chunks():
                yield c

    def call_for_each_nbt(self, callback_function, boundingbox=None):
        """
        Return an iterable that calls callback_function for each NBT structure 
        in the world. This is equivalent to:
        ```
        for the_nbt in iter_nbt():
                yield callback_function(the_nbt)
        ````
        
        This function is threaded. It uses pickle to pass values between threads.
        See [What can be pickled and unpickled?](https://docs.python.org/library/pickle.html#what-can-be-pickled-and-unpickled) in the Python documentation
        for limitation on the output of `callback_function()`.
        """
        raise NotImplementedError()

    def get_chunk(self,x,z):
        """
        Return a chunk specified by the chunk coordinates x,z. Raise InconceivedChunk
        if the chunk is not yet generated. To get the raw NBT data, use get_nbt.
        """
        return self.chunkclass(self.get_nbt(x, z))

    def get_chunks(self, boundingbox=None):
        """
        Return a list of all chunks. Use this function if you access the chunk
        list frequently and want to cache the result.
        Use iter_chunks() if you only want to loop through the chunks once or have a
        very large world.
        """
        if self.chunks == None:
            self.chunks = list(self.iter_chunks())
        return self.chunks

    def iter_chunks(self):
        """
        Return an iterable list of all chunks. Use this function if you only
        want to loop through the chunks once or have a very large world.
        Use get_chunks() if you access the chunk list frequently and want to cache
        the results. Use iter_nbt() if you are concerned about speed and don't want
        to parse the block data.
        """
        # TODO: Implement BoundingBox
        # TODO: Implement sort order
        for c in self.iter_nbt():
            yield self.chunkclass(c)

    def chunk_count(self):
        """Return a count of the chunks in this world folder."""
        c = 0
        for r in self.iter_regions():
            c += r.chunk_count()
        return c

    def get_boundingbox(self):
        """
        Return minimum and maximum x and z coordinates of the chunks that
        make up this world save
        """
        b = BoundingBox()
        for rx,rz in self.regionfiles.keys():
            region = self.get_region(rx,rz)
            rx,rz = 32*rx,32*rz
            for cc in region.get_chunk_coords():
                x,z = (rx+cc['x'],rz+cc['z'])
                b.expand(x,None,z)
        return b

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__,self.worldfolder)


class McRegionWorldFolder(_BaseWorldFolder):
    """Represents a world save using the old McRegion format."""
    type = "McRegion"
    extension = 'mcr'
    chunkclass = chunk.McRegionChunk


class AnvilWorldFolder(_BaseWorldFolder):
    """Represents a world save using the new Anvil format."""
    type = "Anvil"
    extension = 'mca'
    chunkclass = chunk.AnvilChunk


class _WorldFolderFactory(object):
    """Factory class: instantiate the subclassses in order, and the first instance 
    whose nonempty() method returns True is returned. If no nonempty() returns True,
    a UnknownWorldFormat exception is raised."""
    def __init__(self, subclasses):
        self.subclasses = subclasses
    def __call__(self, *args, **kwargs):
        for cls in self.subclasses:
            wf = cls(*args, **kwargs)
            if wf.nonempty(): # Check if the world is non-empty
                return wf
        raise UnknownWorldFormat("Empty world or unknown format")

WorldFolder = _WorldFolderFactory([AnvilWorldFolder, McRegionWorldFolder])
"""
Factory instance that returns a AnvilWorldFolder or McRegionWorldFolder
instance, or raise a UnknownWorldFormat.
"""



class BoundingBox(object):
    """A bounding box of x,y,z coordinates."""
    def __init__(self, minx=None, maxx=None, miny=None, maxy=None, minz=None, maxz=None):
        self.minx,self.maxx = minx, maxx
        self.miny,self.maxy = miny, maxy
        self.minz,self.maxz = minz, maxz
    def expand(self,x,y,z):
        """
        Expands the bounding
        """
        if x != None:
            if self.minx is None or x < self.minx:
                self.minx = x
            if self.maxx is None or x > self.maxx:
                self.maxx = x
        if y != None:
            if self.miny is None or y < self.miny:
                self.miny = y
            if self.maxy is None or y > self.maxy:
                self.maxy = y
        if z != None:
            if self.minz is None or z < self.minz:
                self.minz = z
            if self.maxz is None or z > self.maxz:
                self.maxz = z
    def lenx(self):
        if self.maxx is None or self.minx is None:
            return 0
        return self.maxx-self.minx+1
    def leny(self):
        if self.maxy is None or self.miny is None:
            return 0
        return self.maxy-self.miny+1
    def lenz(self):
        if self.maxz is None or self.minz is None:
            return 0
        return self.maxz-self.minz+1
    def __repr__(self):
        return "%s(%s,%s,%s,%s,%s,%s)" % (self.__class__.__name__,self.minx,self.maxx,
                self.miny,self.maxy,self.minz,self.maxz)
