<?php
/*****************************************************************************************
 * 
 * Copyright (c) 2016 Eida.cz. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this list
 * of conditions and the following disclaimer.
 * 
 * 2. The name of the author may not be used to endorse or promote products derived from
 * this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY EIDA.CZ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************************/

class MinecraftVoxelDrawing
{
    
    // volání přes interface
    private $COMMAND = "service minecraft-server command setblock";
    
    // souřadnice Y může/nemůže klesat pod Y0
    private $CUT_HEIGHT = FALSE;
    // voxely se mají řadit (sekvenční stavba podle X)
    private $SORT_VOXELS = FALSE;
    
    // typ bloku
    private $material;
    // poloměr kruhu/koule/dómu
    private $radius;
    
    // úložiště voxelů
    private $voxels = Array();
    
    // počáteční bod
    private $X0;
    private $Y0;
    private $Z0;
    
    /**
     *  Konstruktor. Nastaví počáteční bod v souřadnicovém systému Minecraftu.
     *
     *  @param int $X 
     *  @param int $Y 
     *  @param int $Z 
     */
    public function __construct($X, $Y, $Z)
    {
        $this->X0 = (int) $X;
        $this->Y0 = (int) $Z;
        $this->Z0 = (int) $Y;
    }
    
    /**
     *  Setter pro materiál, chainovaný.
     *
     *  @param string m Např. "minecraft:glass"
     *
     *  @return MinecraftVoxelDrawing
     */
    public function setMaterial($m)
    {
        $this->material = $m;
        
        return $this;
    }
    
    /**
     *  Getter pro materiál.
     *
     *  @return string
     */
    public function getMaterial()
    {
        return $this->material;
    }
    
    /**
     *  Setter pro poloměr, chainovaný.
     *
     *  @param int $r Poloměr
     *
     *  @return MinecraftVoxelDrawing
     */
    public function setRadius($r)
    {
        $this->radius = (int) $r;
        
        return $this;
    }
    
    /**
     *  Getter pro poloměr.
     *
     *  @return int 
     */
    public function getRadius()
    {
        return $this->radius;
    }
    
    /**
     *  Vytvoření koule.
     *
     */
    public function drawSphere()
    {
        
        $this->prepareMainShape();
        
        $this->optimize();
        $this->plot();
    }
    
    /**
     *  Vytvoření symetrického dómu/kopule - polokoule.
     *
     */
    public function drawDome()
    {
        $this->CUT_HEIGHT = TRUE;
        
        $this->drawSphere();
    }
    
    /**
     *  Vytvoření kruhu - jedna vrstva, v MC rovině XZ.
     *
     */
    public function drawBaseCircle()
    {
        $this->slice1(0, $this->getRadius(), 1 - $this->getRadius());
        
        $this->optimize();
        $this->plot();
    }
    
    /**
     *  Vnější Bresenhamův algoritmus pro vytvoření vrstev ve všech třech rovinách.
     */
    private function prepareMainShape()
    {
        
        // Rovina X->Z
        $x = $this->getRadius();
        $z = 0;
        
        $rE = 1 - $x;
        
        while ($x >= $z) {
            $this->slice1($z, $x, $rE);
            
            if ($rE <= 0) {
                $rE += 2 * ++$z + 1;
            } else {
                $rE += 2 * (++$z - --$x + 1);
            } // else-if
            
        } // while X->Z
        
        // Rovina X->Y
        $x = $this->getRadius();
        $y = 0;
        
        $rE = 1 - $x;
        
        while ($x >= $y) {
            $this->slice2($y, $x, $rE);
            
            if ($rE <= 0) {
                $rE += 2 * ++$y + 1;
            } else {
                $rE += 2 * (++$y - --$x + 1);
            } // else-if
            
        } // while X->Y
        
        // Rovina Y->Z
        $y = $this->getRadius();
        $z = 0;
        
        $rE = 1 - $y;
        
        while ($y >= $z) {
            $this->slice3($z, $y, $rE);
            
            if ($rE <= 0) {
                $rE += 2 * ++$z + 1;
            } else {
                $rE += 2 * (++$z - --$y + 1);
            } // else-if
            
        } // while Y->Z
    }
    
    /**
     *  Kruhová vrstva v rovině XZ.
     */
    private function slice1($z1, $r, $rE0)
    {
        
        $x0 = $this->X0;
        $y0 = $this->Y0;
        $z0 = $this->Z0;
        
        $x = $r;
        $y = 0;
        
        $rE = $rE0;
        
        while ($x >= $y) {
            
            // 1. kvadrant
            $this->setVoxel($x0 + $x, $y0 + $y, $z0 + $z1);
            $this->setVoxel($x0 + $y, $y0 + $x, $z0 + $z1);
            
            if ($z1 != 0 && $this->CUT_HEIGHT != TRUE) {
                $this->setVoxel($x0 + $x, $y0 + $y, $z0 - $z1);
                $this->setVoxel($x0 + $y, $y0 + $x, $z0 - $z1);
            }
            
            // 2. kvadrant
            $this->setVoxel($x0 - $x, $y0 + $y, $z0 + $z1);
            $this->setVoxel($x0 - $y, $y0 + $x, $z0 + $z1);
            
            if ($z1 != 0 && $this->CUT_HEIGHT != TRUE) {
                $this->setVoxel($x0 - $x, $y0 + $y, $z0 - $z1);
                $this->setVoxel($x0 - $y, $y0 + $x, $z0 - $z1);
            }
            
            // 3. kvadrant
            $this->setVoxel($x0 - $x, $y0 - $y, $z0 + $z1);
            $this->setVoxel($x0 - $y, $y0 - $x, $z0 + $z1);
            
            if ($z1 != 0 && $this->CUT_HEIGHT != TRUE) {
                $this->setVoxel($x0 - $x, $y0 - $y, $z0 - $z1);
                $this->setVoxel($x0 - $y, $y0 - $x, $z0 - $z1);
            }
            
            // 4. kvadrant
            $this->setVoxel($x0 + $x, $y0 - $y, $z0 + $z1);
            $this->setVoxel($x0 + $y, $y0 - $x, $z0 + $z1);
            
            if ($z1 != 0 && $this->CUT_HEIGHT != TRUE) {
                $this->setVoxel($x0 + $x, $y0 - $y, $z0 - $z1);
                $this->setVoxel($x0 + $y, $y0 - $x, $z0 - $z1);
            }
            
            if ($rE <= 0) {
                $rE += 2 * ++$y + 1;
            } else {
                $rE += 2 * (++$y - --$x + 1);
            } // else-if
            
        } // while
        
    } // slice1
    
    /**
     *  Kruhová vrstva v rovině XY.
     */
    private function slice2($y1, $r, $rE0)
    {
        
        $x0 = $this->X0;
        $y0 = $this->Y0;
        $z0 = $this->Z0;
        
        $x = $r;
        $z = 0;
        
        $rE = $rE0;
        
        while ($x >= $z) {
            
            // 1. kvadrant
            $this->setVoxel($x0 + $x, $y0 + $y1, $z0 + $z);
            $this->setVoxel($x0 + $z, $y0 + $y1, $z0 + $x);
            $this->setVoxel($x0 + $x, $y0 - $y1, $z0 + $z);
            $this->setVoxel($x0 + $z, $y0 - $y1, $z0 + $x);
            
            // 2. kvadrant
            $this->setVoxel($x0 - $x, $y0 + $y1, $z0 + $z);
            $this->setVoxel($x0 - $z, $y0 + $y1, $z0 + $x);
            $this->setVoxel($x0 - $x, $y0 - $y1, $z0 + $z);
            $this->setVoxel($x0 - $z, $y0 - $y1, $z0 + $x);
            
            if ($this->CUT_HEIGHT != TRUE) {
                // 3. kvadrant
                $this->setVoxel($x0 - $x, $y0 + $y1, $z0 - $z);
                $this->setVoxel($x0 - $z, $y0 + $y1, $z0 - $x);
                $this->setVoxel($x0 - $x, $y0 - $y1, $z0 - $z);
                $this->setVoxel($x0 - $z, $y0 - $y1, $z0 - $x);
                
                // 4. kvadrant
                $this->setVoxel($x0 + $x, $y0 + $y1, $z0 - $z);
                $this->setVoxel($x0 + $z, $y0 + $y1, $z0 - $x);
                $this->setVoxel($x0 + $x, $y0 - $y1, $z0 - $z);
                $this->setVoxel($x0 + $z, $y0 - $y1, $z0 - $x);
            }
            
            if ($rE <= 0) {
                $rE += 2 * ++$z + 1;
            } else {
                $rE += 2 * (++$z - --$x + 1);
            } // else-if
            
        } // while
        
    } // slice2
    
    /**
     *  Kruhová vrstva v rovině YZ.
     */
    private function slice3($x1, $r, $rE0)
    {
        
        $x0 = $this->X0;
        $y0 = $this->Y0;
        $z0 = $this->Z0;
        
        $y = $r;
        $z = 0;
        
        $rE = $rE0;
        
        while ($y >= $z) {
            
            // 1. kvadrant
            $this->setVoxel($x0 + $x1, $y0 + $y, $z0 + $z);
            $this->setVoxel($x0 + $x1, $y0 + $z, $z0 + $y);
            $this->setVoxel($x0 - $x1, $y0 + $y, $z0 + $z);
            $this->setVoxel($x0 - $x1, $y0 + $z, $z0 + $y);
            
            // 2. kvadrant
            $this->setVoxel($x0 + $x1, $y0 - $y, $z0 + $z);
            $this->setVoxel($x0 + $x1, $y0 - $z, $z0 + $y);
            $this->setVoxel($x0 - $x1, $y0 - $y, $z0 + $z);
            $this->setVoxel($x0 - $x1, $y0 - $z, $z0 + $y);
            
            if ($this->CUT_HEIGHT != TRUE) {
                // 3. kvadrant
                $this->setVoxel($x0 + $x1, $y0 - $y, $z0 - $z);
                $this->setVoxel($x0 + $x1, $y0 - $z, $z0 - $y);
                $this->setVoxel($x0 - $x1, $y0 - $y, $z0 - $z);
                $this->setVoxel($x0 - $x1, $y0 - $z, $z0 - $y);
                
                // 4. kvadrant
                $this->setVoxel($x0 + $x1, $y0 + $y, $z0 - $z);
                $this->setVoxel($x0 + $x1, $y0 + $z, $z0 - $y);
                $this->setVoxel($x0 - $x1, $y0 + $y, $z0 - $z);
                $this->setVoxel($x0 - $x1, $y0 + $z, $z0 - $y);
            }
            
            if ($rE <= 0) {
                $rE += 2 * ++$z + 1;
            } else {
                $rE += 2 * (++$z - --$y + 1);
            } // else-if
            
        } // while
        
    } // slice3
    
    /**
     *  Přidá voxel na seznam.
     */
    private function setVoxel($x, $y, $z)
    {
        $this->voxels[] = Array(
            $x,
            $y,
            $z
        );
    }
    
    /**
     *  Hrubá optimalizace - před vykreselním se ze seznamu voxelů odstraní duplicity.
     */
    private function optimize()
    {
        
        if ($this->SORT_VOXELS == TRUE) {
            asort($this->voxels);
        }
        
        $stringVoxels = Array();
        $dedupVoxels  = Array();
        
        foreach ($this->voxels as $coord) {
            $stringVoxels[] = implode(",", $coord);
        }
        
        $strings = array_unique($stringVoxels, SORT_STRING);
        
        foreach ($strings as $str) {
            $dedupVoxels[] = explode(",", $str);
        }
        
        $this->voxels = $dedupVoxels;
    }
    
    /**
     *  Realizace vykreselní do Minecraftu. Při použití 'setblock' musí být v paměti
     *  aktivní množina chunků, které se mají měnit ( = hráč musí být v oblasti).
     * 
     *  Metoda vypisuje na stdout zpracovávanou souřadnici.
     */
    private function plot()
    {
        
        foreach ($this->voxels as $voxel) {
            // Minecraft coordinates
            $NX = $voxel[0]; //$x;
            $NY = $voxel[2]; //$z;
            $NZ = $voxel[1]; //$y;
            
            echo "[" . $NX . ", " . $NY . ", " . $NZ . "]... ";
            // systémové volání
            shell_exec($this->COMMAND . " " . $NX . " " . $NY . " " . $NZ . " " . $this->getMaterial());
            echo "OK\n";
        }
        
    }
    
}
