###############################################################################
# File:        guStarGen.py
# Appointment: Star generator
# Project:     GalaxyU
# Author:      AR
# History:
# 18.07.2016 Created
###############################################################################
import os 
import pygame
import subprocess
import sys
import random
import time

import guApp
import guLogTarget
import guPlanetGenerator
import pgTools
import zx
import zxLog

from ctypes import *

GENERATE_CMD_PARAM = "generate"

#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Creates a star image, returns surface object
# aBeamsColour - basic beams star colour; aCoreColour - the core colour
# aBeams - a number of 'beams'
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def CreateStarImage(\
    aBeamsColour, aCoreColour = pygame.Color("white"), aBeams = 1000, aDelay = None):

    #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    # Creates colours from aStartColour to aDstColour split by steps
    class CColourMaker:
        def __init__(self, aStartColour, aDstColour, aStepsNumber):
            self._startColour = aStartColour
            self._dstColour = aDstColour
            self._stepsNumber = aStepsNumber
            self._dr = float(aDstColour.r - aStartColour.r)/aStepsNumber
            self._dg = float(aDstColour.g - aStartColour.g)/aStepsNumber
            self._db = float(aDstColour.b - aStartColour.b)/aStepsNumber
            self._cr = aStartColour.r
            self._cg = aStartColour.g
            self._cb = aStartColour.b
                    
        # Returns next colour    
        def GetNextColour(self):
            self._cr += self._dr
            self._cg += self._dg
            self._cb += self._db
            return (self._cr, self._cg, self._cb, 0)
    #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
                         
    # Create surface
    sz = 200
    image = pygame.Surface((sz, sz))
            
    # Centre coordinates
    xcentre = float(sz)/2
    ycentre = float(sz)/2

    # Draw 'beams'
    n = 0
    while aBeams > 0:
        x = int(random.random()*sz)
        y = int(random.random()*sz)
        dx = x - xcentre
        dy = y - ycentre
        l = (dx*dx + dy*dy)**0.5 
        if l <= sz/2 and l >= sz/20:
            # New coordinates are fine. Draw a beam with dynamically changing colour
            xstep = dx/3
            ystep = dy/3
                    
            # 'white' area
            pygame.draw.line(image, aCoreColour, (xcentre, ycentre), (xcentre + xstep, ycentre + ystep))
                    
            # 'Colour' area: from 'white' to aColour
            xc = xcentre + xstep
            yc = ycentre + ystep
            d = ((xstep*xstep) + (ystep*ystep))**0.5 # approx number of pixels in a step*
            dx = xstep/d
            dy = ystep/d
            cm = CColourMaker(aCoreColour, aBeamsColour, d)
            for i in range(int(d)):
                pygame.draw.line(image, cm.GetNextColour(), (xc, yc), (xc + dx, yc + dy))
                xc += dx
                yc += dy

            # 'Colour' area: from aColour to black
            cm = CColourMaker(aBeamsColour, pygame.Color(0, 0, 0), d)
            for i in range(int(d)):
                pygame.draw.line(image, cm.GetNextColour(), (xc, yc), (xc + dx, yc + dy))
                xc += dx
                yc += dy
                    
            # Count this 'beam'
            n += 1
            if n > aBeams:
                break;
        if zx.Suspend(aDelay):
            return None # cancelled

    # Drawing the core
    pygame.draw.circle(image, aCoreColour, (int(xcentre), int(ycentre)), int(sz/7))

    return pgTools.BlurImage(\
        image, aDelay,
        lambda str: guApp.Application.Log(zxLog.LOG_INFO, str))



#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Creates star image and saves it in a file. Returns image. Can be None
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def CreateAndSaveStarImage(aFileName, aBeamsColour, aCoreColour, aBeams, aDelay = None):
    image = CreateStarImage(aBeamsColour, aCoreColour, aBeams, aDelay)
    if image is not None:
        pygame.image.save(image, aFileName)
    return image



#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Loads a star image
# aStarId is a string, usually a star id, but can be a word
# aNotReadyStarImage - image used as a replacement, if star image file has not been generated yet.
#    if not None, the star generation will happen asynchronously
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def LoadStarImage(aStarId, aBeams, aBeamsColour, aCoreColour, aNotReadyStarImage = None):
    # Try to load image from file
    fileName = GetStarFileName(aStarId)
    image = None
    try:
        guApp.Application.Log(zxLog.LOG_INFO, "Loading star image from " + fileName + "...")
        image = pygame.image.load(fileName)
    except:
        if aNotReadyStarImage is None:
            guApp.Application.Log(zxLog.LOG_WARNING, "Failed, generating...")
            image = CreateAndSaveStarImage(fileName, aBeamsColour, aCoreColour, aBeams)
        else:
            guApp.Application.Log(zxLog.LOG_WARNING, "Failed, assigned default image")
            image = aNotReadyStarImage
            
    image.set_colorkey((0,0,0)) # make image transparent
    return image



#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Loads a planet image for star aStar
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def LoadPlanetImage(aStar, aNotReadyPlanetImage = None):
    # Try to load image from file
    fileName = GetPlanetFileName(aStar)
    image = None
    try:
        guApp.Application.Log(zxLog.LOG_INFO, "Loading planet image from " + fileName + " ...")
        image = pygame.image.load(fileName)
    except:
        if aNotReadyPlanetImage is None:
            guApp.Application.Log(zxLog.LOG_WARNING, "Failed, generating...")
            image = CreateAndSavePlanetImage(aStar, fileName)
        else:
            guApp.Application.Log(zxLog.LOG_WARNING, "Failed, assigned default image")
            image = aNotReadyPlanetImage
            
    image.set_colorkey((0,0,0)) # make image transparent
    return image



#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Helper - returns planet filename from star
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def GetPlanetFileName(aStar):
    return os.path.join(guApp.CApplication.PLANETS_DIR_NAME, 
                        (str(aStar.Id) if aStar is not None else 'unknown') + '.png')


#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Returns star file name
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def GetStarFileName(aStarId):
    return os.path.join(guApp.CApplication.STARS_DIR_NAME, aStarId + '.png')



#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Returns True if aStar needs image generation (star itself or its planet)
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def NeedsImageGeneration(aStar):
    return not os.path.isfile(GetStarFileName(str(aStar.Id))) or \
                                not os.path.isfile(GetPlanetFileName(aStar))



#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Creates planet image and saves it in a file
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def CreateAndSavePlanetImage(aStar, aFileName = None):
    fileName = aFileName if aFileName is not None else GetPlanetFileName(aStar)
    random.seed(aStar.Id if aStar is not None else 0)
    image = guPlanetGenerator.GeneratePlanetForStar(510, aStar)
    if image is not None:
        pygame.image.save(image, fileName)
    return image



#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Returns a number of running guMain processes
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def CalcProcessesNumber():
    return zx.GetNumberOfRunningProcesses("guMain.exe" if zx.IsWindows() else "guMain")



#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Starts image generation
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def StartImagesGenerationProcess():
    processes = CalcProcessesNumber()
    guApp.Application.Log(zxLog.LOG_INFO, "Found " + str(processes) + " running processes")
    if processes == 1:
        guApp.Application.Log(zxLog.LOG_INFO, "Starting image generation process...")

        # Run generation process
        try:
            subprocess.Popen(\
                [("" if zx.IsWindows() else "./") + "guMain",
                 GENERATE_CMD_PARAM,
                 guApp.Application.GetReport().FileName],
                shell = False, stdin = None, stdout = subprocess.PIPE)
        except:
            pass
    elif processes > 1:
        guApp.Application.Log(zxLog.LOG_INFO, "Image generation is already running")
    else:
        guApp.Application.Log(zxLog.LOG_ERROR, "Found no guMain running processes")



#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Generation of images
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def Generate(aReportFileName):
    guApp.Application.AddLogTarget(guLogTarget.CLogTarget())

    report = guApp.Application.GetReport()
    if report.Load(c_char_p(aReportFileName), True):
        guApp.Application.Log(\
            zxLog.LOG_INFO,
            ">>>Images Generation for report '" + aReportFileName + "' has started")

        # Generation loop
        start = time.time()
        for star in report.VisibleStars:
            if star.IsOwn() or star.IsActualKnown() or report.IsGM():
                # Break if the main application has stopped
                # But check not more often than once a second
                if time.time() - start >= 1:
                    if CalcProcessesNumber() == 1:
                        break
                    start = time.time()

                # Make sure star image is ready
                random.seed(star.Id)
                LoadStarImage(\
                    str(star.Id), 
                    500 + random.random()*star.Size, 
                    star.CalcColour(), pygame.Color("white"))

                # Make sure planets are generated
                LoadPlanetImage(star)

        guApp.Application.Log(zxLog.LOG_INFO, "<<<Images Generation has finished")

