<?php
/**
 *   _____            _____      _       _            _____  ______ 
 *  / ____|          |  __ \    (_)     | |          |  __ \|  ____|
 * | |  __  ___ _ __ | |__) |_ _ _ _ __ | |_ ___ _ __| |__) | |__   
 * | | |_ |/ _ \ '_ \|  ___/ _` | | '_ \| __/ _ \ '__|  ___/|  __|  
 * | |__| |  __/ | | | |  | (_| | | | | | ||  __/ |  | |    | |____ 
 *  \_____|\___|_| |_|_|   \__,_|_|_| |_|\__\___|_|  |_|    |______|
 * Pocketmine Generator for Earth and heightmap based generation.
 *
 * Copyright (C) Ad5001 2017
 * 
 * @author Ad5001
 * @api 3.0.0
 * @copyright 2017 Ad5001
 * @license NTOSL - View LICENSE.md
 * @version 1.0.0
 * @package GenPainterPE
 */

namespace Ad5001\GenPainterPE\generator;

use pocketmine\level\generator\Generator;
use pocketmine\block\Block;
use pocketmine\block\BlockFactory;
use pocketmine\block\CoalOre;
use pocketmine\block\DiamondOre;
use pocketmine\block\Dirt;
use pocketmine\block\GoldOre;
use pocketmine\block\Gravel;
use pocketmine\block\IronOre;
use pocketmine\block\LapisOre;
use pocketmine\block\RedstoneOre;
use pocketmine\level\ChunkManager;
use pocketmine\level\generator\biome\Biome;
use pocketmine\level\generator\noise\Simplex;
use pocketmine\level\generator\normal\object\OreType as OreType2;
use pocketmine\level\generator\object\OreType;
use pocketmine\level\generator\populator\GroundCover;
use pocketmine\level\generator\populator\Ore;
use pocketmine\level\generator\populator\Populator;
use pocketmine\level\Level;
use pocketmine\math\Vector3;
use pocketmine\utils\Random;

use Ad5001\GenPainterPE\Main;
use Ad5001\GenPainterPE\populator\CavePopulator;
use Ad5001\GenPainterPE\populator\RavinePopulator;
use Ad5001\GenPainterPE\utils\BiomeSelector;


class GenPainter extends Generator{
    /** Both two here tells the max and min of water. */
    const WATER_HEIGHT = 100;
    const MIN_HEIGHT = 60;
    /** Maximum height for bedrock */
    const BEDROCK_MAX_HEIGHT = 5;
    /** Converts the original picture height to a minecraft height */
    const DEPTH_MULTIPLICATOR = 0.2;

    /** 
     * @var array 
     * Coordinates of the start point 
     */
    protected $startPoint = [];
    /** @var resource - Heightmap image resource */
    protected $heightmap;

    /** @var ChunkManager */
    protected $level;
    /** @var Random */
    protected $random;
    /** @var BiomeSelector */
    protected $selector;
    /** @var int[] */
    protected static $cachedHeights = [];
    /** @var Biome[] - Biomes that are candidates to be choosen to be generated */
    protected $candidatesBiomes = [];
	/** @var Populator[] */
	private $generationPopulators = [];
	/** @var Populator[] */
    private $populators = [];
    
    /**
     * Constructs the class
     *
     * @param array $options
     */
    public function __construct($options = []){
        $this->genid = Main::$GENERATOR_IDS++;
    }


   	/**
	 * Inits the class for the variables. mw create Test 902874635 worldpainter
     * Executed on the main thread. 
     * $level is actually a level instance.
	 * @param		ChunkManager		$level
	 * @param		Random				$random
	 * @return		void
	 */
	public function init(ChunkManager $level, Random $random) {
		$this->level = $level;
		$this->random = $random;
        $this->random->setSeed($this->level->getSeed());
        if($level instanceof Level && 
            isset($this->genid)) { // First init in main thread
            $this->worldpath = getcwd() . "/worlds/" . $level->getName() . "/";
             // Initing folder data
            @mkdir($this->worldpath . "gendata");
            $config = yaml_parse(file_get_contents(getcwd() . "/plugins/GenPainterPE/config.yml"));
            // Checking heightmap
            if(!file_exists($this->worldpath . "gendata/heightmap.png")) {
                copy(getcwd() . "/plugins/GenPainterPE/heightmaps/" . $config["heightmap_name"] . ".png", $this->worldpath . "gendata/heightmap.png");
            }
            $this->heightmap = \imagecreatefrompng($this->worldpath . "gendata/heightmap.png");
            // Checking gen infos (startpoint, ...)
            if(!file_exists($this->worldpath . "gendata/geninfos.json")) {
                $data = [];
                $data["#"] = "DO NOT MODIFY THIS FILE. IT HAS BEEN GENERATED BY GenPainterPE AND UNEXEPTED ISSUES MAY OCCUR IF YOU MODIFY ANY OF THESES VALUES."; // Do not modify comment in file for noobs.
                $spawn = $this->getSpawnsFromImg();
                $data["startPoint"] = [
                    $spawn->x,
                    $spawn->y,
                    $spawn->z
                ];
                $data = array_merge($data, $config);
                unset($data["heightmap_name"]);
                file_put_contents($this->worldpath . "gendata/geninfos.json", json_encode($data));
                if($this->getServer()->getPluginManager()->getPlugin("PSMCore") !== null) \Ad5001\PSMCore\API::displayNotification("GenPainter", "Generating world " . $level->getName() . "...");
            }
            // Adding symlink to get path later.
            if(file_exists(getcwd() . "/plugins/GenPainterPE/tmp/" . $this->genid)) unlink(getcwd() . "/plugins/GenPainterPE/tmp/" . $this->genid);
            symlink($this->worldpath, getcwd() . "/plugins/GenPainterPE/tmp/" . $this->genid);
        }
        // Regetting the vars from the symlinked folder
        $this->worldpath = readlink(getcwd() . "/plugins/GenPainterPE/tmp/" . $this->genid);
        $this->heightmap = \imagecreatefrompng($this->worldpath . "gendata/heightmap.png");
        $this->options = json_decode(file_get_contents($this->worldpath . "gendata/geninfos.json"));
        $this->startPoint = $this->options->startPoint;

        // Making selector
		$this->selector = new BiomeSelector($this->random, function($temperature, $rainfall){
            // Checking the nearest candidate biome that have the closest $temperature and $rainfall.
            $bestBiome = [405001, Biome::getBiome(Biome::OCEAN)]; // Default biome. Should be enough.
            foreach($this->candidatesBiomes as $b){
                $diffRainFall = abs($b->getRainfall() - $rainfall);
                $diffTemperature = abs($b->getTemperature() - $temperature);
                $diff = $diffRainFall + $diffTemperature; // Total diff.
                if($diff < $bestBiome[0]) $bestBiome = [$diff, $b];
            }
            return $bestBiome[1]->getId();
        }, Biome::getBiome(Biome::OCEAN));

        $this->selector->addBiome(Biome::getBiome(Biome::OCEAN));
		$this->selector->addBiome(Biome::getBiome(Biome::PLAINS));
		$this->selector->addBiome(Biome::getBiome(Biome::DESERT));
		$this->selector->addBiome(Biome::getBiome(Biome::MOUNTAINS));
		$this->selector->addBiome(Biome::getBiome(Biome::FOREST));
		$this->selector->addBiome(Biome::getBiome(Biome::TAIGA));
		$this->selector->addBiome(Biome::getBiome(Biome::SWAMP));
		$this->selector->addBiome(Biome::getBiome(Biome::RIVER));
		$this->selector->addBiome(Biome::getBiome(Biome::ICE_PLAINS));
		$this->selector->addBiome(Biome::getBiome(Biome::SMALL_MOUNTAINS));
		$this->selector->addBiome(Biome::getBiome(Biome::BIRCH_FOREST));
        
        // Populators
        if($this->options->generate_custom_ground){
		    $cover = new GroundCover();
            $this->generationPopulators[] = $cover;
        }
        if($this->options->generate_caves){
            $cave = new CavePopulator ();
            $cave->setBaseAmount(0);
            $cave->setRandomAmount(2);
            $this->generationPopulators[] = $cave;
            $ravine = new RavinePopulator ();
            $ravine->setBaseAmount(0);
            $ravine->setRandomAmount(51);
            $this->generationPopulators[] = $ravine;
        }
        if($this->options->generate_ores) {
		    $ores = new Ore();
		    $ores->setOreTypes([
		    	new OreType(BlockFactory::get(Block::COAL_ORE), 20, 16, 0, 128),
		    	new OreType(BlockFactory::get(Block::IRON_ORE), 20, 8, 0, 64),
		    	new OreType(BlockFactory::get(Block::REDSTONE_ORE), 8, 7, 0, 16),
		    	new OreType(BlockFactory::get(Block::LAPIS_ORE), 1, 6, 0, 32),
		    	new OreType(BlockFactory::get(Block::GOLD_ORE), 2, 8, 0, 32),
		    	new OreType(BlockFactory::get(Block::DIAMOND_ORE), 1, 7, 0, 16),
		    	new OreType(BlockFactory::get(Block::DIRT), 20, 32, 0, 128),
		    	new OreType(BlockFactory::get(Block::GRAVEL), 10, 16, 0, 128)
		    ]);
            $this->generationPopulators[] = $ores;
        }
    }

    /**
     * Generates a chunk
     *
     * @param int $chunkX
     * @param int $chunkZ
     * @return void
     */
	public function generateChunk(int $chunkX, int $chunkZ){
		$this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->level->getSeed());
        $chunk = $this->level->getChunk($chunkX, $chunkZ);
		for($x = 0; $x < 16; $x++) {
			for($z = 0; $z < 16; $z++) {
                // Getting biome & height
                $currentX = $chunkX * 16 + $x;
                $currentZ = $chunkZ * 16 + $z;
                $height = $this->getHeightFromImg($currentX, $currentZ);
                $biome = $this->getBiomeFromPos($currentX, $currentZ);
                $chunk->setBiomeId($x, $z, $biome->getId());
                // Building terrain
                for($y = 0; $y < 256; ++$y) {
					if($y === 0) {
						$chunk->setBlockId($x, $y, $z, Block::BEDROCK);
					} elseif($y <= self::BEDROCK_MAX_HEIGHT && $this->random->nextBoundedInt(2) == 0) {
						$chunk->setBlockId($x, $y, $z, Block::BEDROCK);
                    } elseif($y <= $height) {
						$chunk->setBlockId($x, $y, $z, Block::STONE);
					} elseif($y <= self::WATER_HEIGHT) {
						$chunk->setBlockId($x, $y, $z, Block::STILL_WATER);
					}
				}
            }
        }
        foreach($this->generationPopulators as $populator){
			$populator->populate($this->level, $chunkX, $chunkZ, $this->random);
        }
    }	
    
    
    /**
     * Populates chunks.
     *
     * @param int $chunkX
     * @param int $chunkZ
     * @return void
     */
    public function populateChunk(int $chunkX, int $chunkZ){
        $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->level->getSeed());
        // Check if the generator should generate structures.
        if($this->options->generate_structures) {
	    	foreach($this->populators as $populator){
	    		$populator->populate($this->level, $chunkX, $chunkZ, $this->random);
	    	}
	    	$chunk = $this->level->getChunk($chunkX, $chunkZ);
	    	$biome = Biome::getBiome($chunk->getBiomeId(7, 7));
            $biome->populateChunk($this->level, $chunkX, $chunkZ, $this->random);
        }
	}


    /**
     * Gets a biome from a pos.
     *
     * @param int $x
     * @param int $z
     * @return Biome
     */
    public function getBiomeFromPos(int $x, int $z): Biome{
        $this->candidatesBiomes = [];
        if(count(Main::$BIOMES_BY_RANGE) < 1) Main::generateRanges();
        $height = $this->getHeightFromImg($x, $z);
        // Check if the generator should generate biomes
        if(!$this->options->generate_biomes) {
            if($height > self::WATER_HEIGHT) {
                return Biome::get(Biome::PLAINS);
            } else {
                return Biome::get(Biome::OCEAN);
            }
        }
        // Foreaching all biomes to see which ones could be generated
        foreach(Main::$BIOMES_BY_RANGE as $biomeId => $range){
            if($range->isInRange($height)) $this->candidatesBiomes[] = Biome::getBiome($biomeId);
        }
        // Checking wether there are multiple candidates or not. 
        // If so, choose a biome.
        if(count($this->candidatesBiomes) == 1){
            $biome = $this->candidatesBiomes[0];
        } else {
            $hash = $x * 2345803 ^ $z * 9236449 ^ $this->level->getSeed();
            $hash *= $hash + 223;
            $xNoise = $hash >> 20 & 3;
            $zNoise = $hash >> 22 & 3;
            if($xNoise == 3){
                $xNoise = 1;
            }
            if($zNoise == 3){
                $zNoise = 1;
            }
            $biome = $this->selector->pickBiome($x + $xNoise - 1, $z + $zNoise - 1);
        }
        return $biome;
    }



    /**
     * Returns a block height based on heightmap.
     *
     * @param int $x
     * @param int $z
     * @return int
     */
    public function getHeightFromImg(int $x, int $z): int{
        if(isset(self::$cachedHeights[$x . ";" . $z])) return round(self::$cachedHeights[$x . ";" . $z]);
        // Getting height px of the world
        $imgGetDatX = abs($x) % imagesy($this->heightmap);
        // Getting width px of the world
        $imgGetDatZ = abs($z) % imagesx($this->heightmap);
        // Finally, getting the px to determine the height of the top block
        $imgheight = imagecolorsforindex($this->heightmap, imagecolorat($this->heightmap, $imgGetDatZ, $imgGetDatX))["red"]; // Getting height from the red channel. 
        // In a normal heightmap, all the chanel ouputs the same (exepct alpha)
        // Smoothing out.
        $surroundValues = [];
        // Getting surround values
        if(isset(self::$cachedHeights[($x+1) . ";" . ($z+1)])) $surroundValues[] = self::$cachedHeights[($x+1) . ";" . ($z+1)];
        if(isset(self::$cachedHeights[($x+1) . ";" . ($z)])) $surroundValues[] = self::$cachedHeights[($x+1) . ";" . ($z)];
        if(isset(self::$cachedHeights[($x+1) . ";" . ($z-1)])) $surroundValues[] = self::$cachedHeights[($x+1) . ";" . ($z-1)];
        if(isset(self::$cachedHeights[($x) . ";" . ($z+1)])) $surroundValues[] = self::$cachedHeights[($x) . ";" . ($z+1)];
        if(isset(self::$cachedHeights[($x) . ";" . ($z-1)])) $surroundValues[] = self::$cachedHeights[($x) . ";" . ($z-1)];
        if(isset(self::$cachedHeights[($x-1) . ";" . ($z+1)])) $surroundValues[] = self::$cachedHeights[($x-1) . ";" . ($z+1)];
        if(isset(self::$cachedHeights[($x-1) . ";" . ($z)])) $surroundValues[] = self::$cachedHeights[($x-1) . ";" . ($z)];
        if(isset(self::$cachedHeights[($x-1) . ";" . ($z-1)])) $surroundValues[] = self::$cachedHeights[($x-1) . ";" . ($z-1)];
        
        $imgValue = $imgheight * self::DEPTH_MULTIPLICATOR + self::WATER_HEIGHT;
        if($imgheight == 0) {
            // Calculating presmooth value (to generate $imgheight water value alot smoother and going deeper)
            if(count($surroundValues) !== 0) {
                $preSmoothValue = 0;
                foreach($surroundValues as $v) $preSmoothValue += $v;
                $preSmoothValue /= count($surroundValues);
            } else {
                $preSmoothValue = 100;
            }
            $calcDiffValue = ($preSmoothValue - 100) / 2;
            if(round($calcDiffValue) == -3) $calcDiffValue--;
            $imgValue += -$this->random->nextBoundedInt(3 + round($calcDiffValue)) - 3 + $calcDiffValue;// Used to make water depth.
        }
        // Calculating smooth value
        // $smoothValue = -1;
        // foreach($surroundValues as $v) $smoothValue += $v;
        // $smoothValue += $imgValue * 3;
        // $smoothValue /= count($surroundValues) + 3;
        if($imgValue < self::MIN_HEIGHT) $imgValue = self::MIN_HEIGHT;
        self::$cachedHeights[$x . ";" . $z] = $imgValue;
        return round($imgValue); // Rounding it so that we can use it as a block height 
    }


	
	/**
	 * Return the name of the generator
	 *
	 * @return string
	 */
	public function getName(): string {
		return "worldpainter";
	}
	
	/**
	 * Gives the generators settings.
	 * 
	 * @return array
	 */
	public function getSettings(): array {
		return [];
	}
	/**
	 * Returns spawn location
	 *
	 * @return Vector3
	 */
	public function getSpawn(): Vector3 {
        return new Vector3($this->spawnPoint[0], 
            $this->spawnPoint[1], 
            $this->spawnPoint[2]);
    }
    
	/**
	 * Returns a safe spawn location
	 *
	 * @return Vector3
	 */
	public function getSafeSpawn() {
        return new Vector3($this->spawnPoint[0], 
            $this->spawnPoint[1], 
            $this->spawnPoint[2]);
    }
    
    /*
	 * Gets the top block (y) on an x and z axes
	 * @param $x int
	 * @param $z int
	 */
	protected function getHighestWorkableBlock($x, $z) {
		for($y = Level::Y_MAX - 1; $y > 0; -- $y) {
			$b = $this->level->getBlockIdAt($x, $y, $z);
			if ($b === Block::DIRT or $b === Block::GRASS or $b === Block::PODZOL) {
				break;
			} elseif ($b !== 0 and $b !== Block::SNOW_LAYER) {
				return - 1;
			}
		}
		
		return ++$y;
    }
    

    /**
     * Checks the image for a safes spawns (not in water) then saves it.
     *
     * @return void
     */
    public function getSpawnsFromImg(): Vector3{
        $spawn = new Vector3(128, 128, 128);
        $found = [];
        mt_srand($this->random->getSeed());
        for($i = 0; $i < 1028; $i++){ // Checking 1028 spots to check for a spawn. If none are found, default to 128 128 128.
            $x = mt_rand(0, imagesy($this->heightmap) - 1);
            $z = mt_rand(0, imagesx($this->heightmap) - 1);
            $imgheight = imagecolorsforindex($this->heightmap, imagecolorat($this->heightmap, $z, $x))["red"];
            if($imgheight !== 0){
                $imgValue = $imgheight * self::DEPTH_MULTIPLICATOR + self::WATER_HEIGHT + 2; // +2 is here so that the player will not get stuck in a block.
                $found[] = new Vector3($x, round($imgValue), $z);
            }
        }
        if(count($found) == 0) return $spawn;
        return $found[mt_rand(0, count($found) - 1)];
    }
}