#!/usr/bin/env python

#OPENSCAD_PATH = "C:/Program Files/OpenSCAD/openscad.com"
# If you keep OpenSCAD in an unusual location, uncomment the above line of code and
# set it to the full path to the openscad executable.
# Note: Windows/python now support forward-slash characters in paths, so please use
#       those instead of backslashes which create a lot of confusion in code strings.


# do not edit below unless you know what you are doing!
import os
import configparser
import platform
from shutil import copy, rmtree
import shlex
import random as rd
import time
import numpy as np
import math
import re
from PIL import Image
import subprocess as sp
halt = -1  # debug: terminate skipping this shell (0 to n to enable)

# Make sure we have a fresh random seed
rd.seed()

USE_SCAD_THREAD_TRAVERSAL = False
STL_DIR = "stl_files"
PREV_DIR = "maze_previews"

def openscad():
    try:
        if OPENSCAD_PATH:
            return OPENSCAD_PATH
    except NameError:
        pass
    if os.getenv("OPENSCAD_PATH"):
        return os.getenv("OPENSCAD_PATH")
    if platform.system() == "Darwin":
        return "/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD"
    if platform.system() == "Windows":
        # Note: Windows allows forward slashes now
        return "C:/Program Files/OpenSCAD/openscad.com"
    # Default to linux-friendly CLI program name
    return "openscad"


def prepwd():
    # Linux and other systems that use PATH variables don't need an absolute path configured.
    # if os.path.exists(openscad_exe) == False:
    #     input("ERROR: openscad path not found.")
    #     exit()

    if os.path.exists(STL_DIR):
        rmtree(STL_DIR)
    os.mkdir(STL_DIR)  # Default perms: world-writable

    if os.path.exists(PREV_DIR):
        rmtree(PREV_DIR)
    os.mkdir(PREV_DIR)  # Default perms: world-writable

def has_scad_threading():
    cmd = [openscad(), "--help"]
    out = str(sp.check_output(cmd)).lower()
    if "--parallelism" in out:
        return True
    return False


def scad_version():
    cmd = [openscad(), "--version"]
    ver=sp.run(cmd,capture_output=True).stderr
    ver=str(ver).replace("\\r","").replace("b'OpenSCAD version ","").replace("\\n'","").replace("-",".").split(".")	
    return (int(ver[0]), int(ver[1]), int(ver[2])) if ver else ()


def execscad(threadid=0):
    print("Executing OpenSCAD script...")
    cmd = [openscad()]
    if USE_SCAD_THREAD_TRAVERSAL:
        cmd.append("--enable=thread-traversal")
    cmd.extend(
        [
            "-o",
            os.path.join(os.getcwd(), STL_DIR, str(shell + 1) + ".stl"),
            os.path.join(os.getcwd(), "make_shells.scad"),
        ]
    )
    sp.run(cmd)
    
def udnbers(n, vi, nc, mw, mh, stag):
    for y in range(0, mh):
        for x in range(0, mw):
            x3 = int((x + stag[y]) % mw)
            x2 = [x - 1, x + 1, x, x]
            y2 = [y, y, y - 1, y + 1]
            for i in range(0, 4):
                if stag[y] % mw > 0:
                    x2[i] = int((x2[i] + mw) % mw)
                else:
                    if x2[i] < 0:
                        x2[i] = 0
                    if x2[i] > mw - 1:
                        x2[i] = mw - 1
                if (
                    not ((x3 == 0 and i == 0) or (x3 == mh - 1 and i == 1))
                    and y2[i] > -1
                    and y2[i] < mh
                ):
                    n[x, y, i] = vi[int(x2[i]), int(y2[i])] == 0
                else:
                    n[x, y, i] = 0
            nc[x, y] = len(np.argwhere(n[x, y].astype("int")))


def genmaze(mw, mh, stag):
    visited = np.zeros(mw * mh)
    nbercount = np.zeros(mw * mh)
    nbers = np.ones(mw * mh * 4)
    walls = np.ones(mw * mh * 4)
    r = rd.randint(0, mw*mh-1)
    vcount = 1
    visited[r] = 1
    visited = visited.reshape([mw, mh])
    nbers = nbers.reshape([mw, mh, 4])
    nbercount = nbercount.reshape([mw, mh])
    walls = walls.reshape([mw, mh, 4])
    udnbers(nbers, visited, nbercount, mw, mh, stag)
    while vcount < (mw * mh):
        v = np.transpose(np.nonzero(np.logical_and(visited == 1, nbercount > 0)))
        # choose branch
        r = rd.randint(0, len(v) - 1)
        c = v[r]
        # choose wall to break
        '''if nbers[c[0], c[1]][0] == 1 or nbers[c[0], c[1]][1] == 1 or nbers[c[0], c[1]][3] == 1:
            # horizontal-down bias when possible
            r = rd.randint(0, nbercount[c[0], c[1]] - 1 + hbias)
            if r > nbercount[c[0], c[1]] - 1:
                r = int(r - (nbercount[c[0], c[1]]))
                #anything but up
                nonup=nbercount[c[0], c[1]]
                if nbers[c[0], c[1]][2] == 1:
                    nonup-=1
                if nonup>1:
                    r = int(r % nonup)
                else:
                    r = 0
        else:
            # otherwise just vertical'''
        r = rd.randint(0, nbercount[c[0], c[1]] - 1)
        n = np.argwhere(nbers[c[0], c[1]])[r]
        # break wall
        walls[c[0], c[1], n] = 0
        c2 = c
        # walls: 0=L 1=R 2=U 3=D
        if n == 0:
            n2 = 1
            c2[0] = c[0] - 1
        elif n == 1:
            n2 = 0
            c2[0] = c[0] + 1
        elif n == 2:
            n2 = 3
            c2[1] = c[1] - 1
        else:
            n2 = 2
            c2[1] = c[1] + 1
        c2[0] = int((c2[0] + mw) % mw)
        visited[c2[0], c2[1]] = 1
        walls[c2[0], c2[1], n2] = 0
        udnbers(nbers, visited, nbercount, mw, mh, stag)
        vcount = vcount + 1
    return walls#lrud
def preview(maze):
    im = Image.new("L", [2 * mw + 1, 2 * mh + 1], 0)
    if ((i == 0 and shell < shells - 1) or (i == 1 and shell > 0)) and tpp != 1:
        im.putpixel((1 + ex * 2, 0), 255)
        im.putpixel((1 + st * 2, mh * 2), 255)
        for y in range(0, mh):
            for x in range(0, mw):
                imx = 1 + x * 2
                imy = 1 + y * 2
                imnx = [imx - 1, imx + 1, imx, imx]
                imny = [imy, imy, imy - 1, imy + 1]
                im.putpixel((imx, imy), 255)
                for idx in range(0, 4):
                    if maze[x, y, idx] == 0:
                        im.putpixel((imnx[idx], imny[idx]), 255)
        if tpp == 2:
            im.save(os.path.join(os.getcwd(), PREV_DIR, str(shell + 1) + "a.png"))
        else:
            im.save(os.path.join(os.getcwd(), PREV_DIR, str(shell + 1) + ".png"))
    
def solver(maze,s):
    branches=[[s,mh-1,0,0,4]]
    solved=False
    ret=[]
    while len(ret)<mw:
        temp=[]
        for branch in branches:
            x=branch[0]
            y=branch[1]
            length=branch[2]
            downcnt=branch[3]
            last=branch[4]
            here=maze[x,y]
            opencnt=4-np.sum(here)
            if y==0:
                ret.append(length+downcnt/mh)
            if opencnt>0:
                if here[0]==0 and last!=0:
                    temp.append([(x+mw-1)%mw,y,length+1,downcnt,1])
                if here[1]==0 and last!=1:
                    temp.append([(x+1)%mw,y,length+1,downcnt,0])
                    
                if here[2]==0 and last!=2:
                    temp.append([x,y-1,length+1,downcnt,3])
                if here[3]==0 and last!=3:
                    temp.append([x,y+1,length+1,downcnt+1,2])
        branches=temp.copy()
    return ret

def choose_path(maze):
    global st
    global ex
    #difficulty
    #get path lengths...
    lengths=[]
    for s in range(mw):
        #find the ways out...
        lengths.append(solver(maze,s))
    sortedlengthidxs=np.argsort(np.asarray(lengths).flatten())
    chosen=sortedlengthidxs[int(difficulty/101*len(sortedlengthidxs))]
    st=chosen//mw
    ex=chosen%mw
    preview(maze)
def gen():
    global shell
    global d2
    global mh
    global mw
    global i
    global tpp
    if shell < shells:
        if shell == halt:
            exit()
        if shell + 1 > 0 and shell + 1 < shells and shell + 1 == tp and tpp < 1:
            tpp = -1
        if tpp < 1:
            print("part: " + str(shell + 1))
        wt = mwt
        if tpp < 1:
            if shell == 0:
                d = (mw * us * p) / np.pi + wt - marge * 2
            else:
                if shell == tp:
                    d = d2
                else:
                    d = d2 + us + wt + marge * 2
                if i == 0:
                    mw = int(math.ceil((d / p + us) * np.pi / 2 / us))
                    if shell == (shells - 2):
                        mh += 1
                else:
                    if shell == (shells - 1):
                        mw = int(math.ceil((d / p + us) * np.pi / 2 / us))
                    else:
                        mw = int(math.ceil((d2 / p + us) * np.pi / 2 / us))
                mh += 1
        else:
            d = d2 + us + wt + marge * 2
            mw = int(math.ceil((d / p + us) * np.pi / 2 / us))
            mh += 1

        # stag/shift
        stag = np.zeros(mh)
        if stagmode in (1, 2):
            for y in range(0, mh):
                if y == 0 or stagmode == 1:
                    stag[y] = rd.randint(0, mh - 1)
                else:
                    stag[y] = stag[y - 1] + rd.randint(0, mh - 1)
        elif stagmode == 3:
            stag = np.multiply(np.arange(0, mh), stagconst).astype("int")
        # maze
        
        #st = rd.randint(0, mw - 1)
        #ex = rd.randint(0, mw - 1)
        marr = genmaze(int(mw), int(mh), stag)
        choose_path(marr)
        matrix = []
        for y in range(0, mh):
            row = []
            for x in range(0, mw * p):
                x2 = x % mw
                r = marr[x2, y, 1] == 0
                u = marr[x2, y, 3] == 0
                if u and r:
                    row.append("3")
                elif u:
                    row.append("2")
                elif r:
                    row.append("1")
                else:
                    row.append("0")
            matrix.append(f"[{','.join(row)}]")
        s = f"[{','.join(matrix)}];"

        if tpp < 1:
            maze_num = 1
            open_mode = "w"
        else:
            maze_num = 2
            open_mode = "a+"
        with open("maze.scad", open_mode) as maze:
            maze.write(f"maze{maze_num}=")
            maze.write(
                "\n".join(
                    [
                        s,
                        f"h{maze_num}={mh};",
                        f"w{maze_num}={mw * p};",
                        f"st{maze_num}={st};",
                        f"ex{maze_num}={ex};",
                    ]
                )
            )

        base = 1
        lid = 0
        if shell == shells - 1:
            lid = 1
            base = 0
        if shell > shells - 2:
            mos = 0
        else:
            mos = shells - shell - 2
        with open("config.scad", "w+") as cfg:
            cfg.write(
                "\n".join(
                    [
                        f"p={p};",
                        f"tpp={tpp};",
                        f"is={shell};",
                        f"os={mos};",
                        f"lid={lid};",
                        f"base={base};",
                        f"iw={wt};",
                        f"id={d};",
                        f"s={us};",
                        f"i={i};",
                        f"bd={d + wt * 2 + us * 2};",
                        f"m={marge};",
                    ]
                )
            )
        if shell < shells - 2:
            d2 = d

        if shell > 0 and shell < shells and shell == tp and tpp < 1:
            if i == 0:  # double nub transition
                tpp = 1
                i = 1
            else:  # double maze transition
                tpp = 2
                i = 0
        else:
            tpp = 0
        if tpp < 1:
            execscad()
            shell = shell + 1
        return False
    else:
        return True


if __name__ == "__main__":

    try:
        prepwd()
        # get scad version:
        if has_scad_threading():
            USE_SCAD_THREAD_TRAVERSAL = (
                input("multi-threading available. use it(y/n)?").lower() == "y"
            )

        version = scad_version()
        if version[0] < 2015:
            input("ERROR: invalid scad version. must be at least 2015.xx.xx .")
            exit(1)
    except FileNotFoundError:
        input("ERROR: Could not find OpenSCAD: " + openscad())
        exit(1)

    d2 = 0
    shell = 0

    # make parts:
    p = abs(int(input("nub count (0=2 nubs,1=3 nubs,2=4 nubs, ...):"))) + 2
    tpp = 0
    '''hbias = abs(
        int(input("complexity (horizontal-down bias); 0=none >0= bias; larger= more difficult:"))*100
    )'''
    difficulty=abs(float(input("difficulty (length of path); 0.0 (easy) to 100.0 (hard): ")))
    if difficulty>100:
        difficulty=100
    stagconst = 0
    stagmode = int(input("shift mode (0=none 1=random 2=random change 3=twist):"))
    if stagmode == 3:
        stagconst = abs(int(input("twist amount:")))
    config = configparser.ConfigParser()
    config.read("opt.ini")
    if "DEFAULT" not in config:
        input("ERROR: No DEFAULT section in opt.ini")
        exit(1)
    config = config["DEFAULT"]

    shells = config.getint("levels") + 1  # levels
    marge = config.getfloat("tolerance")
    i = int(config.getboolean("maze_inside"))
    tp = config.getint("transition_shell")
    if tp >= shells:
        tp = 0
    us = config.getfloat("spacing")
    mh = config.getint("units_tall")
    mw = config.getint("units_wide")
    mwt = config.getfloat("wall_thickness")
    while not gen():
        continue
    print("done!")
