#!/usr/bin/env python
import mclevel
import sys
import os
from box import BoundingBox
import numpy
from numpy import zeros, bincount
import logging
import itertools
import traceback
import shlex
import operator
import codecs

from math import floor
try:
    import readline
except:
    pass

class UsageError(RuntimeError): pass
class BlockMatchError(RuntimeError): pass
class PlayerNotFound(RuntimeError): pass

class mce(object):
    """
    Block commands:
       {commandPrefix}clone <sourceBox> <destPoint> [noair] [nowater]
       {commandPrefix}fill <blockType> [ <box> ]
       {commandPrefix}replace <blockType> [with] <newBlockType> [ <box> ]

       {commandPrefix}export <filename> <sourceBox>
       {commandPrefix}import <filename> <destPoint> [noair] [nowater]

       {commandPrefix}createChest <point> <item> [ <count> ]
       {commandPrefix}analyze

    Player commands:
       {commandPrefix}player [ <player> [ <point> ] ]
       {commandPrefix}spawn [ <point> ]

    Entity commands:
       {commandPrefix}removeEntities [ <EntityID> ]
       {commandPrefix}dumpSigns [ <filename> ]
       {commandPrefix}dumpChests [ <filename> ]

    Chunk commands:
       {commandPrefix}createChunks <box>
       {commandPrefix}deleteChunks <box>
       {commandPrefix}prune <box>
       {commandPrefix}relight [ <box> ]

    World commands:
       {commandPrefix}create <filename>
       {commandPrefix}degrief
       {commandPrefix}time [ <time> ]
       {commandPrefix}worldsize
       {commandPrefix}heightmap <filename>
       {commandPrefix}randomseed [ <seed> ]
       {commandPrefix}gametype [ <player> [ <gametype> ] ]

    Editor commands:
       {commandPrefix}save
       {commandPrefix}reload
       {commandPrefix}load <filename> | <world number>
       {commandPrefix}execute <filename>
       {commandPrefix}quit

    Informational:
       {commandPrefix}blocks [ <block name> | <block ID> ]
       {commandPrefix}help [ <command> ]

    **IMPORTANT**
       {commandPrefix}box

       Type 'box' to learn how to specify points and areas.

    """
    random_seed = os.getenv('MCE_RANDOM_SEED', None)
    last_played = os.getenv("MCE_LAST_PLAYED", None)
    def commandUsage(self, command):
        " returns usage info for the named command - just give the docstring for the handler func "
        func = getattr(self, "_" + command)
        return func.__doc__

    commands = [
        "clone",
        "fill",
        "replace",

        "export",
        "execute",
        "import",

        "createchest",

        "player",
        "spawn",

        "removeentities",
        "dumpsigns",
        "dumpchests",

        "createchunks",
        "deletechunks",
        "prune",
        "relight",

        "create",
        "degrief",
        "time",
        "worldsize",
        "heightmap",
        "randomseed",
        "gametype",

        "save",
        "load",
        "reload",
        "dimension",
        "repair",

        "quit",
        "exit",

        "help",
        "blocks",
        "analyze",
        "region",

        "debug",
        "log",
        "box",
    ]
    debug = False
    needsSave = False

    def readInt(self, command):
        try:
            val = int(command.pop(0))
        except ValueError:
            raise UsageError, "Cannot understand numeric input"
        return val

    def prettySplit(self, command):
        cmdstring = " ".join(command)

        lex = shlex.shlex(cmdstring)
        lex.whitespace_split = True
        lex.whitespace += "(),"

        command[:] = list(lex)

    def readBox(self, command):
        self.prettySplit(command)

        sourcePoint = self.readIntPoint(command)
        if command[0].lower() == "to":
            command.pop(0)
            sourcePoint2 = self.readIntPoint(command)
            sourceSize = map(operator.sub, sourcePoint2, sourcePoint)
        else:
            sourceSize = self.readIntPoint(command, isPoint=False)
        if len([p for p in sourceSize if p <= 0]):
            raise UsageError, "Box size cannot be zero or negative"
        box = BoundingBox(sourcePoint, sourceSize)
        return box

    def readIntPoint(self, command, isPoint=True):
        point = self.readPoint(command, isPoint)
        point = map(int, map(floor, point))
        return point

    def readPoint(self, command, isPoint=True):
        self.prettySplit(command)
        try:
            word = command.pop(0)
            if isPoint and (word in self.level.players):
                x, y, z = self.level.getPlayerPosition(word)
                if len(command) and command[0].lower() == "delta":
                    command.pop(0)
                    try:
                        x += int(command.pop(0))
                        y += int(command.pop(0))
                        z += int(command.pop(0))

                    except ValueError:
                        raise UsageError, "Error decoding point input (expected a number)."
                return x, y, z

        except IndexError:
            raise UsageError, "Error decoding point input (expected more values)."

        try:
            try:
                x = float(word)
            except ValueError:
                if isPoint:
                    raise PlayerNotFound, word
                raise
            y = float(command.pop(0))
            z = float(command.pop(0))
        except ValueError:
            raise UsageError, "Error decoding point input (expected a number)."
        except IndexError:
            raise UsageError, "Error decoding point input (expected more values)."

        return x, y, z

    def readBlockInfo(self, command):
        keyword = command.pop(0)

        matches = self.level.materials.blocksMatching(keyword)
        blockInfo = None

        if len(matches):
            if len(matches) == 1:
                blockInfo = matches[0]

            #eat up more words that possibly specify a block.  stop eating when 0 matching blocks.
            while len(command):
                newMatches = self.level.materials.blocksMatching(keyword + " " + command[0])

                if len(newMatches) == 1:
                    blockInfo = newMatches[0]
                if len(newMatches) > 0:
                    matches = newMatches
                    keyword = keyword + " " + command.pop(0)
                else:
                    break

        else:
            try:
                data = 0
                if ":" in keyword:
                    blockID, data = map(int, keyword.split(":"))
                else:
                    blockID = int(keyword)
                blockInfo = self.level.materials.blockWithID(blockID, data)

            except ValueError:
                blockInfo = None

        if blockInfo is None:
                print "Ambiguous block specifier: ", keyword
                if len(matches):
                    print "Matches: "
                    for m in matches:
                        if m == self.level.materials.defaultName: continue
                        print "{0:3}:{1:<2} : {2}".format(m.ID, m.blockData, m.name)
                else:
                    print "No blocks matched."
                raise BlockMatchError

        return blockInfo

    def readBlocksToCopy(self, command):
        blocksToCopy = range(256)
        while len(command):
            word = command.pop()
            if word == "noair":
                blocksToCopy.remove(0)
            if word == "nowater":
                blocksToCopy.remove(8)
                blocksToCopy.remove(9)

        return blocksToCopy

    def _box(self, command):
        """
        Boxes:

    Many commands require a <box> as arguments. A box can be specified with
    a point and a size:
        (12, 5, 15), (5, 5, 5)

    or with two points, making sure to put the keyword "to" between them:
        (12, 5, 15) to (17, 10, 20)

    The commas and parentheses are not important.
    You may add them for improved readability.

        Points:

    Points and sizes are triplets of numbers ordered X Y Z.
    X is position north-south, increasing southward.
    Y is position up-down, increasing upward.
    Z is position east-west, increasing westward.

        Players:

    A player's name can be used as a point - it will use the
    position of the player's head. Use the keyword 'delta' after
    the name to specify a point near the player.

    Example:
       codewarrior delta 0 5 0

    This refers to a point 5 blocks above codewarrior's head.

    """
        raise UsageError

    def _debug(self, command):
        self.debug = not self.debug
        print "Debug", ("disabled", "enabled")[self.debug]

    def _log(self, command):
        """
    log [ <number> ]

    Get or set the log threshold. 0 logs everything; 50 only logs major errors.
    """
        if len(command):
            try:
                logging.getLogger().level = int(command[0])
            except ValueError:
                raise UsageError, "Cannot understand numeric input."
        else:
            print "Log level: {0}".format(logging.getLogger().level)

    def _clone(self, command):
        """
    clone <sourceBox> <destPoint> [noair] [nowater]

    Clone blocks in a cuboid starting at sourcePoint and extending for
    sourceSize blocks in each direction. Blocks and entities in the area
    are cloned at destPoint.
    """
        if len(command) == 0:
            self.printUsage("clone")
            return

        box = self.readBox(command)

        destPoint = self.readPoint(command)

        destPoint = map(int, map(floor, destPoint))
        blocksToCopy = self.readBlocksToCopy(command)

        tempSchematic = self.level.extractSchematic(box)
        self.level.copyBlocksFrom(tempSchematic, BoundingBox((0, 0, 0), box.origin), destPoint, blocksToCopy)

        self.needsSave = True
        print "Cloned 0 blocks."

    def _fill(self, command):
        """
    fill <blockType> [ <box> ]

    Fill blocks with blockType in a cuboid starting at point and
    extending for size blocks in each direction. Without a
    destination, fills the whole world. blockType and may be a
    number from 0-255 or a name listed by the 'blocks' command.
    """
        if len(command) == 0:
            self.printUsage("fill")
            return

        blockInfo = self.readBlockInfo(command)

        if len(command):
            box = self.readBox(command)
        else:
            box = None

        print "Filling with {0}".format(blockInfo.name)

        self.level.fillBlocks(box, blockInfo)

        self.needsSave = True
        print "Filled {0} blocks.".format("all" if box is None else box.volume)

    def _replace(self, command):
        """
    replace <blockType> [with] <newBlockType> [ <box> ]

    Replace all blockType blocks with newBlockType in a cuboid
    starting at point and extending for size blocks in
    each direction. Without a destination, replaces blocks over
    the whole world. blockType and newBlockType may be numbers
    from 0-255 or names listed by the 'blocks' command.
    """
        if len(command) == 0:
            self.printUsage("replace")
            return

        blockInfo = self.readBlockInfo(command)

        if command[0].lower() == "with":
            command.pop(0)
        newBlockInfo = self.readBlockInfo(command)

        if len(command):
            box = self.readBox(command)
        else:
            box = None

        print "Replacing {0} with {1}".format(blockInfo.name, newBlockInfo.name)

        self.level.fillBlocks(box, newBlockInfo, blocksToReplace=[blockInfo])

        self.needsSave = True
        print "Done."

    def _createchest(self, command):
        """
    createChest <point> <item> [ <count> ]

    Create a chest filled with the specified item.
    Stacks are 64 if count is not given.
    """
        point = map(lambda x: int(floor(float(x))), self.readPoint(command))
        itemID = self.readInt(command)
        count = 64
        if len(command):
            count = self.readInt(command)

        chest = mclevel.MCSchematic.chestWithItemID(itemID, count)
        self.level.copyBlocksFrom(chest, chest.bounds, point)
        self.needsSave = True

    def _analyze(self, command):
        """
    analyze

    Counts all of the block types in every chunk of the world.
    Also updates the level's 'SizeOnDisk' field, correcting its size in the
    world select menu.
    """
        blockCounts = zeros((4096,), 'uint64')
        sizeOnDisk = 0

        print "Analyzing {0} chunks...".format(self.level.chunkCount)
        #for input to bincount, create an array of uint16s by
        #shifting the data left and adding the blocks

        for i, cPos in enumerate(self.level.allChunks, 1):
            ch = self.level.getChunk(*cPos)
            btypes = numpy.array(ch.Data.ravel(), dtype='uint16')
            btypes <<= 8
            btypes += ch.Blocks.ravel()
            counts = bincount(btypes)

            blockCounts[:counts.shape[0]] += counts
            sizeOnDisk += ch.compressedSize()
            ch.unload()
            if i % 100 == 0:
                logging.info("Chunk {0}...".format(i))

        for blockID in range(256):
            block = self.level.materials.blockWithID(blockID, 0)
            if block.hasVariants:
                for data in range(16):
                    i = (data << 8) + blockID
                    if blockCounts[i]:
                        idstring = "({id}:{data})".format(id=blockID, data=data)

                        print "{idstring:9} {name:30}: {count:<10}".format(
                            idstring=idstring, name=self.level.materials.blockWithID(blockID, data).name, count=blockCounts[i])

            else:
                count = int(sum(blockCounts[(d << 8) + blockID] for d in range(16)))
                if count:
                    idstring = "({id})".format(id=blockID)
                    print "{idstring:9} {name:30}: {count:<10}".format(
                          idstring=idstring, name=self.level.materials.blockWithID(blockID, 0).name, count=count)

        print "Size on disk: {0:.3}MB".format(sizeOnDisk / 1048576.0)
        self.level.SizeOnDisk = sizeOnDisk
        self.needsSave = True

    def _export(self, command):
        """
    export <filename> <sourceBox>

    Exports blocks in the specified region to a file in schematic format.
    This file can be imported with mce or MCEdit.
    """
        if len(command) == 0:
            self.printUsage("export")
            return

        filename = command.pop(0)
        box = self.readBox(command)

        tempSchematic = self.level.extractSchematic(box)

        tempSchematic.saveToFile(filename)

        print "Exported {0} blocks.".format(tempSchematic.bounds.volume)

    def _import(self, command):
        """
    import <filename> <destPoint> [noair] [nowater]

    Imports a level or schematic into this world, beginning at destPoint.
    Supported formats include
    - Alpha single or multiplayer world folder containing level.dat,
    - Zipfile containing Alpha world folder,
    - Classic single-player .mine,
    - Classic multiplayer server_level.dat,
    - Indev .mclevel
    - Schematic from RedstoneSim, MCEdit, mce
    - .inv from INVEdit (appears as a chest)
    """
        if len(command) == 0:
            self.printUsage("import")
            return

        filename = command.pop(0)
        destPoint = self.readPoint(command)
        blocksToCopy = self.readBlocksToCopy(command)

        importLevel = mclevel.fromFile(filename)

        destBox = BoundingBox(destPoint, importLevel.size)

        self.level.copyBlocksFrom(importLevel, importLevel.bounds, destPoint, blocksToCopy, create=True)

        self.needsSave = True
        print "Imported {0} blocks.".format(importLevel.bounds.volume)

    def _player(self, command):
        """
    player [ <player> [ <point> ] ]

    Move the named player to the specified point.
    Without a point, prints the named player's position.
    Without a player, prints all players and positions.

    In a single-player world, the player is named Player.
    """
        if len(command) == 0:
            print "Players: "
            for player in self.level.players:
                print "    {0}: {1}".format(player, self.level.getPlayerPosition(player))
            return

        player = command.pop(0)
        if len(command) == 0:
            print "Player {0}: {1}".format(player, self.level.getPlayerPosition(player))
            return

        point = self.readPoint(command)
        self.level.setPlayerPosition(point, player)

        self.needsSave = True
        print "Moved player {0} to {1}".format(player, point)

    def _spawn(self, command):
        """
    spawn [ <point> ]

    Move the world's spawn point.
    Without a point, prints the world's spawn point.
    """
        if len(command):
            point = self.readPoint(command)
            point = map(int, map(floor, point))

            self.level.setPlayerSpawnPosition(point)

            self.needsSave = True
            print "Moved spawn point to ", point
        else:
            print "Spawn point: ", self.level.playerSpawnPosition()

    def _dumpsigns(self, command):
        """
    dumpSigns [ <filename> ]

    Saves the text and location of every sign in the world to a text file.
    With no filename, saves signs to <worldname>.signs

    Output is newline-delimited. 5 lines per sign. Coordinates are
    on the first line, followed by four lines of sign text. For example:

        [229, 118, -15]
        "To boldy go
        where no man
        has gone
        before."

    Coordinates are ordered the same as point inputs:
        [North/South, Down/Up, East/West]

    """
        if len(command):
            filename = command[0]
        else:
            filename = self.level.displayName + ".signs"

        outFile = codecs.open(filename, "w", encoding='utf-8')

        print "Dumping signs..."
        signCount = 0

        for i, cPos in enumerate(self.level.allChunks):
            try:
                chunk = self.level.getChunk(*cPos)
            except mclevel.ChunkMalformed:
                continue

            for tileEntity in chunk.TileEntities:
                if tileEntity["id"].value == "Sign":
                    signCount += 1

                    outFile.write(str(map(lambda x:tileEntity[x].value, "xyz")) + "\n")
                    for i in range(4):
                        outFile.write(tileEntity["Text{0}".format(i + 1)].value + u"\n")

            if i % 100 == 0:
                print "Chunk {0}...".format(i)

            chunk.unload()

        print "Dumped {0} signs to {1}".format(signCount, filename)

        outFile.close()

    def _region(self, command):
        """
    region [rx rz]

    List region files in this world.
    """
        level = self.level
        assert(isinstance(level, mclevel.MCInfdevOldLevel))
        assert level.version
        def getFreeSectors(rf):
            runs = []
            start = None
            count = 0
            for i, free in enumerate(rf.freeSectors):
                if free:
                    if start is None:
                        start = i
                        count = 1
                    else:
                        count += 1
                else:
                    if start is None:
                        pass
                    else:
                        runs.append((start, count))
                        start = None
                        count = 0

            return runs

        def printFreeSectors(runs):

            for i, (start, count) in enumerate(runs):
                if i % 4 == 3: print ""
                print "{start:>6}+{count:<4}".format(**locals()),

            print ""

        if len(command):
            if len(command) > 1:
                rx,rz = map(int, command[:2])
                level.allChunks
                rf = level.regionFiles.get((rx,rz))
                if rf is None:
                    print "Region {rx},{rz} not found.".format(**locals())
                    return

                used = rf.usedSectors
                sectors = rf.sectorCount
                print "Region {rx:6}, {rz:6}: {used}/{sectors} sectors".format(**locals())
                print "Offset Table:"
                for cx in range(32):
                    for cz in range(32):
                        if cz % 4 == 0:
                            print ""
                            print "{0:3}, {1:3}: ".format(cx,cz),
                        off = rf.getOffset(cx,cz)
                        sector, length = off>>8, off&0xff
                        print "{sector:>6}+{length:<2} ".format(**locals()),
                    print ""

                runs = getFreeSectors(rf)
                if len(runs):
                    print "Free sectors:",

                    printFreeSectors(runs)

            else:
                if command[0] == "free":
                    level.allChunks
                    for (rx,rz), rf in level.regionFiles.iteritems():

                        runs = getFreeSectors(rf)
                        if len(runs):
                            print "R {0:3}, {1:3}:".format(rx,rz),
                            printFreeSectors(runs)

        else:
            level.allChunks
            coords = (r for r in level.regionFiles)
            for i, (rx, rz) in enumerate(coords):
                count = level.regionFiles[rx,rz].chunkCount
                print "({rx:6}, {rz:6}): {count}, ".format(**locals()),
                if i % 5 == 4: print ""

    def _repair(self, command):
        """
    repair

    Attempt to repair inconsistent region files.
    MAKE A BACKUP. WILL DELETE YOUR DATA.

    Scans for and repairs errors in region files:
        Deletes chunks whose sectors overlap with another chunk
        Rearranges chunks that are in the wrong slot in the offset table
        Deletes completely unreadable chunks

    Only usable with region-format saves.
    """
        if self.level.version:
            self.level.preloadRegions()
            for rf in self.level.regionFiles.itervalues():
                rf.repair()

    def _dumpchests(self, command):
        """
    dumpChests [ <filename> ]

    Saves the content and location of every chest in the world to a text file.
    With no filename, saves signs to <worldname>.chests

    Output is delimited by brackets and newlines. A set of coordinates in
    brackets begins a chest, followed by a line for each inventory slot.
    For example:

        [222, 51, 22]
        2 String
        3 String
        3 Iron bar

    Coordinates are ordered the same as point inputs:
        [North/South, Down/Up, East/West]

    """
        from items import items
        if len(command):
            filename = command[0]
        else:
            filename = self.level.displayName + ".chests"

        outFile = file(filename, "w")

        print "Dumping chests..."
        chestCount = 0

        for i, cPos in enumerate(self.level.allChunks):
            try:
                chunk = self.level.getChunk(*cPos)
            except mclevel.ChunkMalformed:
                continue

            for tileEntity in chunk.TileEntities:
                if tileEntity["id"].value == "Chest":
                    chestCount += 1

                    outFile.write(str(map(lambda x:tileEntity[x].value, "xyz")) + "\n")
                    itemsTag = tileEntity["Items"]
                    if len(itemsTag):
                        for itemTag in itemsTag:
                            try:
                                id = itemTag["id"].value
                                damage = itemTag["Damage"].value
                                item = items.findItem(id, damage)
                                itemname = item.name
                            except KeyError:
                                itemname = "Unknown Item {0}".format(itemTag)
                            except Exception, e:
                                itemname = repr(e)
                            outFile.write("{0} {1}\n".format(itemTag["Count"].value, itemname))
                    else:
                        outFile.write("Empty Chest\n")

            if i % 100 == 0:
                print "Chunk {0}...".format(i)

            chunk.unload()

        print "Dumped {0} chests to {1}".format(chestCount, filename)

        outFile.close()

    def _removeentities(self, command):
        """
    removeEntities [ [except] [ <EntityID> [ <EntityID> ... ] ] ]

    Remove all entities matching one or more entity IDs.
    With the except keyword, removes all entities not
    matching one or more entity IDs.

    Without any IDs, removes all entities in the world,
    except for Paintings.

    Known Mob Entity IDs:
        Mob Monster Creeper Skeleton Spider Giant
        Zombie Slime Pig Sheep Cow Chicken

    Known Item Entity IDs: Item Arrow Snowball Painting

    Known Vehicle Entity IDs: Minecart Boat

    Known Dynamic Tile Entity IDs: PrimedTnt FallingSand
    """
        removedEntities = {}

        if len(command):
            if command[0].lower() == "except":
                command.pop(0)
                print "Removing all entities except ", command
                def match(entityID):
                    return not (entityID.lower() in matchWords)
            else:
                print "Removing {0}...".format(", ".join(command))
                def match(entityID):
                    return entityID.lower() in matchWords

            matchWords = map(lambda x:x.lower(), command)

        else:
            print "Removing all entities except Painting..."
            def match(entityID): return entityID != "Painting";

        for cx, cz in self.level.allChunks:
            chunk = self.level.getChunk(cx, cz)
            entitiesRemoved = 0

            for entity in list(chunk.Entities):
                entityID = entity["id"].value

                if match(entityID):
                    removedEntities[entityID] = removedEntities.get(entityID, 0) + 1

                    chunk.Entities.remove(entity)
                    entitiesRemoved += 1

            if entitiesRemoved:
                chunk.chunkChanged(False)

            chunk.compress()
            chunk.save()
            chunk.unload()

        if len(removedEntities) == 0:
            print "No entities to remove."
        else:
            print "Removed entities:"
            for entityID in sorted(removedEntities.keys()):
                print "  {0}: {1:6}".format(entityID, removedEntities[entityID])

        self.needsSave = True

    def _createchunks(self, command):
        """
    createChunks <box>

    Creates any chunks not present in the specified region.
    New chunks are filled with only air. New chunks are written
    to disk immediately.
    """
        if len(command) == 0:
            self.printUsage("createchunks")
            return

        box = self.readBox(command)

        chunksCreated = self.level.createChunksInBox(box)

        print "Created {0} chunks." .format(len(chunksCreated))

        self.needsSave = True

    def _deletechunks(self, command):
        """
    deleteChunks <box>

    Removes all chunks contained in the specified region.
    Chunks are deleted from disk immediately.
    """
        if len(command) == 0:
            self.printUsage("deletechunks")
            return

        box = self.readBox(command)

        deletedChunks = self.level.deleteChunksInBox(box)

        print "Deleted {0} chunks." .format(len(deletedChunks))

    def _prune(self, command):
        """
    prune <box>

    Removes all chunks not contained in the specified region. Useful for enforcing a finite map size.
    Chunks are deleted from disk immediately.
    """
        if len(command) == 0:
            self.printUsage("prune")
            return

        box = self.readBox(command)

        i = 0
        for cx, cz in list(self.level.allChunks):
            if cx < box.mincx or cx >= box.maxcx or cz < box.mincz or cz >= box.maxcz:
                self.level.deleteChunk(cx, cz)
                i += 1

        print "Pruned {0} chunks." .format(i)

    def _relight(self, command):
        """
    relight [ <box> ]

    Recalculates lights in the region specified. If omitted,
    recalculates the entire world.
    """
        if len(command):
            box = self.readBox(command)
            chunks = itertools.product(range(box.mincx, box.maxcx), range(box.mincz, box.maxcz))

        else:
            chunks = self.level.allChunks

        self.level.generateLights(chunks)

        print "Relit 0 chunks."
        self.needsSave = True

    def _create(self, command):
        """
    create [ <filename> ]

    Create and load a new Minecraft Alpha world. This world will have no
    chunks and a random terrain seed. If run from the shell, filename is not
    needed because you already specified a filename earlier in the command.
    For example:

        mce.py MyWorld create

    """
        if len(command) < 1:
            raise UsageError, "Expected a filename"

        filename = command[0]
        if not os.path.exists(filename):
            os.mkdir(filename)

        if not os.path.isdir(filename):
            raise IOError, "{0} already exists".format(filename)

        if mclevel.MCInfdevOldLevel.isLevel(filename):
            raise IOError, "{0} is already a Minecraft Alpha world".format(filename)

        level = mclevel.MCInfdevOldLevel(filename, create=True)

        self.level = level

    def _degrief(self, command):
        """
    degrief [ <height> ]

    Reverse a few forms of griefing by removing
    Adminium, Obsidian, Fire, and Lava wherever
    they occur above the specified height.
    Without a height, uses height level 32.

    Removes natural surface lava.

    Also see removeEntities
    """
        box = self.level.bounds
        box.miny = 32
        if len(command):
            try:
                box.miny = int(command[0])
            except ValueError:
                pass

        print "Removing grief matter and surface lava above height {0}...".format(box.miny)

        self.level.fillBlocks(box,
                              self.level.materials.Air,
                              blocksToReplace=[self.level.materials.Bedrock,
                                self.level.materials.Obsidian,
                                self.level.materials.Fire,
                                self.level.materials.LavaActive,
                                self.level.materials.Lava,
                                ]
                              )
        self.needsSave = True

    def _time(self, command):
        """
    time [time of day]

    Set or display the time of day. Acceptable values are "morning", "noon",
    "evening", "midnight", or a time of day such as 8:02, 12:30 PM, or 16:45.
    """
        ticks = self.level.Time
        timeOfDay = ticks % 24000
        ageInTicks = ticks - timeOfDay
        if len(command) == 0:

            days = ageInTicks / 24000
            hours = timeOfDay / 1000
            clockHours = (hours + 6) % 24

            ampm = ("AM", "PM")[clockHours > 11]

            minutes = (timeOfDay % 1000) / 60

            print "It is {0}:{1:02} {2} on Day {3}".format(clockHours % 12 or 12, minutes, ampm, days)
        else:
            times = { "morning":6, "noon":12, "evening":18, "midnight":24 }
            word = command[0]
            minutes = 0

            if word in times:
                hours = times[word]
            else:
                try:
                    if ":" in word:
                        h, m = word.split(":")
                        hours = int(h)
                        minutes = int(m)
                    else:
                        hours = int(word)
                except Exception, e:
                    raise UsageError, ("Cannot interpret time, ", e)

                if len(command) > 1:
                    if command[1].lower() == "pm":
                        hours += 12

            ticks = ageInTicks + hours * 1000 + minutes * 1000 / 60 - 6000
            if ticks < 0: ticks += 18000

            ampm = ("AM", "PM")[hours > 11 and hours < 24]
            print "Changed time to {0}:{1:02} {2}".format(hours % 12 or 12, minutes, ampm)
            self.level.Time = ticks
            self.needsSave = True

    def _randomseed(self, command):
        """
    randomseed [ <seed> ]

    Set or display the world's random seed, a 64-bit integer that uniquely
    defines the world's terrain.
    """
        if len(command):
            try:
                seed = long(command[0])
            except ValueError:
                raise UsageError, "Expected a long integer."

            self.level.RandomSeed = seed
            self.needsSave = True
        else:
            print "Random Seed: ", self.level.RandomSeed

    def _gametype(self, command):
        """
    gametype [ <player> [ <gametype> ] ]

    Set or display the player's game type, an integer that identifies whether
    their game is survival (0) or creative (1).  On single-player worlds, the
    player is just 'Player'.
    """
        if len(command) == 0:
            print "Players: "
            for player in self.level.players:
                print "    {0}: {1}".format(player, self.level.getPlayerGameType(player))
            return

        player = command.pop(0)
        if len(command) == 0:
            print "Player {0}: {1}".format(player, self.level.getPlayerGameType(player))
            return

        try:
            gametype = int(command[0])
        except ValueError:
            raise UsageError, "Expected an integer."

        self.level.setPlayerGameType(gametype, player)
        self.needsSave = True

    def _worldsize(self, command):
        """
    worldsize

    Computes and prints the dimensions of the world.  For infinite worlds,
    also prints the most negative corner.
    """
        bounds = self.level.bounds
        if isinstance(self.level, mclevel.MCInfdevOldLevel):
            print "\nWorld size: \n  {0[0]:7} north to south\n  {0[2]:7} east to west\n".format(bounds.size)
            print "Smallest and largest points: ({0[0]},{0[2]}), ({1[0]},{1[2]})".format(bounds.origin, bounds.maximum)

        else:
            print "\nWorld size: \n  {0[0]:7} wide\n  {0[1]:7} tall\n  {0[2]:7} long\n".format(bounds.size)

    def _heightmap(self, command):
        """
    heightmap <filename>

    Takes a png and imports it as the terrain starting at chunk 0,0.
    Data is internally converted to greyscale and scaled to the maximum height.
    The game will fill the terrain with trees and mineral deposits the next
    time you play the level.

    Please please please try out a small test image before using a big source.
    Using the levels tool to get a good heightmap is an art, not a science.
    A smaller map lets you experiment and get it right before having to blow
    all night generating the really big map.

    Requires the PIL library.
    """
        if len(command) == 0:
            self.printUsage("heightmap")
            return

        if not sys.stdin.isatty() or raw_input(
     "This will destroy a large portion of the map and may take a long time.  Did you really want to do this?"
     ).lower() in ("yes", "y", "1", "true"):

            from PIL import Image
            import datetime

            filename = command.pop(0)

            imgobj = Image.open(filename)

            greyimg = imgobj.convert("L") #luminance
            del imgobj

            width, height = greyimg.size

            water_level = 64

            xchunks = (height + 15) / 16
            zchunks = (width + 15) / 16

            start = datetime.datetime.now()
            for cx in range(xchunks):
                for cz in range(zchunks):
                    try:
                        self.level.createChunk(cx, cz)
                    except:
                        pass
                    c = self.level.getChunk(cx, cz)

                    imgarray = numpy.asarray(greyimg.crop((cz * 16, cx * 16, cz * 16 + 16, cx * 16 + 16)))
                    imgarray = imgarray / 2 #scale to 0-127

                    for x in range(16):
                        for z in range(16):
                            if z + (cz * 16) < width - 1 and x + (cx * 16) < height - 1:
                                #world dimension X goes north-south
                                #first array axis goes up-down

                                h = imgarray[x, z]

                                c.Blocks[x, z, h + 1:] = 0 #air
                                c.Blocks[x, z, h:h + 1] = 2 #grass
                                c.Blocks[x, z, h - 4:h] = 3 #dirt
                                c.Blocks[x, z, :h - 4] = 1 #rock

                                if h < water_level:
                                    c.Blocks[x, z, h + 1:water_level] = 9 #water
                                if h < water_level + 2:
                                    c.Blocks[x, z, h - 2:h + 1] = 12 #sand if it's near water level

                                c.Blocks[x, z, 0] = 7 #bedrock

                    c.chunkChanged()
                    c.TerrainPopulated = False
                    #the quick lighting from chunkChanged has already lit this simple terrain completely
                    c.needsLighting = False

                    logging.info("%s Just did chunk %d,%d" % (datetime.datetime.now().strftime("[%H:%M:%S]"), cx, cz))

            logging.info("Done with mapping!")
            self.needsSave = True
            stop = datetime.datetime.now()
            logging.info("Took %s." % str(stop - start))

            spawnz = width / 2
            spawnx = height / 2
            spawny = greyimg.getpixel((spawnx, spawnz))
            logging.info("You probably want to change your spawn point. I suggest {0}".format((spawnx, spawny, spawnz)))

    def _execute(self, command):
        """
    execute <filename>
    Execute all commands in a file and save.
    """
        if len(command) == 0:
            print "You must give the file with commands to execute"
        else:
            commandFile = open(command[0], "r")
            commandsFromFile = commandFile.readlines()
            for commandFromFile in commandsFromFile:
                print commandFromFile
                self.processCommand(commandFromFile)
            self._save("")

    def _quit(self, command):
        """
    quit [ yes | no ]

    Quits the program.
    Without 'yes' or 'no', prompts to save before quitting.

    In batch mode, an end of file automatically saves the level.
    """
        if len(command) == 0 or not (command[0].lower() in ("yes", "no")):
            if raw_input("Save before exit? ").lower() in ("yes", "y", "1", "true"):
                self._save(command)
                raise SystemExit
        if len(command) and command[0].lower == "yes":
            self._save(command)

        raise SystemExit

    def _exit(self, command):
        self._quit(command)

    def _save(self, command):
        if self.needsSave:
            self.level.generateLights()
            self.level.saveInPlace()
            self.needsSave = False

    def _load(self, command):
        """
    load [ <filename> | <world number> ]

    Loads another world, discarding all changes to this world.
    """
        if len(command) == 0:
            self.printUsage("load")
        self.loadWorld(command[0])

    def _reload(self, command):
        self.level = mclevel.fromFile(self.level.filename)

    def _dimension(self, command):
        """
    dimension [ <dim> ]

    Load another dimension, a sub-world of this level. Without options, lists
    all of the dimensions found in this world. <dim> can be a number or one of
    these keywords:
        nether, hell, slip: DIM-1
        earth, overworld, parent: parent world
    """

        if len(command):
            if command[0].lower() in ("earth", "overworld", "parent"):
                if self.level.parentWorld:
                    self.level = self.level.parentWorld
                    return
                else:
                    print "You are already on earth."
                    return

            elif command[0].lower() in ("hell", "nether", "slip"):
                dimNo = -1
            else:
                dimNo = self.readInt(command)

            if dimNo in self.level.dimensions:
                self.level = self.level.dimensions[dimNo]
                return

        if self.level.parentWorld:
            print u"Parent world: {0} ('dimension parent' to return)".format(self.level.parentWorld.displayName)

        if len(self.level.dimensions):
            print u"Dimensions in {0}:".format(self.level.displayName)
            for k in self.level.dimensions:
                print "{0}: {1}".format(k, mclevel.MCAlphaDimension.dimensionNames.get(k, "Unknown"))

    def _help(self, command):
        if len(command):
            self.printUsage(command[0])
        else:
            self.printUsage()

    def _blocks(self, command):
        """
    blocks [ <block name> | <block ID> ]

    Prints block IDs matching the name, or the name matching the ID.
    With nothing, prints a list of all blocks.
    """

        searchName = None
        if len(command):
            searchName = " ".join(command)
            try:
                searchNumber = int(searchName)
            except ValueError:
                searchNumber = None
                matches = self.level.materials.blocksMatching(searchName)
            else:
                matches = [b for b in self.level.materials.allBlocks if b.ID == searchNumber]
#                print "{0:3}: {1}".format(searchNumber, self.level.materials.names[searchNumber])
 #               return

        else:
            matches = self.level.materials.allBlocks

        print "{id:9} : {name} {aka}".format(id="(ID:data)", name="Block name", aka="[Other names]")
        for b in sorted(matches):
            idstring = "({ID}:{data})".format(ID=b.ID, data=b.blockData)
            aka = b.aka and " [{aka}]".format(aka=b.aka) or ""

            print "{idstring:9} : {name} {aka}".format(idstring=idstring, name=b.name, aka=aka)

    def printUsage(self, command=""):
        if command.lower() in self.commands:
            print "Usage: ", self.commandUsage(command.lower())
        else:
            print self.__doc__.format(commandPrefix=("", "mce.py <world> ")[not self.batchMode])

    def printUsageAndQuit(self):
        self.printUsage()
        raise SystemExit

    def loadWorld(self, world):

        worldpath = os.path.expanduser(world)
        if os.path.exists(worldpath):
            self.level = mclevel.fromFile(worldpath)
        else:
            self.level = mclevel.loadWorld(world)

    level = None

    batchMode = False

    def run(self):
        logging.basicConfig(format=u'%(levelname)s:%(message)s')
        logging.getLogger().level = logging.INFO

        appPath = sys.argv.pop(0)

        if len(sys.argv):
            world = sys.argv.pop(0)

            if world.lower() in ("-h", "--help"):
                self.printUsageAndQuit()

            if len(sys.argv) and sys.argv[0].lower() == "create":
                #accept the syntax, "mce world3 create"
                self._create([world])
                print "Created world {0}".format(world)

                sys.exit(0)
            else:
                self.loadWorld(world)
        else:
            self.batchMode = True
            self.printUsage()

            while True:
                try:
                    world = raw_input("Please enter world name or path to world folder: ")
                    self.loadWorld(world)
                except EOFError, e:
                    print "End of input."
                    raise SystemExit
                except Exception, e:
                    print "Cannot open {0}: {1}".format(world, e)
                else:
                    break

        if len(sys.argv):
            #process one command from command line
            try:
                self.processCommand(" ".join(sys.argv))
            except UsageError:
                self.printUsageAndQuit()
            self._save([])

        else:
            #process many commands on standard input, maybe interactively
            command = [""]
            self.batchMode = True
            while True:
                try:
                    command = raw_input(u"{0}> ".format(self.level.displayName))
                    print
                    self.processCommand(command)

                except EOFError, e:
                    print "End of file. Saving automatically."
                    self._save([])
                    raise SystemExit
                except Exception, e:
                    if self.debug:
                        traceback.print_exc()
                    print 'Exception during command: {0!r}'.format(e)
                    print "Use 'debug' to enable tracebacks."

                    #self.printUsage();

    def processCommand(self, command):
        command = command.strip()

        if len(command) == 0: return

        if command[0] == "#": return

        commandWords = command.split()

        keyword = commandWords.pop(0).lower()
        if not keyword in self.commands:
            matches = filter(lambda x:x.startswith(keyword), self.commands)
            if len(matches) == 1:
                keyword = matches[0]
            elif len(matches):
                print "Ambiguous command. Matches: "
                for k in matches:
                    print "  ", k
                return
            else:
                raise UsageError, "Command {0} not recognized.".format(keyword)

        func = getattr(self, "_" + keyword)

        try:
            func(commandWords)
        except PlayerNotFound, e:
            print "Cannot find player {0}".format(e.args[0])
            self._player([])

        except UsageError, e:
            print e
            if self.debug:
                traceback.print_exc()
            self.printUsage(keyword)

def main(argv):
    profile = os.getenv("MCE_PROFILE", None)
    editor = mce()
    if profile:
        print "Profiling enabled"
        import cProfile
        cProfile.runctx('editor.run()', locals(), globals(), profile)
    else:
        editor.run()

    return 0

if __name__ == '__main__':
    sys.exit(main(sys.argv))


'''
---------------------------
 Licensing and Distribution
---------------------------

Program name: Pilgrim
Version     : 2021.5
License     : MIT/x11

Copyright (c) 2021, David Ferro Costas (david.ferro@usc.es) and
Antonio Fernandez Ramos (qf.ramos@usc.es)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
---------------------------

*----------------------------------*
| Module     :  common             |
| Sub-module :  MolGraph           |
| Last Update:  2020/05/03 (Y/M/D) |
| Main Author:  David Ferro-Costas |
*----------------------------------*

This module contains the MolGraph class
'''

#=============================================#
import numpy as np
#---------------------------------------------#
import common.fncs     as     fncs
import common.internal as     intl
from   common.Ugraph   import UGRAPH
#=============================================#

class MolGraph(UGRAPH):

      def __init__(self,xcc,symbols,cscal=1.3,nfrags=1,epslin=4.5):
          # initialize data for UGRAPH
          self._ugdict = {}
          self._nnodes = 0
          self._nedges = 0
          self._cnumber= 0
          # calculate connectivity matrix
          cmatrix = intl.get_adjmatrix(xcc,symbols,cscal,mode="int")[0]
          # autoconnect
          cmatrix = intl.link_fragments(xcc,cmatrix,nfrags)[0]
          # save cmatrix
          cmatrix = np.matrix(cmatrix)
          # set graph
          self.set_from_amatrix(cmatrix)
          # detect atoms in cycles
          nodes_cycles = self.nodes_in_cycles()
          # save data
          self._xcc     = [xi for xi in xcc]
          self._symbols = symbols
          self._incycle = nodes_cycles
          self._cmatrix = cmatrix
          self._epslin  = epslin
          self._cscal   = cscal
          self._angles  = {}

          # see if dummy is required by checking all atoms with two connections (B-A-C)
          dummies = []
          nodeX   = int(self._nnodes) - 1
          for nodeA,neighbors in self._ugdict.items():
              if len(neighbors) != 2: continue
              nodeB,nodeC = neighbors
              # add dummy atom??
              if self.islinear( (nodeB,nodeA,nodeC) ):
                 xB = self._xcc[3*nodeB:3*nodeB+3]
                 xA = self._xcc[3*nodeA:3*nodeA+3]
                 # coordinates of dummy atom
                 xX = intl.zmat_thirdatom(xB,xA,dist=1.0,angle=np.pi/2)
                 # save data
                 nodeX += 1
                 dummies.append( (nodeA,nodeX,xX) )

          #---------------------#
          # Include dummy atoms #
          #---------------------#
          nn = int(self._nnodes)
          nd = len(dummies)
          if nd > 0:
             # increase cmatrix
             zerocols      = np.zeros( (nn,nd   ) , dtype=int)
             zerorows      = np.zeros( (nd,nn+nd) , dtype=int)
             self._cmatrix = np.hstack( (self._cmatrix,zerocols) )
             self._cmatrix = np.vstack( (self._cmatrix,zerorows) )
             # Add dummy atoms!!
             for nodeA,nodeX,xX in dummies:
                 # add connection in graph
                 self.add_node(nodeX)
                 self.add_edge(nodeX,nodeA)
                 # add connection in cmatrix
                 self._cmatrix[nodeA,nodeX] = 1
                 self._cmatrix[nodeX,nodeA] = 1
                 # add dummy to symbols and xcc
                 self._symbols.append("X")
                 self._xcc += [xi for xi in xX]

      def __str__(self):
          return "(n,e)=(%i,%i)"%(self._nnodes,self._nedges)

      def islinear(self,triad):
          if triad not in self._angles:
             nodeA,nodeB,nodeC = triad
             xA = self._xcc[3*nodeA:3*nodeA+3]
             xB = self._xcc[3*nodeB:3*nodeB+3]
             xC = self._xcc[3*nodeC:3*nodeC+3]
             angABC = fncs.angle(xA,xB,xC)
             # save angles
             self._angles[(nodeA,nodeB,nodeC)] = angABC
             self._angles[(nodeC,nodeB,nodeA)] = angABC
          else: angABC = self._angles[triad]
          # return boolean (True if linear)
          return abs(180-np.rad2deg(angABC)) < self._epslin

      def nonlinearpath(self,start_idx,visited=[],ppoint=None):
          '''
          Get longest path without three connected atoms in a line
          To do so, it always choose the dummy atom (if not visited yet)
          '''
          # Get neighbors, excluding previously visited ones
          neighbors = [node for node in self._ugdict[start_idx] if node not in visited]

          if len(neighbors) == 0: return [start_idx]

          # see if any neighbor is X (dummy atom), it must be the end of path
          symbol_neighbors = [self._symbols[node] for node in neighbors]
          if symbol_neighbors.count("X") != 0:
             nodeX = neighbors[symbol_neighbors.index("X")]
             return [start_idx,nodeX]

          # Get longest from non-visited neighbors
          length = - float("inf")
          for neighbor in neighbors:
              # assert path does not contain linear angles
              if ppoint is not None and self.islinear( (ppoint,start_idx,neighbor) ): continue
              # now we now it is not 180, we can continue
              visited_i = visited + [start_idx,neighbor]
              path_i    = self.nonlinearpath(neighbor,visited=visited_i,ppoint=start_idx)
              symbol_path = [self._symbols[node] for node in path_i]
              ppath = [start_idx]+path_i
              if len(path_i) > length:
                 length = len(path_i)
                 the_path = path_i
          return [start_idx] + the_path

      def construct_zmatrix(self,seed=None,visited=set([]),previous=None):
          '''
          Returns $zmatrix and $visited
             * $zmatrix --> a list of tuples
             * $visited --> a list of visited nodes

          Tuples in $zmatrix contains the following elements
                 (at1,at2,at3,at4,boolean1,boolean2)
          where:
             * at1 to at4 are node indices
             * boolean1 is True is the tuple corresponds to a PROPER torsion

          '''
          #---------------------------------------------------------------------------------#
          # For some reason, Python messes when the following line is not included          #
          # If ommited and construct_zmatrix is inside a loop to analize different graphs,  #
          # the $visited variable DO NOT initialize when calling it as:                     #
          #          construct_zmatrix(graph)                                               #
          # I guess this is a Python bug or something....                                   #
          #---------------------------------------------------------------------------------#
          visited = set(visited) # for some reason, this is required
          #---------------------------------------------------------------------------------#

          zmatrix = []
          # find longest path
          if seed is None:
             for node in range(self._nnodes):
                 if len(self.neighbors(node)) == 1: break
             # DFS to find one end point of longest path
             path = self.nonlinearpath(node)
             # DFS to find the actual longest path
             path = self.nonlinearpath(path[-1])
          else:
             path = self.nonlinearpath(seed,list(visited))

    ###########################
    #     for idx in range(1,len(path)-1):
    #         triad = (idx-1,idx,idx+1)
    #         if self.islinear( triad ): print("*linear:",triad)
    ###########################

          tovisit = set([])
          links = {}
          zmatrix2 = []
          added_as_itor = set([])
          for idx,atom in enumerate(path):
              if   idx == 0: atoms = ( atom,   -1       ,  -1        ,  -1         ,False)
              elif idx == 1: atoms = ( atom, path[0]    ,  -1        ,  -1         ,False)
              elif idx == 2: atoms = ( atom, path[1]    , path[0]    ,  -1         ,False)
              else         : atoms = ( atom, path[idx-1], path[idx-2], path[idx-3] ,True )
              zmatrix.append(atoms)
              visited.add(atom)
              # link rest of bonded atoms to path
              for vecino in self.neighbors(atom):
                  if vecino in path         : continue
                  if vecino in added_as_itor: continue
                  if vecino in visited      : continue
                  if idx == 0:
                     # select node in previous such as angle != 180
                     for prev_i in previous:
                         if not self.islinear( (vecino,atom,prev_i) ): break
                     atoms = (vecino,atom,prev_i     ,path[idx+1],False)
                  else:
                     if self.islinear( (vecino,atom,path[idx-1]) ):
                        atoms = (vecino,atom,path[idx+1],path[idx-1],False)
                     else:
                        atoms = (vecino,atom,path[idx-1],path[idx+1],False)
                  zmatrix2.append( atoms )
                  tovisit.add(vecino)
                  added_as_itor.add(vecino)
                  # save link (if X, save X)
                  link1 = path[idx-1]
                  link2 = path[idx-1]
                  if idx+1 != len(path): link2 = path[idx+1]
                  if self._symbols[link2] == "X": links[vecino] = (atom,link2,link1)
                  else                          : links[vecino] = (atom,link1,link2)

          zmatrix += zmatrix2
          # now work with neighbors
          while len(tovisit) != 0:
              node3 = tovisit.pop()
              # update visited
              visited.add(node3)
              # recurrence!!
              zmatrix_i, visited_i = self.construct_zmatrix(node3,visited,links[node3])
              # update visited and tovisit
              visited = visited.union(visited_i)
              tovisit = tovisit.difference(visited)
              # update zmatrix with torsions in ramifications
              for torsion in zmatrix_i:
                  numnegs = torsion.count(-1)
                  if   numnegs == 3: continue
                  # requires connection to main path
                  elif numnegs == 2:
                       atom,link1,link2 = links[node3]
                       triad1 = (node3,atom,link1)
                       triad2 = (node3,atom,link2)
                       if self.islinear( triad1 ):
                          torsion = [torsion[0],node3,atom,link2,True]
                       else:
                          torsion = [torsion[0],node3,atom,link1,True]
                  # requires connection to main path
                  elif numnegs == 1: torsion = torsion[0:3] + links[node3][0:1]+(True,)
                  zmatrix.append(torsion)
          return zmatrix, visited


import math

import bpy
import numpy as np

from .abstract_entity_handlers import get_origin, get_angles, get_scale, Base
from .sbox_entity_classes import *
from .base_entity_handlers import BaseEntityHandler
from .hlvr_entity_handlers import HLVREntityHandler

local_entity_lookup_table = HLVREntityHandler.entity_lookup_table.copy()
local_entity_lookup_table.update(entity_class_handle)

class SteamPalEntityHandler(HLVREntityHandler):
    entity_lookup_table = local_entity_lookup_table
    entity_lookup_table['steampal_paintable_prop'] = Base
    entity_lookup_table['steampal_toaster'] = Base
    entity_lookup_table['steampal_camera_path_node'] = Base
    entity_lookup_table['steampal_camera_path'] = Base
    entity_lookup_table['steampal_kill_volume'] = Base
    entity_lookup_table['steampal_name_form'] = Base
    entity_lookup_table['steampal_touchtarget'] = Base
    entity_lookup_table['steampal_conveyor_path_node'] = Base
    entity_lookup_table['steampal_conveyor_entity_spawner'] = Base
    entity_lookup_table['steampal_conveyor'] = Base
    entity_lookup_table['steampal_picturecard'] = Base
    entity_lookup_table['npc_appliance'] = Base
    entity_lookup_table['path_particle_rope'] = Base
    entity_lookup_table['light_rect'] = Base
    entity_lookup_table['light_barn'] = Base
    entity_lookup_table['light_omni2'] = Base

    def handle_steampal_paintable_prop(self, entity: object, entity_raw: dict):
        obj = self._handle_entity_with_model(entity, entity_raw)
        self._put_into_collection(entity.__class__.__name__, obj, 'props')

    def handle_steampal_toaster(self, entity: object, entity_raw: dict):
        obj = self._handle_entity_with_model(entity, entity_raw)
        self._put_into_collection(entity.__class__.__name__, obj, 'appliance')

    def handle_steampal_touchtarget(self, entity: object, entity_raw: dict):
        obj = self._handle_entity_with_model(entity, entity_raw)
        self._put_into_collection(entity.__class__.__name__, obj, 'props')

    def handle_steampal_picturecard(self, entity: object, entity_raw: dict):
        obj = self._handle_entity_with_model(entity, entity_raw)
        self._put_into_collection(entity.__class__.__name__, obj, 'props')

    def handle_trigger_physics(self, entity: object, entity_raw: dict):
        obj = self._handle_entity_with_model(entity, entity_raw)
        self._put_into_collection(entity.__class__.__name__, obj, 'triggers')

    # def handle_npc_appliance(self, entity: object, entity_raw: dict):
    #     return
    #
    # def handle_path_particle_rope(self, entity: object, entity_raw: dict):
    #     return
    #
    # def handle_steampal_camera_path_node(self, entity: object, entity_raw: dict):
    #     return
    #
    # def handle_steampal_conveyor(self, entity: object, entity_raw: dict):
    #     return
    #
    # def handle_steampal_picturecard(self, entity: object, entity_raw: dict):
    #     return
    #
    # def handle_steampal_camera_path(self, entity: object, entity_raw: dict):
    #     return
    #
    # def handle_steampal_name_form(self, entity: object, entity_raw: dict):
    #     return
    #
    # def handle_steampal_conveyor_entity_spawner(self, entity: object, entity_raw: dict):
    #     return
    #
    # def handle_steampal_conveyor_path_node(self, entity: object, entity_raw: dict):
    #     return
    #
    # def handle_path_track(self, entity: object, entity_raw: dict):
    #     return
    #
    # def handle_light_rect(self, entity: object, entity_raw: dict):
    #     # TODO:
    #     return
    #
    # def handle_light_barn(self, entity: object, entity_raw: dict):
    #     # TODO:
    #     return
    #
    # def handle_light_omni2(self, entity: object, entity_raw: dict):
    #     # TODO:
    #     return
    #
    # def handle_steampal_kill_volume(self, entity: object, entity_raw: dict):
    #     # TODO:
    #     return


#!/usr/bin/env python
import cv2
import math
import numpy as np
from skimage.segmentation import slic
from skimage.segmentation import mark_boundaries
from skimage import img_as_float
# Custom class
import constant as c
from constant import Color
from preprocess import norm_illum_color
from camera import rect_to_distance, circle_to_distance
from config import Config
from vision.msg import DetectedObject
from core import to_hsv, to_lab, to_bgr
from enhancement import finlaynorm, iace, get_salient, shadegrey, chromanorm

# Red, Orange, Green to calculate hue distance
HUE_RANGE = [0, 15, 60]

# Red to Green range in HSV color space
RED_GREEN = [(0, 0, 0), (80, 255, 255)]

# Area in pixels bound for each object
AREA = {'bin': [1200, 10000], 'cover': [1300, 30000], 'horizontal_coin': [50, 5000],
        'vertical_coin': [50, 5000], 'red_coin': [50, 5000], 'green_coin': [50, 5000], 'xmark': [2000, 20000]}

# Maximum number of objects present in the competition
MAX_OBJ = {'coin': 2, 'xmark': 2, 'bin': 2}

# Color range for each object
COLOR_OBJ = {'cover': (20, 128, 128, 128), 'bin': (0, 0, 0, 0),
             'red_coin': (0, 40, 255, 255), 'green_coin': (60, 40, 0, 255),
             'red_xmark': (0, 60, 255, 255), 'green_xmark': (40, 219, 82, 255)}

''' Main objects to be detected for Robosub 2016 '''

def generic(img, data):
    mask = cv2.adaptiveThreshold(cv2.split(to_lab(img))[1], 255, cv2.ADAPTIVE_THRESH_MEAN_C,
                                 cv2.THRESH_BINARY, 401, -20)
    return to_bgr(mask, 'gray'), [DetectedObject()]

def bin(img, data):
    thresh_limit = get_config(data)['salient_thresh']
    # mask = thresh_saturation_binary(img, thresh_limit) | thresh_salient(img, thresh_limit)
    # mask = cv2.bitwise_not(adaptive_thresh(cv2.split(img))[1])
    img = norm_illum_color(img, 0.5)
    mask = thresh_bin(img, thresh_limit)
    '''
    kern = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    mask = cv2.erode(mask, kern, iterations=1)
    mask = cv2.dilate(mask, kern, iterations=2)
    '''
    # return to_bgr(mask, 'gray'), [DetectedObject(detected=True)]
    return analyze_bin(img, mask, data)

def cover(img, data):
    thresh_limit = get_config(data)['salient_thresh']
    img = iace(finlaynorm(img))
    mask = thresh_salient(img, 40)
    return analyze_cover(img, mask, data)

def overall_bin(img, data):
    thresh_limit = get_config(data)['salient_thresh']
    img = norm_illum_color(img, 0.5)
    mask = thresh_bin(img, thresh_limit)
    return analyze_overall_bin(img, mask, data)

def vertical_coin(img, data):
    '''
    thresh_limit = get_config(data)['salient_thresh']
    mask = thresh_salient(img, 20)
    mask = adaptive_thresh(cv2.split(to_lab(img))[1])
    mask = adaptive_thresh(cv2.split(to_hsv(img))[2])
    mask = thresh_hsv(img, (60, 0, 100), (100, 255, 254))
    '''
    mask = thresh_red(img)
    # return to_bgr(mask, 'gray'), [DetectedObject(detected=True)]
    return analyze_vertical_coin(img, mask, data)

def horizontal_coin(img, data):
    '''
    thresh_limit = get_config(data)['salient_thresh']
    mask = thresh_salient(img, 50)
    mask = adaptive_thresh(cv2.split(to_hsv(img))[2])
    '''
    mask = thresh_green(img)
    # return to_bgr(mask, 'gray'), [DetectedObject(detected=True)]
    return analyze_vertical_coin(img, mask, data)

def red_coin(img, data):
    mask = thresh_red(img)
    # return to_bgr(mask, 'gray'), [DetectedObject(detected=True)]
    return analyze_vertical_coin(img, mask, data, 'red_coin', 1)

def green_coin(img, data):
    mask = thresh_green(img)
    # return to_bgr(mask, 'gray'), [DetectedObject(detected=True)]
    return analyze_vertical_coin(img, mask, data, 'green_coin', 1)

def tower(img, data):
    '''
    thresh_limit = get_config(data)['salient_thresh']
    mask = thresh_salient(img, thresh_limit)
    mask = adaptive_thresh(cv2.split(to_hsv(img))[2])
    '''
    mask = thresh_red(img) | thresh_green(img)
    # return to_bgr(mask, 'gray'), [DetectedObject(detected=True)]
    return analyze_tower(img, mask, data)

def xmark(img, data):
    '''
    mask = cv2.bitwise_not(thresh_table(img))
    thresh_limit = get_config(data)['salient_thresh']
    mask = thresh_salient(img, thresh_limit)
    mask = thresh_saturation(img)
    '''
    mask = thresh_table2(img)
    # return to_bgr(mask, 'gray'), [DetectedObject(detected=True)]
    return analyze_xmark(img, mask, data)

def red_x(img, data):
    mask = cv2.inRange(to_lab(img), (0, 140, 125), (255, 255, 255))
    return to_bgr(mask, 'gray'), [DetectedObject(detected=True)]
    return analyze_table(img, mask, data)

def green_x(img, data):
    mask = cv2.inRange(to_lab(img), (20, 0, 100), (255, 125, 255))
    return to_bgr(mask, 'gray'), [DetectedObject(detected=True)]
    return analyze_table(img, mask, data)

def table(img, data):
    mask = cv2.bitwise_not(thresh_table(img))
    return analyze_table(img, mask, data)

''' Sub objects '''

def black_bin(img, gamma=0.5, w_diff=3.5):
    """ Detect black square @feature.getBlack2
        gamma   < 1 turns img darker > 1 turns img lighter
        w_diff  weightage of difference between darkest and lightest pixel for thresholding
        returns thresholded image
    """
    # Normalizes illumination
    h, s, v = cv2.split(to_hsv(img))
    v = norm_illum_color(v, gamma)

    # Thresholding and morphological transformation
    max_pix = np.amax(v)
    min_pix = np.amin(v)
    diff = max_pix - min_pix
    thresh = min_pix + diff / w_diff
    mask = cv2.threshold(v, thresh, 255, cv2.THRESH_BINARY_INV)[1]
    return mask

''' Shape validation '''

def is_circle(cnt, circle_limit=0.6):
    return get_circularity(cnt) >= circle_limit

def is_horizontal_coin(cnt, rectangularity_limit=0.5, ratio_limit=(1.5, 7)):
    """ Checks if rectangle with certain ratio """
    rect = cv2.minAreaRect(cnt)
    if rect[1][0] > 0 and rect[1][1] > 0:
        if rect[1][0] > rect[1][1]:
            asp_rat = rect[1][0] / float(rect[1][1])
        else:
            asp_rat = rect[1][1] / float(rect[1][0])
        rect_area = rect[1][0] * rect[1][1]
        rectanglularity = (cv2.contourArea(cnt) + 0.001) / rect_area
        return rectanglularity > rectangularity_limit and ratio_limit[0] < asp_rat < ratio_limit[1]
    return False

def get_rectangularity(cnt):
    return cv2.contourArea(cnt) / (get_rect_area(cnt) + 0.001)

def get_circularity(cnt):
    return cv2.contourArea(cnt) / (get_circle_area(cnt) + 0.001)

def is_rect(cnt, ratio_limit=0.6):
    """ Checks if rectangle with certain ratio """
    rect = cv2.minAreaRect(cnt)
    if rect[1][0] > 0 and rect[1][1] > 0:
        if rect[1][0] > rect[1][1]:
            asp_rat = rect[1][0] / float(rect[1][1])
        else:
            asp_rat = rect[1][1] / float(rect[1][0])
        return get_rectangularity(cnt) > ratio_limit and asp_rat < 3
    return False

def is_correct_area(area, name):
    return AREA[name][0] <= area <= AREA[name][1]

''' Contour processing '''

def analyze_horizontal_coin(img, mask, data):
    infos = []
    out_img = init_debug_img(img)
    # out_img = to_bgr(mask, 'gray')
    # Contour processing
    cnts = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[1]
    cnts.sort(key=cv2.contourArea, reverse=True)
    if len(cnts) >= 1:
        for cnt in cnts:
            if is_correct_area(get_rect_area(cnt), 'horizontal_coin') and is_horizontal_coin(cnt):
                info = init_detected_object('horizontal_coin', data, img, cnt)
                infos.append(info)
                # Debug img
                draw_detected_object(out_img, info, cnt, len(infos))
            if len(infos) >= MAX_OBJ['coin']:
                break
    draw_text_info(out_img, format_info(infos))
    return out_img, infos

def analyze_vertical_coin(img, mask, data, name='vertical_coin', limit=MAX_OBJ['coin']):
    infos = []
    out_img = init_debug_img(img)
    out_img = to_bgr(mask, 'gray')
    # Contour processing
    cnts = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[1]
    cnts.sort(key=cv2.contourArea, reverse=True)
    if len(cnts) >= 1:
        for cnt in cnts:
            if is_correct_area(get_circle_area(cnt), name) and is_circle(cnt):
                info = init_detected_object(name, data, img, cnt)
                infos.append(info)
                # Debug img
                draw_detected_object(out_img, info, cnt, len(infos))
            if len(infos) >= limit:
                break
    draw_text_info(out_img, format_info(infos))
    return out_img, infos

def analyze_tower(img, mask, data):
    """ Detects tower using horizontal coins """
    out_img, infos = analyze_horizontal_coin(img, mask, data)
    if infos:
        tower_info = init_detected_object('tower', data)
        tower_info.centroid = [np.mean([i.centroid[0] for i in infos]),
                               np.mean([i.centroid[1] for i in infos])]
        tower_info.offset = get_offset(tower_info.centroid)
        tower_info.detected = True
        tower_info.angle = infos[0].angle
        draw_centroid(out_img, tower_info.centroid)
        infos.insert(0, tower_info)
    return out_img, infos

def analyze_xmark(img, mask, data):
    infos = []
    contours = []
    # Contour processing
    out_img = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
    out_img = init_debug_img(img)
    cnts, hierrs = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[1:]
    cnts.sort(key=cv2.contourArea)
    if len(cnts) >= 1:
        for cnt, hierr in zip(cnts, hierrs[0]):
            if is_correct_area(get_circle_area(cnt), 'xmark') and is_circle(cnt):
                info = init_detected_object('xmark', data, img, cnt)
                if not point_in_contours(info.centroid, contours):
                    infos.append(info)
                    contours.append(cnt)
                    # Debug img
                    draw_detected_object(out_img, info, cnt, len(infos))
            if len(infos) >= MAX_OBJ['xmark']:
                break
    draw_text_info(out_img, format_info(infos))
    return out_img, infos

def rank_xmark(cnt):
    pass

def analyze_table(img, mask, data):
    """ Detects table using xmarks """
    out_img, infos = analyze_xmark(img, mask, data)
    if infos:
        table_info = init_detected_object('table', data)
        table_info.centroid = [np.mean([i.centroid[0] for i in infos]),
                               np.mean([i.centroid[1] for i in infos])]
        table_info.offset = get_offset(table_info.centroid)
        table_info.move = get_real_distance(Config.bot_center, table_info.centroid, data['depth'],
                                            'table')
        table_info.detected = True
        draw_centroid(out_img, table_info.centroid)
        infos.insert(0, table_info)
    return out_img, infos

def analyze_overall_bin(img, mask, data):
    infos = []
    # Filter out  blue hue
    mask = mask & thresh_hsv(img, RED_GREEN[0], RED_GREEN[1])
    out_img = init_debug_img(img)
    # Contour processing
    cnts = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
    cnts.sort(key=cv2.contourArea, reverse=True)
    if len(cnts) >= 1:
        for cnt in cnts:
            if is_correct_area(get_rect_area(cnt), 'bin') and is_rect(cnt):
                info = init_detected_object('overall_bin', data, img, cnt)
                infos.append(info)
                draw_detected_object(out_img, info, cnt, len(infos))
            if len(infos) >= MAX_OBJ['bin']:
                break

    if infos:
        infos[0].centroid = [np.mean([i.centroid[0] for i in infos]),
                             np.mean([i.centroid[1] for i in infos])]
        infos[0].offset = get_offset(infos[0].centroid)
        draw_centroid(out_img, infos[0].centroid)
    draw_text_info(out_img, format_info(infos))
    return out_img, infos

def analyze_cover(img, mask, data):
    infos = []
    # Filter out  blue hue
    mask = mask & thresh_hsv(img, RED_GREEN[0], RED_GREEN[1])
    out_img = init_debug_img(img)
    # out_img = to_bgr(mask, 'gray')
    # Contour processing
    cnts = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
    cnts.sort(key=cv2.contourArea, reverse=True)
    if len(cnts) >= 1:
        for cnt in cnts:
            if is_correct_area(get_rect_area(cnt), 'cover') and is_rect(cnt):
                info = init_detected_object('cover', data, img, cnt)
                # if info.predicted_color == 'red':
                infos.append(info)
                # Debug img
                draw_detected_object(out_img, info, cnt, len(infos))
                break
    draw_text_info(out_img, format_info(infos))
    return out_img, infos

def analyze_bin(img, mask, data):
    infos = []
    # Filter out  blue hue
    # mask = mask & thresh_hsv(img, RED_GREEN[0], RED_GREEN[1])
    out_img = init_debug_img(img)
    # out_img = to_bgr(mask, 'gray')
    # Contour processing
    cnts = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[1]
    cnts.sort(key=cv2.contourArea)
    if len(cnts) >= 1:
        for cnt in cnts:
            if is_correct_area(get_rect_area(cnt), 'bin') and is_rect(cnt, 0.7):
                info = init_detected_object('bin', data, img, cnt)
                if info.color[0] >= 30:
                    infos.append(info)
                    # Debug img
                    draw_detected_object(out_img, info, cnt, len(infos))
                    break
    draw_text_info(out_img, format_info(infos))
    return out_img, infos

def analyze_rect(img, mask, data, name):
    infos = []
    # Filter out  blue hue
    mask = mask & thresh_hsv(img, RED_GREEN[0], RED_GREEN[1])
    out_img = init_debug_img(img)
    out_img = to_bgr(mask, 'gray')
    # Contour processing
    cnts = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
    cnts.sort(key=cv2.contourArea, reverse=True)
    if len(cnts) >= 1:
        for cnt in cnts:
            if is_correct_area(get_rect_area(cnt), name) and is_rect(cnt):
                info = init_detected_object(name, data, img, cnt)
                infos.append(info)
                # Debug img
                draw_detected_object(out_img, info, cnt, len(infos))
    draw_text_info(out_img, format_info(infos))
    return out_img, infos

def get_norm_area(area):
    """ Returns area normalized between [0 ... 1] """
    return area / ((Config.processed_img_size[0] * Config.processed_img_size[1]) + 0.001)

def get_longer_edge(cnt):
    rect = cv2.minAreaRect(cnt)
    edge1 = rect[1][0]
    edge2 = rect[1][1]
    return edge1 if edge1 > edge2 else edge2

def get_rect_area(cnt):
    rect = cv2.minAreaRect(cnt)
    return int(rect[1][0] * rect[1][1])

def get_centroid(cnt):
    mom = cv2.moments(cnt)
    centroid_x = int((mom['m10'] + 0.0001) / (mom['m00'] + 0.0001))
    centroid_y = int((mom['m01'] + 0.0001) / (mom['m00'] + 0.0001))
    return (centroid_x, centroid_y)

def get_angle_long(cnt):
    """ Returns angle perpendicular to long side of a rectangle """
    rect = cv2.minAreaRect(cnt)
    points = np.int32(cv2.boxPoints(rect))
    edge1 = points[1] - points[0]
    edge2 = points[2] - points[1]
    if cv2.norm(edge1) > cv2.norm(edge2):
        return math.degrees(math.atan2(edge1[1], edge1[0]))
    else:
        return math.degrees(math.atan2(edge2[1], edge2[0]))

def get_vehicle_angle(rect_angle):
    """ Returns angle needed to be turned to align perpendicularly to long side of object """
    angle = 90 - abs(rect_angle) if rect_angle >= -90 else 90 - abs(rect_angle)
    return (angle + 90) % 360

def get_offset(centroid):
    x = Config.processed_img_size[0]
    y = Config.processed_img_size[1]
    dx = (centroid[0] - (x / 2)) / float(x)
    dy = ((y / 2) - centroid[1]) / float(y)
    return dx, dy

def get_mean_centroids(centroids):
    return [np.mean(centroid) for centroid in zip(*centroids)]

def get_group_contours(contours):
    hull = cv2.convexHull(np.vstack(contours))
    return hull

def point_in_contour(point, cnt):
    """ Checks if a point is located inside given contour """
    return cv2.pointPolygonTest(cnt, (int(point[0]), int(point[1])), False) > 0

def point_in_contours(point, contours):
    for cnt in contours:
        if point_in_contour(point, cnt):
            return True
    return False

def get_solidity(cnt):
    """ Calculates solidity according by area(cnt) / area(convexHull(cnt) """
    return cv2.contourArea(cnt) / cv2.contourArea(cv2.convexHull(cnt))

''' Drawing '''

def draw_square(canvas, center, color=c.WHITE, offset=8, thickness=2):
    """Draws a square
    Args:
        offset: distance from center which determines size of square
    """
    top_left = (center[0] - offset, center[1] + offset)
    bot_right = (center[0] + offset, center[1] - offset)
    cv2.rectangle(canvas, top_left, bot_right, color, thickness)

def draw_centroid(out_img, centroid, rad=4, color=c.PURPLE):
    cv2.circle(out_img, (int(centroid[0]), int(centroid[1])), rad, color, -1)

def draw_angle(cnt, centroid, out_img):
    rect = cv2.minAreaRect(cnt)
    startpt = (int(centroid[0]), int(centroid[1]))
    gradient = np.deg2rad(get_angle_long(cnt))
    endpt = (int(startpt[0] + 200 * math.cos(gradient)),
             int(startpt[1] + 200 * math.sin(gradient)))
    startpt = (int(startpt[0]), int(startpt[1]))
    cv2.line(out_img, startpt, endpt, (0, 255, 0), 2)
    cv2.drawContours(out_img, [np.int0(cv2.boxPoints(rect))], -1, (255, 0, 0), 2)

def format_info(infos):
    # Assuming that object with highest score is inserted first
    area = infos[0].area if infos else 0.0
    predicted_color = infos[0].predicted_color if infos else ""
    area = ("area: ", "{:.4f}".format(area))
    hue = ("color: ", predicted_color)
    detected = ("detected: ", str(len(infos)))
    return [area, hue, detected]

def draw_text_info(canvas, content):
    """ Draw on canvas list of (text, color) tuple in-order """
    start = (10, Config.processed_img_size[1] + 10)
    for label, text in content:
        draw_text(canvas, label, start, color=c.WHITE)
        draw_text(canvas, text, (start[0] + 70, start[1]), color=c.YELLOW)
        start = (start[0], start[1] + 15)

def draw_text(canvas, text, pos, font=cv2.FONT_HERSHEY_DUPLEX, size=0.4, color=c.WHITE,
              thickness=1):
    cv2.putText(canvas, text, pos, font, size, color, thickness, cv2.LINE_AA)

def init_debug_img(img):
    """ Create empty debug image for extra information """
    y, x = img.shape[:2]
    # Even out the size to store text information
    y = y + (x - y)
    canvas = np.zeros((y, x, 3), dtype=np.uint8)
    draw_square(canvas, Config.bot_center, offset=4, thickness=1)
    return canvas

''' Image statistics '''

def get_color_descriptor(img, cnt):
    blue, green, red = cv2.split(img)
    h, s, v = cv2.split(to_hsv(img))
    l, a, b = cv2.split(to_lab(img))
    h_mean = get_avg_intensity(h, cnt)
    l_mean = get_avg_intensity(l, cnt)
    a_mean = get_avg_intensity(a, cnt)
    b_mean = get_avg_intensity(b, cnt)
    return [h_mean, l_mean, a_mean, b_mean]

def get_predicted_color(color_desc):
    if abs(COLOR_OBJ['red_coin'][2] - color_desc[2]) > abs(COLOR_OBJ['green_coin'][2] - color_desc[2]):
        return 'green'
    else:
        return 'red'

def get_avg_intensity(channel, cnt):
    """ Get average intensity for a grayscale channel or colored image from ROI given by a contour """
    mask = np.zeros(channel.shape, np.uint8)
    cv2.drawContours(mask, [cnt], 0, 255, -1)
    return cv2.mean(channel, mask=mask)[0]

def mean_value(img):
    """ Return average intensity of value channel """
    h, s, v = cv2.split(to_hsv(img))
    return np.mean(v)

def get_circle_area(cnt):
    rad = cv2.minEnclosingCircle(cnt)[1]
    return math.pi * rad * rad

def get_real_distance(origin, target, depth, name):
    """ Given pixel location on screen, returns distance (in meter) to target pixel location """
    distance = Config.depth[name] - depth
    dx = distance * math.sin((target[0] - origin[0]) * Config.bot_angular_resolution[0])
    dy = distance * math.sin((origin[1] - target[1]) * Config.bot_angular_resolution[1])
    return [dx, dy]

def get_expected_rect_area(depth, name):
    """ Only works for bottom camera with depth sensor giving range """
    distance = Config.depth[name] - depth
    original_length = Config.length[name]
    perceived = (original_length * Config.bot_focal_length[0] /
                 (distance * Config.bot_resize_factor))
    return perceived**2 / 2.0

def get_expected_circle_area(depth, name):
    """ Only works for bottom camera with depth sensor giving range """
    distance = Config.depth[name] - depth
    original_radius = Config.length[name]
    perceived = (original_radius * Config.bot_focal_length[0] /
                 (distance * Config.bot_resize_factor))
    return perceived**2 * math.pi

def is_blue(img, cnt):
    return get_avg_intensity(img, cnt) > 60

''' Color thresholding '''

def thresh_black(img):
    l, a, b = cv2.split(to_lab(img))
    return adaptive_thresh(l)

def thresh_orange(img):
    processed = finlaynorm(img)
    b, g, r = cv2.split(processed)
    # hsv thresholding
    loThresh1 = (0, 0, 0)
    hiThresh1 = (40, 255, 235)
    loThresh2 = (170, 0, 0)
    hiThresh2 = (180, 255, 235)
    mask = cv2.inRange(to_hsv(processed), loThresh1, hiThresh1)
    mask2 = cv2.inRange(to_hsv(processed), loThresh2, hiThresh2)
    orange = mask | mask2
    # grayscale thresholding using green channel
    min = np.min(g)
    max = np.max(g)
    diff = max - min
    thresh = min + diff / 1.8
    common = cv2.threshold(b, thresh, 255, cv2.THRESH_BINARY_INV)[1]
    # final = common & orange
    # final = cv2.erode(final, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)))
    # final = cv2.dilate(final, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)))
    return common

def thresh_yellow(img):
    processed = finlaynorm(iace(img))
    # hsv thresholding
    b, g, r = cv2.split(processed)
    h, s, v = cv2.split(to_hsv(processed))
    loThresh = (15, 0, 150)
    hiThresh = (50, 255, 255)
    yellow = cv2.inRange(to_hsv(processed), loThresh, hiThresh)
    # grayscale thresholding using blue channel
    min = np.amin(b)
    max = np.amax(b)
    diff = max - min
    thresh = min + diff / 1.2
    common = cv2.threshold(b, thresh, 255, cv2.THRESH_BINARY_INV)[1]
    return common

def thresh_outline(img, w_diff=1.5):
    h, s, v = cv2.split(to_hsv(img))
    min = np.amin(v)
    max = np.amax(v)
    thresh = min + (max - min) / w_diff
    mask = np.uint8(cv2.Canny(v, thresh / 2, thresh))
    return mask

def thresh_green(img):
    mask = cv2.inRange(to_lab(img), (20, 0, 100), (255, 125, 255))
    kern = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    mask = cv2.erode(mask, kern, iterations=2)
    mask = cv2.dilate(mask, kern, iterations=1)
    return mask

def thresh_red(img):
    mask = cv2.inRange(to_lab(img), (0, 140, 125), (255, 255, 255))
    kern = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    mask = cv2.erode(mask, kern, iterations=2)
    mask = cv2.dilate(mask, kern, iterations=1)
    return mask

def thresh_hsv(img, lo_thresh, hi_thresh, mode='hsv'):
    if mode is 'hsv':
        return cv2.inRange(to_hsv(img), lo_thresh, hi_thresh)
    if mode is 'lab':
        return cv2.inRange(to_lab(img), lo_thresh, hi_thresh)

def thresh_bin(img, thresh_limit=60):
    """ Threshold using blue channel """
    b, g, r = cv2.split(img)
    # mask = get_salient(r)
    mask = cv2.threshold(b, 50, 255, cv2.THRESH_BINARY_INV)[1]
    return mask

def thresh_salient(img, thresh_limit=60):
    """ Threshold using blue channel """
    b, g, r = cv2.split(img)
    mask = get_salient(b)
    mask = cv2.threshold(mask, thresh_limit, 255, cv2.THRESH_BINARY)[1]
    return mask

def thresh_saturation_binary(img, thresh_limit=60):
    h, s, v = cv2.split(to_hsv(img))
    mask = cv2.threshold(s, thresh_limit, 255, cv2.THRESH_BINARY)[1]
    return mask

def thresh_saturation(img):
    h, s, v = cv2.split(to_hsv(img))
    return adaptive_thresh(s)

def thresh_table2(img):
    l, a, b = cv2.split(to_lab(img))
    return cv2.adaptiveThreshold(a, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 101, -10)

def thresh_table(img, offset=2):
    b, g, r = cv2.split(img)
    h, s, v = cv2.split(to_hsv(img))
    return adaptive_thresh(s)

def adaptive_thresh(gray, block=31, offset=5):
    res = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY,
                                block, offset)
    return res

""" Segmentation """

def superpixel_slic(img, n_segments=100, sigma=10, compactness=10, max_iter=3):
    """ Generates superpixels based on SLIC
        n_segments    number of superpixels wished to be generated
        sigma         smoothing used prior to segmentation
        compactness   balance between color space proximity and image space proximity.
                      higher more weight to space
        max_iter      iterations for kmeans
        return        segmented image, superpixels """
    image = img_as_float(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    segments = slic(image, n_segments=n_segments, sigma=sigma, compactness=compactness,
                    max_iter=max_iter)
    return segments

def get_superpixels_contours(img, segments):
    contours = []
    for (i, segVal) in enumerate(np.unique(segments)):
        # construct a mask for the segment
        mask = np.zeros(img.shape[:2], dtype="uint8")
        mask[segments == segVal] = 255
        cnts = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[1]
        if len(cnts) >= 1:
            contours.append(cnts[0])
    return contours

def show_superpixels(img, segments):
    """ Given result of SLIC, mark boundaries between regions """
    img = mark_boundaries(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), segments)
    return cv2.cvtColor(np.uint8(img * 255), cv2.COLOR_RGB2BGR)

def kmeans(img):
    Z = np.float32(img.reshape((-1, 3)))
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
    K = 5
    ret, label, center = cv2.kmeans(Z, K, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
    center = np.uint8(center)
    res = center[label.flatten()]
    res2 = res.reshape((img.shape))
    return res2

""" Dealing with ROS messages and services """

def draw_detected_object(out_img, info, cnt, id):
    draw_centroid(out_img, info.centroid, 2, c.YELLOW)
    cv2.drawContours(out_img, [cnt], -1, c.PURPLE, 1)
    cnt_color = Color.hue_to_bgr(info.color[0])
    '''
    draw_text(out_img, "H:{:.1f}".format(info.color[0]), (info.centroid[0] + 20, info.centroid[1] + 10))
    draw_text(out_img, "L:{:.1f}".format(info.color[1]), (info.centroid[0] + 75, info.centroid[1] + 10))
    draw_text(out_img, "A:{:.1f}".format(info.color[2]), (info.centroid[0] + 20, info.centroid[1] + 25))
    draw_text(out_img, "B:{:.1f}".format(info.color[3]), (info.centroid[0] + 75, info.centroid[1] + 25))
    draw_text(out_img, "Area:{:.1f}".format(info.area), (info.centroid[0] + 20, info.centroid[1] + 40))
    draw_text(out_img, "Color:{}".format(info.predicted_color), (info.centroid[0] + 20, info.centroid[1] + 55))
    draw_text(out_img, "Dist:{:.1f}".format(info.distance), (info.centroid[0] + 20, info.centroid[1] + 70))
    '''
    if info.name not in ['xmark', 'vertical_coin']:
        cv2.drawContours(out_img, [np.int0(cv2.boxPoints(cv2.minAreaRect(cnt)))], -1, cnt_color, 2)
    else:
        centroid, radius = cv2.minEnclosingCircle(cnt)
        cv2.circle(out_img, (int(centroid[0]), int(centroid[1])), int(radius), cnt_color, 2)

def init_detected_object(name, data, img=[], cnt=None):
    info = DetectedObject()
    info.name = name
    if len(img) > 0:
        info.centroid = get_centroid(cnt)
        info.offset = get_offset(info.centroid)
        info.move = get_real_distance(Config.bot_center, info.centroid, data['depth'], name)
        info.color = get_color_descriptor(img, cnt)
        info.predicted_color = get_predicted_color(info.color)
        info.angle = get_vehicle_angle(get_angle_long(cnt))
        info.rectangularity = get_rectangularity(cnt)
        info.circularity = get_circularity(cnt)
        if info.name not in ['xmark', 'vertical_coin']:
            info.area = get_rect_area(cnt)
            info.distance = rect_to_distance(get_longer_edge(cnt), Config.length[name])
        else:
            radius = cv2.minEnclosingCircle(cnt)[1]
            info.area = math.pi * (radius**2)
            info.distance = circle_to_distance(radius, Config.length[name])
        info.detected = True
    return info

def get_config(data, obj_id='generic'):
    return data['config'][obj_id]


from collections import defaultdict, Counter

import numpy as np
import time
from utils import one_hot_encode_seq, reverse_complement, overlap
import matplotlib.pyplot as plt

startt = time.time()
psis = []

data_path = '../../data'
save_to_cons = 'hipsci_majiq/exon/cons.npy'
introns_bef_start = 70 # introns
exons_after_start = 70 # exons
exons_bef_end = 70 # exons
introns_after_end = 70 # introns

# want to load chromosome as one giant string
def load_chrom_seq(chrom):
    with open(f'../../data/chromosomes/chr{chrom}.fa') as f:
        loaded_chrom_seq = f.read().replace('\n', '')
        if chrom < 10:
            return loaded_chrom_seq[5:]
        else:
            return loaded_chrom_seq[6:]

l1s, l2s, l3s = [], [], []
cons_exons = []
prev_astart, prev_aend = 0, 0
prev_dstart, prev_dend = 0, 0
prev_jstart, prev_jend = 0, 0
overlaps = 0
exon_mean, exon_std, intron_mean, intron_std = 145.42, 198.0, 5340., 17000.
for ttt in range(-4, 4):
    counts_start, counts_end = defaultdict(lambda: 0), defaultdict(lambda: 0)
    counter = 1000000000
with open('../../majiq/builder/constitutive_junctions_sorted_stranded.tsv') as f:
    loaded_chrom = 1
    chrom_seq = load_chrom_seq(loaded_chrom)
    for i, l in enumerate(f):
        if i==0: continue
        if i % 1000 == 0: # ~ 357500 junctions
            print(f'Reading line {i}')

        geneid, chrom, jstart, jend, dstart, dend, astart, aend, strand = l.replace('\n','').split('\t')
        chrom, jstart, jend, dstart, dend, astart, aend, psi = int(chrom), int(jstart), int(jend), int(dstart), \
                                                               int(dend), int(astart), int(aend), 1.0
        if chrom > loaded_chrom:
            loaded_chrom += 1
            chrom_seq = load_chrom_seq(loaded_chrom)

        if overlap(prev_jstart, prev_jend, jstart, jend):
            overlaps += 1
        if (prev_astart, prev_aend) == (dstart, dend):
            # if strand == '+': continue
            if strand == '+':
                window_around_start = chrom_seq[dstart-introns_bef_start-3:dstart+exons_after_start-3]
                window_around_end = chrom_seq[dend-exons_bef_end+2:dend+introns_after_end+2]
            elif strand == '-':
                window_around_start = chrom_seq[dstart-introns_bef_start-3:dstart+exons_after_start-3]
                window_around_end = chrom_seq[dend-exons_bef_end+2:dend+introns_after_end+2]

            extr1, extr2 = chrom_seq[dstart+ttt:dstart+ttt+2], chrom_seq[dend+ttt:dend+ttt+2]
            if strand == '-':
                window_around_start, window_around_end = reverse_complement(window_around_end[::-1]), \
                                                         reverse_complement(window_around_start[::-1])
                extr1, extr2 = reverse_complement(extr2[::-1]), reverse_complement(extr1[::-1])
            extr3, extr4 = window_around_start[70:72], window_around_end[68:70]
            # + strand works for both, - strand works for none
            # 100% AG for -3 offset for + strand, 0 offset - strand  <--- start
            # 99% GT at 0 offset only + strand, -3 offset for - strand  <--- end

            start, end = one_hot_encode_seq(window_around_start), one_hot_encode_seq(window_around_end)
            start, end = np.array(start), np.array(end)
            l1, l2, l3 = dstart-prev_dend, dend-dstart, astart-dend
            l1, l2, l3 = (l1-intron_mean)/intron_std, (l2-exon_mean)/exon_std, (l3-intron_mean)/intron_std
            lens_and_psi_vector = np.array([l1, l2, l3, psi])
            l1s.append(l1)
            l2s.append(l2)
            l3s.append(l3)
            start_and_end = np.concatenate((start, end))
            sample = np.concatenate((start_and_end,lens_and_psi_vector.reshape(1,4))).astype(np.float32)
            cons_exons.append(sample)

            # GT-AG and GC-AG
            # print(extr1)
            target = 100000000
            if i < target:
                counts_start[extr3.upper()] += 1
                counts_end[extr4.upper()] += 1
            if target == i:
                print('-' * 40)
                for (k, v) in counts_start.items():
                    percent = v / target
                    if percent > 0.05:
                        print(f'{k} :{percent:.2f}')
                print('-' * 40)

                break

            psis.append(psi)
        prev_astart, prev_aend = astart, aend
        prev_dstart, prev_dend = dstart, dend
        prev_jstart, prev_jend = jstart, jend
cons_exons = np.array(cons_exons)

psis = np.array(psis)

l1s, l2s, l3s = np.array(l1s), np.array(l2s), np.array(l3s)
l1avg, l2avg, l3avg = np.mean(l1s), np.mean(l2s), np.mean(l3s)
l1median, l2median, l3median = np.median(l1s), np.median(l2s), np.median(l3s)

plt.hist(l1s)
plt.xlabel('normalized L1 value')
plt.ylabel('number of data points')
plt.title('Constitutive exons MAJIQ')
# plt.show()

print(f'Most common nucleotides right after exon start: {Counter(counts_start).most_common(1)}')
print(f'Most common nucleotides right before exon end: {Counter(counts_end).most_common(1)}')
print(f'L1avg: {l1avg}, l2avg: {l2avg}, l3avg: {l3avg}')
print(f'L1 median: {l1median}, l2 median: {l2median}, l3 median: {l3median}')

print(f'Number of cons exon: {len(cons_exons)}')
print(f'Number of overlapping (supposedly constitutive) junctions: {overlaps}')

np.save(f'{data_path}/{save_to_cons}', cons_exons)
print(f'Runtime {time.time()-startt}')


import keras
import tensorflow as tf

from keras_retinanet import models
from keras_retinanet.utils.image import read_image_bgr, preprocess_image, resize_image
from keras_retinanet.utils.visualization import draw_box, draw_caption
from keras_retinanet.utils.colors import label_color

# import miscellaneous modules
# import matplotlib.pyplot as plt
import cv2
import os
import numpy as np
import time

def get_session():
    config = tf.ConfigProto()
    config.gpu_options.allow_growth = True
    return tf.Session(config=config)

def main():
    keras.backend.tensorflow_backend.set_session(get_session())
    # model_path = './snapshots/1/resnet50_pins_n_solder_12_inference.h5'
    model_path = './snapshots/resnet50_pins_n_solder_14_inference.h5'
    model = models.load_model(model_path, backbone_name='resnet50')

    # load label to names mapping for visualization purposes
    labels_to_names = {0: 'pin', 1: 'solder', 2: 'anomaly_bent_solder'}

    # dir = '/media/trevol/HDD/contacts/frames'
    dir = 'D:/DiskE/Computer_Vision_Task/frames_6/'
    files = {
        178: 'f_0178_11866.67_11.87.jpg',
        265: 'f_0265_17666.67_17.67.jpg',
        266: 'f_0266_17733.33_17.73.jpg',
        267: 'f_0267_17800.00_17.80.jpg',
        268: 'f_0268_17866.67_17.87.jpg',
        269: 'f_0269_17933.33_17.93.jpg',
        270: 'f_0270_18000.00_18.00.jpg'
    }
    # 283 284 285 286 287 288 289
    for num, fileName in files.items():
        frame = cv2.imread(os.path.join(dir, fileName))

        image = predict_on_image(model, labels_to_names, frame, thresh=0.96)
        image = putFramePos(image, num)
        image = resize(image, .5)
        cv2.imshow('Video', image)
        if cv2.waitKey() == 27:
            break

def predict_on_image(model, labels_to_names, image, thresh):
    # copy to draw on
    draw = image.copy()
    # draw = cv2.cvtColor(draw, cv2.COLOR_BGR2RGB, dst=draw)

    # preprocess image for network
    image = preprocess_image(image)
    image, scale = resize_image(image)

    # process image
    start = time.time()
    boxes, scores, labels = model.predict_on_batch(np.expand_dims(image, axis=0))
    print("processing time: ", time.time() - start)

    # correct for image scale
    boxes /= scale

    # visualize detections
    for box, score, label in zip(boxes[0], scores[0], labels[0]):
        # scores are sorted so we can break
        if score < thresh:
            break

        # color = label_color(label)
        if score < 0.99:
            color = (0, 0, 255)
        elif label == 1:
            color = (0, 255, 0)
        else:
            color = (0, 255, 255)
        b = np.round(box, 0).astype(int)
        draw_box(draw, b, color=color, thickness=1)

        caption = f"{labels_to_names[label]} {score:.2f}"
        draw_caption(draw, b, caption)

    # return cv2.cvtColor(draw, cv2.COLOR_RGB2BGR, dst=draw)
    return draw

def putFramePos(frame, pos):
    cv2.putText(frame, str(int(pos)), (10, 40), cv2.FONT_HERSHEY_COMPLEX, 1, (0, 0, 255))
    return frame

def resize(img, factor):
    dsize = tuple(np.multiply(img.shape[1::-1], factor).astype(int))
    return cv2.resize(img, dsize, interpolation=cv2.INTER_AREA)

if __name__ == '__main__':
    main()