#!/usr/bin/env python
#

"""
This file is part of ESA's XMM-Newton Scientific Analysis System (SAS).
SAS is free software: you can redistribute it and/or modify
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

SAS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with SAS.  If not, see .

Name    = 'slewFootCalc'
VERSION = '1.1';
author  = 'Ole Koenig, Jose Vicente Perea';
date    = '2018-07-30';

ChangeLog
=========

Version 1.1 - 2018-07-30 (JVP)
------------

Version 1.0 - 2018-06-26 (Ole Koenig)
------------

+ Initial version
This code aims on determining the footprints of the XMM exposure maps
Author: Ole Koenig ole.koenig@fau.de 26/06/2018

"""

__version__   = "1.1"
__author__    = "Jose V. Perea"

__progname__  = "slewFootCalc"

#------------------------------------------------------------------------------------------------

def usage():

#------------------------------------------------------------------------------------------------
'''
Help.
'''
print("""
usage:  slewFootCalc.py  --expMap=<...>  --ds9footprint=<...>

This code aims on determining the footprints of the XMM exposure maps
Author: Ole Koenig ole.koenig@fau.de 26/06/2018

Basic idea of the code:
Axioms:
The XMM footprint consists of a minimum of 4 and a maximum of 6 points.
It has a clear rhombohedral shape with straight edges.
The four cornerpoints of the image frame are (clock-wise, in pixels):
C1 = bottom left (1,1)
C2 = upper left (1,max_y)
C3 = upper right (max_x,max_y)
C4 = bottom right (max_x,1)

2) Detect NON-illuminated corner points (there are in total 4 corner points).
Reason: By walking the edges from the NON-illuminated corners the code avoids
landing in "gaps" or "stripes of 0 value" in the exposure map.
3) From the corner point walk in x and y direction through the arrays until
finding a value >0. This is the footprint point.
Repeat this in a clock-wise rotation starting in the lower left corner.
4) In total there should be a maximum of 6 footprint points (and minimum of 4).
If the code gives out less then 4 or more then 6 corner points it appends the
filename to "list_of_weird_images.txt"
5) Tranform pixels to WCS world coordinates and write to file
"xmm_expmaps_footprints.txt" in format filename | Polygon(ra1,dec1,ra2,dec2...)

###### DS9 output ######
Additional one can set the option --ds9 which creates a .reg file for ds9
displaying the footprint points as polygon.
########################

The code can be either used with an input file (list of filenames, delimiter=\n)
python3 slewFootCalc.py --file filelist.txt [--ds9]

or with
python3 slewFootCalc.py --file name.fits [--ds9]

which promts the output to stdout.

What the code is NOT capable of:

- Find footprint points in the center of the image (it only walks on the edges)
- Calculate if the shape is a correct parallelogonal hexagon
- handle wavy lines within the image: it assumes a strict rhombohedral shape
- handle gaps (only walks on the edges of the image!)

Note that the x,y pixel definition in fits files runs from 1..n (actually, the
only really reasonable choice!) and not from 0..n-1 as it is in python.

Modified by:
- if either of the input image dimensions is 1 (or 0), exits without producing footprint.
- if the code cannot find the footprints by stepping just use the four corners of the
input image

===================================================================================================
""")

#------------------------------------------------------------------------------------------------
def error(string):
'''
prints the string in red.
'''
sys.stdout.write( string )					# No color string

def version(version_num):
'''
prints version number.
'''
versionString = '{0} ({0}-{1}) [-]'.format(sys.argv.split('/')[-1], version_num)
print(versionString)

# ----------------------------------------------------------------------------------------
def getFootprint(filename, outFootprint):
# {{{
######## PARAMETERS ########
# {{{
# Pixel steps with which the algorithm walks. A larger threshold will
# result in slightly faster computation, however more unprecise footprints
thr = 1

# If image_stopped = True the code will give error messages in case that one
# edge of the image is illuminated and the opposite one NOT (in both x and y
# direction. This was explicetly implemented to filter the xmm newton exposure
# maps which ended on *EXPMAP8000.FTZ because they tend to stop in the middle of
# the image. However, there are also many cases (see e.g. P9031400004PNS003EXPMAP8014.FTZ)
# which fulfill this criterion (so I split the *8000 images from the rest and
# calculated it separately.

# RDS - not used in production code
#image_stopped = False
############################
# }}}

# main() returns two arguments:
# output  = fk5 ra,dec values of footprints
# damaged = if True the image is added to a "list_of_weird_images.txt"
color_red      = '\x1b[6;15;41m'
color_normal   = '\x1b[0m'

from astropy.io import fits
from astropy.wcs import WCS

output, damaged = "",False

# Load the exposure map (this is likely the most time consuming operation)
with fits.open (filename) as hdulist:

# Get x,y axis length (in pixel)

# Image[y][x] rows give x-coord, columns give y-coord
image = hdulist.data
#hdulist.close()

# RDS - handle the case where one of the axes has only one pixel
if NAXIS1<2 or NAXIS2<2:
print("File " + filename + "has too few pixels in at least one dimension to "
+ "calculate a footprint")
damaged = True
return " ",damaged

# Define the four corner points
C1, C2, C3, C4 = (0,0), (0,NAXIS2-1), (NAXIS1-1,NAXIS2-1), (NAXIS1-1,0)

# At beginning all corner points are footprint points and then remove
# illuminated pixels in get_start()
startp = [C1,C2,C3,C4] # foot print points

x_pixel_max = len (image) -1
y_pixel_max = len (image) -1

# Get NON-illuminated pixels as starting points for walking section
# ill are the remaining footprintpoints (illuminated corners) after deleting
# the non-illuminated starting points (non_ill)
non_ill, ill = get_start(image, x_pixel_max, y_pixel_max, C1, C2, C3, C4)

# This code not run unless boolean changed at to pof code
#if image_stopped == True:
# First error handling: The *EXPMAP8000.FTZ tend to stop in the middle of
# image. Since the code can't handle footprints within the image one has to
# find a way to filter them and append to list_of_weird_images

# If the the upper edge is illuminated somewhere, the bottom edge must be
# illuminated, too! If this is not the case --> weird!
#if max(image)==0.0 and max(image[-1])!=0.0 or max(image)!=0.0 and max(image[-1])==0.0:
#   print("     " + color_red + "WARNING" + color_normal + ": One x-edge is illuminated, the other one not, appending to list of weird images.")
#  damaged = True

# Left edge illuminated and right edge not AND only one corner illuminated
#elif image.max(axis=0)==0.0 and image.max(axis=0)[-1]!=0.0 and len(ill)==1 or image.max(axis=0)!=0.0 and image.max(axis=0)[-1]==0.0 and len(ill)==1:
#   print("     " + color_red + "WARNING" + color_normal + ": One y-edge is illuminated, the other one not, appending to list of weird images.")
#  damaged = True

# Actual footprintfinding
fpp=[]
for ii, p in enumerate(non_ill,start=1):
####### WALKING FROM C1 #######
if p=="C1":
# Start at corner 1 (pixel 1/1) and walk first (y=0) row to the right
for i in range(0, x_pixel_max, thr):
if image[i] > 0.0:
fpp.append( (i,0) )
break

if C1 in ill and C1 not in fpp and ii==1:
fpp.append( C1 )

if p=="C1":
# Walk from C1 upwards (x=0)
for i in range(0, y_pixel_max, thr):
if image[i] > 0.0:
fpp.append( (0,i) )
break

####### WALKING FROM C2 #######
if p=="C2":
# Walk down on left edge (x=0)
for i in reversed( range(0, y_pixel_max, thr)):
if image[i] > 0.0:
fpp.append( (0,i) )
break

if C2 in ill and C2 not in fpp and ii==2:
fpp.append( C2 )

if p=="C2":
# Walk to the right on upper edge (y=1008)
for i in range(0, x_pixel_max, thr):
if image[-1][i] > 0.0:
fpp.append( (i,NAXIS2-1) )
break

####### WALKING FROM C3 #######
if p=="C3":
# Walk left on upper edge (y=1008)
for i in reversed( range(0, x_pixel_max, thr)):
if image[-1][i] > 0.0:
fpp.append( (i,NAXIS2-1) )
break

if C3 in ill and C3 not in fpp and ii==3:
fpp.append( C3 )

if p=="C3":
# Walk down on right edge (x=637)
for i in reversed( range(0, y_pixel_max, thr)):
if image[i][-1] > 0.0:
fpp.append( (NAXIS1-1,i) )
break

####### WALKING FROM C4 #######
if p=="C4":
# Walk up on right edge (x=637)
for i in range(0, y_pixel_max, thr):
if image[i][-1] > 0.0:
fpp.append( (NAXIS1-1,i) )
break

if C4 in ill and C4 not in fpp and ii==4:
fpp.append( C4 )

if p=="C4":
# Walk left on bottom edge (y=0)
for i in reversed( range(0, x_pixel_max, thr)):
if image[i] > 0.0:
fpp.append( (i,0) )
break

#print ("Footprint points in pixels are: ", fpp)

# Transform pixels to world coordinates if suitable number (at least 4) of footprints found
if len(fpp) >= 4:# and len(fpp) <= 6:
world = wcs.all_pix2world(fpp,0)

else:
# Error handling
#damaged = True   - RDS can we do this ?
if len(fpp)==0:
# This means that no illuminated corners where found
# --> The footprint is the whole image (all four corners)
print("     " + color_red + "WARNING" + color_normal + ": No corner is illuminated! using image corners")
world = wcs.all_pix2world(startp,0)
elif 06
# Less then 4 footprintpoints found. This is a weird shape!
print("     " + color_red + "WARNING" + color_normal + ": Number of footprints smaller then 4, using image corners.")
## RDS - world = wcs.all_pix2world(fpp,0) # Default to four corners
world = wcs.all_pix2world(startp,0)

#    output += os.path.basename(filename) + " | Polygon("
#    for coord in world:
#        output += str(coord) + "," + str(coord) + ","
#    output = output[:len(output)-1] + ") |{" # remove the last ,
#    for coord in world:
#        output += "(" + str(coord*pi/180) + "," + str(coord*pi/180) + "),"
#    output=output[:len(output)-1] + "}" # Change the last , for a }
#
#    if make_ds9_file == True:
#        try:
#            os.mkdir ('ds9')
#        except OSError:
#            pass
#        #print ("     Creating region file.")

writeds9output( outFootprint, world )

return output, damaged
# }}}

# ----------------------------------------------------------------------------------------
# Function to determine the non-illuminated starting corners
def get_start (image, x_pixel_max, y_pixel_max, C1, C2, C3, C4):
# {{{
footprintpoints = [C1,C2,C3,C4]
non_ill = ["","","",""]
if image==0.0:
footprintpoints.remove( C1 )
non_ill = "C1"
if image[y_pixel_max]==0.0:
footprintpoints.remove( C2 )
non_ill = "C2"
if image[y_pixel_max][x_pixel_max]==0.0:
footprintpoints.remove( C3 )
non_ill = "C3"
if image[x_pixel_max]==0.0:
footprintpoints.remove( C4 )
non_ill = "C4"

return non_ill, footprintpoints
# }}}

# ----------------------------------------------------------------------------------------
# Function to write a ds9 region file where footprint points are displayed
def writeds9output (outFootprint, fpp):
# {{{
import os

### fp = open ('ds9/' + os.path.splitext (os.path.basename(filename)) + '_ds9.reg', 'w')
print('{0}: Slew footprint ds9 file : {1}'.format(__progname__, outFootprint))
fp = open (outFootprint, 'w')

fp.write("# Region file format: DS9 version 4.0\n# Filename: %s\n" % outFootprint)
fp.write("# global color=green font='helvetica 12 bold' select=1 highlite=1 edit=1 move=1 delete=1 include=1 fixed=0 source\n")
fp.write("linear\npolygon(")
for i in range (len(fpp)):
fp.write ("%f, %f," % (fpp[i], fpp[i])) # ra, dec
#remove the comma at end of spoly
cur_pos = fp.tell()
fp.seek (cur_pos-1, os.SEEK_SET)
fp.truncate()
fp.write(") # color=green width=2")
"""
# Circles
fp.write("FK5\n")
for i in range( len(fpp)):
fp.write ("Circle(%f, %f, 0.005)\n" % (fpp[i], fpp[i])) # x,y (pixels)
"""
fp.close()
# }}}

# ----------------------------------------------------------------------------------------
def main(arguments):
'''
This is the main function doing the full loop.

'''
import getopt, os

import ModuleUtil

version_num    = __version__		# Version number to be updated
verbose        = False

expMap         = ''
outFootprint   = 'expMapfootprint.ds9'

arguments = arguments.split()
progName = arguments
argsList = arguments[1:]

try:
opts, args = getopt.getopt(argsList, "vo:Vh", ["expMap=", "outFootprint=", \
"version", "verbose=", "help"])
except getopt.GetoptError as err:
usage()
error("ERROR: Could not understand the options!\n\n")
print(err)  # will print something like "option -a not recognized"
print("================================================================================================")
return

for o, a in opts:
if o in ("-v", "--version"):
version(version_num)
return 0

if o in ("-V", "--verbose"):
verbose = ModuleUtil.varCheck(client = progName, inVar = a).checkBool()

if o in ("-h", "--help"):
usage()
return 0

if o in ("--expMap"):
expMap = a

if o in ("-o", "--outFootprint"):
outFootprint = a

# Get the Slew footprint
if os.path.isfile( expMap ) and os.access(expMap, os.R_OK):
print('{0}: Creating footPrint for Slew exp map : {1}'.format(__progname__, expMap))
output, damaged = getFootprint( expMap, outFootprint )
else:
usage()
error("ERROR: Input exposure map does not exist\n\n")
return

# ----------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------------------------------------
if __name__=='__main__':

import sys
# start = time.time()

main(" ".join(sys.argv))

# print ("\nTotal runtime = ", time.time() - start, "s")
# ------------------------------------------------------------------------------------------------------------------------