#!/usr/bin/env python2
# -*- coding: utf-8 -*-

# This program is free software. It comes without any warranty, to
# the extent permitted by applicable law. You can redistribute it
# and/or modify it under the terms of the Do What The Fuck You Want
# To Public License, Version 2, as published by Sam Hocevar. See
# COPYING for more details.

# Made by Jogge, modified by celeron55
# 2011-05-29: j0gge: initial release
# 2011-05-30: celeron55: simultaneous support for sectors/sectors2, removed
# 2011-06-02: j0gge: command line parameters, coordinates, players, ...
# 2011-06-04: celeron55: added #!/usr/bin/python2 and converted \r\n to \n
#                        to make it easily executable on Linux
# 2011-07-30: WF: Support for content types extension, refactoring
# 2011-07-30: erlehmann: PEP 8 compliance.

# Requires Python Imaging Library: http://www.pythonware.com/products/pil/

# Some speed-up: ...lol, actually it slows it down.
#import psyco ; psyco.full()
#from psyco.classes import *

import zlib
import os
import string
import time
import getopt
import sys
import array
import cStringIO
import traceback
from PIL import Image, ImageDraw, ImageFont, ImageColor

TRANSLATION_TABLE = {
    1: 0x800,  # CONTENT_GRASS
    4: 0x801,  # CONTENT_TREE
    5: 0x802,  # CONTENT_LEAVES
    6: 0x803,  # CONTENT_GRASS_FOOTSTEPS
    7: 0x804,  # CONTENT_MESE
    8: 0x805,  # CONTENT_MUD
    10: 0x806,  # CONTENT_CLOUD
    11: 0x807,  # CONTENT_COALSTONE
    12: 0x808,  # CONTENT_WOOD
    13: 0x809,  # CONTENT_SAND
    18: 0x80a,  # CONTENT_COBBLE
    19: 0x80b,  # CONTENT_STEEL
    20: 0x80c,  # CONTENT_GLASS
    22: 0x80d,  # CONTENT_MOSSYCOBBLE
    23: 0x80e,  # CONTENT_GRAVEL
    24: 0x80f,  # CONTENT_SANDSTONE
    25: 0x810,  # CONTENT_CACTUS
    26: 0x811,  # CONTENT_BRICK
    27: 0x812,  # CONTENT_CLAY
    28: 0x813,  # CONTENT_PAPYRUS
    29: 0x814}  # CONTENT_BOOKSHELF


def hex_to_int(h):
    i = int(h, 16)
    if(i > 2047):
        i -= 4096
    return i


def hex4_to_int(h):
    i = int(h, 16)
    if(i > 32767):
        i -= 65536
    return i


def int_to_hex3(i):
    if(i < 0):
        return "%03X" % (i + 4096)
    else:
        return "%03X" % i


def int_to_hex4(i):
    if(i < 0):
        return "%04X" % (i + 65536)
    else:
        return "%04X" % i


def getBlockAsInteger(p):
    return p[2]*16777216 + p[1]*4096 + p[0]

def unsignedToSigned(i, max_positive):
    if i < max_positive:
        return i
    else:
        return i - 2*max_positive

def getIntegerAsBlock(i):
    x = unsignedToSigned(i % 4096, 2048)
    i = int((i - x) / 4096)
    y = unsignedToSigned(i % 4096, 2048)
    i = int((i - y) / 4096)
    z = unsignedToSigned(i % 4096, 2048)
    return x,y,z

def limit(i, l, h):
    if(i > h):
        i = h
    if(i < l):
        i = l
    return i

def readU8(f):
    return ord(f.read(1))

def readU16(f):
    return ord(f.read(1))*256 + ord(f.read(1))

def readU32(f):
    return ord(f.read(1))*256*256*256 + ord(f.read(1))*256*256 + ord(f.read(1))*256 + ord(f.read(1))

def readS32(f):
    return unsignedToSigned(ord(f.read(1))*256*256*256 + ord(f.read(1))*256*256 + ord(f.read(1))*256 + ord(f.read(1)), 2**31)

usagetext = """minetestmapper.py [options]
  -i/--input <world_path>
  -o/--output <output_image.png>
  --bgcolor <color>
  --scalecolor <color>
  --playercolor <color>
  --origincolor <color>
  --drawscale
  --drawplayers
  --draworigin
  --drawunderground
Color format: '#000000'"""

def usage():
    print(usagetext)

try:
    opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["help", "input=",
        "output=", "bgcolor=", "scalecolor=", "origincolor=",
        "playercolor=", "draworigin", "drawplayers", "drawscale",
        "drawunderground"])
except getopt.GetoptError as err:
    # print help information and exit:
    print(str(err))  # will print something like "option -a not recognized"
    usage()
    sys.exit(2)

path = None
output = "map.png"
border = 0
scalecolor = "black"
bgcolor = "white"
origincolor = "red"
playercolor = "red"
drawscale = False
drawplayers = False
draworigin = False
drawunderground = False

sector_xmin = -1500 / 16
sector_xmax = 1500 / 16
sector_zmin = -1500 / 16
sector_zmax = 1500 / 16

for o, a in opts:
    if o in ("-h", "--help"):
        usage()
        sys.exit()
    elif o in ("-i", "--input"):
        path = a
    elif o in ("-o", "--output"):
        output = a
    elif o == "--bgcolor":
        bgcolor = ImageColor.getrgb(a)
    elif o == "--scalecolor":
        scalecolor = ImageColor.getrgb(a)
    elif o == "--playercolor":
        playercolor = ImageColor.getrgb(a)
    elif o == "--origincolor":
        origincolor = ImageColor.getrgb(a)
    elif o == "--drawscale":
        drawscale = True
        border = 40
    elif o == "--drawplayers":
        drawplayers = True
    elif o == "--draworigin":
        draworigin = True
    elif o == "--drawunderground":
        drawunderground = True
    else:
        assert False, "unhandled option"

if path is None:
    print("Please select world path (eg. -i ../worlds/yourworld) (or use --help)")
    sys.exit(1)

if path[-1:] != "/" and path[-1:] != "\\":
    path = path + "/"

# Load color information for the blocks.
colors = {}
try:
    f = file("colors.txt")
except IOError:
    f = file(os.path.join(os.path.dirname(__file__), "colors.txt"))
for line in f:
    values = string.split(line)
    if len(values) < 4:
        continue
    identifier = values[0]
    is_hex = True
    for c in identifier:
        if c not in "0123456789abcdefABCDEF":
            is_hex = False
            break
    if is_hex:
        colors[int(values[0], 16)] = (
            int(values[1]),
            int(values[2]),
            int(values[3]))
    else:
        colors[values[0]] = (
            int(values[1]),
            int(values[2]),
            int(values[3]))
f.close()

#print("colors: "+repr(colors))
#sys.exit(1)

xlist = []
zlist = []

# List all sectors to memory and calculate the width and heigth of the
# resulting picture.

conn = None
cur = None
if os.path.exists(path + "map.sqlite"):
    import sqlite3
    conn = sqlite3.connect(path + "map.sqlite")
    cur = conn.cursor()
    
    cur.execute("SELECT `pos` FROM `blocks`")
    while True:
        r = cur.fetchone()
        if not r:
            break
        
        x, y, z = getIntegerAsBlock(r[0])
        
        if x < sector_xmin or x > sector_xmax:
            continue
        if z < sector_zmin or z > sector_zmax:
            continue
        
        xlist.append(x)
        zlist.append(z)

if os.path.exists(path + "sectors2"):
    for filename in os.listdir(path + "sectors2"):
        for filename2 in os.listdir(path + "sectors2/" + filename):
            x = hex_to_int(filename)
            z = hex_to_int(filename2)
            if x < sector_xmin or x > sector_xmax:
                continue
            if z < sector_zmin or z > sector_zmax:
                continue
            xlist.append(x)
            zlist.append(z)

if os.path.exists(path + "sectors"):
    for filename in os.listdir(path + "sectors"):
        x = hex4_to_int(filename[:4])
        z = hex4_to_int(filename[-4:])
        if x < sector_xmin or x > sector_xmax:
            continue
        if z < sector_zmin or z > sector_zmax:
            continue
        xlist.append(x)
        zlist.append(z)

if len(xlist) == 0 or len(zlist) == 0:
    print("World does not exist.")
    sys.exit(1)

# Get rid of doubles
xlist, zlist = zip(*sorted(set(zip(xlist, zlist))))

minx = min(xlist)
minz = min(zlist)
maxx = max(xlist)
maxz = max(zlist)

w = (maxx - minx) * 16 + 16
h = (maxz - minz) * 16 + 16

print("Result image (w=" + str(w) + " h=" + str(h) + ") will be written to "
        + output)

im = Image.new("RGB", (w + border, h + border), bgcolor)
draw = ImageDraw.Draw(im)
impix = im.load()

stuff = {}

unknown_node_names = []
unknown_node_ids = []

starttime = time.time()

CONTENT_WATER = 2

def content_is_ignore(d):
    return d in [0, "ignore"]

def content_is_water(d):
    return d in [2, 9]

def content_is_air(d):
    return d in [126, 127, 254, "air"]

def read_content(mapdata, version, datapos):
    if version >= 24:
        return (mapdata[datapos*2] << 8) | (mapdata[datapos*2 + 1])
    elif version >= 20:
        if mapdata[datapos] < 0x80:
            return mapdata[datapos]
        else:
            return (mapdata[datapos] << 4) | (mapdata[datapos + 0x2000] >> 4)
    elif 16 <= version < 20:
        return TRANSLATION_TABLE.get(mapdata[datapos], mapdata[datapos])
    else:
        raise Exception("Unsupported map format: " + str(version))


def read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name):
    global stuff  # oh my :-)
    global unknown_node_names
    global unknown_node_ids

    if(len(mapdata) < 4096):
        print("bad: " + xhex + "/" + zhex + "/" + yhex + " " + \
            str(len(mapdata)))
    else:
        chunkxpos = xpos * 16
        chunkypos = ypos * 16
        chunkzpos = zpos * 16
        content = 0
        datapos = 0
        for (x, z) in reversed(pixellist):
            for y in reversed(range(16)):
                datapos = x + y * 16 + z * 256
                content = read_content(mapdata, version, datapos)
                # Try to convert id to name
                try:
                    content = id_to_name[content]
                except KeyError:
                    pass

                if content_is_ignore(content):
                    pass
                elif content_is_air(content):
                    pass
                elif content_is_water(content):
                    water[(x, z)] += 1
                    # Add dummy stuff for drawing sea without seabed
                    stuff[(chunkxpos + x, chunkzpos + z)] = (
                        chunkypos + y, content, water[(x, z)], day_night_differs)
                elif content in colors:
                    # Memorize information on the type and height of
                    # the block and for drawing the picture.
                    stuff[(chunkxpos + x, chunkzpos + z)] = (
                        chunkypos + y, content, water[(x, z)], day_night_differs)
                    pixellist.remove((x, z))
                    break
                else:
                    if type(content) == str:
                        if content not in unknown_node_names:
                            unknown_node_names.append(content)
                        #print("unknown node: %s/%s/%s x: %d y: %d z: %d block name: %s"
                        #        % (xhex, zhex, yhex, x, y, z, content))
                    else:
                        if content not in unknown_node_ids:
                            unknown_node_ids.append(content)
                        #print("unknown node: %s/%s/%s x: %d y: %d z: %d block id: %x"
                        #        % (xhex, zhex, yhex, x, y, z, content))


# Go through all sectors.
for n in range(len(xlist)):
    #if n > 500:
    #   break
    if n % 200 == 0:
        nowtime = time.time()
        dtime = nowtime - starttime
        try:
            n_per_second = 1.0 * n / dtime
        except ZeroDivisionError:
            n_per_second = 0
        if n_per_second != 0:
            seconds_per_n = 1.0 / n_per_second
            time_guess = seconds_per_n * len(xlist)
            remaining_s = time_guess - dtime
            remaining_minutes = int(remaining_s / 60)
            remaining_s -= remaining_minutes * 60
            print("Processing sector " + str(n) + " of " + str(len(xlist))
                    + " (" + str(round(100.0 * n / len(xlist), 1)) + "%)"
                    + " (ETA: " + str(remaining_minutes) + "m "
                    + str(int(remaining_s)) + "s)")

    xpos = xlist[n]
    zpos = zlist[n]

    xhex = int_to_hex3(xpos)
    zhex = int_to_hex3(zpos)
    xhex4 = int_to_hex4(xpos)
    zhex4 = int_to_hex4(zpos)

    sector1 = xhex4.lower() + zhex4.lower()
    sector2 = xhex.lower() + "/" + zhex.lower()

    ylist = []

    sectortype = ""

    if cur:
        psmin = getBlockAsInteger((xpos, -2048, zpos))
        psmax = getBlockAsInteger((xpos, 2047, zpos))
        cur.execute("SELECT `pos` FROM `blocks` WHERE `pos`>=? AND `pos`<=? AND (`pos` - ?) % 4096 = 0", (psmin, psmax, psmin))
        while True:
            r = cur.fetchone()
            if not r:
                break
            pos = getIntegerAsBlock(r[0])[1]
            ylist.append(pos)
            sectortype = "sqlite"
    try:
        for filename in os.listdir(path + "sectors/" + sector1):
            if(filename != "meta"):
                pos = int(filename, 16)
                if(pos > 32767):
                    pos -= 65536
                ylist.append(pos)
                sectortype = "old"
    except OSError:
        pass

    if sectortype == "":
        try:
            for filename in os.listdir(path + "sectors2/" + sector2):
                if(filename != "meta"):
                    pos = int(filename, 16)
                    if(pos > 32767):
                        pos -= 65536
                    ylist.append(pos)
                    sectortype = "new"
        except OSError:
            pass

    if sectortype == "":
        continue

    ylist.sort()

    # Make a list of pixels of the sector that are to be looked for.
    pixellist = []
    water = {}
    for x in range(16):
        for z in range(16):
            pixellist.append((x, z))
            water[(x, z)] = 0

    # Go through the Y axis from top to bottom.
    for ypos in reversed(ylist):
        try:
            #print("("+str(xpos)+","+str(ypos)+","+str(zpos)+")")

            yhex = int_to_hex4(ypos)

            if sectortype == "sqlite":
                ps = getBlockAsInteger((xpos, ypos, zpos))
                cur.execute("SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1", (ps,))
                r = cur.fetchone()
                if not r:
                    continue
                f = cStringIO.StringIO(r[0])
            else:
                if sectortype == "old":
                    filename = path + "sectors/" + sector1 + "/" + yhex.lower()
                else:
                    filename = path + "sectors2/" + sector2 + "/" + yhex.lower()
                f = file(filename, "rb")

            # Let's just memorize these even though it's not really necessary.
            version = readU8(f)
            flags = f.read(1)
            
            #print("version="+str(version))
            #print("flags="+str(version))

            # Check flags
            is_underground = ((ord(flags) & 1) != 0)
            day_night_differs = ((ord(flags) & 2) != 0)
            lighting_expired = ((ord(flags) & 4) != 0)
            generated = ((ord(flags) & 8) != 0)
            
            #print("is_underground="+str(is_underground))
            #print("day_night_differs="+str(day_night_differs))
            #print("lighting_expired="+str(lighting_expired))
            #print("generated="+str(generated))
            
            if version >= 22:
                content_width = readU8(f)
                params_width = readU8(f)

            # Node data
            dec_o = zlib.decompressobj()
            try:
                mapdata = array.array("B", dec_o.decompress(f.read()))
            except:
                mapdata = []
            
            # Reuse the unused tail of the file
            f.close();
            f = cStringIO.StringIO(dec_o.unused_data)
            #print("unused data: "+repr(dec_o.unused_data))

            # zlib-compressed node metadata list
            dec_o = zlib.decompressobj()
            try:
                metaliststr = array.array("B", dec_o.decompress(f.read()))
                # And do nothing with it
            except:
                metaliststr = []
            
            # Reuse the unused tail of the file
            f.close();
            f = cStringIO.StringIO(dec_o.unused_data)
            #print("* dec_o.unused_data: "+repr(dec_o.unused_data))
            data_after_node_metadata = dec_o.unused_data

            if version <= 21:
                # mapblockobject_count
                readU16(f)

            if version == 23:
                readU8(f) # Unused node timer version (always 0)
            if version == 24:
                ver = readU8(f)
                if ver == 1:
                    num = readU16(f)
                    for i in range(0,num):
                        readU16(f)
                        readS32(f)
                        readS32(f)

            static_object_version = readU8(f)
            static_object_count = readU16(f)
            for i in range(0, static_object_count):
                # u8 type (object type-id)
                object_type = readU8(f)
                # s32 pos_x_nodes * 10000
                pos_x_nodes = readS32(f)/10000
                # s32 pos_y_nodes * 10000
                pos_y_nodes = readS32(f)/10000
                # s32 pos_z_nodes * 10000
                pos_z_nodes = readS32(f)/10000
                # u16 data_size
                data_size = readU16(f)
                # u8[data_size] data
                data = f.read(data_size)
            
            timestamp = readU32(f)
            #print("* timestamp="+str(timestamp))
            
            id_to_name = {}
            if version >= 22:
                name_id_mapping_version = readU8(f)
                num_name_id_mappings = readU16(f)
                #print("* num_name_id_mappings: "+str(num_name_id_mappings))
                for i in range(0, num_name_id_mappings):
                    node_id = readU16(f)
                    name_len = readU16(f)
                    name = f.read(name_len)
                    #print(str(node_id)+" = "+name)
                    id_to_name[node_id] = name

            # Node timers
            if version >= 25:
                timer_size = readU8(f)
                num = readU16(f)
                for i in range(0,num):
                    readU16(f)
                    readS32(f)
                    readS32(f)

            read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name)

            # After finding all the pixels in the sector, we can move on to
            # the next sector without having to continue the Y axis.
            if(len(pixellist) == 0):
                break
        except Exception as e:
            print("Error at ("+str(xpos)+","+str(ypos)+","+str(zpos)+"): "+str(e))
            sys.stdout.write("Block data: ")
            for c in r[0]:
                sys.stdout.write("%2.2x "%ord(c))
            sys.stdout.write(os.linesep)
            sys.stdout.write("Data after node metadata: ")
            for c in data_after_node_metadata:
                sys.stdout.write("%2.2x "%ord(c))
            sys.stdout.write(os.linesep)
            traceback.print_exc()

print("Drawing image")
# Drawing the picture
starttime = time.time()
n = 0
for (x, z) in stuff.iterkeys():
    if n % 500000 == 0:
        nowtime = time.time()
        dtime = nowtime - starttime
        try:
            n_per_second = 1.0 * n / dtime
        except ZeroDivisionError:
            n_per_second = 0
        if n_per_second != 0:
            listlen = len(stuff)
            seconds_per_n = 1.0 / n_per_second
            time_guess = seconds_per_n * listlen
            remaining_s = time_guess - dtime
            remaining_minutes = int(remaining_s / 60)
            remaining_s -= remaining_minutes * 60
            print("Drawing pixel " + str(n) + " of " + str(listlen)
                    + " (" + str(round(100.0 * n / listlen, 1)) + "%)"
                    + " (ETA: " + str(remaining_minutes) + "m "
                    + str(int(remaining_s)) + "s)")
    n += 1

    (r, g, b) = colors[stuff[(x, z)][1]]

    dnd = stuff[(x, z)][3]  # day/night differs?
    if not dnd and not drawunderground:
        if stuff[(x, z)][2] > 0:  # water
            (r, g, b) = colors[CONTENT_WATER]
        else:
            continue

    # Comparing heights of a couple of adjacent blocks and changing
    # brightness accordingly.
    try:
        c = stuff[(x, z)][1]
        c1 = stuff[(x - 1, z)][1]
        c2 = stuff[(x, z + 1)][1]
        dnd1 = stuff[(x - 1, z)][3]
        dnd2 = stuff[(x, z + 1)][3]
        if not dnd:
            d = -69
        elif not content_is_water(c1) and not content_is_water(c2) and \
            not content_is_water(c):
            y = stuff[(x, z)][0]
            y1 = stuff[(x - 1, z)][0] if dnd1 else y
            y2 = stuff[(x, z + 1)][0] if dnd2 else y
            d = ((y - y1) + (y - y2)) * 12
        else:
            d = 0

        if(d > 36):
            d = 36

        r = limit(r + d, 0, 255)
        g = limit(g + d, 0, 255)
        b = limit(b + d, 0, 255)
    except:
        pass

    # Water
    if(stuff[(x, z)][2] > 0):
        r = int(r * .15 + colors[2][0] * .85)
        g = int(g * .15 + colors[2][1] * .85)
        b = int(b * .15 + colors[2][2] * .85)

    impix[x - minx * 16 + border, h - 1 - (z - minz * 16) + border] = (r, g, b)


if draworigin:
    draw.ellipse((minx * -16 - 5 + border, h - minz * -16 - 6 + border,
        minx * -16 + 5 + border, h - minz * -16 + 4 + border),
        outline=origincolor)

font = ImageFont.load_default()

if drawscale:
    draw.text((24, 0), "X", font=font, fill=scalecolor)
    draw.text((2, 24), "Z", font=font, fill=scalecolor)

    for n in range(int(minx / -4) * -4, maxx, 4):
        draw.text((minx * -16 + n * 16 + 2 + border, 0), str(n * 16),
            font=font, fill=scalecolor)
        draw.line((minx * -16 + n * 16 + border, 0,
            minx * -16 + n * 16 + border, border - 1), fill=scalecolor)

    for n in range(int(maxz / 4) * 4, minz, -4):
        draw.text((2, h - 1 - (n * 16 - minz * 16) + border), str(n * 16),
            font=font, fill=scalecolor)
        draw.line((0, h - 1 - (n * 16 - minz * 16) + border, border - 1,
            h - 1 - (n * 16 - minz * 16) + border), fill=scalecolor)

if drawplayers:
    try:
        for filename in os.listdir(path + "players"):
            f = file(path + "players/" + filename)
            lines = f.readlines()
            name = ""
            position = []
            for line in lines:
                p = string.split(line)
                if p[0] == "name":
                    name = p[2]
                    print(filename + ": name = " + name)
                if p[0] == "position":
                    position = string.split(p[2][1:-1], ",")
                    print(filename + ": position = " + p[2])
            if len(name) > 0 and len(position) == 3:
                x = (int(float(position[0]) / 10 - minx * 16))
                z = int(h - (float(position[2]) / 10 - minz * 16))
                draw.ellipse((x - 2 + border, z - 2 + border,
                    x + 2 + border, z + 2 + border), outline=playercolor)
                draw.text((x + 2 + border, z + 2 + border), name,
                    font=font, fill=playercolor)
            f.close()
    except OSError:
        pass

print("Saving")
im.save(output)

if unknown_node_names:
    sys.stdout.write("Unknown node names:")
    for name in unknown_node_names:
        sys.stdout.write(" "+name)
    sys.stdout.write(os.linesep)
if unknown_node_ids:
    sys.stdout.write("Unknown node ids:")
    for node_id in unknown_node_ids:
        sys.stdout.write(" "+str(hex(node_id)))
    sys.stdout.write(os.linesep)

